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
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
|
-

|
|
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
|
|
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
|
|
76
|
-
|
|
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).
|
package/docs/architecture.md
CHANGED
|
@@ -61,8 +61,11 @@ flowchart LR
|
|
|
61
61
|
|
|
62
62
|
## Query boundary
|
|
63
63
|
|
|
64
|
-
Queries are read-only and
|
|
65
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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`
|
|
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
|
|
107
|
-
|
|
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.
|
|
130
|
-
|
|
131
|
-
|
|
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`,
|
|
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.
|
|
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.
|
|
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`.
|
|
214
|
-
|
|
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]`
|
|
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)).
|
package/docs/getting-started.md
CHANGED
|
@@ -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
|
|
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
|
package/docs/model-reference.md
CHANGED
|
@@ -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ddduck",
|
|
3
|
-
"version": "0.1.
|
|
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": ">=
|
|
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:
|
|
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
|
-
|
|
12
|
-
|
|
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
|
|
41
|
+
if (error.code === "ENOENT") throw staleError(root);
|
|
23
42
|
throw error;
|
|
24
43
|
}
|
|
25
|
-
if (actual !== expected) throw
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
42
|
+
if (error.code === "ENOENT") throw staleError(root);
|
|
25
43
|
throw error;
|
|
26
44
|
}
|
|
27
|
-
if (actual !== expected) throw
|
|
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
|
-
|
|
10
|
-
|
|
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
|
|
46
|
+
throw staleError(relativePath, root);
|
|
27
47
|
}
|
|
28
48
|
throw error;
|
|
29
49
|
}
|
|
30
50
|
|
|
31
51
|
if (actual !== expected) {
|
|
32
|
-
throw
|
|
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
|
}
|