ddduck 0.1.2 → 0.2.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.
- package/README.md +4 -2
- package/docs/cli.md +68 -7
- package/docs/definition-workflow.md +130 -0
- package/docs/getting-started.md +15 -45
- package/docs/templates/change-brief.md +48 -0
- package/package.json +4 -2
- package/schemas/model-diff.schema.json +107 -0
- package/scripts/ddduck.mjs +51 -2
- package/scripts/generate-docs.mjs +1 -1
- package/scripts/lib/cli-contract.mjs +16 -5
- package/scripts/lib/fr-to-code-audit.mjs +6 -0
- package/scripts/lib/product-authoring.mjs +52 -0
- package/scripts/lib/product-diff.mjs +155 -0
- package/scripts/lib/product-operation.mjs +9 -1
- package/scripts/lib/skill-installer.mjs +180 -45
- package/skills/update-ddduck-specs/SKILL.md +25 -83
- package/skills/update-ddduck-specs/references/authoring-and-verification.md +60 -0
- package/skills/update-ddduck-specs/references/modeling-and-evidence.md +56 -0
- package/skills/update-ddduck-specs/references/reviewing-changes.md +45 -0
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ ddduck defines a small, machine-readable product-spec format. The framework keep
|
|
|
4
4
|
schemas, validators, policies, and generators separate from each consumer product.
|
|
5
5
|
|
|
6
6
|
Start with [the getting-started guide](docs/getting-started.md) for a working first product.
|
|
7
|
+
For an unsettled product question or a proposed change, use the optional
|
|
8
|
+
[definition-to-planning workflow](docs/definition-workflow.md) and reuse your existing brief.
|
|
7
9
|
Then use [the model guide](docs/model.md), [the model reference](docs/model-reference.md),
|
|
8
10
|
[the CLI reference](docs/cli.md), and [the architecture guide](docs/architecture.md) as needed.
|
|
9
11
|
|
|
@@ -76,8 +78,8 @@ ddduck init ddd --id model:<product-id>
|
|
|
76
78
|
`--root <path>` is always the explicit override; without it, ddduck resolves the enclosing
|
|
77
79
|
product root, then `.ddduck/config.json`, then a unique repository candidate (see
|
|
78
80
|
[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 —
|
|
80
|
-
`generate`, and the `create`/`move`/`split`/`retire` Guarantee lifecycle commands — and every
|
|
81
|
+
example-candidate fallback), and fails with a diagnostic when the choice is ambiguous. The mutation surface is deliberately narrow —
|
|
82
|
+
`generate`, domain/concept/use-case creation, and the `create`/`move`/`split`/`retire` Guarantee lifecycle commands — and every
|
|
81
83
|
successful source mutation regenerates the derived views. The canonical resolution rules and
|
|
82
84
|
command contracts live in [the CLI reference](docs/cli.md#product-root-resolution).
|
|
83
85
|
|
package/docs/cli.md
CHANGED
|
@@ -59,7 +59,7 @@ standard-error note that the repository default still selects that other root.
|
|
|
59
59
|
## Common behavior
|
|
60
60
|
|
|
61
61
|
Product writes are `init`, `generate`, `create`, `move`, `split`, and `retire`; successful
|
|
62
|
-
mutations regenerate all required views. `check` and every `query` are read-only. A successful
|
|
62
|
+
mutations regenerate all required views. `check`, `diff`, and every `query` are read-only. A successful
|
|
63
63
|
`check` writes nothing to standard output; when `--root` was omitted, it prints one standard-error
|
|
64
64
|
note naming the validated root so an implicitly resolved (for example config-pinned) root is never
|
|
65
65
|
validated invisibly. Successful product mutations print one concise result line, or one JSON result
|
|
@@ -129,6 +129,65 @@ ddduck generate [--root <product-root>] [--json]
|
|
|
129
129
|
result identifies the root, canonical paths (none for generate), and generated paths. It exits
|
|
130
130
|
nonzero without an intended product mutation if validation or contained-output checks fail.
|
|
131
131
|
|
|
132
|
+
## `diff`
|
|
133
|
+
|
|
134
|
+
```text
|
|
135
|
+
ddduck diff --base <previous-product-root> [--root <product-root>] [--json]
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Compare two independently valid versions of the same Model by stable node ID. `--base` is
|
|
139
|
+
required and `--root` uses normal root resolution. Generated output may be absent or stale.
|
|
140
|
+
The command does not apply historical retention validation first: removing a Guarantee must
|
|
141
|
+
remain visible in the comparison even when a subsequent `check --base` rejects that removal.
|
|
142
|
+
|
|
143
|
+
The text report shows IDs, field changes, and path relocations. `--json` emits one
|
|
144
|
+
`ModelDiff` version `"1"` document conforming to `schemas/model-diff.schema.json`:
|
|
145
|
+
|
|
146
|
+
- `before` and `after` contain `modelId` and canonical `sourceDigest`.
|
|
147
|
+
- `added` and `removed` contain full records with their product-relative source paths.
|
|
148
|
+
- `changed` contains field changes keyed by escaped JSON Pointer paths. `beforePresent` and
|
|
149
|
+
`afterPresent` distinguish absent fields from explicit null values.
|
|
150
|
+
- `relocated` records path changes without treating the same ID as a new record.
|
|
151
|
+
- `scope` is `canonical-yaml-only`; `excludedScopes` names decision content, evidence content,
|
|
152
|
+
delivery artifacts, and runtime.
|
|
153
|
+
|
|
154
|
+
Object-key order, comments, and YAML formatting are not record differences. Array order is
|
|
155
|
+
preserved, so a reordered list is reported. Raw canonical-file changes still affect digests.
|
|
156
|
+
Neither the digests nor an empty comparison establish ADR/evidence freshness or semantic
|
|
157
|
+
equivalence. Source reads are not atomic snapshots; run comparisons while neither root is
|
|
158
|
+
being edited. Busy and interrupted roots are refused.
|
|
159
|
+
|
|
160
|
+
Exit 0 means comparison completed, including when changes exist; it is not an approval.
|
|
161
|
+
Exit 2 means busy, and exit 1 covers invalid or incompatible inputs. Use existing `impact`
|
|
162
|
+
and `neighbors` queries on both roots to inspect changed/removed context, then review meaning
|
|
163
|
+
and run historical retention checking separately.
|
|
164
|
+
|
|
165
|
+
## Creating domains, concepts, and use cases
|
|
166
|
+
|
|
167
|
+
```text
|
|
168
|
+
ddduck create domain --id domain:<slug> --name <text> --purpose <text> [--root <product-root>] [--json]
|
|
169
|
+
ddduck create concept --id concept:<slug> --owner domain:<slug> --name <text> --purpose <text> [--root <product-root>] [--json]
|
|
170
|
+
ddduck create use-case --file <yaml-file> [--root <product-root>] [--json]
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Domain creation writes the node and adds its ID to the Model's `domains` list. Concept creation
|
|
174
|
+
writes the node and adds its ID to the owning Domain's `concepts` list. These forms require
|
|
175
|
+
every displayed field; they take model identity from the selected product.
|
|
176
|
+
|
|
177
|
+
Use-case creation reads a complete canonical `UseCase` YAML mapping. Its `model` must match
|
|
178
|
+
the selected product and all Guarantee/interface references must already resolve. It writes
|
|
179
|
+
the new node and adds its ID to the Model's `useCases` list without changing the input file.
|
|
180
|
+
An empty prerequisite or outcome list is allowed; the tool does not invent obligations.
|
|
181
|
+
|
|
182
|
+
All three forms derive the destination filename from the validated ID. Duplicate IDs,
|
|
183
|
+
occupied canonical paths, unknown fields, invalid references, or validation/generation
|
|
184
|
+
failures reject the operation without intended publication. Successful operations update
|
|
185
|
+
the owning collection and generate all views through the existing staged mutation runner.
|
|
186
|
+
There is no overwrite mode. Existing YAML comments and unrelated parent fields are retained.
|
|
187
|
+
The result has `operation`, `root`, `affectedIds` (new node and parent), `canonicalPaths`, and
|
|
188
|
+
`generatedPaths`; append `--json` for one JSON document. Publication has the filesystem
|
|
189
|
+
interruption limitations described in the [architecture guide](architecture.md#staged-lifecycle-mutation).
|
|
190
|
+
|
|
132
191
|
## Guarantee mutations
|
|
133
192
|
|
|
134
193
|
All mutation results identify the selected root, affected Guarantee IDs, canonical paths, and
|
|
@@ -242,10 +301,12 @@ no .agents/ or .claude/ -> .agents/skills/update-ddduck-specs/SKILL.md
|
|
|
242
301
|
```
|
|
243
302
|
|
|
244
303
|
The installer writes `.ddduck/agent-skills.lock.json` with the selected canonical path, host
|
|
245
|
-
adapters, package version,
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
304
|
+
adapters, package version, installed file manifest, `SKILL.md` SHA-256, and whole-bundle SHA-256.
|
|
305
|
+
It installs `SKILL.md` and its bundled `references/` directory, refuses locally modified managed
|
|
306
|
+
files, and upgrades legacy single-file locks without overwriting extra local files. It does not
|
|
307
|
+
create a host directory for a host that is absent from the repository, except for the `.agents/`
|
|
308
|
+
fallback when no host directory exists. On success it prints one result line naming the action
|
|
309
|
+
(`created`, `upgraded`, or `no-op`), the repository, the canonical skill path, and the lock path. It does not accept
|
|
249
310
|
`--json`.
|
|
250
311
|
|
|
251
312
|
Invoke the skill from the relevant host:
|
|
@@ -256,5 +317,5 @@ Claude: /update-ddduck-specs
|
|
|
256
317
|
```
|
|
257
318
|
|
|
258
319
|
The skill defaults to plan-only; changing a product model requires explicit apply authorization.
|
|
259
|
-
Its workflow behavior is defined by the installed
|
|
260
|
-
[canonical
|
|
320
|
+
Its workflow behavior is defined by the installed skill bundle (see its packaged
|
|
321
|
+
[canonical entrypoint](../skills/update-ddduck-specs/SKILL.md)).
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# From a product question to a planning brief
|
|
2
|
+
|
|
3
|
+
Start with the question, not a YAML record. Reuse an existing proposal or design brief;
|
|
4
|
+
the [optional change brief](templates/change-brief.md) is a checklist for missing context,
|
|
5
|
+
not a required document. A short note can be enough.
|
|
6
|
+
|
|
7
|
+
## Explore, decide, then model
|
|
8
|
+
|
|
9
|
+
1. Identify the actor and problem with concrete success and refusal examples.
|
|
10
|
+
2. Separate observed evidence from accepted intent and open questions. Record alternatives
|
|
11
|
+
and contradictory evidence with provenance.
|
|
12
|
+
3. Make the semantic decision explicit. Record the decision and rationale in the brief;
|
|
13
|
+
create an ADR only for a consequential, durable choice that needs its own history.
|
|
14
|
+
4. Update canonical YAML only where accepted meaning changed. Preserve stable IDs and use
|
|
15
|
+
lifecycle commands for Guarantee transitions. A partial update can encode independent
|
|
16
|
+
accepted obligations while an unresolved question stays in prose. Zero model change is
|
|
17
|
+
correct when the existing model suffices, the change is implementation-only, or no
|
|
18
|
+
decision has been reached.
|
|
19
|
+
5. Validate structure and refresh derived views, then hand selected context to the planner
|
|
20
|
+
with delivery intent, scope, exclusions, dependencies, and acceptance examples.
|
|
21
|
+
|
|
22
|
+
**Observed** means evidence was inspected, not endorsed. **Open** means undecided.
|
|
23
|
+
**Accepted** means intended behavior, possibly not implemented. **Rejected** records an
|
|
24
|
+
alternative and rationale. These are prose labels, not new schema fields or Guarantee
|
|
25
|
+
statuses. A passing checker settles none of these semantic decisions.
|
|
26
|
+
|
|
27
|
+
For example, an admin capability need not be a member role. Existing code that grants one
|
|
28
|
+
from the other establishes observed behavior; whether that coupling is intended remains a
|
|
29
|
+
decision. Likewise, delivering a relay directive does not establish that the recipient
|
|
30
|
+
enacted it. Leave an unanswered question about who may authorize it open rather than
|
|
31
|
+
inventing an authority boundary.
|
|
32
|
+
|
|
33
|
+
## Model only what helps a decision
|
|
34
|
+
|
|
35
|
+
| Kind | Useful meaning | Counterexample to avoid |
|
|
36
|
+
| ------------ | -------------------------------------------------------------------------- | -------------------------------------------------- |
|
|
37
|
+
| Domain | A distinct product responsibility | A domain for each package or screen |
|
|
38
|
+
| Concept | Vocabulary needed to reason about behavior | A concept for every class, queue, or button |
|
|
39
|
+
| Guarantee | An observable obligation or invariant | “Best effort delivery” with no concrete commitment |
|
|
40
|
+
| Use case | An actor's goal and the obligations it requires, preserves, or establishes | An implementation task list |
|
|
41
|
+
| Relationship | A connection worth reviewing, with its meaning stated | Treating every connection as a causal dependency |
|
|
42
|
+
|
|
43
|
+
Interfaces are optional. A useful behavioral model can have none. Empty prerequisite lists
|
|
44
|
+
are not inherently wrong; add a reference when it expresses a real prerequisite, not to
|
|
45
|
+
make the diagram look complete.
|
|
46
|
+
|
|
47
|
+
For a small relay, introducing separate UI, transport, and receipt domains and
|
|
48
|
+
concepts would obscure the one responsibility under discussion. Three cohesive obligations
|
|
49
|
+
can distinguish authorization/refusal, the limit of delivery, and honest reporting. Do not
|
|
50
|
+
split every clause: split when meaning, change, or verification is independent. Remove an
|
|
51
|
+
extra Guarantee, domain, brief section, or review step when it answers no additional question.
|
|
52
|
+
Consumer practice is inspiration, not proof that its artifact count or boundaries are right.
|
|
53
|
+
|
|
54
|
+
The [relay walkthrough](https://github.com/diegomarino/ddduck/tree/main/examples/control-relay)
|
|
55
|
+
is a fictional educational scenario. The
|
|
56
|
+
[reminders scenario](https://github.com/diegomarino/ddduck/tree/main/examples/reminders)
|
|
57
|
+
illustrates refusal. Neither specifies a real product or runs an application.
|
|
58
|
+
|
|
59
|
+
## Author and review with existing tools
|
|
60
|
+
|
|
61
|
+
After hand edits, `generate` validates the staged source and generated outputs before
|
|
62
|
+
publishing derived views. It is the normal refresh action. Run operations serially against a root.
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
ddduck generate --root <root>
|
|
66
|
+
ddduck check --root <root>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
An optional `ddduck check --root <root> --source-only` diagnoses canonical edits before
|
|
70
|
+
generation; `query spec` can inspect individual view freshness when needed.
|
|
71
|
+
`check` is read-only and suitable for CI/final readback. Fresh generated views establish
|
|
72
|
+
consistency with source, not product acceptance or runtime correctness.
|
|
73
|
+
|
|
74
|
+
For each selected ID, compose the existing reads:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
ddduck query impact --id <id> --root <root> --json
|
|
78
|
+
ddduck query neighbors --id <id> --root <root> --json
|
|
79
|
+
ddduck query node --id <relationship-id> --root <root> --json
|
|
80
|
+
ddduck query context --id <id> --root <root> --json
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Impact follows reverse owns/requires/preserves/establishes/uses/guarantees edges.
|
|
84
|
+
Neighbors exposes direct incoming/outgoing edges, including explicit relationships. Read
|
|
85
|
+
the relationship record for its type, description, and constraints when needed. These are
|
|
86
|
+
review candidates requiring judgment; an empty result does not establish absence of risk.
|
|
87
|
+
To select explicit relationships, filter both edge lists by `edge.kind === 'relationship'`
|
|
88
|
+
and deduplicate by `edge.id`; a self-relationship appears in both lists. If the changed ID
|
|
89
|
+
is itself a Relationship, query its record and inspect its `from` and `to` endpoints:
|
|
90
|
+
neighbors matches endpoint IDs, not the relationship record's ID.
|
|
91
|
+
There is no separate review query. Do not infer transitive effects from relationship names.
|
|
92
|
+
Separate query calls are not an atomic snapshot: keep the source stable during review and
|
|
93
|
+
re-read if it changes. A source digest identifies the read source only when it did not race
|
|
94
|
+
a mutation; it does not make multiple reads transactionally consistent.
|
|
95
|
+
|
|
96
|
+
Inspect before and after roots when something moves or disappears. In the fictional
|
|
97
|
+
ownership fixture, the same Guarantee moves from Members to Reminders within the small
|
|
98
|
+
reference corpus:
|
|
99
|
+
review its owner, both domains' lists, wording, and use-case references without inventing a
|
|
100
|
+
new ID. Read-only current-root queries alone cannot recover removed context. External feature
|
|
101
|
+
references remain the consumer bridge's responsibility.
|
|
102
|
+
|
|
103
|
+
The comparison command is:
|
|
104
|
+
|
|
105
|
+
```text
|
|
106
|
+
ddduck diff --base <root> [--root <root>] [--json]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Its scope is canonical YAML records by stable identity, not decision/evidence
|
|
110
|
+
content, delivery artifacts, or runtime. Keep semantic review separate from structural
|
|
111
|
+
comparison and historical retention checking.
|
|
112
|
+
|
|
113
|
+
Use the implemented [domain, concept, and use-case creation commands](cli.md#creating-domains-concepts-and-use-cases)
|
|
114
|
+
to create records and update their parent lists coherently. Use-case creation consumes a
|
|
115
|
+
complete canonical YAML mapping; it does not invent obligations or decisions. See that
|
|
116
|
+
reference for syntax and refusal behavior. Schema-guided hand edits remain useful for
|
|
117
|
+
changes outside these helpers; update ownership/reference lists together before generation.
|
|
118
|
+
|
|
119
|
+
## Hand off intent, not just a graph
|
|
120
|
+
|
|
121
|
+
Select canonical context explicitly and link it from the receiving plan. Add why the work
|
|
122
|
+
is needed, scope and non-goals, success/refusal examples, dependencies, exclusions, open
|
|
123
|
+
decisions, and the next verification required. Reuse the exploration brief if it already
|
|
124
|
+
answers those questions; do not maintain duplicate requirement prose or a second backlog.
|
|
125
|
+
|
|
126
|
+
State evidence levels precisely: inspected implementation, declared evidence anchors,
|
|
127
|
+
structural checks, tests actually executed, and observed runtime are different claims.
|
|
128
|
+
An audit verdict is a declared assessment whose anchor integrity is checked; readiness
|
|
129
|
+
roles and valid links do not prove behavioral coverage. Keep anchors inside the supported
|
|
130
|
+
product root, and never create fake executable evidence or bridge documents to fill gaps.
|
package/docs/getting-started.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Getting started
|
|
2
2
|
|
|
3
3
|
This executable journey creates a product with one Domain, one Concept, and one Guarantee.
|
|
4
|
+
For help deciding what belongs in that model, start with the
|
|
5
|
+
[definition workflow](definition-workflow.md).
|
|
4
6
|
|
|
5
7
|
## Install ddduck
|
|
6
8
|
|
|
@@ -37,9 +39,9 @@ repository root, and without one writes it inside the new product root instead (
|
|
|
37
39
|
|
|
38
40
|
```mermaid
|
|
39
41
|
flowchart LR
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
Create[ddduck create] --> FreshCheck[ddduck check]
|
|
43
|
+
Source[Manual canonical YAML edit] --> Generate[ddduck generate]
|
|
44
|
+
Generate -->|invalid source| Diagnostic[Actionable diagnostic]
|
|
43
45
|
Generate --> FreshCheck[ddduck check]
|
|
44
46
|
FreshCheck --> Docs[Generated Markdown is fresh]
|
|
45
47
|
FreshCheck --> Graph[Generated graph is fresh]
|
|
@@ -48,54 +50,22 @@ flowchart LR
|
|
|
48
50
|
```bash
|
|
49
51
|
ddduck init ddd --id model:library
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
model: model:library
|
|
56
|
-
name: Catalog
|
|
57
|
-
purpose: Organize the library catalog.
|
|
58
|
-
concepts:
|
|
59
|
-
- concept:book
|
|
60
|
-
interfaces: []
|
|
61
|
-
guarantees: []
|
|
62
|
-
YAML
|
|
63
|
-
|
|
64
|
-
cat > ddd/model/concepts/book.yaml <<'YAML'
|
|
65
|
-
schemaVersion: "1"
|
|
66
|
-
kind: Concept
|
|
67
|
-
id: concept:book
|
|
68
|
-
model: model:library
|
|
69
|
-
ownerDomain: domain:catalog
|
|
70
|
-
name: Book
|
|
71
|
-
purpose: Identify a catalogued book.
|
|
72
|
-
YAML
|
|
73
|
-
|
|
74
|
-
cat > ddd/product.yaml <<'YAML'
|
|
75
|
-
schemaVersion: "1"
|
|
76
|
-
kind: Model
|
|
77
|
-
id: model:library
|
|
78
|
-
name: library
|
|
79
|
-
purpose: Define the library product.
|
|
80
|
-
domains:
|
|
81
|
-
- domain:catalog
|
|
82
|
-
useCases: []
|
|
83
|
-
decisions: []
|
|
84
|
-
YAML
|
|
85
|
-
|
|
86
|
-
ddduck check --root ddd --source-only
|
|
87
|
-
ddduck generate --root ddd
|
|
88
|
-
ddduck check --root ddd
|
|
53
|
+
ddduck create domain --id domain:catalog --name Catalog \
|
|
54
|
+
--purpose "Organize the library catalog." --root ddd
|
|
55
|
+
ddduck create concept --id concept:book --owner domain:catalog --name Book \
|
|
56
|
+
--purpose "Identify a catalogued book." --root ddd
|
|
89
57
|
ddduck create guarantee --origin catalog --classification invariant \
|
|
90
58
|
--owner domain:catalog --statement "A Book has a stable catalog identity." --root ddd
|
|
59
|
+
ddduck check --root ddd
|
|
91
60
|
ddduck query spec --root ddd --json
|
|
92
61
|
```
|
|
93
62
|
|
|
94
63
|
Default `check` requires both valid canonical source and fresh generated views. After a manual
|
|
95
|
-
source edit,
|
|
96
|
-
`
|
|
64
|
+
source edit, `ddduck generate --root ddd` validates the source and refreshes the views;
|
|
65
|
+
use `ddduck check --root ddd` for final readback or CI. To diagnose source without writing
|
|
66
|
+
anything, run `ddduck check --root ddd --source-only`. Do not edit `generated/` by hand.
|
|
97
67
|
|
|
98
|
-
|
|
99
|
-
|
|
68
|
+
Each successful `create` updates the owning collection and refreshes all generated views.
|
|
69
|
+
Guarantee creation allocates the first catalog invariant serial. The final query emits one JSON document suitable for a tool or
|
|
100
70
|
agent. See the [model reference](model-reference.md) before adding other node kinds, and use the
|
|
101
71
|
[CLI reference](cli.md) for the complete command contracts.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Optional change brief
|
|
2
|
+
|
|
3
|
+
Reuse an existing proposal, issue, or design brief first. Keep only sections that help a
|
|
4
|
+
decision; a few paragraphs may suffice. This file is not a canonical schema or a second backlog.
|
|
5
|
+
Use Observed, Open, Accepted, and Rejected as prose labels, never YAML lifecycle states.
|
|
6
|
+
|
|
7
|
+
## Problem and actor
|
|
8
|
+
|
|
9
|
+
Who needs what outcome, and why now?
|
|
10
|
+
|
|
11
|
+
## Examples and refusal cases
|
|
12
|
+
|
|
13
|
+
Describe concrete success, refusal, and uncertainty cases. Label expected behavior separately
|
|
14
|
+
from results actually observed.
|
|
15
|
+
|
|
16
|
+
## Observed evidence
|
|
17
|
+
|
|
18
|
+
Cite paths and headings, symbols, or tests; state provenance, inspected scope, and gaps.
|
|
19
|
+
Observed implementation may be a bug rather than intended behavior.
|
|
20
|
+
|
|
21
|
+
## Accepted constraints
|
|
22
|
+
|
|
23
|
+
Record accepted intent and its authority. Accepted does not mean implemented.
|
|
24
|
+
|
|
25
|
+
## Alternatives
|
|
26
|
+
|
|
27
|
+
Include doing nothing or reusing existing obligations, with tradeoffs and rejected choices.
|
|
28
|
+
|
|
29
|
+
## Open questions and conflicts
|
|
30
|
+
|
|
31
|
+
What still needs a decision? Which work depends on it? Keep unresolved assertions out of YAML.
|
|
32
|
+
|
|
33
|
+
## Affected canonical IDs
|
|
34
|
+
|
|
35
|
+
Select existing records explicitly; label proposed additions as proposals. Use fenced code
|
|
36
|
+
for IDs from another product when the repository scans documentation references.
|
|
37
|
+
Record partial or zero-model-change outcomes with a reason.
|
|
38
|
+
|
|
39
|
+
## Decision and rationale
|
|
40
|
+
|
|
41
|
+
Record who accepted what and why, plus what remains open. Link an ADR only if the durable
|
|
42
|
+
decision warrants a separate artifact.
|
|
43
|
+
|
|
44
|
+
## Planning handoff
|
|
45
|
+
|
|
46
|
+
Link the receiving plan and provide intent, scope, non-goals, acceptance/refusal examples,
|
|
47
|
+
dependencies, exclusions, unresolved decisions, and selected canonical context. State what
|
|
48
|
+
was verified, by which commands, and what has not been executed. A graph is not a delivery plan.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ddduck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "An opinionated DDD framework for authoring, validating, and visualizing machine-readable product-spec models.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ddd",
|
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
"policies/",
|
|
33
33
|
"skills/",
|
|
34
34
|
"docs/architecture.md",
|
|
35
|
+
"docs/definition-workflow.md",
|
|
36
|
+
"docs/templates/change-brief.md",
|
|
35
37
|
"docs/getting-started.md",
|
|
36
38
|
"docs/model.md",
|
|
37
39
|
"docs/model-reference.md",
|
|
@@ -72,6 +74,6 @@
|
|
|
72
74
|
"prettier": "^3.9.6"
|
|
73
75
|
},
|
|
74
76
|
"overrides": {
|
|
75
|
-
"fast-uri": "^3.1.
|
|
77
|
+
"fast-uri": "^3.1.7"
|
|
76
78
|
}
|
|
77
79
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://ddduck.local/schemas/model-diff.schema.json",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"additionalProperties": false,
|
|
6
|
+
"required": [
|
|
7
|
+
"schemaVersion",
|
|
8
|
+
"kind",
|
|
9
|
+
"before",
|
|
10
|
+
"after",
|
|
11
|
+
"scope",
|
|
12
|
+
"excludedScopes",
|
|
13
|
+
"added",
|
|
14
|
+
"removed",
|
|
15
|
+
"changed",
|
|
16
|
+
"relocated"
|
|
17
|
+
],
|
|
18
|
+
"properties": {
|
|
19
|
+
"schemaVersion": { "const": "1" },
|
|
20
|
+
"kind": { "const": "ModelDiff" },
|
|
21
|
+
"before": { "$ref": "#/$defs/identity" },
|
|
22
|
+
"after": { "$ref": "#/$defs/identity" },
|
|
23
|
+
"scope": { "const": "canonical-yaml-only" },
|
|
24
|
+
"excludedScopes": { "const": ["decision-content", "evidence-content", "delivery-artifacts", "runtime"] },
|
|
25
|
+
"added": { "type": "array", "items": { "$ref": "#/$defs/record" } },
|
|
26
|
+
"removed": { "type": "array", "items": { "$ref": "#/$defs/record" } },
|
|
27
|
+
"changed": {
|
|
28
|
+
"type": "array",
|
|
29
|
+
"items": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"additionalProperties": false,
|
|
32
|
+
"required": ["id", "kind", "changes"],
|
|
33
|
+
"properties": {
|
|
34
|
+
"id": { "type": "string" },
|
|
35
|
+
"kind": { "type": "string" },
|
|
36
|
+
"changes": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/change" } }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"relocated": {
|
|
41
|
+
"type": "array",
|
|
42
|
+
"items": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"additionalProperties": false,
|
|
45
|
+
"required": ["id", "kind", "beforePath", "afterPath"],
|
|
46
|
+
"properties": {
|
|
47
|
+
"id": { "type": "string" },
|
|
48
|
+
"kind": { "type": "string" },
|
|
49
|
+
"beforePath": { "type": "string" },
|
|
50
|
+
"afterPath": { "type": "string" }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"$defs": {
|
|
56
|
+
"identity": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"additionalProperties": false,
|
|
59
|
+
"required": ["modelId", "sourceDigest"],
|
|
60
|
+
"properties": {
|
|
61
|
+
"modelId": { "type": "string", "pattern": "^model:" },
|
|
62
|
+
"sourceDigest": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"record": {
|
|
66
|
+
"type": "object",
|
|
67
|
+
"additionalProperties": false,
|
|
68
|
+
"required": ["id", "kind", "sourcePath", "node"],
|
|
69
|
+
"properties": {
|
|
70
|
+
"id": { "type": "string" },
|
|
71
|
+
"kind": { "type": "string" },
|
|
72
|
+
"sourcePath": { "type": "string" },
|
|
73
|
+
"node": { "type": "object" }
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"change": {
|
|
77
|
+
"type": "object",
|
|
78
|
+
"additionalProperties": false,
|
|
79
|
+
"required": ["path", "beforePresent", "afterPresent"],
|
|
80
|
+
"properties": {
|
|
81
|
+
"path": { "type": "string", "pattern": "^(?:/(?:[^~/]|~[01])*)+$" },
|
|
82
|
+
"beforePresent": { "type": "boolean" },
|
|
83
|
+
"afterPresent": { "type": "boolean" },
|
|
84
|
+
"before": {},
|
|
85
|
+
"after": {}
|
|
86
|
+
},
|
|
87
|
+
"allOf": [
|
|
88
|
+
{
|
|
89
|
+
"if": { "properties": { "beforePresent": { "const": true } } },
|
|
90
|
+
"then": { "required": ["before"] },
|
|
91
|
+
"else": { "not": { "required": ["before"] } }
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"if": { "properties": { "afterPresent": { "const": true } } },
|
|
95
|
+
"then": { "required": ["after"] },
|
|
96
|
+
"else": { "not": { "required": ["after"] } }
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"anyOf": [
|
|
100
|
+
{ "properties": { "beforePresent": { "const": true } } },
|
|
101
|
+
{ "properties": { "afterPresent": { "const": true } } }
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
package/scripts/ddduck.mjs
CHANGED
|
@@ -44,6 +44,9 @@ import { defaultConfigIgnore, findRepositoryRoot, loadDdduckConfig } from "./lib
|
|
|
44
44
|
import { installSkill } from "./lib/skill-installer.mjs";
|
|
45
45
|
import { runQuery } from "./query-model.mjs";
|
|
46
46
|
import { CliUsageError, parseCommandArgs, renderHelp, writeCliError } from "./lib/cli-contract.mjs";
|
|
47
|
+
import { buildAuthoringPlan } from "./lib/product-authoring.mjs";
|
|
48
|
+
import { parseYamlMapping } from "./lib/product-layout.mjs";
|
|
49
|
+
import { compareProductRoots, renderProductDiff } from "./lib/product-diff.mjs";
|
|
47
50
|
|
|
48
51
|
const frameworkRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
49
52
|
|
|
@@ -68,7 +71,7 @@ function run(args) {
|
|
|
68
71
|
const [command, ...commandArgs] = args;
|
|
69
72
|
if (
|
|
70
73
|
!command ||
|
|
71
|
-
!["init", "check", "generate", "query", "install", "create", "move", "split", "retire"].includes(command)
|
|
74
|
+
!["init", "check", "generate", "query", "diff", "install", "create", "move", "split", "retire"].includes(command)
|
|
72
75
|
) {
|
|
73
76
|
throw new CliUsageError(`Unknown command ${command ?? "(missing)"}`);
|
|
74
77
|
}
|
|
@@ -93,10 +96,24 @@ function run(args) {
|
|
|
93
96
|
runQuery(commandArgs);
|
|
94
97
|
return;
|
|
95
98
|
}
|
|
99
|
+
if (command === "diff") {
|
|
100
|
+
const { options } = parseCommandArgs(commandArgs, {
|
|
101
|
+
options: { base: { value: true }, root: { value: true }, json: { value: false } },
|
|
102
|
+
});
|
|
103
|
+
const base = requiredOption(options, "base", "diff requires --base <previous-product-root>");
|
|
104
|
+
const root = resolveProductRoot({ explicitRoot: options.root });
|
|
105
|
+
const report = compareProductRoots(path.resolve(base), root);
|
|
106
|
+
process.stdout.write(`${options.json ? JSON.stringify(report) : renderProductDiff(report)}\n`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
96
109
|
if (command === "install") {
|
|
97
110
|
install(commandArgs);
|
|
98
111
|
return;
|
|
99
112
|
}
|
|
113
|
+
if (command === "create" && ["domain", "concept", "use-case"].includes(commandArgs[0])) {
|
|
114
|
+
createNode(commandArgs);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
100
117
|
if (["create", "move", "split", "retire"].includes(command)) {
|
|
101
118
|
transitionGuarantee(command, commandArgs);
|
|
102
119
|
return;
|
|
@@ -105,7 +122,7 @@ function run(args) {
|
|
|
105
122
|
|
|
106
123
|
/**
|
|
107
124
|
* Implement `ddduck install skill update-ddduck-specs`: install the bundled
|
|
108
|
-
* host skill
|
|
125
|
+
* host skill bundle and .ddduck/agent-skills.lock.json into --repo.
|
|
109
126
|
* @param {string[]} args - Arguments after the `install` command word.
|
|
110
127
|
* @returns {void}
|
|
111
128
|
*/
|
|
@@ -358,6 +375,38 @@ function generate(args) {
|
|
|
358
375
|
writeProductOperationResult(result, options.json);
|
|
359
376
|
}
|
|
360
377
|
|
|
378
|
+
function createNode(args) {
|
|
379
|
+
const kind = args[0];
|
|
380
|
+
const shared = { root: { value: true }, json: { value: false } };
|
|
381
|
+
const fields =
|
|
382
|
+
kind === "use-case"
|
|
383
|
+
? { file: { value: true } }
|
|
384
|
+
: {
|
|
385
|
+
id: { value: true },
|
|
386
|
+
name: { value: true },
|
|
387
|
+
purpose: { value: true },
|
|
388
|
+
...(kind === "concept" ? { owner: { value: true } } : {}),
|
|
389
|
+
};
|
|
390
|
+
const { options } = parseCommandArgs(args, {
|
|
391
|
+
positionals: { min: 1, max: 1 },
|
|
392
|
+
options: { ...shared, ...fields },
|
|
393
|
+
});
|
|
394
|
+
const required = (field) => requiredOption(options, field, `create ${kind} requires --${field} <value>`);
|
|
395
|
+
const request =
|
|
396
|
+
kind === "use-case"
|
|
397
|
+
? { kind: "UseCase", node: parseYamlMapping(path.resolve(required("file"))) }
|
|
398
|
+
: {
|
|
399
|
+
kind: kind === "domain" ? "Domain" : "Concept",
|
|
400
|
+
id: required("id"),
|
|
401
|
+
name: required("name"),
|
|
402
|
+
purpose: required("purpose"),
|
|
403
|
+
...(kind === "concept" ? { ownerDomain: required("owner") } : {}),
|
|
404
|
+
};
|
|
405
|
+
const root = resolveProductRoot({ explicitRoot: options.root });
|
|
406
|
+
const result = runProductOperation({ root, transform: (snapshot) => buildAuthoringPlan(snapshot, request) });
|
|
407
|
+
writeProductOperationResult(result, options.json);
|
|
408
|
+
}
|
|
409
|
+
|
|
361
410
|
/**
|
|
362
411
|
* Implement the guarantee lifecycle commands create, move, split, and retire:
|
|
363
412
|
* parse per-command options, require a registered decision for split/retire,
|
|
@@ -137,7 +137,7 @@ function renderModelOverview(view) {
|
|
|
137
137
|
lines.push("## Decisions", "");
|
|
138
138
|
for (const decision of view.decisions) lines.push(`- \`${decision}\``);
|
|
139
139
|
lines.push("");
|
|
140
|
-
return lines.join("\n")
|
|
140
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
function renderNodeList(lines, title, nodes) {
|