@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,210 @@
1
+ // scripts/lib/graph/upsert.mjs — Plan 30.6-03 Task 2
2
+ //
3
+ // upsertNode + upsertEdge: read existing graph.json (bootstrap empty
4
+ // schemaVersion-1.0 envelope if missing), apply mutation, schema-validate
5
+ // the full graph BEFORE atomic write (per D-03 + D-05). Referential
6
+ // integrity is enforced at the upsert layer (NOT in the JSON schema) per
7
+ // 30.6-02's schema design: edges must reference nodes that already exist.
8
+ //
9
+ // Idempotency contract: same (id) for nodes / same (from,to,kind) for
10
+ // edges → last-write-wins, no duplicate rows, action='updated'.
11
+
12
+ import { readFileSync, existsSync } from 'node:fs';
13
+ import { compileValidator, SCHEMA_VERSION } from './schema.mjs';
14
+ import { atomicWriteJson } from './atomic-write.mjs';
15
+
16
+ const DEFAULT_GRAPH = '.design/graph/graph.json';
17
+
18
+ /**
19
+ * Upsert a node into the graph (create if missing, replace by id if present).
20
+ *
21
+ * @param {object} opts
22
+ * @param {string} [opts.graphPath] - default '.design/graph/graph.json'
23
+ * @param {object} opts.node - { id, type, label?, attrs?, source? }
24
+ * @returns {{ok: true, action: 'created'|'updated', nodeCount: number}}
25
+ * @throws GDD_GRAPH_INVALID_NODE on missing/invalid id
26
+ * @throws GDD_GRAPH_SCHEMA_INVALID on schema-violating input
27
+ */
28
+ export function upsertNode({ graphPath = DEFAULT_GRAPH, node } = {}) {
29
+ if (!node || typeof node !== 'object') {
30
+ const err = new Error('upsertNode: node parameter is required and must be an object');
31
+ err.code = 'GDD_GRAPH_INVALID_NODE';
32
+ throw err;
33
+ }
34
+ if (typeof node.id !== 'string' || node.id.length === 0) {
35
+ const err = new Error('upsertNode: node.id is required and must be a non-empty string');
36
+ err.code = 'GDD_GRAPH_INVALID_NODE';
37
+ throw err;
38
+ }
39
+ if (typeof node.type !== 'string' || node.type.length === 0) {
40
+ const err = new Error('upsertNode: node.type is required and must be a non-empty string');
41
+ err.code = 'GDD_GRAPH_INVALID_NODE';
42
+ throw err;
43
+ }
44
+
45
+ const graph = loadOrBootstrap(graphPath);
46
+
47
+ const idx = graph.nodes.findIndex((n) => n.id === node.id);
48
+ let action;
49
+ if (idx === -1) {
50
+ graph.nodes.push(node);
51
+ action = 'created';
52
+ } else {
53
+ graph.nodes[idx] = node;
54
+ action = 'updated';
55
+ }
56
+
57
+ graph.metadata.nodeCount = graph.nodes.length;
58
+ graph.metadata.edgeCount = graph.edges.length;
59
+
60
+ validateOrThrow(graph, 'upsertNode');
61
+ atomicWriteJson(graphPath, graph);
62
+
63
+ return { ok: true, action, nodeCount: graph.nodes.length };
64
+ }
65
+
66
+ /**
67
+ * Upsert an edge into the graph. Identity = `${from}::${to}::${kind}`.
68
+ *
69
+ * Referential integrity: both `from` and `to` must reference existing nodes.
70
+ * Schema enforces only field-shape; the existence check is an upsert-layer
71
+ * concern (per 30.6-02 schema design + RESEARCH.md §upsert-edge contract).
72
+ *
73
+ * @param {object} opts
74
+ * @param {string} [opts.graphPath]
75
+ * @param {object} opts.edge - { from, to, kind, weight?, attrs?, source? }
76
+ * @returns {{ok: true, action: 'created'|'updated', edgeCount: number}}
77
+ * @throws GDD_GRAPH_INVALID_EDGE on missing required fields
78
+ * @throws GDD_GRAPH_MISSING when graph file does not exist (edges can't
79
+ * precede nodes; create at least one node first via upsertNode)
80
+ * @throws GDD_GRAPH_MISSING_ENDPOINT with missingEndpoints[] when from/to
81
+ * do not reference existing nodes
82
+ * @throws GDD_GRAPH_SCHEMA_INVALID when the mutated graph violates schema
83
+ */
84
+ export function upsertEdge({ graphPath = DEFAULT_GRAPH, edge } = {}) {
85
+ if (!edge || typeof edge !== 'object') {
86
+ const err = new Error('upsertEdge: edge parameter is required and must be an object');
87
+ err.code = 'GDD_GRAPH_INVALID_EDGE';
88
+ throw err;
89
+ }
90
+ for (const field of ['from', 'to', 'kind']) {
91
+ if (typeof edge[field] !== 'string' || edge[field].length === 0) {
92
+ const err = new Error(
93
+ `upsertEdge: edge.${field} is required and must be a non-empty string`,
94
+ );
95
+ err.code = 'GDD_GRAPH_INVALID_EDGE';
96
+ throw err;
97
+ }
98
+ }
99
+
100
+ // Edges cannot exist before nodes — file-missing is a hard error.
101
+ if (!existsSync(graphPath)) {
102
+ const err = new Error(
103
+ `upsertEdge: graph file not found at ${graphPath} — create at least one node first via upsertNode`,
104
+ );
105
+ err.code = 'GDD_GRAPH_MISSING';
106
+ throw err;
107
+ }
108
+
109
+ const graph = loadOrBootstrap(graphPath);
110
+
111
+ // Referential integrity check.
112
+ const nodeIds = new Set(graph.nodes.map((n) => n.id));
113
+ const missingEndpoints = [];
114
+ if (!nodeIds.has(edge.from)) missingEndpoints.push(edge.from);
115
+ if (!nodeIds.has(edge.to)) missingEndpoints.push(edge.to);
116
+ if (missingEndpoints.length) {
117
+ const err = new Error(
118
+ `upsertEdge: missing endpoint node(s): ${missingEndpoints.join(', ')}`,
119
+ );
120
+ err.code = 'GDD_GRAPH_MISSING_ENDPOINT';
121
+ err.missingEndpoints = missingEndpoints;
122
+ throw err;
123
+ }
124
+
125
+ // Edge identity per D-03.b + upstream diff formula: from::to::kind.
126
+ const idx = graph.edges.findIndex(
127
+ (e) => e.from === edge.from && e.to === edge.to && e.kind === edge.kind,
128
+ );
129
+ let action;
130
+ if (idx === -1) {
131
+ graph.edges.push(edge);
132
+ action = 'created';
133
+ } else {
134
+ graph.edges[idx] = edge;
135
+ action = 'updated';
136
+ }
137
+
138
+ graph.metadata.nodeCount = graph.nodes.length;
139
+ graph.metadata.edgeCount = graph.edges.length;
140
+
141
+ validateOrThrow(graph, 'upsertEdge');
142
+ atomicWriteJson(graphPath, graph);
143
+
144
+ return { ok: true, action, edgeCount: graph.edges.length };
145
+ }
146
+
147
+ // ────────────────────────── helpers ──────────────────────────
148
+
149
+ /**
150
+ * Load graph from disk, or bootstrap a schema-1.0 envelope if missing.
151
+ * Throws GDD_GRAPH_PARSE_FAILED on JSON parse error (manual edit broke it).
152
+ */
153
+ function loadOrBootstrap(graphPath) {
154
+ if (!existsSync(graphPath)) {
155
+ return {
156
+ schemaVersion: SCHEMA_VERSION,
157
+ metadata: {
158
+ generatedAt: new Date().toISOString(),
159
+ nodeCount: 0,
160
+ edgeCount: 0,
161
+ builderVersion: '1.30.6',
162
+ },
163
+ nodes: [],
164
+ edges: [],
165
+ };
166
+ }
167
+
168
+ try {
169
+ const graph = JSON.parse(readFileSync(graphPath, 'utf8'));
170
+ // Defensive: ensure metadata/nodes/edges containers exist (a hand-edited
171
+ // file may have shed them — upsert should not crash before the
172
+ // validator can flag the corruption).
173
+ if (!graph.metadata) graph.metadata = { generatedAt: new Date().toISOString(), nodeCount: 0, edgeCount: 0 };
174
+ if (!Array.isArray(graph.nodes)) graph.nodes = [];
175
+ if (!Array.isArray(graph.edges)) graph.edges = [];
176
+ return graph;
177
+ } catch (e) {
178
+ const err = new Error(
179
+ `upsert: failed to parse graph JSON at ${graphPath}: ${e.message}`,
180
+ );
181
+ err.code = 'GDD_GRAPH_PARSE_FAILED';
182
+ err.cause = e;
183
+ throw err;
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Validate full graph against schema 1.0 before write.
189
+ * Distinguish input-shape errors (caller passed a bad node/edge) from
190
+ * GDD_GRAPH_INVALID_{NODE,EDGE} (which are pre-write field-presence checks).
191
+ */
192
+ function validateOrThrow(graph, op) {
193
+ const validate = compileValidator();
194
+ if (!validate(graph)) {
195
+ // Map Ajv path errors back to actionable codes when possible.
196
+ const hasNodeError = (validate.errors || []).some((e) =>
197
+ String(e.instancePath || '').startsWith('/nodes'),
198
+ );
199
+ const hasEdgeError = (validate.errors || []).some((e) =>
200
+ String(e.instancePath || '').startsWith('/edges'),
201
+ );
202
+ let code = 'GDD_GRAPH_SCHEMA_INVALID';
203
+ if (op === 'upsertNode' && hasNodeError) code = 'GDD_GRAPH_INVALID_NODE';
204
+ if (op === 'upsertEdge' && hasEdgeError) code = 'GDD_GRAPH_INVALID_EDGE';
205
+ const err = new Error(`${op}: graph failed schema validation`);
206
+ err.code = code;
207
+ err.schemaErrors = validate.errors;
208
+ throw err;
209
+ }
210
+ }
@@ -1,5 +1,5 @@
1
1
  'use strict';
2
- // scripts/lib/gsd-health-mirror/index.cjs — Plan 27.7-02
2
+ // scripts/lib/health-mirror/index.cjs — Plan 27.7-02 (renamed in Phase 30.6-08 per D-10)
3
3
  //
4
4
  // Pure read-only mirror of skills/health/SKILL.md's check surface.
5
5
  // NO subprocess spawn — just inspects 4 well-known files/dirs and
@@ -40,16 +40,40 @@ function isCancel(p, value) {
40
40
  return typeof p.isCancel === 'function' ? p.isCancel(value) : false;
41
41
  }
42
42
 
43
+ // Build a one-line picker hint per runtime kind. Multi-artifact entries
44
+ // intentionally lack a `files` field (see runtimes.cjs header) — destinations
45
+ // are computed by runtime-artifact-layout.cjs from the runtime's configDir.
46
+ function hintForRuntime(r) {
47
+ if (r.kind === 'claude-marketplace') return 'marketplace registration';
48
+ if (r.kind === 'multi-artifact') {
49
+ return r.configDirFallback
50
+ ? `installs into ~/${r.configDirFallback}`
51
+ : 'installs skills/commands/agents';
52
+ }
53
+ // Future-proof fallback for any new kind: prefer files[0] when present,
54
+ // then configDirFallback, then a neutral label. Never crash on missing
55
+ // optional fields.
56
+ if (Array.isArray(r.files) && r.files.length > 0) return `drops ${r.files[0]}`;
57
+ if (r.configDirFallback) return `installs into ~/${r.configDirFallback}`;
58
+ return r.kind || 'install target';
59
+ }
60
+
43
61
  async function runInteractiveInstall() {
44
62
  const p = loadClack();
45
63
 
46
64
  p.intro('get-design-done — multi-runtime installer');
47
65
 
48
- const runtimes = listRuntimes();
66
+ // Tier-2 distribution channels (cursor-marketplace, codex-plugin) carry
67
+ // `configDir: null` per Phase 28.8 — they're out-of-band bundles, not
68
+ // per-user install targets. The interactive picker should hide them; the
69
+ // regular install pipeline already skips them because configDir is null.
70
+ const runtimes = listRuntimes().filter(
71
+ (r) => r.configDir !== null && r.configDirFallback != null,
72
+ );
49
73
  const options = runtimes.map((r) => ({
50
74
  value: r.id,
51
75
  label: r.displayName,
52
- hint: r.kind === 'claude-marketplace' ? 'marketplace registration' : `drops ${r.files[0] || 'AGENTS.md'}`,
76
+ hint: hintForRuntime(r),
53
77
  }));
54
78
 
55
79
  const picked = await p.multiselect({
@@ -139,4 +163,5 @@ async function runInteractiveUninstall(opts) {
139
163
  module.exports = {
140
164
  runInteractiveInstall,
141
165
  runInteractiveUninstall,
166
+ hintForRuntime,
142
167
  };
@@ -310,10 +310,42 @@ function evaluateStageGate(history, config) {
310
310
  return { crossed, stable_cluster_ids, cycles_observed };
311
311
  }
312
312
 
313
+ // ---------------------------------------------------------------------------
314
+ // Plan 30.5-03 — Reflector KFM proposer wiring.
315
+ //
316
+ // After aggregation, downstream callers may pass the cluster list into the
317
+ // KFM proposer (`scripts/lib/reflector-kfm-proposer.cjs`). The proposer
318
+ // only emits a draft when a cluster has size ≥3 AND no existing catalogue
319
+ // entry matches (D-05). The original 5 Phase 29 proposal classes are
320
+ // untouched — this is an additive 6th pass.
321
+ //
322
+ // We deliberately load the proposer lazily inside `proposeKfmDraftsForClusters`
323
+ // so this aggregator module remains importable in environments that don't
324
+ // have the failure-mode catalogue checked in (e.g. minimal CI shards).
325
+ // ---------------------------------------------------------------------------
326
+
327
+ function proposeKfmDraftsForClusters(clusters, options) {
328
+ if (!Array.isArray(clusters) || clusters.length === 0) {
329
+ return { drafted: [], skipped: [] };
330
+ }
331
+ // require lazily — see comment above.
332
+ // eslint-disable-next-line global-require
333
+ const proposer = require('./reflector-kfm-proposer.cjs');
334
+ const drafted = [];
335
+ const skipped = [];
336
+ for (const c of clusters) {
337
+ const result = proposer.proposeKfmDraft(c, options);
338
+ if (result.action === 'drafted') drafted.push(result);
339
+ else skipped.push({ cluster_id: c && c.id, ...result });
340
+ }
341
+ return { drafted, skipped };
342
+ }
343
+
313
344
  module.exports = {
314
345
  aggregateCapabilityGaps,
315
346
  renderGapsSection,
316
347
  evaluateStageGate,
348
+ proposeKfmDraftsForClusters,
317
349
  // Exported for testing / introspection only:
318
350
  _betaStddev: betaStddev,
319
351
  _DEFAULT_GATE_CONFIG: DEFAULT_GATE_CONFIG,