@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +103 -0
- package/README.de.md +2 -0
- package/README.fr.md +2 -0
- package/README.it.md +2 -0
- package/README.ja.md +2 -0
- package/README.ko.md +2 -0
- package/README.md +3 -1
- package/README.zh-CN.md +2 -0
- package/agents/design-authority-watcher.md +42 -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/known-failure-modes.md +337 -1
- package/reference/model-tiers.md +2 -2
- package/reference/schemas/events.schema.json +61 -0
- package/reference/start-interview.md +1 -1
- package/scripts/detect-stale-refs.cjs +6 -0
- package/scripts/lib/apply-reflections/incubator-proposals.cjs +10 -3
- package/scripts/lib/authority-watcher/index.cjs +201 -0
- package/scripts/lib/failure-mode-matcher.cjs +460 -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/lib/install/interactive.cjs +27 -2
- package/scripts/lib/reflector-capability-gap-aggregator.cjs +32 -0
- package/scripts/lib/reflector-kfm-proposer.cjs +468 -0
- package/scripts/mcp-servers/gdd-mcp/tools/gdd_health.ts +3 -3
- package/skills/apply-reflections/SKILL.md +4 -0
- package/skills/apply-reflections/apply-reflections-procedure.md +38 -4
- 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
|
@@ -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/
|
|
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
|
-
|
|
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
|
|
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,
|