kataster 0.3.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/.claude/THIRD_PARTY_NOTICES.md +4 -0
  2. package/.claude/agents/acceptance-tester.md +7 -2
  3. package/.claude/agents/code-reviewer.md +7 -2
  4. package/.claude/agents/domain-architect.md +7 -3
  5. package/.claude/agents/researcher.md +77 -0
  6. package/.claude/agents/ux-designer.md +42 -47
  7. package/.claude/hooks/validate-docs.ts +17 -1
  8. package/.claude/lib/architecture.ts +3 -33
  9. package/.claude/lib/backlog.ts +2 -0
  10. package/.claude/lib/config.ts +49 -0
  11. package/.claude/lib/discovery.ts +200 -32
  12. package/.claude/lib/engineering.ts +18 -6
  13. package/.claude/lib/gaps.ts +179 -0
  14. package/.claude/lib/glob.ts +39 -0
  15. package/.claude/lib/repo.ts +143 -19
  16. package/.claude/lib/research.ts +303 -0
  17. package/.claude/lib/slices.ts +529 -0
  18. package/.claude/package.json +1 -0
  19. package/.claude/settings.json +2 -2
  20. package/.claude/skills/adopt/SKILL.md +111 -34
  21. package/.claude/skills/adopt/scripts/gap-register.ts +61 -0
  22. package/.claude/skills/adopt/scripts/repo-map.ts +34 -2
  23. package/.claude/skills/adopt/scripts/slice-doc.ts +117 -0
  24. package/.claude/skills/adopt/scripts/slice.ts +163 -0
  25. package/.claude/skills/adopt/templates/context-map.as-is.md +22 -0
  26. package/.claude/skills/architect/SKILL.md +6 -1
  27. package/.claude/skills/backlog/SKILL.md +2 -2
  28. package/.claude/skills/bootstrap/SKILL.md +9 -7
  29. package/.claude/skills/domain-model/CONTEXT-FORMAT.md +1 -1
  30. package/.claude/skills/domain-model/SKILL.md +24 -4
  31. package/.claude/skills/event-storming/SKILL.md +3 -2
  32. package/.claude/skills/event-storming/schema/events.schema.json +2 -0
  33. package/.claude/skills/event-storming/templates/events.example.yaml +6 -0
  34. package/.claude/skills/feature/SKILL.md +16 -13
  35. package/.claude/skills/feature/scripts/intake.ts +1 -1
  36. package/.claude/skills/grilling/SKILL.md +15 -0
  37. package/.claude/skills/groom-backlog/SKILL.md +19 -17
  38. package/.claude/skills/implement/SKILL.md +199 -78
  39. package/.claude/skills/implement/scripts/characterize.ts +252 -0
  40. package/.claude/skills/implement/scripts/check-scenarios.ts +1 -1
  41. package/.claude/skills/implement/scripts/story-context.ts +65 -1
  42. package/.claude/skills/implement/scripts/verify.ts +4 -1
  43. package/.claude/skills/improve-codebase-architecture/SKILL.md +4 -1
  44. package/.claude/skills/prototype/SKILL.md +1 -1
  45. package/.claude/skills/research/SKILL.md +149 -0
  46. package/.claude/skills/research/scripts/research-check.ts +105 -0
  47. package/.claude/skills/research/templates/research.md +29 -0
  48. package/.claude/skills/review-stories/SKILL.md +49 -19
  49. package/.claude/skills/shape/SKILL.md +26 -7
  50. package/.claude/skills/tdd/SKILL.md +10 -0
  51. package/.claude/skills/user-stories/SKILL.md +128 -51
  52. package/.claude/skills/user-stories/schema/story.schema.json +10 -3
  53. package/.claude/skills/user-stories/scripts/lint-story.ts +56 -0
  54. package/.claude/skills/user-stories/templates/story-template.md +6 -2
  55. package/.claude/skills/ux-design/SKILL.md +9 -7
  56. package/.opencode/agents/acceptance-tester.md +8 -3
  57. package/.opencode/agents/code-reviewer.md +8 -3
  58. package/.opencode/agents/domain-architect.md +7 -3
  59. package/.opencode/agents/researcher.md +91 -0
  60. package/.opencode/agents/ux-designer.md +42 -52
  61. package/.opencode/commands/adopt.md +1 -1
  62. package/.opencode/commands/bootstrap.md +1 -1
  63. package/.opencode/commands/feature.md +1 -1
  64. package/.opencode/commands/groom-backlog.md +1 -1
  65. package/.opencode/commands/implement.md +1 -1
  66. package/.opencode/commands/research.md +7 -0
  67. package/.opencode/commands/review-stories.md +1 -1
  68. package/.opencode/commands/shape.md +1 -1
  69. package/AGENTS.md +30 -0
  70. package/CHANGELOG.md +73 -0
  71. package/README.md +108 -24
  72. package/guide/brownfield.md +41 -12
  73. package/guide/concepts.md +23 -6
  74. package/guide/greenfield.md +4 -3
  75. package/package.json +1 -1
  76. package/src/cli.mjs +42 -4
  77. package/src/detect.mjs +9 -0
  78. package/src/install.mjs +66 -6
  79. package/templates/docs/domain/context-map.md +5 -1
  80. package/templates/docs/domain/events.yaml +3 -0
  81. package/templates/docs/research/README.md +70 -0
  82. package/templates/docs/stories/README.md +12 -3
  83. package/templates/docs/workflow.md +76 -19
  84. package/.claude/agents/lead-dev.md +0 -52
  85. package/.claude/agents/product-owner.md +0 -39
  86. package/.claude/agents/requirements-engineer.md +0 -44
  87. package/.opencode/agents/lead-dev.md +0 -69
  88. package/.opencode/agents/product-owner.md +0 -59
  89. package/.opencode/agents/requirements-engineer.md +0 -58
