ddduck 0.1.0 → 0.1.2
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/README.md +8 -5
- package/docs/architecture.md +5 -2
- package/docs/cli.md +73 -52
- package/docs/getting-started.md +6 -3
- package/docs/model-reference.md +4 -0
- package/docs/model.md +1 -0
- package/package.json +5 -4
- package/scripts/audit-fr-to-code.mjs +25 -0
- package/scripts/check-generated-docs.mjs +24 -5
- package/scripts/check-generated-graph-svg.mjs +26 -8
- package/scripts/check-generated-graph.mjs +25 -5
- package/scripts/check-model.mjs +95 -5
- package/scripts/ddduck.mjs +166 -21
- package/scripts/generate-agent-readiness-report.mjs +8 -0
- package/scripts/generate-docs.mjs +28 -2
- package/scripts/generate-graph-svg.mjs +53 -16
- package/scripts/generate-graph.mjs +26 -1
- package/scripts/lib/agent-readiness-evals.mjs +32 -0
- package/scripts/lib/agent-readiness-report.mjs +14 -0
- package/scripts/lib/cli-contract.mjs +49 -10
- package/scripts/lib/context-pack.mjs +49 -1
- package/scripts/lib/ddduck-config.mjs +31 -1
- package/scripts/lib/fr-to-code-audit.mjs +24 -0
- package/scripts/lib/product-layout.mjs +42 -1
- package/scripts/lib/product-operation.mjs +132 -22
- package/scripts/lib/product-paths.mjs +16 -0
- package/scripts/lib/product-query.mjs +102 -20
- package/scripts/lib/product-root-resolver.mjs +40 -0
- package/scripts/lib/scan-ignore.mjs +14 -3
- package/scripts/lib/skill-installer.mjs +57 -4
- package/scripts/query-model.mjs +18 -7
- package/scripts/run-agent-readiness-evals.mjs +18 -2
- package/skills/update-ddduck-specs/SKILL.md +1 -1
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installer for the bundled update-ddduck-specs agent skill, behind `ddduck
|
|
3
|
+
* install skill`. Selects a host topology from the repository state (codex
|
|
4
|
+
* .agents/, claude-code .claude/, or shared via symlink), plans create,
|
|
5
|
+
* upgrade, or no-op against the canonical SKILL.md and the
|
|
6
|
+
* .ddduck/agent-skills.lock.json lock, and applies the plan with atomic
|
|
7
|
+
* writes plus rollback of everything touched on failure. Conflicting host
|
|
8
|
+
* state or a locally modified canonical file refuses with a nextAction
|
|
9
|
+
* naming the exact path to resolve.
|
|
10
|
+
*/
|
|
11
|
+
|
|
1
12
|
import {
|
|
2
13
|
lstatSync,
|
|
3
14
|
mkdirSync,
|
|
@@ -112,12 +123,22 @@ const installTopologies = [
|
|
|
112
123
|
},
|
|
113
124
|
];
|
|
114
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Install (or upgrade) the bundled skill into a repository: load, plan, apply.
|
|
128
|
+
* @param {{repository: string, skillName: string, skillPath: string, packageVersion: string, operations?: object}} options - Repository root, skill identity, bundled asset path, and ddduck version for the lock.
|
|
129
|
+
* @returns {{action: "create"|"upgrade"|"no-op", skillSha256: string, canonicalPath: string, lockPath: string}} The installation result.
|
|
130
|
+
*/
|
|
115
131
|
export function installSkill(options) {
|
|
116
132
|
const bundle = loadSkillBundle(options);
|
|
117
133
|
const plan = planSkillInstall({ ...options, bundle });
|
|
118
134
|
return applySkillInstall({ ...options, bundle, plan });
|
|
119
135
|
}
|
|
120
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Read the bundled skill asset and compute its sha256.
|
|
139
|
+
* @param {{skillName: string, skillPath: string, operations?: object}} options - Skill name, SKILL.md path, and fs overrides for tests.
|
|
140
|
+
* @returns {{name: string, bytes: Buffer, sha256: string}} The loaded bundle.
|
|
141
|
+
*/
|
|
121
142
|
export function loadSkillBundle({ skillName, skillPath, operations = {} }) {
|
|
122
143
|
const resolvedOperations = { ...defaultOperations, ...operations };
|
|
123
144
|
if (pathState(skillPath, resolvedOperations).type !== "file") {
|
|
@@ -127,6 +148,13 @@ export function loadSkillBundle({ skillName, skillPath, operations = {} }) {
|
|
|
127
148
|
return { name: skillName, bytes, sha256: sha256(bytes) };
|
|
128
149
|
}
|
|
129
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Inspect the repository's lock, canonical file, and host adapters and decide
|
|
153
|
+
* the action: create, upgrade, or no-op — or throw on conflicting host state,
|
|
154
|
+
* an incomplete lock, or a locally modified canonical skill.
|
|
155
|
+
* @param {{repository: string, skillName: string, bundle: {sha256: string}, operations?: object}} options - Repository root, skill name, loaded bundle, and fs overrides.
|
|
156
|
+
* @returns {{action: string, paths: object, topology: object, adaptersToMaterialize: object[], writeCanonical: boolean}} The install plan for applySkillInstall.
|
|
157
|
+
*/
|
|
130
158
|
export function planSkillInstall({ repository, skillName, bundle, operations = {} }) {
|
|
131
159
|
const resolvedOperations = { ...defaultOperations, ...operations };
|
|
132
160
|
const root = path.resolve(repository);
|
|
@@ -149,13 +177,16 @@ export function planSkillInstall({ repository, skillName, bundle, operations = {
|
|
|
149
177
|
canonical.type === "absent" &&
|
|
150
178
|
resolvedOperations.readdirSync(paths.canonicalDirectory).length > 0
|
|
151
179
|
) {
|
|
152
|
-
throw
|
|
180
|
+
throw conflictingHostState(
|
|
181
|
+
`Conflicting canonical skill directory: ${paths.canonicalDirectory}`,
|
|
182
|
+
paths.canonicalDirectory,
|
|
183
|
+
);
|
|
153
184
|
}
|
|
154
185
|
if (canonical.type === "absent") {
|
|
155
186
|
return createPlan({ paths, adapters, writeCanonical: true });
|
|
156
187
|
}
|
|
157
188
|
if (canonical.type !== "file" || sha256(resolvedOperations.readFileSync(paths.canonical)) !== bundle.sha256) {
|
|
158
|
-
throw
|
|
189
|
+
throw conflictingHostState(`Conflicting canonical skill destination: ${paths.canonical}`, paths.canonical);
|
|
159
190
|
}
|
|
160
191
|
return createPlan({ paths, adapters, writeCanonical: false });
|
|
161
192
|
}
|
|
@@ -178,6 +209,13 @@ export function planSkillInstall({ repository, skillName, bundle, operations = {
|
|
|
178
209
|
};
|
|
179
210
|
}
|
|
180
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Execute an install plan: atomically write the canonical skill, materialize
|
|
214
|
+
* host adapters, and write the lock; on failure roll back everything touched
|
|
215
|
+
* and report any recovery failures in the thrown error.
|
|
216
|
+
* @param {{packageVersion: string, bundle: {name: string, bytes: Buffer, sha256: string}, plan: object, operations?: object}} options - ddduck version for the lock, loaded bundle, plan from planSkillInstall, and fs overrides.
|
|
217
|
+
* @returns {{action: string, skillSha256: string, canonicalPath: string, lockPath: string}} The installation result.
|
|
218
|
+
*/
|
|
181
219
|
export function applySkillInstall({ packageVersion, bundle, plan, operations = {} }) {
|
|
182
220
|
const resolvedOperations = { ...defaultOperations, ...operations };
|
|
183
221
|
if (plan.action === "no-op") return installResult("no-op", bundle, plan);
|
|
@@ -347,7 +385,18 @@ function expectedAdapters(topology) {
|
|
|
347
385
|
|
|
348
386
|
function conflictingHostAdapter({ adapter }, paths) {
|
|
349
387
|
const label = adapter.host === "claude-code" ? "Claude Code" : adapter.host;
|
|
350
|
-
return
|
|
388
|
+
return conflictingHostState(
|
|
389
|
+
`Conflicting ${label} adapter: ${paths.adapters[adapter.host]}`,
|
|
390
|
+
paths.adapters[adapter.host],
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Host-state conflicts are environment failures, not input failures: name the
|
|
395
|
+
// pre-existing path the user must resolve instead of the usage hint.
|
|
396
|
+
function conflictingHostState(message, conflictingPath) {
|
|
397
|
+
const error = new Error(message);
|
|
398
|
+
error.nextAction = `Move ${conflictingPath} aside or remove it, then re-run ddduck install skill update-ddduck-specs.`;
|
|
399
|
+
return error;
|
|
351
400
|
}
|
|
352
401
|
|
|
353
402
|
function atomicWrite(destination, content, operations, touched) {
|
|
@@ -392,7 +441,11 @@ function restoreAfterFailure({ plan, originalCanonical, canonicalWritten, materi
|
|
|
392
441
|
}
|
|
393
442
|
|
|
394
443
|
function incompleteLock(lockPath) {
|
|
395
|
-
|
|
444
|
+
const error = new Error(`Incomplete or inconsistent skill lock: ${lockPath}`);
|
|
445
|
+
// Deleting the lock is safe: the next install rebuilds it from the repository
|
|
446
|
+
// state, and any canonical mismatch then surfaces as its own conflict.
|
|
447
|
+
error.nextAction = `Delete ${lockPath}, then re-run ddduck install skill update-ddduck-specs to rebuild it.`;
|
|
448
|
+
return error;
|
|
396
449
|
}
|
|
397
450
|
|
|
398
451
|
function locallyModifiedCanonical(canonicalPath) {
|
package/scripts/query-model.mjs
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Implements `ddduck query`, the read-only query surface: dispatches the
|
|
5
|
+
* operations node, neighbors, impact, anchors, spec, and context to
|
|
6
|
+
* lib/product-query.mjs and lib/context-pack.mjs, emitting exactly one JSON
|
|
7
|
+
* document on stdout. The product root auto-resolves (enclosing directory,
|
|
8
|
+
* config, or unique discovery) unless --root is passed; busy or interrupted
|
|
9
|
+
* roots are refused before any answer is served. Also imported by ddduck.mjs
|
|
10
|
+
* and the agent-readiness evals as the runQuery entry point.
|
|
11
|
+
*/
|
|
12
|
+
|
|
4
13
|
import { fileURLToPath } from "node:url";
|
|
5
14
|
import { resolveContextPack } from "./lib/context-pack.mjs";
|
|
6
15
|
import { resolveProductRoot } from "./lib/product-root-resolver.mjs";
|
|
@@ -14,6 +23,12 @@ import {
|
|
|
14
23
|
} from "./lib/product-query.mjs";
|
|
15
24
|
import { CliUsageError, parseCommandArgs, renderHelp, writeCliError } from "./lib/cli-contract.mjs";
|
|
16
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Parse and run one query invocation, writing the JSON document to stdout.
|
|
28
|
+
* @param {string[]} args - Arguments after the `query` command word.
|
|
29
|
+
* @param {{cwd?: string, stdout?: {write: (chunk: string) => unknown}}} [io] - Working directory for root resolution and the output stream (used by the evals harness to capture output).
|
|
30
|
+
* @returns {void}
|
|
31
|
+
*/
|
|
17
32
|
export function runQuery(args, { cwd = process.cwd(), stdout = process.stdout } = {}) {
|
|
18
33
|
if (args.includes("--help")) {
|
|
19
34
|
stdout.write(renderHelp("query"));
|
|
@@ -30,14 +45,10 @@ export function runQuery(args, { cwd = process.cwd(), stdout = process.stdout }
|
|
|
30
45
|
const ids = options.id;
|
|
31
46
|
const id = ids[0];
|
|
32
47
|
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
|
-
}
|
|
48
|
+
if (operation === "context" && options.history) throw new CliUsageError("query context does not support --history");
|
|
37
49
|
if (operation !== "spec" && !id) throw new CliUsageError(`query ${operation} requires --id <model-node-id>`);
|
|
38
50
|
|
|
39
|
-
const root =
|
|
40
|
-
operation === "context" ? path.resolve(cwd, options.root) : resolveProductRoot({ cwd, explicitRoot: options.root });
|
|
51
|
+
const root = resolveProductRoot({ cwd, explicitRoot: options.root });
|
|
41
52
|
const product = loadQueryProduct(root, { history: options.history });
|
|
42
53
|
if (operation === "spec" && id && id !== product.rootModelId) {
|
|
43
54
|
throw new CliUsageError(`query spec --id must be ${product.rootModelId}`);
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* CLI wrapper for the agent-readiness eval harness: reads a JSONL file of eval
|
|
5
|
+
* records (--input) that each name a product root below --repo-root, a query
|
|
6
|
+
* to run, and candidate evidence to verify, then delegates to
|
|
7
|
+
* lib/agent-readiness-evals.mjs. Emits one JSON result document on stdout,
|
|
8
|
+
* optionally mirrors it to --output (contained below the repo root, never
|
|
9
|
+
* .git), and exits 1 unless every case passed.
|
|
10
|
+
*/
|
|
11
|
+
|
|
3
12
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
4
13
|
import path from "node:path";
|
|
5
|
-
import { format } from "prettier";
|
|
6
14
|
import { runAgentReadinessEvals } from "./lib/agent-readiness-evals.mjs";
|
|
7
15
|
import { resolveContainedOutput } from "./lib/product-paths.mjs";
|
|
8
16
|
|
|
@@ -10,7 +18,9 @@ try {
|
|
|
10
18
|
const options = parseArgs(process.argv.slice(2));
|
|
11
19
|
const outputTarget = options.output ? resolveOutputPath(options.repoRoot, options.output) : null;
|
|
12
20
|
const result = runAgentReadinessEvals(options);
|
|
13
|
-
|
|
21
|
+
// Deterministic serialization with no runtime formatter dependency: the
|
|
22
|
+
// published package must run without devDependencies.
|
|
23
|
+
const output = `${JSON.stringify(result, null, 2)}\n`;
|
|
14
24
|
process.stdout.write(output);
|
|
15
25
|
if (outputTarget) writeOutput(outputTarget, output);
|
|
16
26
|
process.exitCode = result.passed ? 0 : 1;
|
|
@@ -19,6 +29,12 @@ try {
|
|
|
19
29
|
process.exitCode = 1;
|
|
20
30
|
}
|
|
21
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Parse the --input/--repo-root/--output arguments, rejecting duplicates and
|
|
34
|
+
* missing values.
|
|
35
|
+
* @param {string[]} args - Raw CLI arguments.
|
|
36
|
+
* @returns {{inputPath: string, repoRoot: string, output: string|undefined}} Resolved option paths.
|
|
37
|
+
*/
|
|
22
38
|
function parseArgs(args) {
|
|
23
39
|
const options = {};
|
|
24
40
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -10,7 +10,7 @@ Maintain or bootstrap a repository's ddduck product model from evidence visible
|
|
|
10
10
|
## Inputs and boundaries
|
|
11
11
|
|
|
12
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.
|
|
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 (the ddduck CLI reference documents the full order, including the example-candidate fallback). `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
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
15
|
- Preserve unrelated and uncommitted work. Stop when intended target files overlap user changes inseparably.
|
|
16
16
|
- Write only `<root>/product.yaml`, `<root>/model/**`, `<root>/decisions/**`, and regenerated `<root>/generated/**`.
|