kataster 0.4.0 → 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.
- package/.claude/THIRD_PARTY_NOTICES.md +4 -0
- package/.claude/agents/domain-architect.md +7 -3
- package/.claude/agents/researcher.md +77 -0
- package/.claude/hooks/validate-docs.ts +15 -0
- package/.claude/lib/architecture.ts +3 -33
- package/.claude/lib/backlog.ts +2 -0
- package/.claude/lib/config.ts +48 -0
- package/.claude/lib/discovery.ts +71 -1
- package/.claude/lib/engineering.ts +6 -0
- package/.claude/lib/gaps.ts +179 -0
- package/.claude/lib/glob.ts +39 -0
- package/.claude/lib/repo.ts +143 -19
- package/.claude/lib/research.ts +303 -0
- package/.claude/lib/slices.ts +529 -0
- package/.claude/settings.json +1 -0
- package/.claude/skills/adopt/SKILL.md +106 -31
- package/.claude/skills/adopt/scripts/gap-register.ts +61 -0
- package/.claude/skills/adopt/scripts/repo-map.ts +34 -2
- package/.claude/skills/adopt/scripts/slice-doc.ts +117 -0
- package/.claude/skills/adopt/scripts/slice.ts +163 -0
- package/.claude/skills/adopt/templates/context-map.as-is.md +22 -0
- package/.claude/skills/architect/SKILL.md +6 -1
- package/.claude/skills/domain-model/CONTEXT-FORMAT.md +1 -1
- package/.claude/skills/domain-model/SKILL.md +23 -3
- package/.claude/skills/event-storming/SKILL.md +3 -2
- package/.claude/skills/event-storming/schema/events.schema.json +2 -0
- package/.claude/skills/event-storming/templates/events.example.yaml +6 -0
- package/.claude/skills/feature/SKILL.md +8 -5
- package/.claude/skills/grilling/SKILL.md +15 -0
- package/.claude/skills/implement/SKILL.md +39 -3
- package/.claude/skills/implement/scripts/characterize.ts +252 -0
- package/.claude/skills/implement/scripts/story-context.ts +64 -0
- package/.claude/skills/implement/scripts/verify.ts +3 -0
- package/.claude/skills/improve-codebase-architecture/SKILL.md +4 -1
- package/.claude/skills/research/SKILL.md +149 -0
- package/.claude/skills/research/scripts/research-check.ts +105 -0
- package/.claude/skills/research/templates/research.md +29 -0
- package/.claude/skills/review-stories/SKILL.md +15 -5
- package/.claude/skills/shape/SKILL.md +24 -5
- package/.claude/skills/tdd/SKILL.md +10 -0
- package/.claude/skills/user-stories/SKILL.md +21 -2
- package/.claude/skills/user-stories/schema/story.schema.json +4 -0
- package/.claude/skills/user-stories/templates/story-template.md +1 -0
- package/.opencode/agents/domain-architect.md +7 -3
- package/.opencode/agents/researcher.md +91 -0
- package/.opencode/commands/adopt.md +1 -1
- package/.opencode/commands/feature.md +1 -1
- package/.opencode/commands/research.md +7 -0
- package/.opencode/commands/shape.md +1 -1
- package/AGENTS.md +14 -1
- package/CHANGELOG.md +49 -0
- package/README.md +78 -19
- package/guide/brownfield.md +41 -12
- package/guide/concepts.md +12 -1
- package/package.json +1 -1
- package/src/detect.mjs +9 -0
- package/src/install.mjs +4 -0
- package/templates/docs/domain/context-map.md +5 -1
- package/templates/docs/domain/events.yaml +3 -0
- package/templates/docs/research/README.md +70 -0
- package/templates/docs/stories/README.md +2 -0
- package/templates/docs/workflow.md +54 -13
|
@@ -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: "[
|
|
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
|
|
24
|
-
`docs/architecture/
|
|
25
|
-
and any `AGENTS.md`/`CLAUDE.md`.
|
|
26
|
-
|
|
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
|
|
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
|
|
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:
|
|
52
|
-
suggestion from `repo-map.md` –
|
|
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.**
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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
|
-
-
|
|
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
|
-
- **
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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,6 +262,15 @@ 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
276
|
- write the stories yourself as the spec engineer (the `user-stories` skill is the method) with the **selected**
|
|
@@ -233,7 +295,8 @@ Date: YYYY-MM-DD · Commit: <sha> · Slice: <paths> · Analysts: <n> lenses
|
|
|
233
295
|
## Repository map (tooling, packages, why this slice)
|
|
234
296
|
## What the repository is (facts)
|
|
235
297
|
## Confirmed domain model
|
|
236
|
-
- contexts, aggregates, glossary changes
|
|
298
|
+
- contexts, aggregates (with status), glossary changes
|
|
299
|
+
- as-is context map and the target links the gap register found
|
|
237
300
|
## Package documentation written
|
|
238
301
|
- <path>/AGENTS.md + CLAUDE.md – what was confirmed and what stays [OPEN]
|
|
239
302
|
## Contradictions found and how they were resolved
|
|
@@ -248,7 +311,11 @@ Date: YYYY-MM-DD · Commit: <sha> · Slice: <paths> · Analysts: <n> lenses
|
|
|
248
311
|
Mark the slice adopted so the next session sees the progress:
|
|
249
312
|
|
|
250
313
|
```bash
|
|
314
|
+
# a real domain package
|
|
251
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
|
|
252
319
|
```
|
|
253
320
|
|
|
254
321
|
Then run `node .claude/skills/event-storming/scripts/validate-events.ts` and
|
|
@@ -270,8 +337,16 @@ checks created, gap candidates (and stories only if requested), ADRs proposed. T
|
|
|
270
337
|
|
|
271
338
|
- **Never change production code in `/adopt`.** Tests and tooling are allowed only as the safety net above.
|
|
272
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.
|
|
273
345
|
- **The user owns the language.** You may propose a better word; you may not rename the domain on your own.
|
|
274
|
-
- **
|
|
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.
|
|
275
350
|
- **The root `AGENTS.md` stays free of domain.** Structure, commands, hard conventions and links only.
|
|
276
351
|
- **Package docs are short and on demand.** `<package>/AGENTS.md` + `<package>/CLAUDE.md`, under ~80 lines.
|
|
277
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
|
|
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);
|