@@ -2,7 +2,7 @@
2
2
  name: adopt
3
3
  description: Brownfield entry point – maps the repository, adopts it in slices (one domain plus at most one shared library), drafts the domain model with evidence, grills the user, documents each package where it lives and records the gap; it derives stories only when asked (--stories). Use when the workflow is dropped into a repository that already has code.
4
4
  disable-model-invocation: true
5
- argument-hint: "[scope, e.g. 'apps/billing' or 'the order process' – empty = map the repository and choose a slice; add --stories to derive stories now]"
5
+ argument-hint: "[package path or concept scope (--paths/--route/--table/--event/--cluster/term) – empty = map the repository and choose a slice; add --stories to derive stories now]"
6
6
  ---
7
7
 
8
8
  # Adopt an Existing Repository
@@ -20,10 +20,12 @@ kebab-case slug for the adoption (`adoption-<scope>`) for the report file.
20
20
  1. Read `.claude/project.json`. If it is missing, tell the user to run `npx kataster init` first and stop.
21
21
  2. If the repository has no code yet, this is the wrong command: use `/bootstrap`.
22
22
  3. Read what already exists, before analysing anything: `CONTEXT.md`, `docs/product/vision.md`,
23
- `docs/domain/events.yaml`, `docs/domain/context-map.md`, `docs/architecture/data-model.md`,
24
- `docs/architecture/repo-map.md`, `docs/engineering/conventions.md`, `docs/adr/`, `README*`, existing `docs/`
25
- and any `AGENTS.md`/`CLAUDE.md`. Existing documentation is **primary evidence** – it outranks your inference
26
- from code. Contradictions between docs and code are findings, not something to resolve silently.
23
+ `docs/domain/events.yaml`, `docs/domain/context-map.md` (**target**), `docs/domain/context-map.as-is.md`
24
+ (**as-is**), `docs/architecture/data-model.md`, `docs/architecture/repo-map.md`,
25
+ `docs/engineering/conventions.md`, `docs/adr/`, `README*`, existing `docs/` and any `AGENTS.md`/`CLAUDE.md`.
26
+ Existing documentation is **primary evidence** – it outranks your inference from code. The target map is
27
+ read-only here: it says where the project wants to go, not what the code does. Contradictions between docs and
28
+ code are findings, not something to resolve silently.
27
29
 
28
30
  ## Phase 0 – Repository map (deterministic, no questions)
29
31
 
@@ -33,10 +35,10 @@ Run:
33
35
  node .claude/skills/adopt/scripts/repo-map.ts
34
36
  ```
35
37
 
36
- It scans manifests and git history and writes:
38
+ It scans manifests, source files and git history and writes:
37
39
 
38
- - `docs/architecture/repo-map.json` – packages, paths, kinds, internal dependencies, consumers, adoption ranking,
39
- and the build tool's own graph when it answers (Turbo, Nx, Bazel)
40
+ - `docs/architecture/repo-map.json` – packages, paths, kinds, roles, internal dependencies, consumers, adoption
41
+ ranking, concept-slice suggestions, and the build tool's own graph when it answers (Turbo, Nx, Bazel)
40
42
  - `docs/architecture/repo-map.md` – the same as a readable map (never edit it by hand)
41
43
  - the generated repository block in `AGENTS.md` – tooling, layout, commands, adopted slices
42
44
  - `.claude/project.json` → `repo.tooling` – Nx, Turborepo, Bazel, pnpm/npm/yarn workspaces, Go, Cargo, Gradle, Maven
@@ -44,20 +46,47 @@ It scans manifests and git history and writes:
44
46
  Read the map yourself. **Do not hand-scan the repository for packages**; if the map is stale, run it again.
45
47
  The map is facts, not domain: it says where code lives and what depends on what, never what a word means.
46
48
 
49
+ Pay attention to two readings the map adds for layered repositories:
50
+
51
+ - **Layer packages** – generic names such as `services`, `repositories` or `utils` that host many concepts. They
52
+ are layers, not domains; do not adopt “the services package” as if it were one bounded context.
53
+ - **Concept slices** – clusters that share a file stem or a table across layers (`ordersController`,
54
+ `orderService`, `orderRepository`, `orders` table). They are slice *suggestions*, not confirmed boundaries.
55
+
47
56
  ## Phase 1 – Choose the slice (one round, at most three questions)
48
57
 
49
- The map suggests a first slice. Ask, each with a recommended answer:
58
+ The scope is either a package path, a **concept scope** or a mix. A concept scope can be:
59
+
60
+ - a glob set: `--paths "src/api/order*,src/services/order*,db/migrations/*order*"`
61
+ - an entry point: `--route "POST /orders"`, `--table orders`, `--event EVT-OrderPlaced`
62
+ - a suggestion from the map: `--cluster order`
63
+ - a free term: `--term order`
64
+
65
+ Resolve a non-package scope deterministically before asking:
66
+
67
+ ```bash
68
+ node .claude/skills/adopt/scripts/slice.ts --cluster order
69
+ node .claude/skills/adopt/scripts/slice.ts --table orders
70
+ node .claude/skills/adopt/scripts/slice.ts --route "POST /orders"
71
+ ```
72
+
73
+ The resolver follows imports in both directions, adds same-concept files from other layers and prints the
74
+ candidate files with reasons. **It is evidence, not a verdict** – show the candidates and let the user confirm the
75
+ scope. If the resolver finds nothing, widen it with `--paths` or `--term`; do not invent a boundary.
76
+
77
+ Ask, each with a recommended answer:
50
78
 
51
- 1. **Slice**: the suggested domain plus its most-used shared library, or a different one? Recommended: the
52
- suggestion from `repo-map.md` – it weighs recent churn, fan-in and kind, not size.
79
+ 1. **Slice**: the suggested concept or domain plus its most-used shared library, or a different one? Recommended:
80
+ the suggestion from `repo-map.md` – in a layered repository, prefer the concept slice over the package.
53
81
  2. **Intent**: a domain model for future work, a map for onboarding, or a backlog for a rewrite? Recommended:
54
82
  the domain model first; the other two follow from it.
55
83
  3. **Language**: confirm `languages.ui` and `languages.artifacts` from the profile. Recommended: what the code
56
84
  and UI already use.
57
85
 
58
- Hard rule: **one domain plus at most one shared library per session.** A simple repository without monorepo
59
- tooling is one slice – then the whole repository is in scope. Never "the whole monorepo"; a shallow map of 80
60
- packages is worse than a confirmed model of two.
86
+ Hard rule: **one concept or domain plus at most one shared library per session.** The scope may cross packages
87
+ and layers – a domain is not a package. A simple repository without monorepo tooling is one slice only when its
88
+ code is not layered; otherwise use the resolver. Never "the whole monorepo"; a shallow map of 80 packages is worse
89
+ than a confirmed model of two.
61
90
 
62
91
  Create `docs/domain/adoption-report.md` with the answers under `## Scope`; fill the rest as you go. If earlier
