@hegemonart/get-design-done 1.30.5 → 1.30.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,46 +1,43 @@
1
1
  # Graphify — Connection Specification
2
2
 
3
- This file is the connection specification for Graphify within the get-design-done pipeline. Graphify builds a queryable knowledge graph over the codebase — mapping component↔token↔decision relationships via Tree-sitter static analysis and LLM semantic extraction. See `connections/connections.md` for the full connection index and capability matrix.
3
+ This file is the connection specification for **Graphify** within the get-design-done pipeline. Graphify builds a queryable knowledge graph over the codebase and design intel — mapping component↔token↔decision relationships from `.design/intel/` slices. See `connections/connections.md` for the full connection index and capability matrix.
4
+
5
+ > **Native, no external dependency.** Phase 30.6 (v1.30.6) replaced the previous runtime dispatch to `~/.claude/get-shit-done/bin/gsd-tools.cjs graphify *` with a native CLI shipped in this repo at `bin/gdd-graph`. No Python, no separate install — just Node ≥22.
4
6
 
5
7
  ---
6
8
 
7
9
  ## Setup
8
10
 
9
11
  **Prerequisites:**
10
- - Python 3.9+ available on PATH
11
- - GSD framework with `graphify.enabled = true` in `.planning/config.json`
12
+ - Node ≥22 (per `package.json` engines)
13
+ - get-design-done installed (provides `bin/gdd-graph`)
12
14
 
13
- **Install:**
14
- ```
15
- pip install graphifyy
16
- graphify install # installs skill files into ~/.claude/skills/graphify/
17
- ```
15
+ **Enable in project config** (per D-09 — direct file edit, no CLI subcommand):
18
16
 
19
- **Enable in GSD config:**
20
- ```
21
- node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-set graphify.enabled true
17
+ Edit `.design/config.json` and set:
18
+ ```json
19
+ {
20
+ "graphify": {
21
+ "enabled": true
22
+ }
23
+ }
22
24
  ```
23
25
 
24
- **Build the graph (initial):**
25
- ```
26
- graphify . # run in project root; produces graphify-out/graph.json
27
- # or via GSD tools:
28
- node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" graphify build
29
- ```
26
+ If `.design/config.json` does not exist, create it. Missing file = disabled.
30
27
 
31
- **Recommended: auto-rebuild after commits:**
28
+ **Build the graph (initial):**
32
29
  ```
33
- graphify hook install
30
+ node bin/gdd-graph build
34
31
  ```
32
+ Produces `.design/graph/graph.json` (Ajv-validated against `scripts/lib/graph/schema.json`).
35
33
 
36
34
  **Verification:**
37
- After building, run:
38
35
  ```
