diff --git a/CLAUDE.md b/CLAUDE.md index ecd1971..c1f29b6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,6 +10,13 @@ OCCTSwiftScripts is a script harness for rapid OCCTSwift geometry iteration — See `docs/SCRIPT_WORKFLOW.md` for the script iteration workflow. +**Knowledge management**: durable, cross-cutting project knowledge lives in `docs/knowledge/` +— an **OKF** bundle (Open Knowledge Format: categorized markdown + YAML frontmatter, from +Google's Knowledge Catalog). Start at `docs/knowledge/index.md`. `CLAUDE.md` remains the +detailed quick reference; the bundle holds policies, durable decisions, and the relationship +to downstream consumers (e.g. the commercial OCCTStudio app). Record durable decisions/policies +as OKF entries (+ a `log.md` line), not only in chat or commit messages. + ## Build & Run ```bash diff --git a/Sources/occtkit/Commands/GraphML.swift b/Sources/occtkit/Commands/GraphML.swift index c9125fb..9e64120 100644 --- a/Sources/occtkit/Commands/GraphML.swift +++ b/Sources/occtkit/Commands/GraphML.swift @@ -18,9 +18,19 @@ enum GraphMLCommand: Subcommand { let edgeToVertex: COO let faces: [Face] let edges: [Edge] + // Convexity-attributed face-adjacency (the gAAG edge attribute used by + // B-rep GNNs). Face indices follow shape.faces() order — the `face[N]` + // scheme query-topology emits. Added OCCTSwiftScripts#55. + let faceAdjacency: [FaceAdjacency] let sampling: Sampling struct COO: Codable { let sources: [Int]; let targets: [Int] } + struct FaceAdjacency: Codable { + let face1: Int + let face2: Int + let convexity: String // convex | concave | smooth + let sharedEdgeCount: Int + } struct Face: Codable { let index: Int let uSamples: Int @@ -64,6 +74,17 @@ enum GraphMLCommand: Subcommand { return Payload.Edge(index: i, samples: pts.map { [$0.x, $0.y, $0.z] }) } + // Attributed face-adjacency with per-adjacency convexity, from the AAG. + let aag = AAG(shape: shape) + let faceAdjacency: [Payload.FaceAdjacency] = aag.edges.map { + Payload.FaceAdjacency( + face1: $0.face1Index, + face2: $0.face2Index, + convexity: $0.convexity.label, + sharedEdgeCount: $0.sharedEdgeCount + ) + } + let payload = Payload( vertexPositions: g.vertexPositions, edgeBoundaryFlags: g.edgeBoundaryFlags, @@ -74,6 +95,7 @@ enum GraphMLCommand: Subcommand { edgeToVertex: Payload.COO(sources: g.edgeToVertex.sources, targets: g.edgeToVertex.targets), faces: faces, edges: edges, + faceAdjacency: faceAdjacency, sampling: Payload.Sampling(uvSamples: uvSamples, edgeSamples: edgeSamples) ) try GraphIO.emitJSON(payload) diff --git a/Sources/occtkit/Commands/GraphSelect.swift b/Sources/occtkit/Commands/GraphSelect.swift new file mode 100644 index 0000000..f2b0ad6 --- /dev/null +++ b/Sources/occtkit/Commands/GraphSelect.swift @@ -0,0 +1,191 @@ +// GraphSelect — direct B-rep graph adjacency / selection queries. +// +// Closes OCCTSwiftScripts#54. Lets a consumer answer a *local* topology +// question — "what is adjacent to face[N]?", "faces of edge[M]?", "edges of +// vertex[K]?", "all boundary edges", "the convex/concave face adjacencies" — +// without exporting and re-parsing the whole graph (graph-ml). This is the +// selection / "pointer" primitive behind DSL-style selectors and the +// face-adjacency-graph + GNN selection used in the generative-CAD literature +// (UV-Net, Pointer-CAD, AAGNet). +// +// Index spaces: face queries run over the Attributed Adjacency Graph (AAG), +// whose face indices follow `shape.faces()` order — the same `face[N]` scheme +// `query-topology` emits. Edge/vertex queries run over the TopologyGraph +// (`edge[M]` / `vertex[K]`). Convexity is a property of the dihedral between two +// faces, so it is reported on face *adjacencies* (AAG edges), not on a lone edge. +// +// Usage: +// graph-select --query face-neighbors --face N +// graph-select --query edge-faces --edge M +// graph-select --query vertex-edges --vertex K +// graph-select --query face-adjacency +// graph-select --query edges-class --class boundary|non-manifold|seam|degenerate + +import Foundation +import OCCTSwift +import ScriptHarness + +extension EdgeConvexity { + /// Stable lowercase label for JSON output. + var label: String { + switch self { + case .concave: return "concave" + case .smooth: return "smooth" + case .convex: return "convex" + } + } +} + +enum GraphSelectCommand: Subcommand { + static let name = "graph-select" + static let summary = "Query B-rep graph adjacency / selection (face neighbours, edge faces, vertex edges, convexity)" + static let usage = """ + Usage: + graph-select --query [ids] + + Queries: + --query face-neighbors --face N faces adjacent to face N (+ convexity, shared-edge count) + --query edge-faces --edge M faces / vertices / flags of edge M + --query vertex-edges --vertex K edges incident to vertex K + --query face-adjacency full attributed face-adjacency graph (gAAG) + --query edges-class --class K edge indices matching: boundary | non-manifold | seam | degenerate + + Face indices follow shape.faces() order (the `face[N]` scheme query-topology emits); + edge/vertex indices are TopologyGraph indices (`edge[M]` / `vertex[K]`). + """ + + // MARK: responses + + struct NeighbourOut: Encodable { let face: Int; let convexity: String; let sharedEdgeCount: Int } + struct FaceNeighboursResponse: Encodable { + let query = "face-neighbors" + let face: Int + let isPlanar: Bool + let isVertical: Bool + let isHorizontal: Bool + let normal: [Double]? + let neighbors: [NeighbourOut] + } + struct EdgeFacesResponse: Encodable { + let query = "edge-faces" + let edge: Int + let faces: [Int] + let startVertex: Int? + let endVertex: Int? + let boundary: Bool + let manifold: Bool + } + struct VertexEdgesResponse: Encodable { + let query = "vertex-edges" + let vertex: Int + let edges: [Int] + } + struct FaceAdj: Encodable { let face1: Int; let face2: Int; let convexity: String; let sharedEdgeCount: Int } + struct FaceAdjacencyResponse: Encodable { + let query = "face-adjacency" + let faceCount: Int + let adjacencies: [FaceAdj] + } + struct EdgesClassResponse: Encodable { + let query = "edges-class" + let `class`: String + let edges: [Int] + } + + // MARK: run + + static func run(args: [String]) throws -> Int32 { + guard let path = args.first(where: { !$0.hasPrefix("--") }) else { + throw ScriptError.message(usage) + } + let queryType = value("--query", in: args) ?? "face-adjacency" + let shape = try GraphIO.loadBREP(at: path) + + switch queryType { + case "face-neighbors": + let aag = AAG(shape: shape) + let face = try intValue("--face", in: args) + guard face >= 0 && face < aag.nodes.count else { + throw ScriptError.message("face \(face) out of range (0..<\(aag.nodes.count))") + } + let node = aag.nodes[face] + let neighbors = aag.neighbors(of: face).sorted().map { nb -> NeighbourOut in + let e = aag.edge(between: face, and: nb) + return NeighbourOut(face: nb, + convexity: (e?.convexity ?? .smooth).label, + sharedEdgeCount: e?.sharedEdgeCount ?? 0) + } + try GraphIO.emitJSON(FaceNeighboursResponse( + face: face, + isPlanar: node.isPlanar, + isVertical: node.isVertical, + isHorizontal: node.isHorizontal, + normal: node.normal.map { [$0.x, $0.y, $0.z] }, + neighbors: neighbors)) + + case "edge-faces": + let g = try GraphIO.buildGraph(from: shape) + let edge = try intValue("--edge", in: args) + guard edge >= 0 && edge < g.edgeCount else { + throw ScriptError.message("edge \(edge) out of range (0..<\(g.edgeCount))") + } + try GraphIO.emitJSON(EdgeFacesResponse( + edge: edge, + faces: g.faces(of: edge), + startVertex: g.edgeStartVertex(edge), + endVertex: g.edgeEndVertex(edge), + boundary: g.isBoundaryEdge(edge), + manifold: g.isManifoldEdge(edge))) + + case "vertex-edges": + let g = try GraphIO.buildGraph(from: shape) + let vertex = try intValue("--vertex", in: args) + guard vertex >= 0 && vertex < g.vertexCount else { + throw ScriptError.message("vertex \(vertex) out of range (0..<\(g.vertexCount))") + } + try GraphIO.emitJSON(VertexEdgesResponse(vertex: vertex, edges: g.edges(of: vertex))) + + case "face-adjacency": + let aag = AAG(shape: shape) + let adjacencies = aag.edges.map { + FaceAdj(face1: $0.face1Index, face2: $0.face2Index, + convexity: $0.convexity.label, sharedEdgeCount: $0.sharedEdgeCount) + } + try GraphIO.emitJSON(FaceAdjacencyResponse(faceCount: aag.nodes.count, adjacencies: adjacencies)) + + case "edges-class": + let g = try GraphIO.buildGraph(from: shape) + let kind = value("--class", in: args) ?? "boundary" + let matches: [Int] = (0.. String? { + guard let i = args.firstIndex(of: name), i + 1 < args.count else { return nil } + return args[i + 1] + } + private static func intValue(_ name: String, in args: [String]) throws -> Int { + guard let raw = value(name, in: args), let n = Int(raw) else { + throw ScriptError.message("\(name) is required for this query") + } + return n + } +} diff --git a/Sources/occtkit/Subcommand.swift b/Sources/occtkit/Subcommand.swift index de28d0c..678bb28 100644 --- a/Sources/occtkit/Subcommand.swift +++ b/Sources/occtkit/Subcommand.swift @@ -19,6 +19,7 @@ enum Registry { GraphDedupCommand.self, GraphQueryCommand.self, GraphMLCommand.self, + GraphSelectCommand.self, FeatureRecognizeCommand.self, DXFExportCommand.self, DrawingExportCommand.self, diff --git a/docs/knowledge/architecture/index.md b/docs/knowledge/architecture/index.md new file mode 100644 index 0000000..b3fabce --- /dev/null +++ b/docs/knowledge/architecture/index.md @@ -0,0 +1,6 @@ +# Architecture + +The detailed architecture (targets, the `occtkit` umbrella + verb registry, the `--serve` +envelope protocol, the output pipeline, the dependency cohort) is documented in **`CLAUDE.md`** +at the repo root — that is the source of truth. Add OKF entries here only for durable +architectural decisions that need rationale captured beyond the reference in `CLAUDE.md`. diff --git a/docs/knowledge/decisions/index.md b/docs/knowledge/decisions/index.md new file mode 100644 index 0000000..c62bd09 --- /dev/null +++ b/docs/knowledge/decisions/index.md @@ -0,0 +1,4 @@ +# Decisions + +Recorded engineering decisions and their rationale. (Seeded as durable decisions arise; the +historical decisions to date are captured in `CLAUDE.md` and the git log.) diff --git a/docs/knowledge/index.md b/docs/knowledge/index.md new file mode 100644 index 0000000..5cad3c7 --- /dev/null +++ b/docs/knowledge/index.md @@ -0,0 +1,19 @@ +# OCCTSwiftScripts — Knowledge Bundle + +OKF-conformant durable knowledge for OCCTSwiftScripts (the OSS script harness + `occtkit` CLI +for OCCTSwift geometry iteration). See [What is OKF](references/okf.md) for the format; start +at the [overview](overview.md). + +This complements `CLAUDE.md` (the always-loaded, detailed quick reference — the per-verb +detail lives there). The bundle holds durable, cross-cutting knowledge: policies, key +decisions, and the relationship to downstream consumers. + +## Sections + +* [Overview](overview.md) — what this repo is and its boundary. +* [Architecture](architecture/index.md) — pointer to the `CLAUDE.md` architecture. +* [Strategies](strategies/index.md) — how-we-do-it notes. +* [Policies](policies/index.md) — standing rules (the open-source boundary). +* [Decisions](decisions/index.md) — recorded decisions. +* [References](references/index.md) — OKF, the commercial-app relationship. +* [Operations](operations/index.md) — build / run playbooks. diff --git a/docs/knowledge/log.md b/docs/knowledge/log.md new file mode 100644 index 0000000..654ba99 --- /dev/null +++ b/docs/knowledge/log.md @@ -0,0 +1,7 @@ +# Knowledge Log + +## 2026-06-18 + +* **Creation**: Initialised the OKF knowledge bundle. +* **Creation**: Recorded the open-source-boundary policy and the relationship to the + downstream commercial app (OCCTStudio), which consumes the `reconstruct` feature-graph IR. diff --git a/docs/knowledge/operations/index.md b/docs/knowledge/operations/index.md new file mode 100644 index 0000000..3270896 --- /dev/null +++ b/docs/knowledge/operations/index.md @@ -0,0 +1,5 @@ +# Operations + +Build / test / run playbooks. The canonical commands (`swift build`, `swift run Script`, +`swift run occtkit `, `make install`) and conventions are in `CLAUDE.md`; add playbooks +here for multi-step operational procedures that don't fit there. diff --git a/docs/knowledge/overview.md b/docs/knowledge/overview.md new file mode 100644 index 0000000..8169bb3 --- /dev/null +++ b/docs/knowledge/overview.md @@ -0,0 +1,26 @@ +--- +type: Project +title: OCCTSwiftScripts overview +description: OSS script harness for rapid OCCTSwift geometry iteration plus the headless occtkit CLI of reusable verbs for downstream consumers. +resource: / +tags: [project, overview, occtkit, oss] +timestamp: 2026-06-18T00:00:00Z +--- + +# What this is + +A script harness for rapid OCCTSwift geometry iteration — the OCCTSwift equivalent of CadQuery +or OpenSCAD — plus a headless CLI (`occtkit`) bundling reusable verbs (graph-validate, +reconstruct, drawing-export, render-preview, …) for downstream consumers (OCCTMCP, the +OCCTStudio app, Python pipelines). + +The full architecture, the verb inventory, the `--serve` protocol, and the dependency cohort +are documented in detail in **`CLAUDE.md`** — that remains the source of truth for +implementation detail. This bundle holds the durable, cross-cutting knowledge. + +# Boundary + +LGPL-2.1, OSS, depends only on open-source Swift packages. See +[policies/open-source-boundary](policies/open-source-boundary.md). The commercial **OCCTStudio** +app consumes this repo's `reconstruct` verb but lives in a separate private repo — see +[references/commercial-app-relationship](references/commercial-app-relationship.md). diff --git a/docs/knowledge/policies/index.md b/docs/knowledge/policies/index.md new file mode 100644 index 0000000..8225871 --- /dev/null +++ b/docs/knowledge/policies/index.md @@ -0,0 +1,3 @@ +# Policies + +* [Open-source boundary](open-source-boundary.md) - LGPL-only deps; never depend on closed-source projects. diff --git a/docs/knowledge/policies/open-source-boundary.md b/docs/knowledge/policies/open-source-boundary.md new file mode 100644 index 0000000..a19a7e0 --- /dev/null +++ b/docs/knowledge/policies/open-source-boundary.md @@ -0,0 +1,22 @@ +--- +type: Policy +title: Open-source boundary +description: This repo is LGPL-2.1 and depends only on open-source Swift packages. Never propose anything that makes it depend on a closed-source project. +resource: / +tags: [policy, oss, licensing, boundary] +timestamp: 2026-06-18T00:00:00Z +--- + +# Policy + +OCCTSwiftScripts is **LGPL-2.1** and depends only on open-source Swift packages (the OCCTSwift +family). **Never propose a verb, dependency, or change that would make this repo depend on a +closed-source project.** Downstream closed-source consumers (e.g. the OCCTStudio app) wire +their own proprietary pieces — constraint-solving (the former `solve-sketch`) was removed when +the swiftGCS dep was dropped for exactly this reason. + +# Direction + +Dependencies flow OSS-internal only. Downstream commercial consumers depend on *this* repo; +this repo never depends on *them* (see +[references/commercial-app-relationship](/docs/knowledge/references/commercial-app-relationship.md)). diff --git a/docs/knowledge/references/commercial-app-relationship.md b/docs/knowledge/references/commercial-app-relationship.md new file mode 100644 index 0000000..2f065d7 --- /dev/null +++ b/docs/knowledge/references/commercial-app-relationship.md @@ -0,0 +1,28 @@ +--- +type: Reference +title: Relationship to the OCCTStudio commercial app +description: OCCTStudio (private, commercial) is built on this OSS repo and consumes its reconstruct feature-graph IR; this repo stays OSS and must not depend on the app. +resource: https://github.com/gsdali/OCCTStudio +tags: [reference, commercial, occtstudio, boundary] +timestamp: 2026-06-18T00:00:00Z +--- + +# Relationship + +**OCCTStudio** (`gsdali/OCCTStudio`, private/commercial) is a freemium parametric-CAD app — a +B-Rep alternative to OpenSCAD/CadQuery/ManifoldCAD — built on the OCCTSwift stack. It consumes +this repo: + +- OCCTStudio's portable DSL compiles to an IR that its **native adapter** translates to the + JSON consumed by this repo's `reconstruct` verb (`FeatureReconstructor.buildJSON`). Changes + to `reconstruct`'s schema affect the app's native adapter. +- The cookbook recipes (`recipes/01`–`07`) are the app's v0 acceptance set for the DSL/IR. + +# Direction + +Dependencies point **app → OSS, never the reverse.** This repo must never depend on OCCTStudio +(see [policies/open-source-boundary](/docs/knowledge/policies/open-source-boundary.md)). If a +capability seems shared, it lands here under LGPL and the app consumes it. + +The app's own design (DSL grammar, IR schema, AI architecture, freemium/feature-flags) lives in +the OCCTStudio repo's `docs/DSL_DESIGN.md` and `docs/knowledge/` bundle, not here. diff --git a/docs/knowledge/references/index.md b/docs/knowledge/references/index.md new file mode 100644 index 0000000..27c9b38 --- /dev/null +++ b/docs/knowledge/references/index.md @@ -0,0 +1,4 @@ +# References + +* [What is OKF](okf.md) - the markdown+frontmatter knowledge format this bundle uses. +* [Commercial-app relationship](commercial-app-relationship.md) - how OCCTStudio (private, commercial) consumes this OSS repo. diff --git a/docs/knowledge/references/okf.md b/docs/knowledge/references/okf.md new file mode 100644 index 0000000..728547d --- /dev/null +++ b/docs/knowledge/references/okf.md @@ -0,0 +1,38 @@ +--- +type: Reference +title: Open Knowledge Framework (OKF) +description: The vendor-neutral markdown+YAML-frontmatter format this knowledge bundle conforms to, from Google's Knowledge Catalog. +resource: https://github.com/GoogleCloudPlatform/knowledge-catalog/tree/main/okf +tags: [reference, okf, knowledge, format, meta] +timestamp: 2026-06-18T00:00:00Z +--- + +**OKF (Open Knowledge Format / Framework)** is a universal, vendor-neutral format for +representing knowledge as plain markdown files with YAML frontmatter, from Google Cloud's +**Knowledge Catalog** repo (community-maintained, Apache 2.0). This bundle conforms to OKF v0.1. + +# Schema + +**Frontmatter** — `type` is the only REQUIRED field. Recommended: `title`, `description` +(single sentence), `resource` (a URI/path), `tags` (list), `timestamp` (ISO 8601). Producers +may add custom keys; consumers preserve unknown fields. + +**Concept ID** = the file path within the bundle minus `.md`. + +**Cross-links** — bundle-relative `[x](/path.md)` or relative `[x](./other.md)`. Broken links +tolerated. + +**Reserved files** (no frontmatter): `index.md` (directory listing) and `log.md` (date-grouped +history — `## YYYY-MM-DD` + `* **Creation**:` / `* **Update**:`). + +**Conventional body headings**: `# Schema`, `# Examples`, `# Citations`. + +# How we use it + +`docs/knowledge/` is the OKF bundle — durable, in-repo project knowledge. It complements +`CLAUDE.md` (the detailed quick reference) and the agent-private Claude memory. `type` values +in use: `Project`, `Architecture`, `Strategy`, `Policy`, `Decision`, `Reference`, `Playbook`. + +# Citations + +[1] [knowledge-catalog/okf](https://github.com/GoogleCloudPlatform/knowledge-catalog/tree/main/okf) diff --git a/docs/knowledge/strategies/index.md b/docs/knowledge/strategies/index.md new file mode 100644 index 0000000..33982e3 --- /dev/null +++ b/docs/knowledge/strategies/index.md @@ -0,0 +1,4 @@ +# Strategies + +How-we-do-it notes that outlive a single change. (Populate as patterns stabilise; the recipe +cookbook in `recipes/` and `docs/SCRIPT_WORKFLOW.md` cover the current workflows.)