63
92
  slices are already adopted, name the next one and what changed since it was adopted (`git log --since=<date>`).
@@ -67,8 +96,9 @@ slices are already adopted, name the next one and what changed since it was adop
67
96
  Call the `skill` tool for `grilling`, `domain-model` and `architect` first: grilling sets how you ask,
68
97
  domain-model sets what you are looking for, architect decides what becomes an ADR.
69
98
 
70
- Dispatch `repo-analyst` subagents **in one message**, each with the slice path, the repository map path and one
71
- lens. They are read-only.
99
+ Dispatch `repo-analyst` subagents **in one message**, each with the confirmed scope (the package path or the
100
+ resolved file list), the repository map path and one lens. They are read-only. For a concept slice, say which
101
+ files are in scope and which layer they belong to; do not let an analyst widen the scope on its own.
72
102
 
73
103
  - **Monorepo / several packages**: three analysts.
74
104
  | Lens | What it looks for |
@@ -89,25 +119,46 @@ Merge the reports into one picture. Write the artifacts **as a draft**, every un
89
119
  - **`CONTEXT.md`** – only the slice's terms, grouped by guessed bounded context, competing spellings under
90
120
  `_Avoid_`, `[OPEN]` where the meaning is unclear. Do not touch terms of other slices.
91
121
  - **`docs/domain/events.yaml`** – 10–25 candidate events/commands/actors/read models/policies/flows for this
92
- slice. Every uncertainty becomes a hotspot (`HS-n`, `status: open`).
93
- - **`docs/domain/context-map.md`** – the boundaries the module structure suggests, marked as a proposal.
122
+ slice. Every uncertainty becomes a hotspot (`HS-n`, `status: open`). Every aggregate carries a status:
123
+ `as-is` with `evidence` (table, service method, path) when the code shows a factual consistency boundary, or
124
+ `unresolved` plus an open hotspot when the boundary is unclear. **Never `target` for a boundary inferred from
125
+ code** – a target is a decision the user makes, not a reading of the repository.
126
+ - **`docs/domain/context-map.as-is.md`** – the boundaries the module structure actually shows, with evidence.
127
+ Create it from `.claude/skills/adopt/templates/context-map.as-is.md` on the first slice (replace
128
+ `{{project}}`, `{{date}}` and `{{commit}}`) and add one `### BC-… – <name>` section per recovered context
129
+ (the gap register links those headings). **Never write
130
+ `docs/domain/context-map.md`** – that is the target map, owned by `/bootstrap`, `/shape`, `/feature`,
131
+ `/event-storming` and `/improve-codebase-architecture`. If no target exists yet, leave it absent; the gap
132
+ register shows “target not decided”.
94
133
  - **`docs/architecture/data-model.md`** – the **as-is** model for the slice, derived from schema and code.
95
134
  - **`docs/engineering/conventions.md`** – fill what the slice actually does: module layout, how a command runs,
96
135
  read models, "now", texts, test data, seam catalog. Include how the monorepo tooling is used.
97
136
  - **`docs/product/vision.md`** – only if missing or empty, reconstruct and mark `[OPEN]`.
98
137
  - **`docs/adr/`** – de-facto decisions worth recording, `status: proposed`. Only the user accepts.
