@hegemonart/get-design-done 1.30.0 → 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.
Files changed (49) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +103 -0
  4. package/README.de.md +2 -0
  5. package/README.fr.md +2 -0
  6. package/README.it.md +2 -0
  7. package/README.ja.md +2 -0
  8. package/README.ko.md +2 -0
  9. package/README.md +3 -1
  10. package/README.zh-CN.md +2 -0
  11. package/agents/design-authority-watcher.md +42 -1
  12. package/agents/design-integration-checker.md +1 -1
  13. package/agents/design-planner.md +1 -1
  14. package/agents/gdd-graph-refresh.md +90 -0
  15. package/bin/gdd-graph +261 -0
  16. package/connections/connections.md +10 -9
  17. package/connections/graphify.md +65 -54
  18. package/package.json +4 -2
  19. package/reference/capability-gap-stage-gate.md +7 -4
  20. package/reference/known-failure-modes.md +337 -1
  21. package/reference/model-tiers.md +2 -2
  22. package/reference/schemas/events.schema.json +61 -0
  23. package/reference/start-interview.md +1 -1
  24. package/scripts/detect-stale-refs.cjs +6 -0
  25. package/scripts/lib/apply-reflections/incubator-proposals.cjs +10 -3
  26. package/scripts/lib/authority-watcher/index.cjs +201 -0
  27. package/scripts/lib/failure-mode-matcher.cjs +460 -0
  28. package/scripts/lib/graph/atomic-write.mjs +68 -0
  29. package/scripts/lib/graph/build.mjs +124 -0
  30. package/scripts/lib/graph/diff.mjs +90 -0
  31. package/scripts/lib/graph/index.mjs +14 -0
  32. package/scripts/lib/graph/query.mjs +155 -0
  33. package/scripts/lib/graph/schema.json +69 -0
  34. package/scripts/lib/graph/schema.mjs +47 -0
  35. package/scripts/lib/graph/status.mjs +88 -0
  36. package/scripts/lib/graph/token-estimate.mjs +27 -0
  37. package/scripts/lib/graph/upsert.mjs +210 -0
  38. package/scripts/lib/{gsd-health-mirror → health-mirror}/index.cjs +1 -1
  39. package/scripts/lib/install/interactive.cjs +27 -2
  40. package/scripts/lib/reflector-capability-gap-aggregator.cjs +32 -0
  41. package/scripts/lib/reflector-kfm-proposer.cjs +468 -0
  42. package/scripts/mcp-servers/gdd-mcp/tools/gdd_health.ts +3 -3
  43. package/skills/apply-reflections/SKILL.md +4 -0
  44. package/skills/apply-reflections/apply-reflections-procedure.md +38 -4
  45. package/skills/connections/connections-onboarding.md +6 -6
  46. package/skills/graphify/SKILL.md +11 -10
  47. package/skills/scan/scan-procedure.md +9 -8
  48. package/agents/gdd-graphify-sync.md +0 -110
  49. /package/scripts/lib/{gsd-health-mirror → health-mirror}/index.d.cts +0 -0
