ddduck 0.1.0

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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -0
  3. package/docs/architecture.md +77 -0
  4. package/docs/cli.md +239 -0
  5. package/docs/getting-started.md +98 -0
  6. package/docs/model-reference.md +92 -0
  7. package/docs/model.md +75 -0
  8. package/package.json +76 -0
  9. package/policies/concept-owner-domain.yaml +8 -0
  10. package/policies/documentation-model-reference-resolution.yaml +8 -0
  11. package/policies/no-dangling-model-reference.yaml +8 -0
  12. package/policies/policy-spec.schema.json +23 -0
  13. package/schemas/context-pack.schema.json +83 -0
  14. package/schemas/fr-to-code-audit.schema.json +85 -0
  15. package/schemas/product/concept.schema.json +17 -0
  16. package/schemas/product/domain-interface.schema.json +18 -0
  17. package/schemas/product/domain.schema.json +24 -0
  18. package/schemas/product/evidence-anchor.schema.json +30 -0
  19. package/schemas/product/guarantee.schema.json +35 -0
  20. package/schemas/product/model.schema.json +20 -0
  21. package/schemas/product/relationship.schema.json +21 -0
  22. package/schemas/product/use-case.schema.json +27 -0
  23. package/scripts/audit-fr-to-code.mjs +162 -0
  24. package/scripts/check-generated-docs.mjs +58 -0
  25. package/scripts/check-generated-graph-svg.mjs +60 -0
  26. package/scripts/check-generated-graph.mjs +66 -0
  27. package/scripts/check-model.mjs +488 -0
  28. package/scripts/ddduck.mjs +542 -0
  29. package/scripts/generate-agent-readiness-report.mjs +23 -0
  30. package/scripts/generate-docs.mjs +205 -0
  31. package/scripts/generate-graph-svg.mjs +359 -0
  32. package/scripts/generate-graph.mjs +268 -0
  33. package/scripts/lib/agent-readiness-evals.mjs +433 -0
  34. package/scripts/lib/agent-readiness-report.mjs +79 -0
  35. package/scripts/lib/cli-contract.mjs +162 -0
  36. package/scripts/lib/context-pack.mjs +107 -0
  37. package/scripts/lib/ddduck-config.mjs +57 -0
  38. package/scripts/lib/fr-to-code-audit.mjs +144 -0
  39. package/scripts/lib/product-layout.mjs +93 -0
  40. package/scripts/lib/product-operation.mjs +431 -0
  41. package/scripts/lib/product-paths.mjs +43 -0
  42. package/scripts/lib/product-query.mjs +284 -0
  43. package/scripts/lib/product-root-resolver.mjs +167 -0
  44. package/scripts/lib/scan-ignore.mjs +8 -0
  45. package/scripts/lib/skill-installer.mjs +410 -0
  46. package/scripts/query-model.mjs +64 -0
  47. package/scripts/run-agent-readiness-evals.mjs +57 -0
  48. package/skills/update-ddduck-specs/SKILL.md +98 -0
