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,162 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from "node:child_process";
4
+ import { readFileSync } from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+ import { parseDocument } from "yaml";
7
+ import { verifyFrToCodeAudit } from "./lib/fr-to-code-audit.mjs";
8
+ import { CliUsageError, parseCommandArgs, renderHelp, writeCliError } from "./lib/cli-contract.mjs";
9
+
10
+ export function runFrToCodeAudit(args, { stdout = process.stdout } = {}) {
11
+ if (args.length === 1 && args[0] === "--help") {
12
+ stdout.write(renderHelp("audit-fr-to-code"));
13
+ return;
14
+ }
15
+ const { options } = parseCommandArgs(args, {
16
+ options: { input: { value: true }, "source-root": { value: true, repeatable: true }, json: {} },
17
+ });
18
+ options.sourceRoots = options["source-root"].map(parseSourceRoot);
19
+ if (!options.json) throw new CliUsageError("audit-fr-to-code requires --json");
20
+ if (!options.input) throw new CliUsageError("audit-fr-to-code requires --input <audit.yaml>");
21
+
22
+ const record = readAuditRecord(options.input);
23
+ const sourceReader = createGitSourceReader(record, options.sourceRoots);
24
+ const report = verifyFrToCodeAudit(record, sourceReader);
25
+ stdout.write(`${JSON.stringify(report)}\n`);
26
+ }
27
+
28
+ function parseSourceRoot(value) {
29
+ const separator = value.indexOf("=");
30
+ if (separator <= 0 || separator === value.length - 1) {
31
+ throw new Error("--source-root requires <source-id>=<checkout>");
32
+ }
33
+ return { id: value.slice(0, separator), root: value.slice(separator + 1) };
34
+ }
35
+
36
+ function readAuditRecord(filePath) {
37
+ const document = parseDocument(readFileSync(filePath, "utf8"), { strict: true, uniqueKeys: true });
38
+ if (document.errors.length > 0) {
39
+ throw new Error(`invalid YAML in ${filePath}: ${document.errors.map((error) => error.message).join("; ")}`);
40
+ }
41
+ const record = document.toJSON();
42
+ if (!isPlainObject(record)) throw new Error(`audit input must contain a YAML mapping: ${filePath}`);
43
+ validateDeclaredSources(record, filePath);
44
+ return record;
45
+ }
46
+
47
+ function validateDeclaredSources(record, filePath) {
48
+ if (!Array.isArray(record.sources) || record.sources.length === 0) {
49
+ throw new Error(`audit input must declare sources: ${filePath}`);
50
+ }
51
+ for (const source of record.sources) {
52
+ if (!isPlainObject(source) || typeof source.id !== "string" || source.id.length === 0) {
53
+ throw new Error(`audit source id must be a non-empty string: ${filePath}`);
54
+ }
55
+ if (typeof source.repository !== "string" || source.repository.length === 0) {
56
+ throw new Error(`audit source repository must be a non-empty string for ${source.id}`);
57
+ }
58
+ if (typeof source.revision !== "string" || !/^[0-9a-f]{40}$/.test(source.revision)) {
59
+ throw new Error(`audit source revision must be a 40-character lowercase hex commit ID for ${source.id}`);
60
+ }
61
+ }
62
+ }
63
+
64
+ function createGitSourceReader(record, sourceRoots) {
65
+ if (!Array.isArray(record.sources)) throw new Error("audit input must declare sources");
66
+ const declaredIds = new Set(record.sources.map((source) => source?.id));
67
+ for (const { id } of sourceRoots) {
68
+ if (!declaredIds.has(id)) throw new Error(`--source-root maps undeclared source ID ${id}`);
69
+ }
70
+
71
+ const rootsById = new Map();
72
+ for (const source of record.sources) {
73
+ const matches = sourceRoots.filter(({ id }) => id === source.id);
74
+ if (matches.length !== 1) {
75
+ throw new Error(`audit-fr-to-code requires exactly one --source-root mapping for ${source.id}`);
76
+ }
77
+ rootsById.set(source.id, matches[0].root);
78
+ }
79
+
80
+ for (const source of record.sources) {
81
+ verifyGitSource(source, rootsById.get(source.id));
82
+ }
83
+
84
+ return {
85
+ readFile(source, relativePath) {
86
+ const root = rootsById.get(source.id);
87
+ verifyRegularFileEntry(source, root, relativePath);
88
+ return runGit(source, root, ["show", "--end-of-options", `${source.revision}:${relativePath}`]);
89
+ },
90
+ };
91
+ }
92
+
93
+ function verifyGitSource(source, root) {
94
+ const origin = runGit(source, root, ["remote", "get-url", "origin"]);
95
+ if (origin.trim() !== source.repository) {
96
+ throw new Error(`origin URL does not match declared repository for ${source.id}`);
97
+ }
98
+ try {
99
+ runGit(source, root, ["cat-file", "-e", "--end-of-options", `${source.revision}^{commit}`]);
100
+ } catch {
101
+ throw new Error(`declared revision is absent from checkout for ${source.id}`);
102
+ }
103
+ }
104
+
105
+ function verifyRegularFileEntry(source, root, relativePath) {
106
+ const entry = runGit(source, root, [
107
+ "ls-tree",
108
+ "-z",
109
+ "--full-tree",
110
+ "--end-of-options",
111
+ source.revision,
112
+ "--",
113
+ relativePath,
114
+ ]);
115
+ const [mode, type, object, entryPath] = parseTreeEntry(entry, source, relativePath);
116
+ if ((mode !== "100644" && mode !== "100755") || type !== "blob" || !object) {
117
+ throw new Error(`pinned tree entry is not a regular file for ${source.id}:${relativePath}`);
118
+ }
119
+ if (entryPath !== relativePath) {
120
+ throw new Error(`pinned tree entry is missing for ${source.id}:${relativePath}`);
121
+ }
122
+ }
123
+
124
+ function parseTreeEntry(output, source, relativePath) {
125
+ const entries = output.split("\0").filter(Boolean);
126
+ if (entries.length !== 1) {
127
+ throw new Error(`pinned tree entry is missing for ${source.id}:${relativePath}`);
128
+ }
129
+ const tab = entries[0].indexOf("\t");
130
+ if (tab < 0) {
131
+ throw new Error(`could not inspect pinned tree entry for ${source.id}:${relativePath}`);
132
+ }
133
+ const fields = entries[0].slice(0, tab).split(" ");
134
+ const entryPath = entries[0].slice(tab + 1);
135
+ if (fields.length !== 3 || !entryPath)
136
+ throw new Error(`could not inspect pinned tree entry for ${source.id}:${relativePath}`);
137
+ return [...fields, entryPath];
138
+ }
139
+
140
+ function runGit(source, root, args) {
141
+ const result = spawnSync("git", ["-C", root, ...args], { encoding: "utf8" });
142
+ if (result.error) throw result.error;
143
+ if (result.status !== 0) {
144
+ const target = args[0] === "show" ? ` ${source.id}:${args.at(-1).slice(args.at(-1).indexOf(":") + 1)}` : "";
145
+ throw new Error(`git ${args[0]} failed${target}: ${result.stderr.trim()}`);
146
+ }
147
+ return result.stdout;
148
+ }
149
+
150
+ function isPlainObject(value) {
151
+ return value !== null && typeof value === "object" && !Array.isArray(value);
152
+ }
153
+
154
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
155
+ try {
156
+ runFrToCodeAudit(process.argv.slice(2));
157
+ } catch (error) {
158
+ writeCliError(error, {
159
+ nextAction: "Run audit-fr-to-code --help, correct the input, and retry.",
160
+ });
161
+ }
162
+ }
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { buildModelOverview } from "./generate-docs.mjs";
7
+ import { resolveProductRoot } from "./lib/product-root-resolver.mjs";
8
+
9
+ const outputPath = path.join("generated", "docs", "model-overview.md");
10
+
11
+ function staleMessage(root) {
12
+ return `${outputPath} is missing or stale; run ddduck generate --root ${root}`;
13
+ }
14
+
15
+ export function checkGeneratedDocs(rootPath) {
16
+ const root = path.resolve(rootPath);
17
+ const expected = buildModelOverview(root);
18
+ let actual;
19
+ try {
20
+ actual = readFileSync(path.join(root, outputPath), "utf8");
21
+ } catch (error) {
22
+ if (error.code === "ENOENT") throw new Error(staleMessage(root));
23
+ throw error;
24
+ }
25
+ if (actual !== expected) throw new Error(staleMessage(root));
26
+ }
27
+
28
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
29
+ const options = parseArgs(process.argv.slice(2));
30
+ try {
31
+ checkGeneratedDocs(resolveProductRoot({ explicitRoot: options.root }));
32
+ if (options.verbose) console.log("generated docs ok");
33
+ } catch (error) {
34
+ console.error(error.message);
35
+ process.exit(1);
36
+ }
37
+ }
38
+
39
+ function parseArgs(args) {
40
+ const parsed = {};
41
+
42
+ for (let index = 0; index < args.length; index += 1) {
43
+ const arg = args[index];
44
+ if (arg === "--root") {
45
+ parsed.root = args[index + 1];
46
+ index += 1;
47
+ continue;
48
+ }
49
+ if (arg === "--verbose") {
50
+ parsed.verbose = true;
51
+ continue;
52
+ }
53
+
54
+ throw new Error(`Unknown argument: ${arg}`);
55
+ }
56
+
57
+ return parsed;
58
+ }
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { buildModelGraph } from "./generate-graph.mjs";
7
+ import { buildModelGraphSvg, svgOutputPath } from "./generate-graph-svg.mjs";
8
+ import { resolveProductRoot } from "./lib/product-root-resolver.mjs";
9
+
10
+ function staleMessage(root) {
11
+ return `${svgOutputPath} is missing or stale; run ddduck generate --root ${root}`;
12
+ }
13
+
14
+ // Pin the canonical SVG byte-for-byte: rebuild it from the source model and
15
+ // compare against the committed file. Deterministic because @hpcc-js/wasm is
16
+ // version-pinned in the lockfile (the SVG embeds its Graphviz version).
17
+ export async function checkGeneratedGraphSvg(rootPath) {
18
+ const root = path.resolve(rootPath);
19
+ const expected = await buildModelGraphSvg(buildModelGraph(root));
20
+ let actual;
21
+ try {
22
+ actual = readFileSync(path.join(root, svgOutputPath), "utf8");
23
+ } catch (error) {
24
+ if (error.code === "ENOENT") throw new Error(staleMessage(root));
25
+ throw error;
26
+ }
27
+ if (actual !== expected) throw new Error(staleMessage(root));
28
+ }
29
+
30
+ function parseArgs(args) {
31
+ const parsed = {};
32
+
33
+ for (let index = 0; index < args.length; index += 1) {
34
+ const arg = args[index];
35
+ if (arg === "--root") {
36
+ parsed.root = args[index + 1];
37
+ index += 1;
38
+ continue;
39
+ }
40
+ if (arg === "--verbose") {
41
+ parsed.verbose = true;
42
+ continue;
43
+ }
44
+
45
+ throw new Error(`Unknown argument: ${arg}`);
46
+ }
47
+
48
+ return parsed;
49
+ }
50
+
51
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
52
+ const options = parseArgs(process.argv.slice(2));
53
+ try {
54
+ await checkGeneratedGraphSvg(resolveProductRoot({ explicitRoot: options.root }));
55
+ if (options.verbose) console.log("generated graph svg ok");
56
+ } catch (error) {
57
+ console.error(error.message);
58
+ process.exit(1);
59
+ }
60
+ }
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { buildModelGraphOutputs, jsonOutputPath, ndjsonOutputPath } from "./generate-graph.mjs";
7
+ import { resolveProductRoot } from "./lib/product-root-resolver.mjs";
8
+
9
+ function staleMessage(relativePath, root) {
10
+ return `${relativePath} is missing or stale; run ddduck generate --root ${root}`;
11
+ }
12
+
13
+ export function checkGeneratedGraph(rootPath) {
14
+ const root = path.resolve(rootPath);
15
+ const expected = buildModelGraphOutputs(root);
16
+ checkOutput(root, jsonOutputPath, expected.json);
17
+ checkOutput(root, ndjsonOutputPath, expected.ndjson);
18
+ }
19
+
20
+ function checkOutput(root, relativePath, expected) {
21
+ let actual;
22
+ try {
23
+ actual = readFileSync(path.join(root, relativePath), "utf8");
24
+ } catch (error) {
25
+ if (error.code === "ENOENT") {
26
+ throw new Error(staleMessage(relativePath, root));
27
+ }
28
+ throw error;
29
+ }
30
+
31
+ if (actual !== expected) {
32
+ throw new Error(staleMessage(relativePath, root));
33
+ }
34
+ }
35
+
36
+ function parseArgs(args) {
37
+ const parsed = {};
38
+
39
+ for (let index = 0; index < args.length; index += 1) {
40
+ const arg = args[index];
41
+ if (arg === "--root") {
42
+ parsed.root = args[index + 1];
43
+ index += 1;
44
+ continue;
45
+ }
46
+ if (arg === "--verbose") {
47
+ parsed.verbose = true;
48
+ continue;
49
+ }
50
+
51
+ throw new Error(`Unknown argument: ${arg}`);
52
+ }
53
+
54
+ return parsed;
55
+ }
56
+
57
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
58
+ const options = parseArgs(process.argv.slice(2));
59
+ try {
60
+ checkGeneratedGraph(resolveProductRoot({ explicitRoot: options.root }));
61
+ if (options.verbose) console.log("generated graph ok");
62
+ } catch (error) {
63
+ console.error(error.message);
64
+ process.exit(1);
65
+ }
66
+ }