39
- node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" graphify status
36
+ node bin/gdd-graph status --format json
40
37
  ```
41
- Expect: `{ enabled: true, graph_path: "...", node_count: N, edge_count: N, stale: false }`
38
+ Expect: `{ configured: true, exists: true, node_count: N, edge_count: M, built_at: "<iso>", schema_version: "1.0" }`.
42
39
 
43
- **Note:** Graphify is an optional external dependency. It requires Python and takes 1-5 minutes to build on a large codebase. Do NOT add to plugin bootstrap users opt in manually.
40
+ **Note:** Graphify is optional. It is a pre-search oracle for planner and verifier agents, never a hard requirement. All stages MUST degrade gracefully when graphify is `unavailable` or `not_configured`.
44
41
 
45
42
  ---
46
43
 
@@ -50,7 +47,7 @@ Expect: `{ enabled: true, graph_path: "...", node_count: N, edge_count: N, stale
50
47
 
51
48
  | Node type | Source | ID pattern |
52
49
  |-----------|--------|-----------|
53
- | component | `.stories.tsx` / `src/components/*.tsx` | `component:<name>` |
50
+ | component | `.stories.tsx` / `src/components/*.tsx` (via intel slices) | `component:<name>` |
54
51
  | token:color | CSS custom properties / Figma variables | `token:color/<name>` |
55
52
  | token:spacing | CSS custom properties | `token:spacing/<name>` |
56
53
  | token:typography | CSS custom properties | `token:typography/<name>` |
@@ -65,7 +62,7 @@ Expect: `{ enabled: true, graph_path: "...", node_count: N, edge_count: N, stale
65
62
 
66
63
  ### Edge Types
67
64
 
68
- | Edge | From | To | Meaning |
65
+ | Edge kind | From | To | Meaning |
69
66
  |------|------|-----|---------|
70
67
  | `uses` | component | token | Component references this token |
71
68
  | `renders` | page | component | Page renders this component |
@@ -74,22 +71,32 @@ Expect: `{ enabled: true, graph_path: "...", node_count: N, edge_count: N, stale
74
71
  | `maps-to` | figma-variable | token | Figma variable corresponds to CSS token |
75
72
  | `detected-at` | anti-pattern | component | Anti-pattern found in this component |
76
73
 
77
- ### graph.json structure
74
+ ### graph.json structure (schema v1.0)
78
75
 
79
76
  ```json
80
77
  {
78
+ "schema_version": "1.0",
79
+ "built_at": "2026-05-28T12:00:00.000Z",
81
80
  "nodes": [
82
- { "id": "component:Button", "label": "Button", "type": "component",
83
- "description": "Primary interactive element", "source": "src/components/Button.tsx" }
81
+ {
82
+ "id": "component:Button",
83
+ "type": "component",
84
+ "label": "Button",
85
+ "attrs": { "source": "src/components/Button.tsx" }
86
+ }
84
87
  ],
85
88
  "edges": [
86
- { "source": "component:Button", "target": "token:color/primary/500",
87
- "label": "uses", "confidence": "EXTRACTED", "confidence_score": 0.95 }
89
+ {
90
+ "from": "component:Button",
91
+ "to": "token:color/primary/500",
92
+ "kind": "uses",
93
+ "weight": 0.95
94
+ }
88
95
  ]
89
96
  }
90
97
  ```
91
98
 
92
- Edge confidence tiers: EXTRACTED (found in source), INFERRED (semantic inference), AMBIGUOUS (flagged for review).
99
+ Edges use the `{from, to, kind, weight?}` shape (per D-03.b — matches intel-store schema verbatim, no translation layer). The optional `weight: number` replaces the upstream three-tier confidence enum (per D-03.c).
93
100
 
94
101
  ---
95
102
 
@@ -110,18 +117,19 @@ Unlike MCP connections, Graphify has no ToolSearch check. The probe is file-exis
110
117
 
111
118
  **Graphify probe sequence (execute at agent entry, before using graph):**
112
119
 
113
- Step G1 — Config check:
120
+ Step G1 — Config check (per D-09 — direct read, no CLI subcommand):
114
121
  ```
115
- node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" graphify status
116
- Error or { enabled: false } → graphify: not_configured (skip all graph steps)
117
- { enabled: true } → proceed to Step G2
122
+ node -e "try{const c=JSON.parse(require('fs').readFileSync('.design/config.json','utf8'));process.stdout.write(String(c.graphify?.enabled===true))}catch{process.stdout.write('false')}"
123
+ → false → graphify: not_configured (skip all graph steps)
124
+ → true → proceed to Step G2
118
125
  ```
119
126
 
120
- Step G2 — Graph file check:
127
+ Step G2 — Graph status check (native CLI):
121
128
  ```
122
- Check if graphify-out/graph.json exists in project root
123
- Absent → graphify: unavailable (graph not built yet)
124
- Present → graphify: available
129
+ node bin/gdd-graph status --format json
130
+ { configured: true, exists: true } graphify: available
131
+ { configured: true, exists: false } → graphify: unavailable (graph not built yet)
132
+ → { configured: false, ... } → graphify: not_configured (mirrors G1; defensive)
125
133
  ```
126
134
 
127
135
  Write graphify status to `.design/STATE.md` `<connections>`.
@@ -138,8 +146,8 @@ This is the canonical pre-search pattern for agents. Copy inline — SKILL.md an
138
146
 
139
147
  Step 1: Query graph for decision node and its neighbors
140
148
  ```
141
- node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" graphify query "decision:D-<nn>" --budget 1500
142
- → Returns: connected components + tokens as JSON
149
+ node bin/gdd-graph query "decision:D-<nn>" --budget 1500
150
+ → Returns: ranked match list — connected components + tokens as JSON
143
151
  → Use returned component IDs as grep seed list (reduces false-negative "not found")
144
152
  ```
145
153
 
@@ -150,15 +158,15 @@ Step 2: Grep each returned component for the decision pattern
150
158
 
151
159
  Step 1: Query graph for token node and its neighbors
152
160
  ```
153
- node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" graphify query "<token-name>" --budget 1500
154
- → Returns: all components that reference this token
161
+ node bin/gdd-graph query "<token-name>" --budget 1500
162
+ → Returns: all components that reference this token (ranked by D-04.a score)
155
163
  → Annotate planned task with "N components affected" before scoping
156
164
  ```
157
165
 
158
166
  Step 2: Include component list in the task description
159
167
  (then continue standard planning behavior)
160
168
 
161
- **Budget note:** Use `--budget 1500` for pre-search queries. High confidence_score edges (>= 0.8) are more reliable; AMBIGUOUS edges are hints only.
169
+ **Budget note:** Use `--budget 1500` for pre-search queries. Higher-weight edges (`weight >= 0.8`) are more reliable; lower-weight edges are hints only.
162
170
 
163
171
  ---
164
172
 
@@ -177,21 +185,24 @@ The graph is a performance optimization and accuracy enhancer. It is never a har
177
185
  ## Anti-Patterns
178
186
 
179
187
  - **Do NOT use graphify to replace grep.** The graph is a seed list, not a complete index. Always grep after querying the graph.
180
- - **Do NOT embed graph.json contents in agent context.** Query specific nodes via gsd-tools; never read graph.json directly.
188
+ - **Do NOT embed `graph.json` contents in agent context.** Query specific nodes via `bin/gdd-graph query`; never read `graph.json` directly.
181
189
  - **Do NOT query the graph during scan or design stages.** The graph is read-only and only useful when decisions already exist (plan, verify).
182
- - **Do NOT block on graph build time.** If `graphify build` takes >30 seconds mid-session, log "graphify build deferred — run /gdd:graphify build manually" and continue without graph.
183
- - **Do NOT assume graph covers .design/ artifacts.** Graphify analyzes source code (src/, components/). DESIGN-CONTEXT.md and DESIGN-PLAN.md are not graph nodes unless explicitly indexed.
190
+ - **Do NOT block on graph build time.** If `gdd-graph build` takes >30 seconds mid-session, log "graphify build deferred — run /gdd:graphify build manually" and continue without graph.
191
+ - **Do NOT assume graph covers `.design/` artifacts.** The build walks `.design/intel/` slices and project source; arbitrary planning docs are not graph nodes unless explicitly indexed.
184
192
 
185
193
  ---
186
194
 
187
195
  ## /gdd:graphify Commands
188
196
 
189
- | Subcommand | GSD tools call | Purpose |
197
+ | Subcommand | Native CLI call | Purpose |
190
198
  |------------|----------------|---------|
191
- | `build` | `gsd-tools graphify build` | Build or rebuild the knowledge graph |
192
- | `query <term>` | `gsd-tools graphify query "<term>" --budget 2000` | Query the graph for a node and its neighbors |
193
- | `status` | `gsd-tools graphify status` | Check graph age, node count, enabled status |
194
- | `diff` | `gsd-tools graphify diff` | Show topology changes since last build |
199
+ | `build` | `node bin/gdd-graph build` | Build or rebuild the knowledge graph |
200
+ | `query <term>` | `node bin/gdd-graph query "<term>" --budget 2000` | Query the graph for a node and its neighbors |
201
+ | `status` | `node bin/gdd-graph status` | Check graph age, node count, enabled status |
202
+ | `diff` | `node bin/gdd-graph diff` | Show topology changes since last build |
203
+ | `upsert-node` | `node bin/gdd-graph upsert-node --id X --type T --label L` | Programmatic single-node insert (used by gdd-graph-refresh agent) |
204
+ | `upsert-edge` | `node bin/gdd-graph upsert-edge --from A --to B --kind R` | Programmatic single-edge insert (used by gdd-graph-refresh agent) |
205
+
206
+ If `graphify.enabled` is `false` (or `.design/config.json` is missing), the `bin/gdd-graph` subcommands graceful-degrade — `status` returns `{ configured: false, exists: false }`, other subcommands no-op with exit 0 and a one-line stderr notice.
195
207
 
196
- If `graphify.enabled = false` in `.planning/config.json`, the skill prompts:
197
- "Graphify is not enabled. Enable with: gsd-tools config-set graphify.enabled true — then run /gdd:graphify build."
208
+ To enable: edit `.design/config.json` and set `graphify.enabled: true`, then `node bin/gdd-graph build`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hegemonart/get-design-done",
3
- "version": "1.30.5",
3
+ "version": "1.30.6",
4
4
  "description": "A design-quality pipeline for AI coding agents: brief, plan, implement, and verify UI work against your design system.",
5
5
  "author": "Hegemon",
6
6
  "homepage": "https://github.com/hegemonart/get-design-done",
@@ -28,6 +28,7 @@
28
28
  ],
29
29
  "bin": {
30
30
  "gdd-events": "./scripts/cli/gdd-events.mjs",
31
+ "gdd-graph": "./bin/gdd-graph",
31
32
  "gdd-mcp": "./scripts/mcp-servers/gdd-mcp/server.ts",
32
33
  "gdd-sdk": "./bin/gdd-sdk",
33
34
  "gdd-state-mcp": "./scripts/mcp-servers/gdd-state/server.ts",
@@ -88,7 +89,8 @@
88
89
  "dependencies": {
89
90
  "@anthropic-ai/claude-agent-sdk": "^0.3.143",
90
91
  "@clack/prompts": "^1.2.0",
91
- "@modelcontextprotocol/sdk": "^1.0.0"
92
+ "@modelcontextprotocol/sdk": "^1.0.0",
93
+ "ajv": "^8.18.0"
92
94
  },
93
95
  "optionalDependencies": {
94
96
  "pngjs": "^7.0.0",
@@ -159,10 +159,13 @@ appends the following verbatim block to the cycle markdown:
159
159
  > opt in with the project-local command below. You can always opt out
160
160
  > later by deleting the timestamps from `.design/config.json` (§ 7).
161
161
  >
162
- > <!-- TODO: confirm opt-in command likely
163
- > `node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config set capability_gap_gate.opted_in_at "$(date -Iseconds)"`
164
- > or a project-local equivalent. Plan 29-05 (apply-reflections
165
- > extension) will land the canonical command. -->
162
+ > <!-- Phase 30.6 (D-09) update: per the convention adopted across the
163
+ > knowledge layer in v1.30.6, project-local config flips are direct
164
+ > edits to .design/config.json — no CLI subcommand. The canonical
165
+ > opt-in is therefore: edit .design/config.json to add
166
+ > { "capability_gap_gate": { "opted_in_at": "<ISO date>" } }.
167
+ > Plan 29-05 (apply-reflections extension) surfaces this as a guided
168
+ > prompt rather than a CLI invocation. -->
166
169
  >
167
170
  > This prompt is emitted at most once per project. If you ignore it,
168
171
  > the gate continues to evaluate every cycle but does not re-prompt
@@ -13,7 +13,7 @@ Phase 10.1 (OPT-06) locked the initial per-agent tier assignment. Phase 11's `de
13
13
  Pick `haiku` when the agent is:
14
14
  - Applying a fixed scoring rubric (`design-verifier` runs five deterministic passes with numeric category scores).
15
15
  - Producing a boolean + rationale answer (`design-plan-checker`, `design-context-checker`, `design-integration-checker` all return "gaps found" + structured list).
16
- - Performing a read-only state sync (`gdd-graphify-sync` mirrors graph state — no reasoning density beyond schema matching).
16
+ - Performing a read-only state sync (`gdd-graph-refresh` mirrors graph state — no reasoning density beyond schema matching).
17
17
  - Running at high frequency where cost compounds (checkers run on every `/gdd:verify` pass — cost multiplies with iterations).
18
18
 
19
19
  Haiku's ~20x price advantage over Opus per 1M tokens (see `reference/model-prices.md`) makes it correct for deterministic-rubric work where the marginal quality gain of larger models is negligible.
@@ -49,7 +49,7 @@ Opus cost (~5x Sonnet, ~25x Haiku) is justified only when a single wrong decisio
49
49
  | design-plan-checker | Checker | haiku | Checks the plan against a fixed schema — boolean + gap-list output. |
50
50
  | design-context-checker | Checker | haiku | Checks context completeness against a schema — boolean + gap-list output. |
51
51
  | design-integration-checker | Checker | haiku | Checks cross-artifact references — deterministic link-integrity work. |
52
- | gdd-graphify-sync | Sync agent | haiku | Mirrors graph state to the graph store — no reasoning density required. |
52
+ | gdd-graph-refresh | Refresh agent | haiku | Rebuilds graph at .design/graph/graph.json from intel slices — no reasoning density required. |
53
53
  | a11y-mapper | Mapper | sonnet | Open-ended a11y pattern recognition across many files; Sonnet's breadth matters. |
54
54
  | component-taxonomy-mapper | Mapper | sonnet | Classifies components by role — requires nuance Haiku lacks, not enough to warrant Opus. |
55
55
  | design-auditor | Worker | sonnet | Emits structured findings from code inspection; Sonnet balances depth with cost. |
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Purpose:** collect the minimum signal needed to steer the findings engine without slowing first-run completion past 30 seconds of interview wall-clock. Autodetectable dimensions collapse to a one-key confirmation; genuinely non-derivable dimensions are asked explicitly.
4
4
 
5
- **Hard constraint:** v1.14.7 ships this fixed question set. Do not branch, re-order, or insert new questions without an explicit `/gsd-discuss-phase` override captured in a future DISCUSSION.md.
5
+ **Hard constraint:** v1.14.7 ships this fixed question set. Do not branch, re-order, or insert new questions without an explicit `/gdd:discuss` override captured in a future DISCUSSION.md.
6
6
 
7
7
  ---
8
8
 
@@ -19,6 +19,12 @@ const DEPRECATIONS_PATH = path.join(REPO_ROOT, 'reference/DEPRECATIONS.md');
19
19
  // would over-fire. Cover those cases via targeted review rather than grep.
20
20
  const PATTERNS = [
21
21
  { name: '/design: namespace (replaced by /gdd:)', regex: /\/design:[a-z-]+/g },
22
+ // Phase 30.6: runtime dispatch to upstream gsd-tools.cjs is now disallowed.
23
+ // Native CLI lives at bin/gdd-graph (graph ops) and is invoked as `node bin/gdd-graph <sub>`.
24
+ // The pattern is precise — only the explicit bash dispatch is flagged. Prose mentions
25
+ // of "gsd-tools" or "get-shit-done" in attribution contexts (NOTICE, CHANGELOG, README)
26
+ // are NOT matched because they don't include the leading `node "$HOME/.claude/...` token.
27
+ { name: 'gsd-tools.cjs runtime dispatch (use bin/gdd-graph instead)', regex: /node\s+["']?\$HOME\/\.claude\/get-shit-done\/bin\/gsd-tools\.cjs/g },
22
28
  ];
23
29
 
24
30
  const EXCLUDE_DIRS = new Set([
@@ -0,0 +1,68 @@
1
+ // scripts/lib/graph/atomic-write.mjs — Plan 30.6-02 Task 1
2
+ //
3
+ // Atomic JSON write seam per D-05: writeFile(tmp) + rename(tmp, target) in
4
+ // the SAME directory (Windows atomicity guarantee — fs.rename is only
5
+ // atomic across same-volume same-device renames). No proper-lockfile.
6
+ // Single-writer assumption for the design pipeline; revisit in Phase 41
7
+ // if multi-writer becomes a real need.
8
+
9
+ import {
10
+ writeFileSync,
11
+ renameSync,
12
+ unlinkSync,
13
+ mkdirSync,
14
+ existsSync,
15
+ } from 'node:fs';
16
+ import { dirname, basename, join, resolve } from 'node:path';
17
+
18
+ /**
19
+ * Atomically write a JSON payload to `target` using the tmp+rename pattern.
20
+ *
21
+ * Guarantees:
22
+ * - Readers either see the previous file or the new file, never a
23
+ * partial write.
24
+ * - If rename fails, the tmp file is unlinked (no orphan tmp files).
25
+ * - Tmp file lives in the SAME directory as target (Windows-safe).
26
+ *
27
+ * @param {string} target - Absolute or repo-relative path to final file
28
+ * @param {unknown} payload - JSON-serializable value (stringified pretty 2-space)
29
+ */
30
+ export function atomicWriteJson(target, payload) {
31
+ const parent = dirname(target);
32
+ const base = basename(target);
33
+ const tmp = join(
34
+ parent,
35
+ `.${base}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}`,
36
+ );
37
+
38
+ // Defense per D-05: assert tmp is in same dir as target (cross-device
39
+ // rename is NOT atomic on Windows). Resolve both to normalize forward-
40
+ // vs back-slash separators on Windows so the comparison is path-shape-
41
+ // agnostic — string equality on dirname() outputs is fragile when the
42
+ // caller passes a POSIX-style path on Windows (`/tmp/foo`) and Node
43
+ // resolves it to a native-style temp dir (`C:\...\Temp\foo`).
44
+ if (resolve(dirname(tmp)) !== resolve(parent)) {
45
+ throw new Error(
46
+ `atomicWriteJson invariant: tmp not in same dir as target (tmp=${tmp}, target=${target})`,
47
+ );
48
+ }
49
+
50
+ mkdirSync(parent, { recursive: true });
51
+
52
+ const body = JSON.stringify(payload, null, 2) + '\n';
53
+
54
+ try {
55
+ writeFileSync(tmp, body, 'utf8');
56
+ renameSync(tmp, target);
57
+ } catch (err) {
58
+ // Clean up orphan tmp file on failure (best-effort).
59
+ if (existsSync(tmp)) {
60
+ try {
61
+ unlinkSync(tmp);
62
+ } catch {
63
+ // Swallow cleanup errors — original throw takes precedence.
64
+ }
65
+ }
66
+ throw err;
67
+ }
68
+ }
@@ -0,0 +1,124 @@
1
+ // scripts/lib/graph/build.mjs — Plan 30.6-02 Task 2
2
+ //
3
+ // buildGraph: read .design/intel/graph.json, transform per RESEARCH.md
4
+ // intel→graph mapping, validate against schema 1.0, atomic-write to
5
+ // .design/graph/graph.json. Deterministic when `now` is passed (test seam).
6
+
7
+ import { readFileSync, existsSync } from 'node:fs';
8
+ import { compileValidator, SCHEMA_VERSION } from './schema.mjs';
9
+ import { atomicWriteJson } from './atomic-write.mjs';
10
+
11
+ const DEFAULT_INTEL = '.design/intel/graph.json';
12
+ const DEFAULT_OUT = '.design/graph/graph.json';
13
+ const DEFAULT_BUILDER_VERSION = '1.30.6';
14
+ const DEFAULT_SOURCE_MARKER = 'gdd-intel-store';
15
+
16
+ /**
17
+ * Build .design/graph/graph.json from a .design/intel/graph.json slice.
18
+ *
19
+ * @param {object} opts
20
+ * @param {string} [opts.intelPath] - default '.design/intel/graph.json'
21
+ * @param {string} [opts.outPath] - default '.design/graph/graph.json'
22
+ * @param {string} [opts.builderVersion] - default '1.30.6'
23
+ * @param {string} [opts.now] - ISO timestamp override (deterministic tests)
24
+ * @returns {{ok: true, nodeCount: number, edgeCount: number, outPath: string}}
25
+ * @throws on missing intel, parse failure, schema-invalid output
26
+ */
27
+ export function buildGraph({
28
+ intelPath = DEFAULT_INTEL,
29
+ outPath = DEFAULT_OUT,
30
+ builderVersion = DEFAULT_BUILDER_VERSION,
31
+ now = undefined,
32
+ } = {}) {
33
+ if (!existsSync(intelPath)) {
34
+ const err = new Error(`buildGraph: intel file not found at ${intelPath}`);
35
+ err.code = 'INTEL_MISSING';
36
+ throw err;
37
+ }
38
+
39
+ let intel;
40
+ try {
41
+ intel = JSON.parse(readFileSync(intelPath, 'utf8'));
42
+ } catch (e) {
43
+ const err = new Error(
44
+ `buildGraph: failed to parse intel JSON at ${intelPath}: ${e.message}`,
45
+ );
46
+ err.code = 'INTEL_PARSE_FAILED';
47
+ err.cause = e;
48
+ throw err;
49
+ }
50
+
51
+ const nodes = (Array.isArray(intel.nodes) ? intel.nodes : []).map(
52
+ (n) => transformNode(n),
53
+ );
54
+ const edges = (Array.isArray(intel.edges) ? intel.edges : []).map(
55
+ (e) => transformEdge(e),
56
+ );
57
+
58
+ const payload = {
59
+ schemaVersion: SCHEMA_VERSION,
60
+ metadata: {
61
+ generatedAt: now ?? new Date().toISOString(),
62
+ intelSource: intelPath,
63
+ nodeCount: nodes.length,
64
+ edgeCount: edges.length,
65
+ builderVersion,
66
+ },
67
+ nodes,
68
+ edges,
69
+ };
70
+
71
+ const validate = compileValidator();
72
+ if (!validate(payload)) {
73
+ const err = new Error('buildGraph: payload failed schema validation');
74
+ err.code = 'SCHEMA_INVALID';
75
+ err.schemaErrors = validate.errors;
76
+ throw err;
77
+ }
78
+
79
+ atomicWriteJson(outPath, payload);
80
+ return {
81
+ ok: true,
82
+ nodeCount: nodes.length,
83
+ edgeCount: edges.length,
84
+ outPath,
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Intel → graph node transform per RESEARCH.md §Intel → graph transformation.
90
+ * intel.name → graph.label; any extra fields land in attrs blob.
91
+ */
92
+ function transformNode(n) {
93
+ if (!n || typeof n !== 'object') return n;
94
+ // Pull off named intel fields; spread rest into attrs (lenient passthrough).
95
+ const { id, type, name, label, attrs, source, ...rest } = n;
96
+ const out = { id, type };
97
+ // Honor explicit label first; fall back to intel.name; otherwise omit.
98
+ const labelOut = label ?? name;
99
+ if (labelOut !== undefined) out.label = labelOut;
100
+ // Merge intel-extra fields into attrs (existing attrs win).
101
+ const restKeys = Object.keys(rest);
102
+ if (attrs || restKeys.length) {
103
+ out.attrs = { ...rest, ...(attrs || {}) };
104
+ }
105
+ out.source = source ?? DEFAULT_SOURCE_MARKER;
106
+ return out;
107
+ }
108
+
109
+ /**
110
+ * Intel → graph edge transform. Edges already use {from,to,kind} verbatim
111
+ * per D-03.b — pure passthrough plus attrs absorption.
112
+ */
113
+ function transformEdge(e) {
114
+ if (!e || typeof e !== 'object') return e;
115
+ const { from, to, kind, weight, attrs, source, ...rest } = e;
116
+ const out = { from, to, kind };
117
+ if (typeof weight === 'number') out.weight = weight;
118
+ const restKeys = Object.keys(rest);
119
+ if (attrs || restKeys.length) {
120
+ out.attrs = { ...rest, ...(attrs || {}) };
121
+ }
122
+ out.source = source ?? DEFAULT_SOURCE_MARKER;
123
+ return out;
124
+ }
@@ -0,0 +1,90 @@
1
+ // scripts/lib/graph/diff.mjs — Plan 30.6-02 Task 2
2
+ //
3
+ // diffGraph: compare two graph.json files, emit {addedNodes, removedNodes,
4
+ // changedNodes, addedEdges, removedEdges}. Node identity = .id; edge
5
+ // identity = `${from}::${to}::${kind}` per upstream-key formula.
6
+
7
+ import { readFileSync, existsSync } from 'node:fs';
8
+ import { isDeepStrictEqual } from 'node:util';
9
+ import { compileValidator } from './schema.mjs';
10
+
11
+ /**
12
+ * Diff two graphs by file path.
13
+ *
14
+ * @param {object} opts
15
+ * @param {string} opts.fromPath - baseline graph path
16
+ * @param {string} opts.toPath - current graph path
17
+ * @returns {{addedNodes: any[], removedNodes: any[], changedNodes: Array<{id:string, before:any, after:any}>, addedEdges: any[], removedEdges: any[]}}
18
+ */
19
+ export function diffGraph({ fromPath, toPath } = {}) {
20
+ if (!fromPath || !toPath) {
21
+ const err = new Error('diffGraph: fromPath and toPath are required');
22
+ err.code = 'DIFF_ARGS_MISSING';
23
+ throw err;
24
+ }
25
+ const from = readAndValidate(fromPath, 'fromPath');
26
+ const to = readAndValidate(toPath, 'toPath');
27
+
28
+ const fromNodeMap = new Map(from.nodes.map((n) => [n.id, n]));
29
+ const toNodeMap = new Map(to.nodes.map((n) => [n.id, n]));
30
+
31
+ const addedNodes = [];
32
+ const removedNodes = [];
33
+ const changedNodes = [];
34
+
35
+ for (const [id, after] of toNodeMap) {
36
+ if (!fromNodeMap.has(id)) {
37
+ addedNodes.push(after);
38
+ } else {
39
+ const before = fromNodeMap.get(id);
40
+ if (!isDeepStrictEqual(before, after)) {
41
+ changedNodes.push({ id, before, after });
42
+ }
43
+ }
44
+ }
45
+ for (const [id, before] of fromNodeMap) {
46
+ if (!toNodeMap.has(id)) removedNodes.push(before);
47
+ }
48
+
49
+ const edgeKey = (e) => `${e.from}::${e.to}::${e.kind}`;
50
+ const fromEdgeMap = new Map(from.edges.map((e) => [edgeKey(e), e]));
51
+ const toEdgeMap = new Map(to.edges.map((e) => [edgeKey(e), e]));
52
+
53
+ const addedEdges = [];
54
+ const removedEdges = [];
55
+ for (const [k, e] of toEdgeMap) if (!fromEdgeMap.has(k)) addedEdges.push(e);
56
+ for (const [k, e] of fromEdgeMap) if (!toEdgeMap.has(k)) removedEdges.push(e);
57
+
58
+ return {
59
+ addedNodes,
60
+ removedNodes,
61
+ changedNodes,
62
+ addedEdges,
63
+ removedEdges,
64
+ };
65
+ }
66
+
67
+ function readAndValidate(path, label) {
68
+ if (!existsSync(path)) {
69
+ const err = new Error(`diffGraph: ${label} not found at ${path}`);
70
+ err.code = 'DIFF_FILE_MISSING';
71
+ throw err;
72
+ }
73
+ let parsed;
74
+ try {
75
+ parsed = JSON.parse(readFileSync(path, 'utf8'));
76
+ } catch (e) {
77
+ const err = new Error(`diffGraph: ${label} parse failed: ${e.message}`);
78
+ err.code = 'DIFF_PARSE_FAILED';
79
+ err.cause = e;
80
+ throw err;
81
+ }
82
+ const validate = compileValidator();
83
+ if (!validate(parsed)) {
84
+ const err = new Error(`diffGraph: ${label} failed schema validation`);
85
+ err.code = 'DIFF_SCHEMA_INVALID';
86
+ err.schemaErrors = validate.errors;
87
+ throw err;
88
+ }
89
+ return parsed;
90
+ }
@@ -0,0 +1,14 @@
1
+ // scripts/lib/graph/index.mjs — Plan 30.6-02 Task 2
2
+ //
3
+ // Barrel re-export for graph subcommand handlers. 30.6-03 layers query,
4
+ // upsertNode, upsertEdge on top of these exports + the schema/atomic-write
5
+ // foundation; 30.6-04 verifies the union decouples from upstream GSD.
6
+
7
+ export { buildGraph } from './build.mjs';
8
+ export { statusGraph } from './status.mjs';
9
+ export { diffGraph } from './diff.mjs';
10
+ export { compileValidator, SCHEMA_VERSION, SCHEMA } from './schema.mjs';
11
+ export { atomicWriteJson } from './atomic-write.mjs';
12
+ export { queryGraph } from './query.mjs'; // 30.6-03 Task 1
13
+ export { estimateTokens } from './token-estimate.mjs'; // 30.6-03 Task 1
14
+ export { upsertNode, upsertEdge } from './upsert.mjs'; // 30.6-03 Task 2