99
- - **Package docs** – for the domain and the shared library (in a monorepo):
100
- ```bash
101
- node .claude/skills/adopt/scripts/package-doc.ts apps/billing
102
- node .claude/skills/adopt/scripts/package-doc.ts libs/shared-kernel
103
- ```
104
- A single-package repository has no package file: its root `AGENTS.md` is the package doc (`package-doc.ts .`
105
- deliberately refuses). The script writes `<package>/AGENTS.md` (source of truth) and `<package>/CLAUDE.md`
106
- (`@AGENTS.md`, for Claude Code's on-demand nested loading). It generates the facts block; **you** fill the prose
107
- during the grilling:
138
+ - **Domain docs** – choose the mechanism by scope, not by habit:
139
+ - **Real domain package** (the code lives in one package): write `<package>/AGENTS.md` + `<package>/CLAUDE.md`
140
+ with `package-doc.ts`:
141
+ ```bash
142
+ node .claude/skills/adopt/scripts/package-doc.ts apps/billing
143
+ node .claude/skills/adopt/scripts/package-doc.ts libs/shared-kernel
144
+ ```
145
+ - **Cross-cutting concept slice** (the domain is spread over `api/`, `services/`, `repositories/`, …): write a
146
+ path-scoped document under `.claude/rules/` instead, so both harnesses load it when a matching file is
147
+ touched:
148
+ ```bash
149
+ node .claude/skills/adopt/scripts/slice-doc.ts order \
150
+ --context BC-Order \
151
+ --paths "src/api/order*,src/services/order*,src/repositories/order*" \
152
+ --title "Order"
153
+ ```
154
+ Do **not** also write a package `AGENTS.md` for the same concept – one mechanism per scope.
155
+ A single-package repository without layers has no package file: its root `AGENTS.md` is the package doc
156
+ (`package-doc.ts .` deliberately refuses). The package script writes `<package>/AGENTS.md` (source of truth) and
157
+ `<package>/CLAUDE.md` (`@AGENTS.md`, for Claude Code's on-demand nested loading). It generates the facts block;
158
+ **you** fill the prose during the grilling:
108
159
  - domain/service: Purpose, Key entities, External interfaces, Typical pitfalls, Boundaries
109
160
  - shared library: Purpose, Public API, Consumers, Compatibility rules, Typical pitfalls, Boundaries
110
- Keep each file under ~80 lines. This file is loaded only when someone works in that package.
161
+ Keep each file under ~80 lines. This file is loaded only when someone works in that package or path.
111
162
  - **Root `AGENTS.md`** – the generated block comes from the map. Below it, add only structure, commands, hard
112
163
  conventions and links. Keep at most ten bullets under `## Hard conventions`, each naming where it is enforced
113
164
  (`lint`, a test, `.claude/rules/<file>.md`, `docs/engineering/checks.yaml`) – a rule without an enforcement
@@ -138,7 +189,9 @@ skill says: numbered questions, each with a recommended answer, whole frontier p
138
189
  Questions brownfield needs, roughly in this order:
139
190
 
140
191
  1. **Terms** – one question per cluster of competing spellings, worst first.
141
- 2. **Boundaries** – which package owns which state; where a shared library ends and a domain begins.
192
+ 2. **Boundaries** – which package owns which state; where a shared library ends and a domain begins. For every
193
+ aggregate: is this the factual consistency boundary (`as-is`), is it unclear (`unresolved` + hotspot), or is
194
+ this the boundary we want (`target`, a decision)? Prefer `as-is`/`unresolved` over an invented aggregate.
142
195
  3. **Lifecycle** – states the schema allows, transitions it does not block.
143
196
  4. **Invariants** – what the code checks only in the UI; what must always hold.
144
197
  5. **Missing behaviour** – what has no trace: never needed, or a gap?
@@ -209,11 +262,22 @@ backlog. Write the table into `docs/domain/adoption-report.md`; open questions g
209
262
  `docs/stories/OPEN_QUESTIONS.md` or a hotspot. This keeps the model reviewable without committing to work the
210
263
  user has not prioritised.
211
264
 
265
+ Run the gap register after the grilling and before the report:
266
+
267
+ ```bash
268
+ node .claude/skills/adopt/scripts/gap-register.ts
269
+ ```
270
+
271
+ Every `as-is` aggregate gets a generated row with its evidence and its as-is/target context links; every
272
+ `unresolved` aggregate gets a row that names its open hotspot. Add the decisions outside the managed block.
273
+
212
274
  Only when the invocation contains `--stories`, or the user explicitly asks after seeing the table:
213
275
 
214
- - delegate to `requirements-engineer` with the **selected** gaps only, the confirmed IDs from `events.yaml`, the
215
- label `adoption:<slug>`, and `ui` for stories with a frontend part;
216
- - new stories start as `status: draft`; the ones the user wants first go through `/review-stories`.
276
+ - write the stories yourself as the spec engineer (the `user-stories` skill is the method) with the **selected**
277
+ gaps only, the confirmed IDs from `events.yaml`, `type: domain` with one `command` and its `aggregate`, `ui: true`
278
+ for stories with a frontend part, and the label `adoption:<slug>`;
279
+ - new stories start as `status: draft`; the ones the user wants first go through the spec linter and
280
+ `/review-stories`.
217
281
 
218
282
  **Do not write stories for behaviour that already works** – that turns the backlog into a description of the
219
283
  past. Existing behaviour is covered by its tests, and the domain model is the map. If an earlier run created
@@ -231,7 +295,8 @@ Date: YYYY-MM-DD · Commit: <sha> · Slice: <paths> · Analysts: <n> lenses
231
295
  ## Repository map (tooling, packages, why this slice)
232
296
  ## What the repository is (facts)
233
297
  ## Confirmed domain model
234
- - contexts, aggregates, glossary changes
298
+ - contexts, aggregates (with status), glossary changes
299
+ - as-is context map and the target links the gap register found
235
300
  ## Package documentation written
236
301
  - <path>/AGENTS.md + CLAUDE.md – what was confirmed and what stays [OPEN]
237
302
  ## Contradictions found and how they were resolved
@@ -246,7 +311,11 @@ Date: YYYY-MM-DD · Commit: <sha> · Slice: <paths> · Analysts: <n> lenses
246
311
  Mark the slice adopted so the next session sees the progress:
247
312
 
248
313
  ```bash
314
+ # a real domain package
249
315
  node .claude/skills/adopt/scripts/repo-map.ts --adopt apps/billing
316
+
317
+ # a concept slice across layers
318
+ node .claude/skills/adopt/scripts/repo-map.ts --adopt-slice order
250
319
  ```
251
320
 
252
321
  Then run `node .claude/skills/event-storming/scripts/validate-events.ts` and
@@ -268,8 +337,16 @@ checks created, gap candidates (and stories only if requested), ADRs proposed. T
268
337
 
269
338
  - **Never change production code in `/adopt`.** Tests and tooling are allowed only as the safety net above.
270
339
  - **Never present an inference as a fact.** Confidence and evidence, or a hotspot.
340
+ - **Aggregates are honest.** `as-is` needs evidence; `unresolved` needs an open hotspot; `target` is a user
341
+ decision. An invented aggregate is worse than an unresolved one – it makes the story look ready when it is not.
342
+ - **Adopt writes as-is, never target.** `docs/domain/context-map.as-is.md` and the `as-is` aggregates describe
343
+ the repository. The target map (`docs/domain/context-map.md`) and `target` aggregates are decisions made in
344
+ `/bootstrap`, `/shape` or `/feature` – do not “fix” them while adopting.
271
345
  - **The user owns the language.** You may propose a better word; you may not rename the domain on your own.
272
- - **Adopt in slices.** One domain plus at most one shared library. Never the whole monorepo in one pass.
346
+ - **Slice by concept, not by package.** In a layered repository a domain runs through `api/`, `services/` and
347
+ `repositories/`. Use the concept clusters and the resolver; never treat a generic `services`/`utils` package as
348
+ a domain just because it has a manifest.
349
+ - **Adopt in slices.** One concept or domain plus at most one shared library. Never the whole monorepo in one pass.
273
350
  - **The root `AGENTS.md` stays free of domain.** Structure, commands, hard conventions and links only.
274
351
  - **Package docs are short and on demand.** `<package>/AGENTS.md` + `<package>/CLAUDE.md`, under ~80 lines.
275
352
  - **Directory docs before glob rules.** `.claude/rules/` is for patterns a directory cannot express. Both
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Writes or checks `docs/domain/gap-register.md` – one generated row per as-is or unresolved aggregate.
3
+ *
4
+ * node .claude/skills/adopt/scripts/gap-register.ts # create or refresh the managed block
5
+ * node .claude/skills/adopt/scripts/gap-register.ts --check # exit 1 when the block is stale (advisory in verify)
6
+ * node .claude/skills/adopt/scripts/gap-register.ts --json
7
+ *
8
+ * Decisions and manual gaps live outside the managed block; a hand-written file without markers is left alone.
9
+ * A missing register is not an error: a project that has not adopted anything yet has no gap register.
10
+ */
11
+ import { checkGapRegister, writeGapRegister } from "../../../lib/gaps.ts";
12
+ import { rel, ROOT } from "../../../lib/discovery.ts";
13
+ import { join } from "node:path";
14
+
15
+ const args = process.argv.slice(2);
16
+
17
+ if (args.includes("--help") || args.includes("-h")) {
18
+ console.log(
19
+ [
20
+ "Usage: node .claude/skills/adopt/scripts/gap-register.ts [--check|--json]",
21
+ "",
22
+ " (no option) create or refresh docs/domain/gap-register.md",
23
+ " --check exit 1 when the generated block is stale",
24
+ " --json machine-readable result",
25
+ ].join("\n"),
26
+ );
27
+ process.exit(0);
28
+ }
29
+
30
+ if (args.includes("--check")) {
31
+ const result = checkGapRegister();
32
+ if (args.includes("--json")) {
33
+ console.log(JSON.stringify(result, null, 2));
34
+ } else if (result.ok) {
35
+ console.log(`OK – gap register: ${result.reason}.`);
36
+ } else {
37
+ console.error(`Gap register: ${result.reason}.`);
38
+ }
39
+ process.exit(result.ok ? 0 : 1);
40
+ }
41
+
42
+ try {
43
+ const result = writeGapRegister();
44
+ if (result === null) {
45
+ console.log("No gap register written.");
46
+ process.exit(0);
47
+ }
48
+ const path = rel(result.path);
49
+ if (result.status === "created") console.log(`Created ${path}.`);
50
+ else if (result.status === "updated") console.log(`Refreshed the generated block in ${path}.`);
51
+ else if (result.status === "unchanged") console.log(`Kept ${path}: already up to date.`);
52
+ else if (result.status === "kept") console.log(`Kept ${path}: it has no generated markers, so it is treated as hand-written.`);
53
+ if (args.includes("--json")) console.log(JSON.stringify({ ...result, path: rel(result.path) }, null, 2));
54
+ console.log(
55
+ `As-is and target context maps: ${rel(join(ROOT, "docs", "domain", "context-map.as-is.md"))} / ` +
56
+ `${rel(join(ROOT, "docs", "domain", "context-map.md"))}.`,
57
+ );
58
+ } catch (error) {
59
+ console.error(error instanceof Error ? error.message : String(error));
60
+ process.exit(1);
61
+ }
@@ -9,7 +9,7 @@
9
9
  * The map is generated from manifests and git history. It is not a domain model: it says where code lives,
10
10
  * how packages depend on each other and which slice to adopt first. Domain meaning comes from the grilling.
11
11
  */
12
- import { ROOT, findPackage, markAdopted, packageCounts, proposeSlices, readRepoMap, scanRepo, stableMap, updateProjectTooling, writeRepoMap, writeRootBlock } from "../../../lib/repo.ts";
12
+ import { ROOT, findPackage, markAdopted, markSliceAdopted, packageCounts, proposeSlices, readRepoMap, scanRepo, stableMap, updateProjectTooling, writeRepoMap, writeRootBlock } from "../../../lib/repo.ts";
13
13
  import { graphLabel, graphToolLabel } from "../../../lib/toolgraph.ts";
14
14
 
15
15
  const args = process.argv.slice(2);
@@ -24,6 +24,7 @@ function usage(): void {
24
24
  " --json print the map as JSON without writing",
25
25
  " --check exit 1 when the checked-in map no longer matches the repository",
26
26
  " --adopt <path> mark a package as adopted (after the grilling)",
27
+ " --adopt-slice <name> mark a concept slice as adopted",
27
28
  " --no-tool-graph skip the build-tool graph query (Turbo/Nx/Bazel); manifest graph only",
28
29
  ].join("\n"),
29
30
  );
@@ -53,6 +54,24 @@ if (adoptIndex !== -1) {
53
54
  process.exit(0);
54
55
  }
55
56
 
57
+ const adoptSliceIndex = args.indexOf("--adopt-slice");
58
+ if (adoptSliceIndex !== -1) {
59
+ const name = args[adoptSliceIndex + 1];
60
+ if (!name) {
61
+ console.error("--adopt-slice needs a concept name, e.g. order");
62
+ process.exit(1);
63
+ }
64
+ try {
65
+ markSliceAdopted(root, name);
66
+ console.log(`Adopted concept slice: ${name}`);
67
+ console.log("The root AGENTS.md block now lists it as adopted.");
68
+ } catch (error) {
69
+ console.error(error instanceof Error ? error.message : String(error));
70
+ process.exit(1);
71
+ }
72
+ process.exit(0);
73
+ }
74
+
56
75
  const map = scanRepo(root, {
57
76
  // --check stays fast and environment-independent; the graph is enrichment, not part of the stable map.
58
77
  toolGraph: !args.includes("--check") && !args.includes("--no-tool-graph"),
@@ -67,6 +86,13 @@ if (previous) {
67
86
  pkg.adoptedAt = old.adoptedAt;
68
87
  }
69
88
  }
89
+ for (const slice of map.slices ?? []) {
90
+ const old = previous.slices?.find((entry) => entry.name === slice.name);
91
+ if (old?.adopted) {
92
+ slice.adopted = true;
93
+ slice.adoptedAt = old.adoptedAt;
94
+ }
95
+ }
70
96
  }
71
97
 
72
98
  if (args.includes("--json")) {
@@ -114,10 +140,16 @@ console.log(`Packages: ${map.packages.length} (${packageCounts(map)})`);
114
140
  if (profileUpdated) console.log("Updated .claude/project.json → repo.tooling");
115
141
  if (agentsUpdated) console.log("Updated the generated repository block in AGENTS.md");
116
142
 
143
+ if (proposal.cluster) {
144
+ console.log("");
145
+ console.log(`Suggested concept slice: ${proposal.cluster.name} (${proposal.cluster.layers.join(", ") || "one layer"})`);
146
+ console.log(` Start with: node .claude/skills/adopt/scripts/slice.ts --cluster ${proposal.cluster.name}`);
147
+ console.log(` Mark it adopted after the grilling: node .claude/skills/adopt/scripts/repo-map.ts --adopt-slice ${proposal.cluster.name}`);
148
+ }
117
149
  if (proposal.domain) {
118
150
  console.log("");
119
151
  const label = proposal.domain.path === "." ? "root package" : proposal.domain.path;
120
- console.log(`Suggested first slice: ${label} (${proposal.domain.name})`);
152
+ console.log(`Suggested package slice: ${label} (${proposal.domain.name})`);
121
153
  if (proposal.library) console.log(` with shared library: ${proposal.library.path} (${proposal.library.name})`);
122
154
  console.log(`Mark it adopted after the grilling: node .claude/skills/adopt/scripts/repo-map.ts --adopt ${proposal.domain.path}`);
123
155
  }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Writes a path-scoped domain document for a concept slice that crosses packages or layers.
3
+ *
4
+ * node .claude/skills/adopt/scripts/slice-doc.ts order \
5
+ * --context BC-Order \
6
+ * --paths "src/api/order*,src/services/order*,src/repositories/order*" \
7
+ * --title "Order"
8
+ *
9
+ * The file lands in `.claude/rules/<slug>.md` with `paths:` frontmatter. Claude Code scopes it natively;
10
+ * OpenCode's plugin attaches it when a matching file is read or written. A real domain package still gets
11
+ * `<package>/AGENTS.md` through package-doc.ts – this command is for the cross-cutting case, not a duplicate.
12
+ */
13
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
14
+ import { dirname, join } from "node:path";
15
+ import { relPath } from "../../../lib/architecture.ts";
16
+ import { ROOT } from "../../../lib/repo.ts";
17
+
18
+ const args = process.argv.slice(2);
19
+
20
+ function usage(): void {
21
+ console.log(
22
+ [
23
+ "Usage: node .claude/skills/adopt/scripts/slice-doc.ts <slug> --paths <glob> [--paths <glob>] [--context BC-…] [--title <title>]",
24
+ "",
25
+ "Creates .claude/rules/<slug>.md for a concept slice that crosses packages or layers.",
26
+ ].join("\n"),
27
+ );
28
+ }
29
+
30
+ if (args.includes("--help") || args.includes("-h")) {
31
+ usage();
32
+ process.exit(0);
33
+ }
34
+
35
+ const slug = args.find((arg) => !arg.startsWith("--"));
36
+ const paths: string[] = [];
37
+ let context = "";
38
+ let title = "";
39
+ for (let i = 0; i < args.length; i += 1) {
40
+ const take = (): string | undefined => {
41
+ const value = args[i + 1];
42
+ if (!value || value.startsWith("--")) return undefined;
43
+ i += 1;
44
+ return value;
45
+ };
46
+ if (args[i] === "--paths") {
47
+ const value = take();
48
+ if (value) paths.push(...value.split(",").map((p) => p.trim()).filter(Boolean));
49
+ } else if (args[i] === "--context") {
50
+ context = take() ?? context;
51
+ } else if (args[i] === "--title") {
52
+ title = take() ?? title;
53
+ }
54
+ }
55
+
56
+ if (!slug || !/^[a-z0-9][a-z0-9-]*$/.test(slug)) {
57
+ console.error("The slug must be kebab-case (a-z, 0-9, -).");
58
+ process.exit(1);
59
+ }
60
+ if (!paths.length) {
61
+ console.error("At least one --paths glob is required.");
62
+ process.exit(1);
63
+ }
64
+
65
+ const path = join(ROOT, ".claude", "rules", `${slug}.md`);
66
+ if (existsSync(path)) {
67
+ console.error(`${relPath(ROOT, path)} already exists – edit it instead of overwriting.`);
68
+ process.exit(1);
69
+ }
70
+
71
+ const heading = title || slug.replaceAll("-", " ").replace(/^\w/, (c) => c.toUpperCase());
72
+ const frontmatter = `paths:\n${paths.map((p) => ` - ${JSON.stringify(p)}`).join("\n")}`;
73
+ const contextLine = context ? ` (${context})` : "";
74
+ const content = [
75
+ "---",
76
+ frontmatter,
77
+ "---",
78
+ "",
79
+ `# ${heading}${contextLine}`,
80
+ "",
81
+ `> Concept slice across layers. Recovered by \`/adopt\`; the target boundary lives in \`docs/domain/context-map.md\`.`,
82
+ "",
83
+ "## Purpose",
84
+ "",
85
+ "[OPEN] Why does this concept exist? One or two sentences, in the domain's words.",
86
+ "",
87
+ "## Key terms",
88
+ "",
89
+ "[OPEN] The terms from `CONTEXT.md` this slice owns or changes; aggregate IDs from `docs/domain/events.yaml`.",
90
+ "",
91
+ "## As-is boundary",
92
+ "",
93
+ "[OPEN] Tables, service methods, entry points and paths that hold this slice together today. Evidence, not guesses.",
94
+ "",
95
+ "## Boundaries",
96
+ "",
97
+ "- May change: [OPEN]",
98
+ "- Must not change: [OPEN]",
99
+ "- Cross-context communication: only through published contracts or events.",
100
+ "",
101
+ "## Typical pitfalls",
102
+ "",
103
+ "[OPEN] What breaks here? From tests, incidents and git history – not guesses.",
104
+ "",
105
+ "## Read next",
106
+ "",
107
+ "- `docs/architecture/repo-map.md`",
108
+ "- `CONTEXT.md`",
109
+ "- `docs/domain/events.yaml`",
110
+ "- `docs/domain/context-map.md` and `docs/domain/context-map.as-is.md`",
111
+ "",
112
+ ].join("\n");
113
+
114
+ mkdirSync(dirname(path), { recursive: true });
115
+ writeFileSync(path, content, "utf8");
116
+ console.log(`Created ${relPath(ROOT, path)}.`);
117
+ console.log("Fill the [OPEN] sections during the grilling – do not invent domain meaning.");
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Resolves a concept scope to candidate files across all layers.
3
+ *
4
+ * node .claude/skills/adopt/scripts/slice.ts --paths "src/api/order*,src/services/order*,db/migrations/*order*"
5
+ * node .claude/skills/adopt/scripts/slice.ts --route "POST /orders"
6
+ * node .claude/skills/adopt/scripts/slice.ts --table orders
7
+ * node .claude/skills/adopt/scripts/slice.ts --event EVT-OrderPlaced
8
+ * node .claude/skills/adopt/scripts/slice.ts --cluster order
9
+ * node .claude/skills/adopt/scripts/slice.ts --term order
10
+ *
11
+ * The resolver follows imports in both directions, adds same-concept files from other layers and prints the
12
+ * candidate set with reasons. It is evidence, not a verdict: `/adopt` confirms the scope with the user.
13
+ */
14
+ import { readRepoMap } from "../../../lib/repo.ts";
15
+ import { resolveSlice, type EventLookup, type ResolveOptions } from "../../../lib/slices.ts";
16
+ import * as d from "../../../lib/discovery.ts";
17
+
18
+ const args = process.argv.slice(2);
19
+
20
+ function usage(): void {
21
+ console.log(
22
+ [
23
+ "Usage: node .claude/skills/adopt/scripts/slice.ts <scope> [options]",
24
+ "",
25
+ "Scope (at least one):",
26
+ " --paths <glob> a file glob (repeatable or comma-separated)",
27
+ " --route <route> an entry point, e.g. 'POST /orders' or '/orders'",
28
+ " --table <name> a table name from schema or code",
29
+ " --event <id> an event, command or aggregate ID from events.yaml",
30
+ " --cluster <name> a concept slice from the repository map",
31
+ " --term <word> a domain term from file names or code",
32
+ "",
33
+ "Options:",
34
+ " --max-files <n> cap the candidate set (default 200)",
35
+ " --depth <n> import/reverse-import hops (default 2)",
36
+ " --json machine-readable result",
37
+ ].join("\n"),
38
+ );
39
+ }
40
+
41
+ if (args.includes("--help") || args.includes("-h")) {
42
+ usage();
43
+ process.exit(0);
44
+ }
45
+
46
+ const options: ResolveOptions = { paths: [] };
47
+ let eventId = "";
48
+ for (let i = 0; i < args.length; i += 1) {
49
+ const take = (): string | undefined => {
50
+ const v = args[i + 1];
51
+ if (!v || v.startsWith("--")) return undefined;
52
+ i += 1;
53
+ return v;
54
+ };
55
+ switch (args[i]) {
56
+ case "--paths": {
57
+ const v = take();
58
+ if (v) options.paths!.push(...v.split(",").map((p) => p.trim()).filter(Boolean));
59
+ break;
60
+ }
61
+ case "--route":
62
+ options.route = take() ?? options.route;
63
+ break;
64
+ case "--table":
65
+ options.table = take() ?? options.table;
66
+ break;
67
+ case "--event":
68
+ eventId = take() ?? eventId;
69
+ break;
70
+ case "--cluster":
71
+ options.cluster = take() ?? options.cluster;
72
+ break;
73
+ case "--term":
74
+ options.term = take() ?? options.term;
75
+ break;
76
+ case "--max-files": {
77
+ const v = Number(take());
78
+ if (Number.isFinite(v) && v > 0) options.maxFiles = v;
79
+ break;
80
+ }
81
+ case "--depth": {
82
+ const v = Number(take());
83
+ if (Number.isFinite(v) && v >= 0) options.depth = v;
84
+ break;
85
+ }
86
+ default:
87
+ break;
88
+ }
89
+ }
90
+
91
+ if (eventId) {
92
+ const model = d.loadEventsModel();
93
+ if (model === null) {
94
+ console.error("--event needs a readable docs/domain/events.yaml.");
95
+ process.exit(1);
96
+ }
97
+ const idx = d.indexModel(model);
98
+ const item = idx.get(eventId) ?? [...idx.entries()].find(([id]) => id.toLowerCase() === eventId.toLowerCase())?.[1];
99
+ if (!item) {
100
+ console.error(`'${eventId}' is not in events.yaml.`);
101
+ process.exit(1);
102
+ }
103
+ const list = (v: unknown): string[] => (Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : []);
104
+ const terms = new Set<string>();
105
+ if (typeof item.name === "string") terms.add(item.name);
106
+ if (typeof item.aggregate === "string") {
107
+ terms.add(item.aggregate);
108
+ const aggregate = idx.get(item.aggregate);
109
+ if (typeof aggregate?.name === "string") terms.add(aggregate.name);
110
+ }
111
+ for (const command of (model.commands ?? []) as d.Obj[]) {
112
+ if (!list(command.produces).includes(item.id)) continue;
113
+ terms.add(String(command.id));
114
+ if (typeof command.name === "string") terms.add(command.name);
115
+ }
116
+ const lookup: EventLookup = { id: item.id, terms: [...terms] };
117
+ options.event = lookup;
118
+ }
119
+
120
+ const map = readRepoMap(d.ROOT);
121
+ if (!map) {
122
+ console.error("No docs/architecture/repo-map.json yet – run `node .claude/skills/adopt/scripts/repo-map.ts` first.");
123
+ process.exit(1);
124
+ }
125
+
126
+ const hasScope =
127
+ Boolean(options.paths?.length) ||
128
+ Boolean(options.route || options.table || options.event || options.cluster || options.term);
129
+ if (!hasScope) {
130
+ usage();
131
+ process.exit(1);
132
+ }
133
+
134
+ const result = resolveSlice(d.ROOT, map, options);
135
+ if (args.includes("--json")) {
136
+ console.log(JSON.stringify(result, null, 2));
137
+ process.exit(result.seeds.length ? 0 : 1);
138
+ }
139
+
140
+ console.log(`Scope: ${result.query}`);
141
+ if (result.seeds.length) {
142
+ console.log(`\nSeeds (${result.seeds.length}):`);
143
+ for (const seed of result.seeds) console.log(`- ${seed.file} – ${seed.reasons.join("; ")}`);
144
+ } else {
145
+ console.log("\nNo seed file found.");
146
+ }
147
+
148
+ if (result.files.length) {
149
+ console.log(`\nCandidate files (${result.files.length}):`);
150
+ for (const [layer, files] of Object.entries(result.byLayer)) {
151
+ console.log(`\n## ${layer}`);
152
+ for (const file of files) {
153
+ const entry = result.files.find((candidate) => candidate.file === file);
154
+ console.log(`- ${file} – ${entry?.reasons.join("; ") ?? ""}`);
155
+ }
156
+ }
157
+ }
158
+
159
+ for (const warning of result.warnings) console.log(`\nWARNING ${warning}`);
160
+ if (result.files.length) {
161
+ console.log("\nThis is a candidate scope, not a verdict. Confirm it with the user before dispatching the analysts.");
162
+ }
163
+ process.exit(result.seeds.length ? 0 : 1);