@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +68 -0
- package/README.md +1 -1
- package/agents/design-integration-checker.md +1 -1
- package/agents/design-planner.md +1 -1
- package/agents/gdd-graph-refresh.md +90 -0
- package/bin/gdd-graph +261 -0
- package/connections/connections.md +10 -9
- package/connections/graphify.md +65 -54
- package/package.json +4 -2
- package/reference/capability-gap-stage-gate.md +7 -4
- package/reference/model-tiers.md +2 -2
- package/reference/start-interview.md +1 -1
- package/scripts/detect-stale-refs.cjs +6 -0
- package/scripts/lib/graph/atomic-write.mjs +68 -0
- package/scripts/lib/graph/build.mjs +124 -0
- package/scripts/lib/graph/diff.mjs +90 -0
- package/scripts/lib/graph/index.mjs +14 -0
- package/scripts/lib/graph/query.mjs +155 -0
- package/scripts/lib/graph/schema.json +69 -0
- package/scripts/lib/graph/schema.mjs +47 -0
- package/scripts/lib/graph/status.mjs +88 -0
- package/scripts/lib/graph/token-estimate.mjs +27 -0
- package/scripts/lib/graph/upsert.mjs +210 -0
- package/scripts/lib/{gsd-health-mirror → health-mirror}/index.cjs +1 -1
- package/scripts/mcp-servers/gdd-mcp/tools/gdd_health.ts +3 -3
- package/skills/connections/connections-onboarding.md +6 -6
- package/skills/graphify/SKILL.md +11 -10
- package/skills/scan/scan-procedure.md +9 -8
- package/agents/gdd-graphify-sync.md +0 -110
- /package/scripts/lib/{gsd-health-mirror → health-mirror}/index.d.cts +0 -0
package/connections/graphify.md
CHANGED
|
@@ -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
|
|
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
|
-
-
|
|
11
|
-
-
|
|
12
|
+
- Node ≥22 (per `package.json` engines)
|
|
13
|
+
- get-design-done installed (provides `bin/gdd-graph`)
|
|
12
14
|
|
|
13
|
-
**
|
|
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
|
-
|
|
20
|
-
```
|
|
21
|
-
|
|
17
|
+
Edit `.design/config.json` and set:
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"graphify": {
|
|
21
|
+
"enabled": true
|
|
22
|
+
}
|
|
23
|
+
}
|
|
22
24
|
```
|
|
23
25
|
|
|
24
|
-
|
|
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
|
-
**
|
|
28
|
+
**Build the graph (initial):**
|
|
32
29
|
```
|
|
33
|
-
|
|
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
|
|
36
|
+
node bin/gdd-graph status --format json
|
|
40
37
|
```
|
|
41
|
-
Expect: `{
|
|
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
|
|
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
|
-
{
|
|
83
|
-
"
|
|
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
|
-
{
|
|
87
|
-
"
|
|
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
|
-
|
|
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 "
|
|
116
|
-
→
|
|
117
|
-
→
|
|
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
|
|
127
|
+
Step G2 — Graph status check (native CLI):
|
|
121
128
|
```
|
|
122
|
-
|
|
123
|
-
→
|
|
124
|
-
→
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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 `
|
|
183
|
-
- **Do NOT assume graph covers
|
|
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 |
|
|
197
|
+
| Subcommand | Native CLI call | Purpose |
|
|
190
198
|
|------------|----------------|---------|
|
|
191
|
-
| `build` | `
|
|
192
|
-
| `query <term>` | `
|
|
193
|
-
| `status` | `
|
|
194
|
-
| `diff` | `
|
|
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
|
-
|
|
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.
|
|
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
|
-
> <!--
|
|
163
|
-
>
|
|
164
|
-
>
|
|
165
|
-
>
|
|
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
|
package/reference/model-tiers.md
CHANGED
|
@@ -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-
|
|
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-
|
|
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 `/
|
|
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
|