@@ -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
@@ -0,0 +1,155 @@
1
+ // scripts/lib/graph/query.mjs — Plan 30.6-03 Task 1
2
+ //
3
+ // queryGraph: read .design/graph/graph.json, tokenize the query, score
4
+ // nodes per D-04.a (+100 exact-id / +50 exact-label / +20 seed / +10
5
+ // token-in-label / +5 token-in-type / +1 token-in-attrs), walk 1-hop
6
+ // outbound edges to attach neighbors, then enforce a token budget
7
+ // (D-04 chars/4 heuristic via token-estimate.mjs) by dropping
8
+ // lowest-scored matches until the payload fits.
9
+ //
10
+ // Deterministic: identical (graph, query, budget) inputs produce
11
+ // byte-identical JSON output. Ties broken by lexicographic id.
12
+
13
+ import { readFileSync, existsSync } from 'node:fs';
14
+ import { compileValidator } from './schema.mjs';
15
+ import { estimateTokens } from './token-estimate.mjs';
16
+
17
+ const DEFAULT_GRAPH = '.design/graph/graph.json';
18
+ const DEFAULT_BUDGET = 8000;
19
+ const DEFAULT_TOP_K = 10;
20
+
21
+ /**
22
+ * Query the native graph.
23
+ *
24
+ * @param {object} opts
25
+ * @param {string} [opts.graphPath] - default '.design/graph/graph.json'
26
+ * @param {string} opts.query - search term (lowercased, tokenized)
27
+ * @param {number} [opts.budget] - max estimated tokens for return payload
28
+ * @returns {{query:string, matches:Array<{node:object, score:number, neighbors:object[]}>, truncated:boolean}}
29
+ * @throws GRAPH_MISSING when graphPath does not exist
30
+ * @throws GRAPH_PARSE_FAILED when graph JSON cannot be parsed
31
+ * @throws GRAPH_SCHEMA_INVALID when graph fails schema validation
32
+ */
33
+ export function queryGraph({
34
+ graphPath = DEFAULT_GRAPH,
35
+ query = '',
36
+ budget = DEFAULT_BUDGET,
37
+ } = {}) {
38
+ if (!existsSync(graphPath)) {
39
+ const err = new Error(`queryGraph: graph file not found at ${graphPath}`);
40
+ err.code = 'GRAPH_MISSING';
41
+ throw err;
42
+ }
43
+
44
+ let graph;
45
+ try {
46
+ graph = JSON.parse(readFileSync(graphPath, 'utf8'));
47
+ } catch (e) {
48
+ const err = new Error(
49
+ `queryGraph: failed to parse graph JSON at ${graphPath}: ${e.message}`,
50
+ );
51
+ err.code = 'GRAPH_PARSE_FAILED';
52
+ err.cause = e;
53
+ throw err;
54
+ }
55
+
56
+ const validate = compileValidator();
57
+ if (!validate(graph)) {
58
+ const err = new Error('queryGraph: graph failed schema validation');
59
+ err.code = 'GRAPH_SCHEMA_INVALID';
60
+ err.schemaErrors = validate.errors;
61
+ throw err;
62
+ }
63
+
64
+ // ── Tokenize ────────────────────────────────────────────────────────
65
+ const lowerQuery = String(query).toLowerCase().trim();
66
+ const tokens = lowerQuery
67
+ .split(/[^a-z0-9:_\-/.]+/)
68
+ .filter((t) => t.length > 0);
69
+
70
+ // ── Empty / no-token query short-circuit ────────────────────────────
71
+ if (tokens.length === 0) {
72
+ return { query, matches: [], truncated: false };
73
+ }
74
+
75
+ // ── Score each node ─────────────────────────────────────────────────
76
+ // Identify seed nodes (label or id contains the full lowercase query) for the
77
+ // +20 seed bonus before per-token scoring.
78
+ const seedIds = new Set();
79
+ for (const n of graph.nodes) {
80
+ const idLower = String(n.id || '').toLowerCase();
81
+ const labelLower = String(n.label || '').toLowerCase();
82
+ if (
83
+ lowerQuery.length > 0 &&
84
+ (idLower.includes(lowerQuery) || labelLower.includes(lowerQuery))
85
+ ) {
86
+ seedIds.add(n.id);
87
+ }
88
+ }
89
+
90
+ const scored = graph.nodes
91
+ .map((node) => ({ node, score: scoreNode(node, tokens, lowerQuery, seedIds) }))
92
+ .filter((m) => m.score > 0)
93
+ .sort((a, b) => b.score - a.score || a.node.id.localeCompare(b.node.id));
94
+
95
+ // ── Build adjacency for 1-hop outbound walk ─────────────────────────
96
+ const outbound = new Map();
97
+ for (const e of graph.edges) {
98
+ if (!outbound.has(e.from)) outbound.set(e.from, []);
99
+ outbound.get(e.from).push(e);
100
+ }
101
+ const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
102
+
103
+ // ── Assemble matches with neighbors ─────────────────────────────────
104
+ const candidates = scored.slice(0, DEFAULT_TOP_K).map(({ node, score }) => {
105
+ const outs = outbound.get(node.id) || [];
106
+ const neighbors = outs
107
+ .map((e) => nodeById.get(e.to))
108
+ .filter((n) => n !== undefined);
109
+ return { node, score, neighbors };
110
+ });
111
+
112
+ // ── Budget-truncation loop ──────────────────────────────────────────
113
+ let payload = { query, matches: candidates.slice(), truncated: false };
114
+ while (
115
+ estimateTokens(payload) > budget &&
116
+ payload.matches.length > 0
117
+ ) {
118
+ payload.matches.pop(); // drop lowest-score (sorted desc → tail = lowest)
119
+ }
120
+ if (estimateTokens(payload) > budget) {
121
+ payload = { query, matches: [], truncated: true };
122
+ } else if (payload.matches.length < candidates.length) {
123
+ payload.truncated = true;
124
+ }
125
+
126
+ return payload;
127
+ }
128
+
129
+ /**
130
+ * Score a node per D-04.a:
131
+ * +100 exact-id match (case-insensitive)
132
+ * +50 exact-label match
133
+ * +20 seed bonus (node contained the full query as substring)
134
+ * +10 per token substring-match in label
135
+ * +5 per token substring-match in type
136
+ * +1 per token substring-match in attrs (JSON-stringified, lowercased)
137
+ */
138
+ function scoreNode(node, tokens, lowerQuery, seedIds) {
139
+ let score = 0;
140
+ const idLower = String(node.id || '').toLowerCase();
141
+ const labelLower = String(node.label || '').toLowerCase();
142
+ const typeLower = String(node.type || '').toLowerCase();
143
+ const attrsLower = JSON.stringify(node.attrs || {}).toLowerCase();
144
+
145
+ if (lowerQuery && idLower === lowerQuery) score += 100;
146
+ if (lowerQuery && labelLower === lowerQuery) score += 50;
147
+ if (seedIds.has(node.id)) score += 20;
148
+
149
+ for (const t of tokens) {
150
+ if (labelLower.includes(t)) score += 10;
151
+ if (typeLower.includes(t)) score += 5;
152
+ if (attrsLower.includes(t)) score += 1;
153
+ }
154
+ return score;
155
+ }
@@ -0,0 +1,69 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://get-design-done.example/schemas/graph.schema.json",
4
+ "title": ".design/graph/graph.json",
5
+ "description": "Native gdd-graph store. Schema version 1.0 — frozen at Phase 30.6 per D-03.",
6
+ "type": "object",
7
+ "required": ["schemaVersion", "metadata", "nodes", "edges"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "schemaVersion": {
11
+ "type": "string",
12
+ "const": "1.0",
13
+ "description": "Pinned to 1.0 at ship; future migrations bump explicitly (D-03)."
14
+ },
15
+ "metadata": {
16
+ "type": "object",
17
+ "required": ["generatedAt", "nodeCount", "edgeCount"],
18
+ "additionalProperties": true,
19
+ "properties": {
20
+ "generatedAt": { "type": "string", "description": "ISO 8601 timestamp" },
21
+ "intelSource": { "type": "string", "description": "Path to source intel file, e.g. .design/intel/graph.json" },
22
+ "nodeCount": { "type": "integer", "minimum": 0 },
23
+ "edgeCount": { "type": "integer", "minimum": 0 },
24
+ "builderVersion": { "type": "string", "description": "gdd-graph version that wrote this file" }
25
+ }
26
+ },
27
+ "nodes": {
28
+ "type": "array",
29
+ "items": {
30
+ "type": "object",
31
+ "required": ["id", "type"],
32
+ "additionalProperties": true,
33
+ "properties": {
34
+ "id": { "type": "string", "minLength": 1, "description": "Stable globally-unique node ID" },
35
+ "type": { "type": "string", "description": "Node category (component, token, decision, etc.) — lenient enum per D-03" },
36
+ "label": { "type": "string", "description": "Human-readable display name" },
37
+ "attrs": { "type": "object", "additionalProperties": true, "description": "Free-form node attributes" },
38
+ "source": {
39
+ "oneOf": [
40
+ { "type": "string", "description": "Source file path or origin marker" },
41
+ { "type": "object", "additionalProperties": true, "description": "Structured source descriptor per D-03.a" }
42
+ ]
43
+ }
44
+ }
45
+ }
46
+ },
47
+ "edges": {
48
+ "type": "array",
49
+ "items": {
50
+ "type": "object",
51
+ "required": ["from", "to", "kind"],
52
+ "additionalProperties": true,
53
+ "properties": {
54
+ "from": { "type": "string", "minLength": 1, "description": "Source node id (matches a nodes[].id)" },
55
+ "to": { "type": "string", "minLength": 1, "description": "Target node id" },
56
+ "kind": { "type": "string", "description": "Edge type per D-03.b" },
57
+ "weight": { "type": "number", "description": "Optional edge weight per D-03.c; used by query ranking + budget eviction" },
58
+ "attrs": { "type": "object", "additionalProperties": true },
59
+ "source": {
60
+ "oneOf": [
61
+ { "type": "string" },
62
+ { "type": "object", "additionalProperties": true }
63
+ ]
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,47 @@
1
+ // scripts/lib/graph/schema.mjs — Plan 30.6-02 Task 1
2
+ //
3
+ // Ajv-compiled validator factory for .design/graph/graph.json (schema 1.0,
4
+ // frozen at Phase 30.6 per D-03). Compile-once + memoized so callers can
5
+ // invoke compileValidator() liberally without paying per-call cost.
6
+
7
+ import { readFileSync } from 'node:fs';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { dirname, join } from 'node:path';
10
+ import Ajv from 'ajv';
11
+
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+
14
+ // Read schema via fs.readFileSync rather than `import ... with { type:'json' }`
15
+ // to keep this working on Node 22+ without flag tweaks across runtimes.
16
+ const schemaJson = JSON.parse(
17
+ readFileSync(join(__dirname, 'schema.json'), 'utf8'),
18
+ );
19
+
20
+ export const SCHEMA_VERSION = '1.0';
21
+ export const SCHEMA = schemaJson;
22
+
23
+ let _validator = null;
24
+
25
+ /**
26
+ * Returns a compiled Ajv validator function for the native graph schema.
27
+ * Memoized: subsequent calls return the same instance (no recompile).
28
+ *
29
+ * Strict mode is disabled so additionalProperties:true on nodes/edges is
30
+ * honored (forward-compat lenience per 30.6-01 RESEARCH.md).
31
+ *
32
+ * @returns {(payload: unknown) => boolean} validator with .errors after a failed call
33
+ */
34
+ export function compileValidator() {
35
+ if (_validator) return _validator;
36
+ // Ajv ESM: the default export is the constructor.
37
+ const AjvCtor = Ajv.default || Ajv;
38
+ const ajv = new AjvCtor({ strict: false, allErrors: true });
39
+ _validator = ajv.compile(schemaJson);
40
+ return _validator;
41
+ }
42
+
43
+ // Test-only hook: reset the memo so test runs can verify compile-once semantics
44
+ // without polluting other suites. Not part of the public CLI surface.
45
+ export function _resetValidatorMemoForTests() {
46
+ _validator = null;
47
+ }
@@ -0,0 +1,88 @@
1
+ // scripts/lib/graph/status.mjs — Plan 30.6-02 Task 2
2
+ //
3
+ // statusGraph: read .design/graph/graph.json, return structured status JSON.
4
+ // Gracefully degrades when graph file is missing — returns
5
+ // {configured:false, exists:false} without throwing (the 6 status callsites
6
+ // probe before issuing other commands).
7
+
8
+ import { readFileSync, existsSync, statSync } from 'node:fs';
9
+ import { compileValidator } from './schema.mjs';
10
+
11
+ const DEFAULT_GRAPH = '.design/graph/graph.json';
12
+ const DEFAULT_INTEL = '.design/intel/graph.json';
13
+
14
+ /**
15
+ * Report status of the native graph store.
16
+ *
17
+ * @param {object} opts
18
+ * @param {string} [opts.graphPath] - default '.design/graph/graph.json'
19
+ * @param {string} [opts.intelPath] - default '.design/intel/graph.json' (used for staleness)
20
+ * @returns {object} structured status — see RESEARCH.md §Subcommand inventory
21
+ */
22
+ export function statusGraph({
23
+ graphPath = DEFAULT_GRAPH,
24
+ intelPath = DEFAULT_INTEL,
25
+ } = {}) {
26
+ if (!existsSync(graphPath)) {
27
+ return { configured: false, exists: false };
28
+ }
29
+
30
+ let raw;
31
+ try {
32
+ raw = readFileSync(graphPath, 'utf8');
33
+ } catch (e) {
34
+ return {
35
+ configured: true,
36
+ exists: true,
37
+ schemaInvalid: true,
38
+ errors: [{ message: `read failed: ${e.message}` }],
39
+ };
40
+ }
41
+
42
+ let graph;
43
+ try {
44
+ graph = JSON.parse(raw);
45
+ } catch (e) {
46
+ return {
47
+ configured: true,
48
+ exists: true,
49
+ schemaInvalid: true,
50
+ errors: [{ message: `parse failed: ${e.message}` }],
51
+ };
52
+ }
53
+
54
+ const validate = compileValidator();
55
+ if (!validate(graph)) {
56
+ return {
57
+ configured: true,
58
+ exists: true,
59
+ schemaInvalid: true,
60
+ errors: validate.errors,
61
+ };
62
+ }
63
+
64
+ const lastBuiltAt = graph.metadata?.generatedAt ?? null;
65
+
66
+ let stale = false;
67
+ if (existsSync(intelPath) && lastBuiltAt) {
68
+ try {
69
+ const intelMtime = statSync(intelPath).mtimeMs;
70
+ const builtMs = Date.parse(lastBuiltAt);
71
+ if (Number.isFinite(builtMs) && intelMtime > builtMs) {
72
+ stale = true;
73
+ }
74
+ } catch {
75
+ // Stat failed — leave stale=false; not a hard error.
76
+ }
77
+ }
78
+
79
+ return {
80
+ configured: true,
81
+ exists: true,
82
+ nodeCount: graph.nodes.length,
83
+ edgeCount: graph.edges.length,
84
+ schemaVersion: graph.schemaVersion,
85
+ lastBuiltAt,
86
+ stale,
87
+ };
88
+ }
@@ -0,0 +1,27 @@
1
+ // scripts/lib/graph/token-estimate.mjs — Plan 30.6-03 Task 1
2
+ //
3
+ // Token-budget heuristic per D-04: ceil(JSON.stringify(payload).length / 4).
4
+ // Crude approximation of the Anthropic tokenizer's chars-per-token ratio;
5
+ // overridable via GDD_GRAPH_TOKEN_FACTOR env var for test seams (D-04 +
6
+ // RESEARCH.md §Query algorithm).
7
+ //
8
+ // This module is the single place to swap heuristic for a real tokenizer
9
+ // (tiktoken, anthropic-tokenizer, etc.) if/when we want a more faithful
10
+ // budget calculation. All callers funnel through estimateTokens().
11
+
12
+ /**
13
+ * Estimate the token count of a payload.
14
+ *
15
+ * @param {unknown} payload - string OR JSON-serializable value
16
+ * @returns {number} ceiling of chars/divisor; divisor = GDD_GRAPH_TOKEN_FACTOR
17
+ * (when finite, positive number) or 4 (default)
18
+ */
19
+ export function estimateTokens(payload) {
20
+ const str = typeof payload === 'string' ? payload : JSON.stringify(payload);
21
+ // JSON.stringify can return undefined for symbol/function inputs — guard.
22
+ const safe = typeof str === 'string' ? str : '';
23
+ const envFactor = Number(process.env.GDD_GRAPH_TOKEN_FACTOR);
24
+ const divisor =
25
+ Number.isFinite(envFactor) && envFactor > 0 ? envFactor : 4;
26
+ return Math.ceil(safe.length / divisor);
27
+ }