@@ -0,0 +1,410 @@
1
+ import {
2
+ lstatSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ readdirSync,
6
+ readlinkSync,
7
+ renameSync,
8
+ rmSync,
9
+ symlinkSync,
10
+ writeFileSync,
11
+ } from "node:fs";
12
+ import { createHash, randomUUID } from "node:crypto";
13
+ import path from "node:path";
14
+
15
+ const lockSchemaVersion = 1;
16
+ const lockRelativePath = path.join(".ddduck", "agent-skills.lock.json");
17
+
18
+ const defaultOperations = {
19
+ lstatSync,
20
+ mkdirSync,
21
+ readFileSync,
22
+ readdirSync,
23
+ readlinkSync,
24
+ renameSync,
25
+ rmSync,
26
+ symlinkSync,
27
+ writeFileSync,
28
+ };
29
+
30
+ const codexSkillDirectory = path.join(".agents", "skills", "update-ddduck-specs");
31
+ const claudeSkillDirectory = path.join(".claude", "skills", "update-ddduck-specs");
32
+ const skillFileName = "SKILL.md";
33
+
34
+ const hostSkillAdapters = {
35
+ codex: {
36
+ host: "codex",
37
+ relativePath: codexSkillDirectory,
38
+ lockEntry() {
39
+ return { host: this.host, path: ".agents/skills/update-ddduck-specs" };
40
+ },
41
+ parentPaths(adapterPath) {
42
+ return [path.dirname(path.dirname(adapterPath)), path.dirname(adapterPath)];
43
+ },
44
+ inspect({ adapterPath, operations }) {
45
+ const state = pathState(adapterPath, operations);
46
+ if (state.type === "absent") return "absent";
47
+ return state.type === "directory" ? "valid" : "conflict";
48
+ },
49
+ materialize({ adapterPath, operations, touched }) {
50
+ touched.push(adapterPath);
51
+ operations.mkdirSync(adapterPath, { recursive: true });
52
+ },
53
+ },
54
+ claudeDirectory: {
55
+ host: "claude-code",
56
+ relativePath: claudeSkillDirectory,
57
+ lockEntry() {
58
+ return { host: this.host, path: ".claude/skills/update-ddduck-specs" };
59
+ },
60
+ parentPaths(adapterPath) {
61
+ return [path.dirname(path.dirname(adapterPath)), path.dirname(adapterPath)];
62
+ },
63
+ inspect({ adapterPath, operations }) {
64
+ const state = pathState(adapterPath, operations);
65
+ if (state.type === "absent") return "absent";
66
+ return state.type === "directory" ? "valid" : "conflict";
67
+ },
68
+ materialize({ adapterPath, operations, touched }) {
69
+ touched.push(adapterPath);
70
+ operations.mkdirSync(adapterPath, { recursive: true });
71
+ },
72
+ },
73
+ claudeSymlink: {
74
+ host: "claude-code",
75
+ relativePath: claudeSkillDirectory,
76
+ target: "../../.agents/skills/update-ddduck-specs",
77
+ lockEntry() {
78
+ return { host: this.host, path: ".claude/skills/update-ddduck-specs", target: this.target };
79
+ },
80
+ parentPaths(adapterPath) {
81
+ return [path.dirname(path.dirname(adapterPath)), path.dirname(adapterPath)];
82
+ },
83
+ inspect({ adapterPath, operations }) {
84
+ const state = pathState(adapterPath, operations);
85
+ if (state.type === "absent") return "absent";
86
+ if (state.type !== "symlink" || operations.readlinkSync(adapterPath) !== this.target) return "conflict";
87
+ return pathState(path.join(adapterPath, skillFileName), operations).type === "file" ? "valid" : "conflict";
88
+ },
89
+ materialize({ adapterPath, operations, touched }) {
90
+ touched.push(path.dirname(adapterPath), adapterPath);
91
+ operations.mkdirSync(path.dirname(adapterPath), { recursive: true });
92
+ operations.symlinkSync(this.target, adapterPath);
93
+ },
94
+ },
95
+ };
96
+
97
+ const installTopologies = [
98
+ {
99
+ id: "codex",
100
+ canonicalRelativePath: path.join(codexSkillDirectory, skillFileName),
101
+ adapters: [hostSkillAdapters.codex],
102
+ },
103
+ {
104
+ id: "claude-code",
105
+ canonicalRelativePath: path.join(claudeSkillDirectory, skillFileName),
106
+ adapters: [hostSkillAdapters.claudeDirectory],
107
+ },
108
+ {
109
+ id: "shared",
110
+ canonicalRelativePath: path.join(codexSkillDirectory, skillFileName),
111
+ adapters: [hostSkillAdapters.codex, hostSkillAdapters.claudeSymlink],
112
+ },
113
+ ];
114
+
115
+ export function installSkill(options) {
116
+ const bundle = loadSkillBundle(options);
117
+ const plan = planSkillInstall({ ...options, bundle });
118
+ return applySkillInstall({ ...options, bundle, plan });
119
+ }
120
+
121
+ export function loadSkillBundle({ skillName, skillPath, operations = {} }) {
122
+ const resolvedOperations = { ...defaultOperations, ...operations };
123
+ if (pathState(skillPath, resolvedOperations).type !== "file") {
124
+ throw new Error(`Missing bundled skill asset: ${skillPath}`);
125
+ }
126
+ const bytes = resolvedOperations.readFileSync(skillPath);
127
+ return { name: skillName, bytes, sha256: sha256(bytes) };
128
+ }
129
+
130
+ export function planSkillInstall({ repository, skillName, bundle, operations = {} }) {
131
+ const resolvedOperations = { ...defaultOperations, ...operations };
132
+ const root = path.resolve(repository);
133
+ const lockPath = path.join(root, lockRelativePath);
134
+ const lockState = readLock(lockPath, resolvedOperations);
135
+ const topology = selectTopology({ root, lockState, operations: resolvedOperations });
136
+ const paths = installationPaths(root, topology);
137
+ assertDirectoryParents(paths, topology, resolvedOperations);
138
+ const canonical = pathState(paths.canonical, resolvedOperations);
139
+ const adapters = inspectHostAdapters(paths, topology, resolvedOperations);
140
+ const conflictingAdapter = adapters.find(({ state }) => state === "conflict");
141
+
142
+ if (lockState.type === "invalid") throw incompleteLock(paths.lock);
143
+ if (conflictingAdapter) throw conflictingHostAdapter(conflictingAdapter, paths);
144
+
145
+ if (lockState.type === "absent") {
146
+ const canonicalDirectory = pathState(paths.canonicalDirectory, resolvedOperations);
147
+ if (
148
+ canonicalDirectory.type === "directory" &&
149
+ canonical.type === "absent" &&
150
+ resolvedOperations.readdirSync(paths.canonicalDirectory).length > 0
151
+ ) {
152
+ throw new Error(`Conflicting canonical skill directory: ${paths.canonicalDirectory}`);
153
+ }
154
+ if (canonical.type === "absent") {
155
+ return createPlan({ paths, adapters, writeCanonical: true });
156
+ }
157
+ if (canonical.type !== "file" || sha256(resolvedOperations.readFileSync(paths.canonical)) !== bundle.sha256) {
158
+ throw new Error(`Conflicting canonical skill destination: ${paths.canonical}`);
159
+ }
160
+ return createPlan({ paths, adapters, writeCanonical: false });
161
+ }
162
+
163
+ const lock = lockState.value;
164
+ if (!isValidLock(lock, skillName, topology)) throw incompleteLock(paths.lock);
165
+ if (canonical.type === "absent") return createPlan({ paths, adapters, writeCanonical: true });
166
+ if (canonical.type !== "file") throw locallyModifiedCanonical(paths.canonical);
167
+ if (sha256(resolvedOperations.readFileSync(paths.canonical)) !== lock.skillSha256) {
168
+ throw locallyModifiedCanonical(paths.canonical);
169
+ }
170
+ if (adapters.some(({ state }) => state !== "valid")) throw incompleteLock(paths.lock);
171
+
172
+ return {
173
+ action: lock.skillSha256 === bundle.sha256 ? "no-op" : "upgrade",
174
+ paths,
175
+ topology,
176
+ adaptersToMaterialize: [],
177
+ writeCanonical: lock.skillSha256 !== bundle.sha256,
178
+ };
179
+ }
180
+
181
+ export function applySkillInstall({ packageVersion, bundle, plan, operations = {} }) {
182
+ const resolvedOperations = { ...defaultOperations, ...operations };
183
+ if (plan.action === "no-op") return installResult("no-op", bundle, plan);
184
+
185
+ const touched = [];
186
+ const originalCanonical =
187
+ plan.writeCanonical && pathState(plan.paths.canonical, resolvedOperations).type === "file"
188
+ ? resolvedOperations.readFileSync(plan.paths.canonical)
189
+ : null;
190
+ let canonicalWritten = false;
191
+ const materializedAdapters = [];
192
+
193
+ try {
194
+ if (plan.writeCanonical) {
195
+ atomicWrite(plan.paths.canonical, bundle.bytes, resolvedOperations, touched);
196
+ canonicalWritten = true;
197
+ }
198
+ for (const adapter of plan.adaptersToMaterialize) {
199
+ adapter.materialize({
200
+ adapterPath: plan.paths.adapters[adapter.host],
201
+ operations: resolvedOperations,
202
+ touched,
203
+ });
204
+ materializedAdapters.push(adapter);
205
+ }
206
+
207
+ atomicWrite(
208
+ plan.paths.lock,
209
+ `${JSON.stringify(createLock({ packageVersion, bundle, topology: plan.topology }), null, 2)}\n`,
210
+ resolvedOperations,
211
+ touched,
212
+ );
213
+ return installResult(plan.action, bundle, plan);
214
+ } catch (error) {
215
+ const recoveryFailures = restoreAfterFailure({
216
+ plan,
217
+ originalCanonical,
218
+ canonicalWritten,
219
+ materializedAdapters,
220
+ operations: resolvedOperations,
221
+ touched,
222
+ });
223
+ const paths = [...new Set(touched)].join(", ") || "none";
224
+ const recovery = recoveryFailures.length === 0 ? "" : ` Recovery failures: ${recoveryFailures.join("; ")}.`;
225
+ throw new Error(`Skill installation failed after touching: ${paths}. ${error.message}.${recovery}`);
226
+ }
227
+ }
228
+
229
+ function installResult(action, bundle, plan) {
230
+ return {
231
+ action,
232
+ skillSha256: bundle.sha256,
233
+ canonicalPath: toPosixPath(plan.topology.canonicalRelativePath),
234
+ lockPath: toPosixPath(lockRelativePath),
235
+ };
236
+ }
237
+
238
+ function createPlan({ paths, adapters, writeCanonical }) {
239
+ return {
240
+ action: "create",
241
+ paths,
242
+ topology: paths.topology,
243
+ adaptersToMaterialize: adapters.filter(({ state }) => state === "absent").map(({ adapter }) => adapter),
244
+ writeCanonical,
245
+ };
246
+ }
247
+
248
+ function installationPaths(root, topology) {
249
+ const canonical = path.join(root, topology.canonicalRelativePath);
250
+ return {
251
+ canonical,
252
+ canonicalDirectory: path.dirname(canonical),
253
+ topology,
254
+ adapters: Object.fromEntries(
255
+ topology.adapters.map((adapter) => [adapter.host, path.join(root, adapter.relativePath)]),
256
+ ),
257
+ lock: path.join(root, lockRelativePath),
258
+ };
259
+ }
260
+
261
+ function inspectHostAdapters(paths, topology, operations) {
262
+ return topology.adapters.map((adapter) => ({
263
+ adapter,
264
+ state: adapter.inspect({ adapterPath: paths.adapters[adapter.host], operations }),
265
+ }));
266
+ }
267
+
268
+ function assertDirectoryParents(paths, topology, operations) {
269
+ const directories = [
270
+ ...topology.adapters.flatMap((adapter) => adapter.parentPaths(paths.adapters[adapter.host])),
271
+ path.dirname(paths.lock),
272
+ ];
273
+ for (const directory of new Set(directories)) {
274
+ const state = pathState(directory, operations);
275
+ if (!["absent", "directory"].includes(state.type)) {
276
+ throw new Error(`Conflicting skill installation path: ${directory}`);
277
+ }
278
+ }
279
+ }
280
+
281
+ function readLock(lockPath, operations) {
282
+ const state = pathState(lockPath, operations);
283
+ if (state.type === "absent") return { type: "absent" };
284
+ if (state.type !== "file") return { type: "invalid" };
285
+ try {
286
+ return { type: "valid", value: JSON.parse(operations.readFileSync(lockPath, "utf8")) };
287
+ } catch {
288
+ return { type: "invalid" };
289
+ }
290
+ }
291
+
292
+ function pathState(filePath, operations) {
293
+ try {
294
+ const stat = operations.lstatSync(filePath);
295
+ if (stat.isFile()) return { type: "file" };
296
+ if (stat.isDirectory()) return { type: "directory" };
297
+ if (stat.isSymbolicLink()) return { type: "symlink" };
298
+ return { type: "other" };
299
+ } catch (error) {
300
+ if (error.code === "ENOENT") return { type: "absent" };
301
+ throw error;
302
+ }
303
+ }
304
+
305
+ function selectTopology({ root, lockState, operations }) {
306
+ if (lockState.type === "valid") {
307
+ const topology = installTopologies.find(
308
+ (candidate) => toPosixPath(candidate.canonicalRelativePath) === toPosixPath(lockState.value.canonicalPath),
309
+ );
310
+ return topology ?? installTopologies[0];
311
+ }
312
+
313
+ const agents = pathState(path.join(root, ".agents"), operations).type;
314
+ const claude = pathState(path.join(root, ".claude"), operations).type;
315
+ if (agents === "directory" && claude === "directory") return installTopologies.find(({ id }) => id === "shared");
316
+ if (claude === "directory") return installTopologies.find(({ id }) => id === "claude-code");
317
+ return installTopologies.find(({ id }) => id === "codex");
318
+ }
319
+
320
+ function isValidLock(lock, skillName, topology) {
321
+ return (
322
+ lock &&
323
+ lock.schemaVersion === lockSchemaVersion &&
324
+ lock.skill === skillName &&
325
+ typeof lock.ddduckVersion === "string" &&
326
+ lock.ddduckVersion.length > 0 &&
327
+ toPosixPath(lock.canonicalPath) === toPosixPath(topology.canonicalRelativePath) &&
328
+ /^[a-f0-9]{64}$/.test(lock.skillSha256) &&
329
+ JSON.stringify(lock.adapters) === JSON.stringify(expectedAdapters(topology))
330
+ );
331
+ }
332
+
333
+ function createLock({ packageVersion, bundle, topology }) {
334
+ return {
335
+ schemaVersion: lockSchemaVersion,
336
+ skill: bundle.name,
337
+ ddduckVersion: packageVersion,
338
+ canonicalPath: toPosixPath(topology.canonicalRelativePath),
339
+ skillSha256: bundle.sha256,
340
+ adapters: expectedAdapters(topology),
341
+ };
342
+ }
343
+
344
+ function expectedAdapters(topology) {
345
+ return topology.adapters.map((adapter) => adapter.lockEntry());
346
+ }
347
+
348
+ function conflictingHostAdapter({ adapter }, paths) {
349
+ const label = adapter.host === "claude-code" ? "Claude Code" : adapter.host;
350
+ return new Error(`Conflicting ${label} adapter: ${paths.adapters[adapter.host]}`);
351
+ }
352
+
353
+ function atomicWrite(destination, content, operations, touched) {
354
+ const directory = path.dirname(destination);
355
+ const temporary = path.join(directory, `.${path.basename(destination)}.${randomUUID()}.tmp`);
356
+ touched.push(directory, temporary, destination);
357
+ try {
358
+ operations.mkdirSync(directory, { recursive: true });
359
+ operations.writeFileSync(temporary, content);
360
+ operations.renameSync(temporary, destination);
361
+ } finally {
362
+ if (pathState(temporary, operations).type !== "absent") operations.rmSync(temporary, { force: true });
363
+ }
364
+ }
365
+
366
+ function restoreAfterFailure({ plan, originalCanonical, canonicalWritten, materializedAdapters, operations, touched }) {
367
+ const failures = [];
368
+ for (const adapter of materializedAdapters.toReversed()) {
369
+ try {
370
+ const adapterPath = plan.paths.adapters[adapter.host];
371
+ touched.push(adapterPath);
372
+ if (pathState(adapterPath, operations).type !== "absent") {
373
+ operations.rmSync(adapterPath, { recursive: true, force: true });
374
+ }
375
+ } catch (error) {
376
+ failures.push(`remove ${adapter.host} adapter: ${error.message}`);
377
+ }
378
+ }
379
+ if (!canonicalWritten) return failures;
380
+
381
+ try {
382
+ if (originalCanonical) {
383
+ atomicWrite(plan.paths.canonical, originalCanonical, operations, touched);
384
+ } else {
385
+ touched.push(plan.paths.canonical);
386
+ operations.rmSync(plan.paths.canonical, { force: true });
387
+ }
388
+ } catch (error) {
389
+ failures.push(`restore canonical skill: ${error.message}`);
390
+ }
391
+ return failures;
392
+ }
393
+
394
+ function incompleteLock(lockPath) {
395
+ return new Error(`Incomplete or inconsistent skill lock: ${lockPath}`);
396
+ }
397
+
398
+ function locallyModifiedCanonical(canonicalPath) {
399
+ const error = new Error(`Locally modified canonical skill: ${canonicalPath}`);
400
+ error.nextAction = `Revert or remove ${canonicalPath}, then re-run ddduck install skill update-ddduck-specs.`;
401
+ return error;
402
+ }
403
+
404
+ function sha256(bytes) {
405
+ return createHash("sha256").update(bytes).digest("hex");
406
+ }
407
+
408
+ function toPosixPath(value) {
409
+ return String(value).replaceAll("\\", "/");
410
+ }
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { resolveContextPack } from "./lib/context-pack.mjs";
6
+ import { resolveProductRoot } from "./lib/product-root-resolver.mjs";
7
+ import {
8
+ loadQueryProduct,
9
+ queryAnchors,
10
+ queryImpact,
11
+ queryNeighbors,
12
+ queryNode,
13
+ querySpec,
14
+ } from "./lib/product-query.mjs";
15
+ import { CliUsageError, parseCommandArgs, renderHelp, writeCliError } from "./lib/cli-contract.mjs";
16
+
17
+ export function runQuery(args, { cwd = process.cwd(), stdout = process.stdout } = {}) {
18
+ if (args.includes("--help")) {
19
+ stdout.write(renderHelp("query"));
20
+ return;
21
+ }
22
+ const { positionals, options } = parseCommandArgs(args, {
23
+ positionals: { min: 1, max: 1 },
24
+ options: { id: { value: true, repeatable: true }, root: { value: true }, history: {}, json: {} },
25
+ });
26
+ const [operation] = positionals;
27
+ if (!operation || !["node", "neighbors", "impact", "anchors", "spec", "context"].includes(operation)) {
28
+ throw new CliUsageError("query requires operation node, neighbors, impact, anchors, spec, or context");
29
+ }
30
+ const ids = options.id;
31
+ const id = ids[0];
32
+ if (operation !== "context" && ids.length > 1) throw new CliUsageError(`query ${operation} accepts exactly one --id`);
33
+ if (operation === "context") {
34
+ if (options.history) throw new CliUsageError("query context does not support --history");
35
+ if (!options.root) throw new CliUsageError("query context requires --root <product-root>");
36
+ }
37
+ if (operation !== "spec" && !id) throw new CliUsageError(`query ${operation} requires --id <model-node-id>`);
38
+
39
+ const root =
40
+ operation === "context" ? path.resolve(cwd, options.root) : resolveProductRoot({ cwd, explicitRoot: options.root });
41
+ const product = loadQueryProduct(root, { history: options.history });
42
+ if (operation === "spec" && id && id !== product.rootModelId) {
43
+ throw new CliUsageError(`query spec --id must be ${product.rootModelId}`);
44
+ }
45
+ const document = {
46
+ node: () => queryNode(product, id, { history: options.history }),
47
+ neighbors: () => queryNeighbors(product, id, { history: options.history }),
48
+ impact: () => queryImpact(product, id, { history: options.history }),
49
+ anchors: () => queryAnchors(product, id, { history: options.history }),
50
+ spec: () => querySpec(product, { history: options.history }),
51
+ context: () => resolveContextPack(product, ids),
52
+ }[operation]();
53
+ stdout.write(`${JSON.stringify(document)}\n`);
54
+ }
55
+
56
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
57
+ try {
58
+ runQuery(process.argv.slice(2));
59
+ } catch (error) {
60
+ writeCliError(error, {
61
+ nextAction: "Run ddduck query --help, correct the input, and retry.",
62
+ });
63
+ }
64
+ }
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { mkdirSync, writeFileSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { format } from "prettier";
6
+ import { runAgentReadinessEvals } from "./lib/agent-readiness-evals.mjs";
7
+ import { resolveContainedOutput } from "./lib/product-paths.mjs";
8
+
9
+ try {
10
+ const options = parseArgs(process.argv.slice(2));
11
+ const outputTarget = options.output ? resolveOutputPath(options.repoRoot, options.output) : null;
12
+ const result = runAgentReadinessEvals(options);
13
+ const output = await format(JSON.stringify(result), { parser: "json", printWidth: 120 });
14
+ process.stdout.write(output);
15
+ if (outputTarget) writeOutput(outputTarget, output);
16
+ process.exitCode = result.passed ? 0 : 1;
17
+ } catch (error) {
18
+ process.stderr.write(`${error.message}\n`);
19
+ process.exitCode = 1;
20
+ }
21
+
22
+ function parseArgs(args) {
23
+ const options = {};
24
+ for (let index = 0; index < args.length; index += 1) {
25
+ const argument = args[index];
26
+ if (!["--input", "--repo-root", "--output"].includes(argument)) {
27
+ throw new Error(
28
+ "usage: node scripts/run-agent-readiness-evals.mjs --input <jsonl> --repo-root <path> [--output <json>]",
29
+ );
30
+ }
31
+ if (Object.hasOwn(options, argument)) throw new Error(`duplicate CLI argument: ${argument}`);
32
+ const value = args[index + 1];
33
+ if (!value || value.startsWith("--")) throw new Error(`missing value for ${argument}`);
34
+ options[argument] = value;
35
+ index += 1;
36
+ }
37
+ if (!options["--input"] || !options["--repo-root"]) {
38
+ throw new Error(
39
+ "usage: node scripts/run-agent-readiness-evals.mjs --input <jsonl> --repo-root <path> [--output <json>]",
40
+ );
41
+ }
42
+ return {
43
+ inputPath: path.resolve(options["--input"]),
44
+ repoRoot: path.resolve(options["--repo-root"]),
45
+ output: options["--output"],
46
+ };
47
+ }
48
+
49
+ function resolveOutputPath(repoRoot, outputPath) {
50
+ if (outputPath.split(/[\\/]/)[0] === ".git") throw new Error("output path must not target .git");
51
+ return resolveContainedOutput(repoRoot, outputPath);
52
+ }
53
+
54
+ function writeOutput(target, output) {
55
+ mkdirSync(path.dirname(target), { recursive: true });
56
+ writeFileSync(target, output);
57
+ }
@@ -0,0 +1,98 @@
1
+ ---
2
+ name: update-ddduck-specs
3
+ description: Use when a repository's ddduck product model needs to be created, audited against current code, tests, and documentation, or reconciled after product changes.
4
+ ---
5
+
6
+ # Update ddduck Specs
7
+
8
+ Maintain or bootstrap a repository's ddduck product model from evidence visible in the current working tree.
9
+
10
+ ## Inputs and boundaries
11
+
12
+ - Resolve the repository root, read all applicable repository instructions, and inspect Git status before analysis.
13
+ - Use an explicitly requested product root; otherwise let ddduck resolve it in its own order: the enclosing product root of the current directory, else the `productRoot` in `.ddduck/config.json`, else the unique discovered product root in the repository. `ddduck query spec` reports the resolved root. Ambiguous resolution is a stop condition: report the candidates and ask; never bootstrap a second product root beside an existing one.
14
+ - Default to plan-only. Mutate files only when the current user request explicitly authorizes application, including prose that clearly authorizes the evidence-backed changes. `--root <path>` and `--apply` may be convenient shorthand, but ordinary prose must work.
15
+ - Preserve unrelated and uncommitted work. Stop when intended target files overlap user changes inseparably.
16
+ - Write only `<root>/product.yaml`, `<root>/model/**`, `<root>/decisions/**`, and regenerated `<root>/generated/**`.
17
+ - Never edit generated views directly. Regenerate them with ddduck.
18
+ - Use only evidence visible in the current working tree. Do not rely on prior chat, cursor, cache, or an assumed previous revision.
19
+ - Preserve stable IDs and Guarantee lifecycle. Never silently delete or reuse a Guarantee ID. Use ddduck lifecycle commands where they cover the mutation.
20
+ - Never create an ADR merely to satisfy validation or justify an inferred change.
21
+ - Keep unresolved questions out of canonical model facts.
22
+ - Prefer the smallest coherent product-model change. Avoid ornamental DDD vocabulary and speculative structure.
23
+ - Do not commit or push consumer changes unless the user separately requests it.
24
+
25
+ Use a repository-compatible ddduck executable. Do not install dependencies or silently fall back to an unrelated global version.
26
+
27
+ ## Establish the baseline
28
+
29
+ Classify the selected root exactly once:
30
+
31
+ - `existing`: `product.yaml` exists. Run `ddduck query spec --root <root> --json` and `ddduck check --root <root>`, recording both outcomes independently. A failing existing model is invalid, not absent, and must not be reinitialized.
32
+ - `absent`: the root is missing or empty. Inspect the repository before proposing initialization.
33
+ - `path-collision`: the root is non-empty but not a recognizable ddduck product. Report the collision and never initialize over it.
34
+
35
+ Stop before mutation when the ddduck executable is missing or incompatible, multiple product roots are plausible and none was selected, the selected root is a non-empty path collision, bootstrap identity, purpose, or initial domain seams are not grounded, evidence conflicts materially change the proposed model, target model files overlap inseparable user changes, or the analyzed working-tree state changed before application.
36
+
37
+ ## Gather evidence
38
+
39
+ Inspect relevant current code, tests, public interfaces, documentation, configuration, schemas, workspace structure, and accepted decisions. Record material exclusions and coverage gaps.
40
+
41
+ For every candidate fact, record:
42
+
43
+ - proposed model assertion;
44
+ - repository-relative path plus line, symbol, heading, or test name;
45
+ - evidence role: implementation, verification, documentation, decision, or configuration;
46
+ - contradictory evidence;
47
+ - inspected scope and remaining unknowns.
48
+
49
+ Executable behavior and passing tests establish observed behavior. Accepted requirements and decisions establish intended behavior. Treat conflicts between them as inconsistencies; do not silently encode either a possible bug or an unimplemented requirement as product truth.
50
+
51
+ The current schema restricts persisted interface evidence anchors to paths inside the product root. Cite repository-wide evidence in the plan and final report, but persist only schema-supported product-root anchors. Do not copy source evidence into the product root, invent unsupported metadata, or create bridge documents merely to manufacture provenance.
52
+
53
+ For a greenfield repository, use runtime-supported subagents only when no model exists and the relevant corpus spans several substantial, independent packages, applications, or domain areas that cannot be covered reliably in the coordinating context. Repository file count alone is not sufficient. Subagents are read-only evidence adapters: assign non-overlapping scopes, provide applicable repository instructions, forbid writes and canonical model synthesis, require candidate facts with exact evidence locations, conflicts, unknowns, and coverage, then re-read decisive evidence before adopting it. The coordinator is the sole writer. If subagents are unavailable, inspect the same scopes sequentially and disclose the coverage limitations.
54
+
55
+ ## Compare and classify
56
+
57
+ Classify every material difference as exactly one of:
58
+
59
+ - verified omission;
60
+ - stale modeled fact;
61
+ - structural inconsistency with an unambiguous repair;
62
+ - contradiction or uncertainty requiring a human decision;
63
+ - irrelevant implementation detail;
64
+ - insufficiently covered.
65
+
66
+ For existing models, preserve identity and history.
67
+
68
+ ## Plan-only workflow
69
+
70
+ Before any mutation, report in this order:
71
+
72
+ 1. Mode, resolved root, and model state.
73
+ 2. Baseline query and validation status.
74
+ 3. Inspected coverage, exclusions, and gaps.
75
+ 4. Proposed changes with classification, concrete evidence, and exact target files.
76
+ 5. Contradictions, uncertainties, and required decisions.
77
+ 6. Exact generation and verification commands.
78
+
79
+ Without explicit application authorization, stop before all writes. Plan mode performs no writes, including initialization and generation.
80
+
81
+ ## Apply workflow
82
+
83
+ When application is explicitly authorized:
84
+
85
+ 1. Recheck Git status, intended target files, and decisive evidence.
86
+ 2. Initialize only an absent or empty root whose model identity, purpose, and initial domain seams are explicit or unambiguously grounded. Otherwise request the missing decision.
87
+ 3. Apply only planned, evidence-backed changes whose meaning is unambiguous.
88
+ 4. Leave unresolved findings unchanged.
89
+ 5. Use ddduck lifecycle commands for Guarantee transitions. Author other canonical YAML against installed schemas and existing model conventions.
90
+ 6. Run `ddduck check --root <root> --source-only` before generation; hand-authored canonical edits legitimately leave generated views stale until step 7.
91
+ 7. Run `ddduck generate --root <root>`.
92
+ 8. Run `ddduck check --root <root>` again.
93
+ 9. Run `ddduck query spec --root <root> --json` and require every generated view to be fresh.
94
+ 10. Inspect the final diff for scope.
95
+
96
+ Any command failure makes the result incomplete. Inspect and report the resulting working tree; never destructively roll back unrelated user work.
97
+
98
+ Report changed canonical, decision, and generated files; evidence supporting each material change; skipped and unresolved findings; exact command outcomes and diagnostics; final generated-view freshness; and remaining coverage gaps. Explicitly report `incomplete` when any verification or scope check fails. A verified no-op is a valid result; do not create model content merely to demonstrate activity.