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 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 — `check`,
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, 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
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 `SKILL.md` (and its packaged
260
- [canonical source](../skills/update-ddduck-specs/SKILL.md)).
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.
@@ -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
- Source[Manual canonical YAML edit] --> SourceCheck[ddduck check --source-only]
41
- SourceCheck -->|valid| Generate[ddduck generate]
42
- SourceCheck -->|invalid| Diagnostic[Actionable diagnostic]
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
- cat > ddd/model/domains/catalog.yaml <<'YAML'
52
- schemaVersion: "1"
53
- kind: Domain
54
- id: domain:catalog
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, use `check --source-only`, run `generate`, then use default `check`. Do not edit
96
- `generated/` by hand.
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
- The successful `create` allocates the first catalog invariant serial, adds it to the Domain, and
99
- refreshes all generated views. The final query emits one JSON document suitable for a tool or
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.1.2",
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.5"
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
+ }
@@ -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 adapter and .ddduck/agent-skills.lock.json into --repo.
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) {