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 CHANGED
@@ -14,7 +14,7 @@ Every product renders to a verified graph of its model — the ownership spine
14
14
  line styles. `ddduck generate` produces it as a deterministic SVG. This
15
15
  framework's own model:
16
16
 
17
- ![ddduck model graph: the framework's Model, its Domains and owned Concepts, Interfaces, Guarantees, and Use Cases, with a legend](https://raw.githubusercontent.com/diegomarino/ddduck/main/docs/ddd/generated/graph/model-graph.svg)
17
+ ![ddduck model graph: the framework's Model, its Domains and owned Concepts, and typed relationships, with a legend](https://raw.githubusercontent.com/diegomarino/ddduck/main/docs/ddd/generated/graph/model-graph.svg)
18
18
 
19
19
  ## Product layout
20
20
 
@@ -40,7 +40,9 @@ docs/ddd/
40
40
  `product.yaml` and files under `model/` are canonical source. `generated/` is derived
41
41
  output: never edit it by hand.
42
42
 
43
- Repository-local ddduck tool metadata lives outside the product root in `.ddduck/`. For example,
43
+ Repository-local ddduck tool metadata lives outside the product root in `.ddduck/` at the
44
+ enclosing repository root; without one, `ddduck init` writes it inside the new product root
45
+ instead (see [the CLI reference](docs/cli.md#product-root-resolution)). For example,
44
46
  this framework repository stores its own model in `docs/ddd/` and records that selection in:
45
47
 
46
48
  ```json
@@ -57,7 +59,7 @@ and fully editable (see [the CLI reference](docs/cli.md)).
57
59
 
58
60
  ## Authoring
59
61
 
60
- Install the CLI from npm:
62
+ Install the CLI from npm (requires Node.js 22 or newer):
61
63
 
62
64
  ```bash
63
65
  npm install -g ddduck
@@ -72,8 +74,9 @@ ddduck init ddd --id model:<product-id>
72
74
  ```
73
75
 
74
76
  `--root <path>` is always the explicit override; without it, ddduck resolves the enclosing
75
- product root, then `.ddduck/config.json`, then a unique repository candidate, and fails with a
76
- diagnostic when the choice is ambiguous. The mutation surface is deliberately narrow — `check`,
77
+ product root, then `.ddduck/config.json`, then a unique repository candidate (see
78
+ [the CLI reference](docs/cli.md#product-root-resolution) for the full order, including the
79
+ example-candidate fallback), and fails with a diagnostic when the choice is ambiguous. The mutation surface is deliberately narrow — `check`,
77
80
  `generate`, and the `create`/`move`/`split`/`retire` Guarantee lifecycle commands — and every
78
81
  successful source mutation regenerates the derived views. The canonical resolution rules and
79
82
  command contracts live in [the CLI reference](docs/cli.md#product-root-resolution).
@@ -61,8 +61,11 @@ flowchart LR
61
61
 
62
62
  ## Query boundary
63
63
 
64
- Queries are read-only and require `--json`. A context query returns selected canonical records,
65
- direct touching edges, one-hop summaries for unselected neighbors, and a source digest. It does
64
+ Queries are read-only and always emit exactly one JSON document (`--json` is accepted as a
65
+ no-op). A context query returns selected canonical records,
66
+ direct touching edges, one-hop summaries for unselected neighbors, and a source digest. The
67
+ source digest covers the canonical node YAML sources only — `product.yaml` and the files under
68
+ `model/` — so decision records under `decisions/` are outside its scope. It does
66
69
  not recursively expand context or write canonical or generated files.
67
70
 
68
71
  ```mermaid
package/docs/cli.md CHANGED
@@ -7,7 +7,12 @@ with `--` must use the `--option=value` form. Every command rejects unknown opti
7
7
  options, missing option values, and unexpected positional arguments. Expected failures write a
8
8
  concise diagnostic (multi-error validation reports keep one line per error) plus a safe next
9
9
  action to standard error and exit nonzero; errors without a specific next action fall back to a
10
- command-specific hint. Help exits zero.
10
+ command-specific hint. Help exits zero. Exit codes are part of the contract: the one retryable
11
+ failure — a busy product root, whose operation lock is held by a running process — exits 2, so
12
+ retry logic never has to string-match standard error; every other failure exits 1.
13
+
14
+ This package is published to npm as `ddduck`; install the CLI globally with `npm install -g ddduck`
15
+ (see [the getting-started guide](getting-started.md#install-ddduck)).
11
16
 
12
17
  ## Product root resolution
13
18
 
@@ -44,51 +49,20 @@ Use `.ddduck/config.json` for a repository default:
44
49
  every ddduck scanner skips, in addition to the always-skipped dot-directories
45
50
  and `node_modules`. Omit the key to accept the defaults shown above; set it to
46
51
  `[]` to skip nothing beyond the built-in defaults. `ddduck init` writes this
47
- file pre-filled when it does not already exist.
48
-
49
- ## Install an agent skill
50
-
51
- ```text
52
- ddduck install skill update-ddduck-specs [--repo <repository-root>]
53
- ```
54
-
55
- `--repo` defaults to the current directory. The installer chooses the least intrusive host
56
- topology from the repository's existing directories:
57
-
58
- ```text
59
- no .agents/ or .claude/ -> .agents/skills/update-ddduck-specs/SKILL.md
60
- .agents/ only -> .agents/skills/update-ddduck-specs/SKILL.md
61
- .claude/ only -> .claude/skills/update-ddduck-specs/SKILL.md
62
- .agents/ and .claude/ -> .agents/skills/update-ddduck-specs/SKILL.md
63
- .claude/skills/update-ddduck-specs -> ../../.agents/skills/update-ddduck-specs
64
- ```
65
-
66
- The installer writes `.ddduck/agent-skills.lock.json` with the selected canonical path, host
67
- adapters, package version, and installed `SKILL.md` SHA-256. It does not create a host directory
68
- for a host that is absent from the repository, except for the `.agents/` fallback when no host
69
- directory exists. On success it prints one result line naming the action (`created`, `upgraded`,
70
- or `no-op`), the repository, the canonical skill path, and the lock path. It does not accept
71
- `--json`.
72
-
73
- Invoke the skill from the relevant host:
74
-
75
- ```text
76
- Codex: $update-ddduck-specs
77
- Claude: /update-ddduck-specs
78
- ```
79
-
80
- The skill defaults to plan-only; changing a product model requires explicit apply authorization.
81
- Its workflow behavior is defined by the installed `SKILL.md` (and its packaged
82
- [canonical source](../skills/update-ddduck-specs/SKILL.md)).
83
-
84
- This package is published to npm as `ddduck`; install the CLI globally with `npm install -g ddduck`
85
- (see [the getting-started guide](getting-started.md#install-ddduck)).
52
+ file pre-filled when it does not already exist and reports the write in its result
53
+ (`config: <path> (created)` in text, `configPath` in `--json`; omitted when the file
54
+ pre-existed). The config is written at the enclosing repository root (the nearest ancestor
55
+ containing `.git`); without one, at the destination directory itself, so the file then lives
56
+ inside the new product root with `"productRoot": "."`. When the file already exists and selects a different product root, `init` prints a
57
+ standard-error note that the repository default still selects that other root.
86
58
 
87
59
  ## Common behavior
88
60
 
89
61
  Product writes are `init`, `generate`, `create`, `move`, `split`, and `retire`; successful
90
62
  mutations regenerate all required views. `check` and every `query` are read-only. A successful
91
- `check` is silent. Successful product mutations print one concise result line, or one JSON result
63
+ `check` writes nothing to standard output; when `--root` was omitted, it prints one standard-error
64
+ note naming the validated root so an implicitly resolved (for example config-pinned) root is never
65
+ validated invisibly. Successful product mutations print one concise result line, or one JSON result
92
66
  object when `--json` is available.
93
67
 
94
68
  ## `init`
@@ -103,8 +77,10 @@ ddduck init [destination] --id model:<product-id> [--json]
103
77
  | `--id` | yes | none | Root Model ID, matching `model:<lowercase-slug>`. |
104
78
  | `--json` | no | false | Emit one JSON result object instead of text. |
105
79
 
106
- `init` writes the canonical directory layout, `product.yaml`, and fresh generated docs and graph
107
- views. It refuses a non-empty destination. On success it reports the Model ID, normalized root,
80
+ `init` writes the canonical directory layout, `product.yaml`, and the four fresh generated views
81
+ (`generated/docs/model-overview.md`, `generated/graph/model-graph.json`,
82
+ `generated/graph/model-graph.ndjson`, and `generated/graph/model-graph.svg`). It refuses a
83
+ non-empty destination. On success it reports the Model ID, normalized root,
108
84
  `product.yaml`, and all generated paths as one text line or, with `--json`, one object containing
109
85
  `operation`, `root`, `affectedIds`, `canonicalPaths`, and `generatedPaths`. On failure it exits
110
86
  nonzero without reporting success.
@@ -126,9 +102,12 @@ ddduck check [--root <product-root>] [--base <previous-product-root>] \
126
102
  By default, `check` validates canonical source, then requires fresh generated Markdown and graph
127
103
  views. Documentation references are validated only inside the product root; pass one or more
128
104
  `--docs-root` directories to widen (and replace) that scope, mirroring the framework's own
129
- repository-wide gate. It writes nothing and is quiet on success. It exits nonzero for invalid source, stale
130
- views, invalid roots, invalid options, a busy root (`.ddduck-operation.lock` held by a live
131
- ddduck operation), or leftover state from an interrupted operation
105
+ repository-wide gate. Documentation-reference scanning always skips the `docs/audits` and
106
+ `docs/superpowers` directories (paths relative to each scanned root). It writes nothing and keeps standard output empty on success; when `--root`
107
+ was omitted, one standard-error note names the validated root. It exits 2 for a busy root
108
+ (`.ddduck-operation.lock` held by a live ddduck operation — the retryable case), and 1 for
109
+ invalid source, stale views, invalid roots, invalid options, or leftover state from an
110
+ interrupted operation
132
111
  (`.ddduck-operation.lock`, `.ddduck-operation.reclaim`, or `.ddduck-operation-stage-*` with no
133
112
  live owning process); running any mutation, such as `ddduck generate`, reclaims that leftover
134
113
  state.
@@ -145,7 +124,8 @@ ddduck generate [--root <product-root>] [--json]
145
124
  | `--json` | false | Emit one JSON mutation-result object instead of the text result line. |
146
125
 
147
126
  `generate` validates canonical source before writing `generated/docs/model-overview.md`,
148
- `generated/graph/model-graph.json`, and `generated/graph/model-graph.ndjson`. A successful text
127
+ `generated/graph/model-graph.json`, `generated/graph/model-graph.ndjson`, and
128
+ `generated/graph/model-graph.svg`. A successful text
149
129
  result identifies the root, canonical paths (none for generate), and generated paths. It exits
150
130
  nonzero without an intended product mutation if validation or contained-output checks fail.
151
131
 
@@ -205,13 +185,19 @@ staged validation exits nonzero without an intended mutation.
205
185
 
206
186
  Every query emits exactly one JSON document and writes no source or generated output. While a
207
187
  live ddduck mutation holds `.ddduck-operation.lock`, queries and `check` fail with a busy
208
- diagnostic instead of reading a partially published snapshot. A read racing the very start of a
188
+ diagnostic (exit code 2, the retryable case) instead of reading a partially published snapshot.
189
+ Queries also refuse leftover state from an interrupted operation — the same
190
+ `.ddduck-operation.lock`, `.ddduck-operation.reclaim`, or `.ddduck-operation-stage-*` entries
191
+ `check` reports — with exit code 1 and the reclaim next action (run any mutation, such as
192
+ `ddduck generate`, to reclaim), because the snapshot may be partially published. A read racing the very start of a
209
193
  mutation, before the lock exists, may still observe a partial snapshot, so `sourceDigest` is
210
- authoritative only for reads that did not race a mutation. JSON is
194
+ authoritative only for reads that did not race a mutation. `sourceDigest` covers the canonical
195
+ node YAML sources only — `product.yaml` and the files under `model/` — not decision records:
196
+ editing a file under `decisions/` does not change the digest. JSON is
211
197
  the only output format, so `--json` is optional and accepted as a no-op for compatibility. The
212
198
  document contains `schemaVersion`, `query`, `rootModelId`, `result`, and
213
- `diagnostics`. Non-context query roots are resolved through the common product-root rules.
214
- `context` requires an explicit `--root`. Non-context queries accept optional `--history`; without it, a split or retired
199
+ `diagnostics`. Every query root is resolved through the common product-root rules.
200
+ Non-context queries accept optional `--history`; without it, a split or retired
215
201
  Guarantee resolves to its lifecycle redirect rather than its historical contract.
216
202
 
217
203
  ```mermaid
@@ -232,8 +218,43 @@ sequenceDiagram
232
218
  | `ddduck query impact --id <id> [--root <root>] [--history] [--json]` | `--id` | Reverse impact closure over ownership and behavioral references. |
233
219
  | `ddduck query anchors --id <id> [--root <root>] [--history] [--json]` | `--id` | Evidence, reachable decisions, policies, and view freshness. |
234
220
  | `ddduck query spec [--id <model-id>] [--root <root>] [--history] [--json]` | none | Root, domains, view freshness, and verification commands. |
235
- | `ddduck query context --id <id> [--id <id> ...] --root <root> [--json]` | `--id`, `--root` | Selected records, touching edges, one-hop summaries, and source digest. |
221
+ | `ddduck query context --id <id> [--id <id> ...] [--root <root>] [--json]` | `--id` | Selected records, touching edges, one-hop summaries, and source digest. |
236
222
 
237
223
  `context` accepts one or more distinct, repeatable `--id` options; it rejects `--history`.
238
224
  Its selected records are complete, while unselected endpoints appear only as one-hop summaries.
239
225
  The command does not recursively expand the perimeter.
226
+
227
+ ## Install an agent skill
228
+
229
+ ```text
230
+ ddduck install skill update-ddduck-specs [--repo <repository-root>]
231
+ ```
232
+
233
+ `--repo` defaults to the current directory. The installer chooses the least intrusive host
234
+ topology from the repository's existing directories:
235
+
236
+ ```text
237
+ no .agents/ or .claude/ -> .agents/skills/update-ddduck-specs/SKILL.md
238
+ .agents/ only -> .agents/skills/update-ddduck-specs/SKILL.md
239
+ .claude/ only -> .claude/skills/update-ddduck-specs/SKILL.md
240
+ .agents/ and .claude/ -> .agents/skills/update-ddduck-specs/SKILL.md
241
+ .claude/skills/update-ddduck-specs -> ../../.agents/skills/update-ddduck-specs
242
+ ```
243
+
244
+ The installer writes `.ddduck/agent-skills.lock.json` with the selected canonical path, host
245
+ adapters, package version, and installed `SKILL.md` SHA-256. It does not create a host directory
246
+ for a host that is absent from the repository, except for the `.agents/` fallback when no host
247
+ directory exists. On success it prints one result line naming the action (`created`, `upgraded`,
248
+ or `no-op`), the repository, the canonical skill path, and the lock path. It does not accept
249
+ `--json`.
250
+
251
+ Invoke the skill from the relevant host:
252
+
253
+ ```text
254
+ Codex: $update-ddduck-specs
255
+ Claude: /update-ddduck-specs
256
+ ```
257
+
258
+ The skill defaults to plan-only; changing a product model requires explicit apply authorization.
259
+ Its workflow behavior is defined by the installed `SKILL.md` (and its packaged
260
+ [canonical source](../skills/update-ddduck-specs/SKILL.md)).
@@ -4,7 +4,7 @@ This executable journey creates a product with one Domain, one Concept, and one
4
4
 
5
5
  ## Install ddduck
6
6
 
7
- Install the published CLI globally from npm:
7
+ Requires Node.js 22 or newer. Install the published CLI globally from npm:
8
8
 
9
9
  ```sh
10
10
  npm install -g ddduck
@@ -15,7 +15,7 @@ To contribute or run an unreleased revision, work from a local checkout instead.
15
15
  `ddduck` bin on `PATH` with `npm link`:
16
16
 
17
17
  ```sh
18
- git clone <ddduck-repository-url> ddduck
18
+ git clone https://github.com/diegomarino/ddduck.git ddduck
19
19
  cd ddduck
20
20
  npm install
21
21
  npm link
@@ -30,7 +30,10 @@ node <checkout>/scripts/ddduck.mjs --help
30
30
 
31
31
  ## Create the first product
32
32
 
33
- Run the complete Bash block from an empty working directory with `ddduck` on `PATH`.
33
+ Run the complete Bash block from an empty working directory with `ddduck` on `PATH`, normally
34
+ inside a Git repository: `init` records the repository default in `.ddduck/config.json` at the
35
+ repository root, and without one writes it inside the new product root instead (see
36
+ [the CLI reference](cli.md#product-root-resolution)).
34
37
 
35
38
  ```mermaid
36
39
  flowchart LR
@@ -11,6 +11,8 @@ The checker requires exactly one Model root.
11
11
  `product.yaml` is the only `Model` node. Unlike every child node, it has no `model` field.
12
12
  Required fields are `schemaVersion`, `kind`, `id`, `name`, `purpose`, and `domains`. Optional
13
13
  top-level fields include `nameStatus`, `useCases`, `relationships`, `decisions`, and `notes`.
14
+ `nameStatus` is a free-form string describing how settled the model name is (for example
15
+ `stable` or `provisional`); generated views show it only when it is declared.
14
16
 
15
17
  ```yaml
16
18
  schemaVersion: "1"
@@ -87,6 +89,8 @@ records former owning Domains. Evidence anchors are also supported.
87
89
 
88
90
  Evidence anchors contain a product-relative `path`, `anchor`, and `role` (`source`, `decision`,
89
91
  or `verification`). The path must resolve to a regular file inside the selected product root.
92
+ For Markdown (`.md`) paths the checker also verifies that the `anchor` string occurs in the
93
+ file content; for non-Markdown paths the anchor is a free-form label and is not content-checked.
90
94
 
91
95
  For full schema constraints, inspect the shipped files under `schemas/product/`; use
92
96
  [the getting-started guide](getting-started.md) for the minimal working path.
package/docs/model.md CHANGED
@@ -20,6 +20,7 @@ docs/ddd/
20
20
  docs/model-overview.md
21
21
  graph/model-graph.json
22
22
  graph/model-graph.ndjson
23
+ graph/model-graph.svg
23
24
  ```
24
25
 
25
26
  `product.yaml` and `model/**/*.yaml` are canonical. `generated/` is derived output and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ddduck",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "An opinionated DDD framework for authoring, validating, and visualizing machine-readable product-spec models.",
5
5
  "keywords": [
6
6
  "ddd",
@@ -23,7 +23,7 @@
23
23
  "url": "https://github.com/diegomarino/ddduck/issues"
24
24
  },
25
25
  "engines": {
26
- "node": ">=20"
26
+ "node": ">=22"
27
27
  },
28
28
  "type": "module",
29
29
  "files": [
@@ -41,7 +41,8 @@
41
41
  "ddduck": "scripts/ddduck.mjs"
42
42
  },
43
43
  "scripts": {
44
- "check": "npm run check:model && npm run check:docs && npm run check:graph && npm run check:graph:svg && npm run lint && npm run lint:md && npm run format:check && npm test",
44
+ "check": "npm run check:static && npm test",
45
+ "check:static": "npm run check:model && npm run check:docs && npm run check:graph && npm run check:graph:svg && npm run lint && npm run lint:md && npm run format:check",
45
46
  "check:docs": "node scripts/check-generated-docs.mjs",
46
47
  "check:graph": "node scripts/check-generated-graph.mjs",
47
48
  "check:graph:svg": "node scripts/check-generated-graph-svg.mjs",
@@ -51,7 +52,7 @@
51
52
  "generate:graph": "node scripts/generate-graph.mjs",
52
53
  "generate:graph:svg": "node scripts/generate-graph-svg.mjs",
53
54
  "lint": "eslint .",
54
- "lint:md": "markdownlint-cli2 \"*.md\" \"docs/**/*.md\" \"examples/**/*.md\"",
55
+ "lint:md": "markdownlint-cli2 \"*.md\" \"docs/**/*.md\" \"examples/**/*.md\" \"#CHANGELOG.md\" \"#docs/audits\" \"#docs/superpowers\"",
55
56
  "test": "node --test test/*.test.mjs",
56
57
  "test:coverage": "node --test --experimental-test-coverage test/*.test.mjs",
57
58
  "pack:dry-run": "npm pack --dry-run --cache .npm-cache",
@@ -1,5 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ /**
4
+ * CLI for verifying FR-to-code audit records (`audit-fr-to-code --input
5
+ * <audit.yaml> --source-root <source-id>=<checkout> --json`). Reads the YAML
6
+ * audit record, binds each declared source to a local git checkout whose
7
+ * origin URL and pinned 40-hex revision must match, reads anchored files via
8
+ * `git show` at that exact revision (regular-file tree entries only), and
9
+ * delegates verdict verification to lib/fr-to-code-audit.mjs, emitting one
10
+ * JSON report on stdout.
11
+ */
12
+
3
13
  import { spawnSync } from "node:child_process";
4
14
  import { readFileSync } from "node:fs";
5
15
  import { fileURLToPath } from "node:url";
@@ -7,6 +17,13 @@ import { parseDocument } from "yaml";
7
17
  import { verifyFrToCodeAudit } from "./lib/fr-to-code-audit.mjs";
8
18
  import { CliUsageError, parseCommandArgs, renderHelp, writeCliError } from "./lib/cli-contract.mjs";
9
19
 
20
+ /**
21
+ * Parse arguments, verify the audit record against its pinned sources, and
22
+ * write the JSON report.
23
+ * @param {string[]} args - CLI arguments (--input, repeatable --source-root, --json).
24
+ * @param {{stdout?: {write: (chunk: string) => unknown}}} [io] - Output stream override for tests.
25
+ * @returns {void}
26
+ */
10
27
  export function runFrToCodeAudit(args, { stdout = process.stdout } = {}) {
11
28
  if (args.length === 1 && args[0] === "--help") {
12
29
  stdout.write(renderHelp("audit-fr-to-code"));
@@ -61,6 +78,14 @@ function validateDeclaredSources(record, filePath) {
61
78
  }
62
79
  }
63
80
 
81
+ /**
82
+ * Build the source reader that serves file contents from pinned git revisions.
83
+ * Requires exactly one --source-root mapping per declared source and verifies
84
+ * each checkout's origin URL and revision presence up front.
85
+ * @param {{sources: {id: string, repository: string, revision: string}[]}} record - The validated audit record.
86
+ * @param {{id: string, root: string}[]} sourceRoots - Parsed --source-root mappings.
87
+ * @returns {{readFile: (source: object, relativePath: string) => string}} Reader handed to verifyFrToCodeAudit.
88
+ */
64
89
  function createGitSourceReader(record, sourceRoots) {
65
90
  if (!Array.isArray(record.sources)) throw new Error("audit input must declare sources");
66
91
  const declaredIds = new Set(record.sources.map((source) => source?.id));
@@ -1,5 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ /**
4
+ * Freshness gate for the generated docs view: rebuilds
5
+ * generated/docs/model-overview.md from the canonical YAML and compares it
6
+ * byte-for-byte against the committed file. Consumed by `ddduck check` and the
7
+ * staged operation runner (both import checkGeneratedDocs); also runnable
8
+ * standalone, where a stale view exits 1 with a regenerate remedy.
9
+ */
10
+
3
11
  import { readFileSync } from "node:fs";
4
12
  import path from "node:path";
5
13
  import { fileURLToPath } from "node:url";
@@ -8,10 +16,21 @@ import { resolveProductRoot } from "./lib/product-root-resolver.mjs";
8
16
 
9
17
  const outputPath = path.join("generated", "docs", "model-overview.md");
10
18
 
11
- function staleMessage(root) {
12
- return `${outputPath} is missing or stale; run ddduck generate --root ${root}`;
19
+ // The message states the fact only, so callers that add their own next-action
20
+ // line never state the remedy twice; `remedy` carries the regenerate hint for
21
+ // callers with no next-action surface (the standalone gate below).
22
+ function staleError(root) {
23
+ const error = new Error(`${outputPath} is missing or stale`);
24
+ error.remedy = `run ddduck generate --root ${root}`;
25
+ return error;
13
26
  }
14
27
 
28
+ /**
29
+ * Throw if generated/docs/model-overview.md is missing or differs from the
30
+ * output rebuilt from the current canonical YAML.
31
+ * @param {string} rootPath - Product root path.
32
+ * @returns {void}
33
+ */
15
34
  export function checkGeneratedDocs(rootPath) {
16
35
  const root = path.resolve(rootPath);
17
36
  const expected = buildModelOverview(root);
@@ -19,10 +38,10 @@ export function checkGeneratedDocs(rootPath) {
19
38
  try {
20
39
  actual = readFileSync(path.join(root, outputPath), "utf8");
21
40
  } catch (error) {
22
- if (error.code === "ENOENT") throw new Error(staleMessage(root));
41
+ if (error.code === "ENOENT") throw staleError(root);
23
42
  throw error;
24
43
  }
25
- if (actual !== expected) throw new Error(staleMessage(root));
44
+ if (actual !== expected) throw staleError(root);
26
45
  }
27
46
 
28
47
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
@@ -31,7 +50,7 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
31
50
  checkGeneratedDocs(resolveProductRoot({ explicitRoot: options.root }));
32
51
  if (options.verbose) console.log("generated docs ok");
33
52
  } catch (error) {
34
- console.error(error.message);
53
+ console.error(error.remedy ? `${error.message}; ${error.remedy}` : error.message);
35
54
  process.exit(1);
36
55
  }
37
56
  }
@@ -1,5 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ /**
4
+ * Freshness gate for the canonical SVG view generated/graph/model-graph.svg.
5
+ * Rendering uses the async Graphviz WASM engine, so unlike the docs and
6
+ * JSON/NDJSON gates this one is always executed as a child process by
7
+ * `ddduck check`, the staged operation runner, and query freshness — never
8
+ * imported into their synchronous flows. Standalone runs exit 1 on a missing
9
+ * or stale SVG with a regenerate remedy.
10
+ */
11
+
3
12
  import { readFileSync } from "node:fs";
4
13
  import path from "node:path";
5
14
  import { fileURLToPath } from "node:url";
@@ -7,13 +16,22 @@ import { buildModelGraph } from "./generate-graph.mjs";
7
16
  import { buildModelGraphSvg, svgOutputPath } from "./generate-graph-svg.mjs";
8
17
  import { resolveProductRoot } from "./lib/product-root-resolver.mjs";
9
18
 
10
- function staleMessage(root) {
11
- return `${svgOutputPath} is missing or stale; run ddduck generate --root ${root}`;
19
+ // The message states the fact only, so callers that add their own next-action
20
+ // line never state the remedy twice; `remedy` carries the regenerate hint for
21
+ // callers with no next-action surface (the standalone gate below).
22
+ function staleError(root) {
23
+ const error = new Error(`${svgOutputPath} is missing or stale`);
24
+ error.remedy = `run ddduck generate --root ${root}`;
25
+ return error;
12
26
  }
13
27
 
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).
28
+ /**
29
+ * Pin the canonical SVG byte-for-byte: rebuild it from the source model and
30
+ * compare against the committed file. Deterministic because @hpcc-js/wasm is
31
+ * version-pinned in the lockfile (the SVG embeds its Graphviz version).
32
+ * @param {string} rootPath - Product root path.
33
+ * @returns {Promise<void>} Rejects when the SVG is missing or stale.
34
+ */
17
35
  export async function checkGeneratedGraphSvg(rootPath) {
18
36
  const root = path.resolve(rootPath);
19
37
  const expected = await buildModelGraphSvg(buildModelGraph(root));
@@ -21,10 +39,10 @@ export async function checkGeneratedGraphSvg(rootPath) {
21
39
  try {
22
40
  actual = readFileSync(path.join(root, svgOutputPath), "utf8");
23
41
  } catch (error) {
24
- if (error.code === "ENOENT") throw new Error(staleMessage(root));
42
+ if (error.code === "ENOENT") throw staleError(root);
25
43
  throw error;
26
44
  }
27
- if (actual !== expected) throw new Error(staleMessage(root));
45
+ if (actual !== expected) throw staleError(root);
28
46
  }
29
47
 
30
48
  function parseArgs(args) {
@@ -54,7 +72,7 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
54
72
  await checkGeneratedGraphSvg(resolveProductRoot({ explicitRoot: options.root }));
55
73
  if (options.verbose) console.log("generated graph svg ok");
56
74
  } catch (error) {
57
- console.error(error.message);
75
+ console.error(error.remedy ? `${error.message}; ${error.remedy}` : error.message);
58
76
  process.exit(1);
59
77
  }
60
78
  }
@@ -1,15 +1,35 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ /**
4
+ * Freshness gate for the generated graph views: rebuilds
5
+ * generated/graph/model-graph.json and .ndjson from the canonical YAML and
6
+ * compares each byte-for-byte against the committed files. Consumed by
7
+ * `ddduck check` and the staged operation runner (both import
8
+ * checkGeneratedGraph); also runnable standalone, where a stale view exits 1
9
+ * with a regenerate remedy.
10
+ */
11
+
3
12
  import { readFileSync } from "node:fs";
4
13
  import path from "node:path";
5
14
  import { fileURLToPath } from "node:url";
6
15
  import { buildModelGraphOutputs, jsonOutputPath, ndjsonOutputPath } from "./generate-graph.mjs";
7
16
  import { resolveProductRoot } from "./lib/product-root-resolver.mjs";
8
17
 
9
- function staleMessage(relativePath, root) {
10
- return `${relativePath} is missing or stale; run ddduck generate --root ${root}`;
18
+ // The message states the fact only, so callers that add their own next-action
19
+ // line never state the remedy twice; `remedy` carries the regenerate hint for
20
+ // callers with no next-action surface (the standalone gate below).
21
+ function staleError(relativePath, root) {
22
+ const error = new Error(`${relativePath} is missing or stale`);
23
+ error.remedy = `run ddduck generate --root ${root}`;
24
+ return error;
11
25
  }
12
26
 
27
+ /**
28
+ * Throw if model-graph.json or model-graph.ndjson is missing or differs from
29
+ * the outputs rebuilt from the current canonical YAML.
30
+ * @param {string} rootPath - Product root path.
31
+ * @returns {void}
32
+ */
13
33
  export function checkGeneratedGraph(rootPath) {
14
34
  const root = path.resolve(rootPath);
15
35
  const expected = buildModelGraphOutputs(root);
@@ -23,13 +43,13 @@ function checkOutput(root, relativePath, expected) {
23
43
  actual = readFileSync(path.join(root, relativePath), "utf8");
24
44
  } catch (error) {
25
45
  if (error.code === "ENOENT") {
26
- throw new Error(staleMessage(relativePath, root));
46
+ throw staleError(relativePath, root);
27
47
  }
28
48
  throw error;
29
49
  }
30
50
 
31
51
  if (actual !== expected) {
32
- throw new Error(staleMessage(relativePath, root));
52
+ throw staleError(relativePath, root);
33
53
  }
34
54
  }
35
55
 
@@ -60,7 +80,7 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
60
80
  checkGeneratedGraph(resolveProductRoot({ explicitRoot: options.root }));
61
81
  if (options.verbose) console.log("generated graph ok");
62
82
  } catch (error) {
63
- console.error(error.message);
83
+ console.error(error.remedy ? `${error.message}; ${error.remedy}` : error.message);
64
84
  process.exit(1);
65
85
  }
66
86
  }