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
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# As-is context map – {{project}}
|
|
2
|
+
|
|
3
|
+
> Recovered from code by `/adopt` on {{date}} at {{commit}}. This is **not a target**.
|
|
4
|
+
> Target: [context-map.md](context-map.md) · Gap register: [gap-register.md](gap-register.md).
|
|
5
|
+
|
|
6
|
+
<!--
|
|
7
|
+
One section per recovered bounded context. Use `### BC-… – <name>` so gap-register.ts can link to it.
|
|
8
|
+
Evidence is a table, a service method or a path; no target statements, no invented terms.
|
|
9
|
+
-->
|
|
10
|
+
|
|
11
|
+
```mermaid
|
|
12
|
+
flowchart LR
|
|
13
|
+
%% BC-ExampleA -->|recovered dependency| BC-ExampleB
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Contexts
|
|
17
|
+
|
|
18
|
+
### BC-Example – <name>
|
|
19
|
+
Purpose (as recovered): …
|
|
20
|
+
Core terms: …
|
|
21
|
+
Evidence: `<path:line>`, `<table>`, `<service method>`.
|
|
22
|
+
Boundaries: …
|
|
@@ -21,10 +21,15 @@ Domain language (the glossary in `CONTEXT.md`), bounded contexts, aggregates and
|
|
|
21
21
|
├── architecture/
|
|
22
22
|
│ └── data-model.md ← logical data model (domain-model skill)
|
|
23
23
|
└── domain/
|
|
24
|
-
├── context-map.md
|
|
24
|
+
├── context-map.md ← target map
|
|
25
|
+
├── context-map.as-is.md ← as-is snapshot, written only by /adopt
|
|
26
|
+
├── gap-register.md ← as-is ↔ target gaps (generated rows + decisions)
|
|
25
27
|
└── events.yaml
|
|
26
28
|
```
|
|
27
29
|
|
|
30
|
+
`docs/domain/context-map.md` is the target; `context-map.as-is.md` is the snapshot recovered by `/adopt`. Do not
|
|
31
|
+
write the as-is map from an architecture session – a target decision belongs in the target map.
|
|
32
|
+
|
|
28
33
|
Once code exists and contexts live in separate modules, context-specific decisions may move next to the code (`src/<context>/docs/adr/`); system-wide decisions stay in `docs/adr/`.
|
|
29
34
|
|
|
30
35
|
## During the session
|
|
@@ -42,6 +42,6 @@ _Avoid_: Fix, service
|
|
|
42
42
|
|
|
43
43
|
## Single vs multi-context
|
|
44
44
|
|
|
45
|
-
During discovery there is one root `CONTEXT.md`, grouped by bounded context; the relationships between contexts are in `docs/domain/context-map.md
|
|
45
|
+
During discovery there is one root `CONTEXT.md`, grouped by bounded context; the relationships between contexts are in `docs/domain/context-map.md` (target). `/adopt` records what the code does today in `docs/domain/context-map.as-is.md` – not a target.
|
|
46
46
|
|
|
47
47
|
When code exists and contexts become separate modules, the glossary may be split: a root `CONTEXT-MAP.md` lists the contexts and links to one `CONTEXT.md` per module (e.g. `src/workshop/CONTEXT.md`). Only split when the code is actually structured that way – decide it via ADR.
|
|
@@ -15,7 +15,9 @@ Architecture decisions (ADRs, tech stack) belong to the `architect` skill. The d
|
|
|
15
15
|
|---|---|
|
|
16
16
|
| Glossary (ubiquitous language) | `CONTEXT.md` at the repo root – format: [CONTEXT-FORMAT.md](./CONTEXT-FORMAT.md) |
|
|
17
17
|
| Event storming model incl. `bounded_contexts`, `aggregates` | `docs/domain/events.yaml` |
|
|
18
|
-
|
|
|
18
|
+
| Target context map | `docs/domain/context-map.md` – where the domain is going |
|
|
19
|
+
| As-is context map | `docs/domain/context-map.as-is.md` – recovered from code, written only by `/adopt` |
|
|
20
|
+
| Gap register | `docs/domain/gap-register.md` – links as-is and target, generated rows + user decisions |
|
|
19
21
|
| Logical data model | `docs/architecture/data-model.md` – template: `templates/data-model.md` |
|
|
20
22
|
|
|
21
23
|
## 1. Language (during every session)
|
|
@@ -30,7 +32,7 @@ When the user uses vague or overloaded terms, propose a precise canonical term.
|
|
|
30
32
|
When domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts. "A customer reports the same problem on an item that already has an open case – a new case or the same one?"
|
|
31
33
|
|
|
32
34
|
### Cross-reference with docs and code
|
|
33
|
-
When the user states how something works, check whether `docs/domain/events.yaml`, the stories (and the code, once it exists) agree. If you find a contradiction, surface it.
|
|
35
|
+
When the user states how something works, check whether `docs/domain/events.yaml`, the stories (and the code, once it exists) agree. If you find a contradiction, surface it. A source that contradicts the model is a grilling question, not a silent rewrite of the model (see the `research` skill).
|
|
34
36
|
|
|
35
37
|
### Update CONTEXT.md inline
|
|
36
38
|
When a term is resolved, update `CONTEXT.md` right there. Don't batch these up: capture them as they happen. `CONTEXT.md` is totally devoid of implementation details. It is a glossary and nothing else – not a spec, not a scratch pad.
|
|
@@ -39,7 +41,8 @@ Same word, different meaning in two places → a hint at a bounded context bound
|
|
|
39
41
|
|
|
40
42
|
## 2. Bounded Contexts & Context Map
|
|
41
43
|
- Draw boundaries along language (same word, different meaning → different context) and responsibility (who decides?).
|
|
42
|
-
- `docs/domain/context-map.md
|
|
44
|
+
- `docs/domain/context-map.md` is the **target** map: per context its purpose, core terms, owner role. Relationships as a Mermaid `flowchart` using the patterns Customer/Supplier, Conformist, Anti-Corruption Layer, Shared Kernel, Published Language.
|
|
45
|
+
- `docs/domain/context-map.as-is.md` is the **as-is** snapshot: what the code does today, with evidence. Only `/adopt` writes it; every other discovery path writes the target. The gap register links both.
|
|
43
46
|
- 2–4 contexts are typical for a small system. More only with justification.
|
|
44
47
|
- Maintain them in `events.yaml` under `bounded_contexts`, and group the terms in `CONTEXT.md` by context.
|
|
45
48
|
|
|
@@ -50,6 +53,23 @@ Same word, different meaning in two places → a hint at a bounded context bound
|
|
|
50
53
|
- **An invariant is stated once, on the aggregate** (user, 2026-10-01). A command's `rules` list only what it adds beyond them: defaults, actor permissions no invariant carries, required input, checks across aggregate or context boundaries, and consequences of the transition. Everything the stored state has to satisfy is an invariant, not a rule – the spec engineer reads both when deriving scenarios.
|
|
51
54
|
- Sign of a wrong aggregate: a command needs data from several aggregates to check a rule.
|
|
52
55
|
|
|
56
|
+
### Aggregate status: target, as-is, unresolved
|
|
57
|
+
Every aggregate carries a status; a missing `status` means `target`, so existing models stay valid.
|
|
58
|
+
|
|
59
|
+
| Status | Meaning | Rule |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `target` | The boundary the project wants. The default in greenfield and in a confirmed target model. | Only a user decision makes an inferred boundary `target`. |
|
|
62
|
+
| `as-is` | The factual consistency boundary recovered from code. | Requires `evidence`: the table, service method or path that marks it. |
|
|
63
|
+
| `unresolved` | The boundary is unclear. | Requires an open hotspot (`refs: [AGG-…]`) and grilling. No story on it can become ready. |
|
|
64
|
+
|
|
65
|
+
**Prefer `as-is` or `unresolved` over an invented aggregate.** `/adopt` never writes `target` for something it
|
|
66
|
+
inferred from code. If you cannot name the table, method or path that holds the boundary together, it is not
|
|
67
|
+
`as-is` – it is `unresolved`. After changing a status or evidence, refresh the gap register:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
node .claude/skills/adopt/scripts/gap-register.ts
|
|
71
|
+
```
|
|
72
|
+
|
|
53
73
|
## 4. Logical Data Model (`docs/architecture/data-model.md`)
|
|
54
74
|
- Per aggregate: root entity, entities, value objects, attributes with **domain** types (e.g. `SerialNumber`, `Money`, `Timestamp`), required/optional.
|
|
55
75
|
- Lifecycle as a Mermaid `stateDiagram-v2` (derive states from the events).
|
|
@@ -12,7 +12,8 @@ Focus of this session: the arguments from the invocation (empty = big picture ac
|
|
|
12
12
|
|
|
13
13
|
## Preparation
|
|
14
14
|
1. Call the `skill` tool for `grilling` and `domain-model`. Grilling sets how you ask questions; domain-model sets how you guard the language in `CONTEXT.md`.
|
|
15
|
-
2. Read `docs/product/vision.md`, `CONTEXT.md`, `docs/domain/events.yaml`, `docs/domain/context-map.md
|
|
15
|
+
2. Read `docs/product/vision.md`, `CONTEXT.md`, `docs/domain/events.yaml`, `docs/domain/context-map.md` (target),
|
|
16
|
+
`docs/domain/context-map.as-is.md` and `docs/domain/gap-register.md` when they exist.
|
|
16
17
|
3. Format reference: `templates/events.example.yaml` in this skill folder (unrelated example domain – copy the format only, no content).
|
|
17
18
|
|
|
18
19
|
## Facilitation Rules
|
|
@@ -33,7 +34,7 @@ Focus of this session: the arguments from the invocation (empty = big picture ac
|
|
|
33
34
|
4. **Commands & actors** – What triggers each event, and who does it? Capture external systems (`EXT-`). Business rules of the command go into `rules` – only what the command adds beyond the `invariants` of its aggregate, which are stated once, there (user, 2026-10-01).
|
|
34
35
|
5. **Policies** – "Whenever <event>, then <command>". Include time-triggered ones (event with `origin: time`) and manual routines.
|
|
35
36
|
6. **Read models** – What information does an actor need to trigger the command? (`fed_by`, `used_by`, `fields`)
|
|
36
|
-
7. **Aggregates (proposal)** – Which rules must be protected consistently? Rough assignment of commands/events. Detailed work is done by `domain-architect`.
|
|
37
|
+
7. **Aggregates (proposal)** – Which rules must be protected consistently? Rough assignment of commands/events. Detailed work is done by `domain-architect`. Every aggregate gets `status: target` in greenfield; a boundary the user cannot settle becomes `status: unresolved` with an open hotspot. When adopting existing code, the status is `as-is` (with evidence) or `unresolved`, never an invented `target`.
|
|
37
38
|
8. **Bounded contexts (proposal)** – Where do language or responsibility change? Propose rough boundaries.
|
|
38
39
|
|
|
39
40
|
When focusing on a single process: phases 0–6 for that process (phase 0 only for what the vision doesn't cover yet), 7–8 only if something new emerges.
|
|
@@ -51,6 +51,8 @@
|
|
|
51
51
|
"id": { "type": "string", "pattern": "^AGG-[A-Za-z0-9]+$" },
|
|
52
52
|
"name": { "$ref": "#/$defs/text" },
|
|
53
53
|
"context": { "type": "string" },
|
|
54
|
+
"status": { "enum": ["target", "as-is", "unresolved"] },
|
|
55
|
+
"evidence": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/text" } },
|
|
54
56
|
"invariants": { "type": "array", "items": { "$ref": "#/$defs/text" } },
|
|
55
57
|
"description": { "type": "string" }
|
|
56
58
|
}
|
|
@@ -24,12 +24,18 @@ aggregates:
|
|
|
24
24
|
- id: AGG-Bike
|
|
25
25
|
name: Bike
|
|
26
26
|
context: BC-Rental
|
|
27
|
+
status: target
|
|
27
28
|
invariants:
|
|
28
29
|
- A bike can only be rented out once at a time
|
|
29
30
|
- A bike marked as damaged cannot be rented out
|
|
30
31
|
- id: AGG-RepairOrder
|
|
31
32
|
name: Repair order
|
|
32
33
|
context: BC-Workshop
|
|
34
|
+
# status: as-is # factual boundary recovered from code; add evidence below
|
|
35
|
+
# evidence:
|
|
36
|
+
# - repair_orders table (db/schema.sql:40)
|
|
37
|
+
# - RepairOrderService.open() (src/services/repairOrderService.ts:22)
|
|
38
|
+
# status: unresolved # boundary unclear; add an open hotspot with refs: [AGG-RepairOrder]
|
|
33
39
|
|
|
34
40
|
events:
|
|
35
41
|
- id: EVT-BikeRented
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: feature
|
|
3
|
-
description: Entry point for a new feature or change once the initial discovery is done – accepts a free-text idea, an existing story (`ST-NNN`) or a ticket (`#123`, `PROJ-123`, URL), grills the idea, runs a focused event storming, updates domain model and ADRs, derives stories into the backlog (local, or the tracker mirror) and runs the story review; bug tickets go to a regression story instead; `--prototype` adds a throwaway prototype before the stories.
|
|
3
|
+
description: Entry point for a new feature or change once the initial discovery is done – accepts a free-text idea, an existing story (`ST-NNN`) or a ticket (`#123`, `PROJ-123`, URL), grills the idea, runs a focused event storming, updates domain model and ADRs, derives stories into the backlog (local, or the tracker mirror) and runs the story review; bug tickets go to a regression story instead; `--prototype` adds a throwaway prototype before the stories; `--research` forces the research step for a cross-cutting question.
|
|
4
4
|
disable-model-invocation: true
|
|
5
|
-
argument-hint: "<idea | ST-NNN | #issue | PROJ-123 | URL> [--prototype[=ui|logic]]"
|
|
5
|
+
argument-hint: "<idea | ST-NNN | #issue | PROJ-123 | URL> [--prototype[=ui|logic]] [--research]"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# New Feature
|
|
@@ -11,6 +11,8 @@ The argument is a feature idea, an existing story or a ticket. Empty → ask the
|
|
|
11
11
|
Pick a short kebab-case slug for it (e.g. `order-photos`); stories get the label `feature:<slug>`.
|
|
12
12
|
`--prototype[=ui|logic]` is a flag, not part of the idea: strip it before resolving a reference. It asks for a
|
|
13
13
|
prototype before the stories are written; without a branch the `prototype` skill picks it from the question.
|
|
14
|
+
`--research` is a flag too: strip it before resolving a reference. It forces the research step for a topic the
|
|
15
|
+
trigger list in step 2 does not catch.
|
|
14
16
|
|
|
15
17
|
## 0. Intake – when the argument is a reference
|
|
16
18
|
|
|
@@ -57,10 +59,11 @@ ADRs and `docs/product/ux-guidelines.md` first.
|
|
|
57
59
|
Do not run an event storming for a bug and do not create a feature slug unless the rule changes.
|
|
58
60
|
|
|
59
61
|
## 1. Preparation
|
|
60
|
-
Read `docs/product/vision.md`, `CONTEXT.md`, `docs/domain/events.yaml`, `docs/domain/context-map.md`, `docs/architecture/data-model.md`, `docs/adr/`, `docs/stories/BACKLOG.md`. Find out what already exists for this idea (events, stories, hotspots) before asking anything. With an intake result, use its source and matching story instead of searching from scratch. If a linked story exists, pull the tracker first (`/backlog-sync pull`) so the mirror is current.
|
|
62
|
+
Read `docs/product/vision.md`, `CONTEXT.md`, `docs/domain/events.yaml`, `docs/domain/context-map.md` (target), `docs/domain/context-map.as-is.md`, `docs/domain/gap-register.md`, `docs/architecture/data-model.md`, `docs/adr/`, `docs/stories/BACKLOG.md`. Find out what already exists for this idea (events, stories, hotspots) before asking anything. With an intake result, use its source and matching story instead of searching from scratch. If a linked story exists, pull the tracker first (`/backlog-sync pull`) so the mirror is current.
|
|
61
63
|
|
|
62
64
|
## 2. Grill the idea
|
|
63
65
|
Call the `skill` tool four times, for `grilling`, `domain-model`, `architect` and `ux-design` (the first three are the set of `/grill-with-docs`, which can't be called from a skill). Grill the user until the frontier is empty and they confirm a shared understanding. The grilling carries the three perspectives the spec linter cannot check – product, engineering, UX. With an intake result, the ticket's claims and the intake's `signals` seed the frontier; unresolved comments and contradictions are the first questions. Typical branches:
|
|
66
|
+
- **Research – does the idea rest on an external fact?** Auth/identity, security, payment, privacy/data protection, accessibility, performance, a new library or technology, or `--research`: load the `research` skill before grilling the affected branch. Check `docs/research/` first; a trigger is not a mandate, and one run covers a topic, not a story. Findings enter the round as cited recommendations – evidence, not decisions.
|
|
64
67
|
- **Product**: Which problem, for which user group, and what value? Measurable outcome? Does it fit the vision, or touch a non-goal? If it changes scope → update `docs/product/vision.md` with the user. Must vs. could – what ships first?
|
|
65
68
|
- **Engineering**: Which existing code, data and stories change? Is there a migration? What is hard to test? Where does it cut into stories – one command per story, at most 4 scenarios?
|
|
66
69
|
- Which existing processes, events and stories does it change? What's explicitly out of scope?
|
|
@@ -103,10 +106,10 @@ Read `.claude/skills/event-storming/SKILL.md` and follow it with the feature as
|
|
|
103
106
|
- A UX decision changes or adds a guideline → delegate to `ux-designer`: write exactly these decisions into `docs/product/ux-guidelines.md`. A prototype verdict is a decision like any other; the branch pointer and the verdict go into the story through step 5. The audit of existing screens belongs to `/groom-backlog`, not to the feature.
|
|
104
107
|
|
|
105
108
|
## 5. Stories
|
|
106
|
-
Write the stories yourself as the spec engineer (the `user-stories` skill is the method): derive one story per new or changed command, policy or read model; name `aggregate` and `command` (exactly one each), the event/command IDs, `ui: true` for every story with a frontend part, `type: chore`/`task` for non-domain work, the label `feature:<slug>`, the label `bug` for the bug path, the source reference (`Source: <ref> (<url>)`) for every story derived from a ticket, the UX decisions from step 2 (with the G-numbers of the guidelines they reference), the prototype verdict and branch pointer from step 2b, and existing stories that need revising. Do not merge two commands into one story; if the cut needs more than 4 scenarios, split it. Finished stories get `status: review` and are run through the spec linter.
|
|
109
|
+
Write the stories yourself as the spec engineer (the `user-stories` skill is the method): derive one story per new or changed command, policy or read model; name `aggregate` and `command` (exactly one each), the event/command IDs, `ui: true` for every story with a frontend part, `type: chore`/`task` for non-domain work, the label `feature:<slug>`, the label `bug` for the bug path, the source reference (`Source: <ref> (<url>)`) for every story derived from a ticket, the research files they cite (`sources: [docs/research/<topic>.md]`) with adopted/rejected findings and their reasons in `## Context`, the UX decisions from step 2 (with the G-numbers of the guidelines they reference), the prototype verdict and branch pointer from step 2b, and existing stories that need revising. Do not merge two commands into one story; if the cut needs more than 4 scenarios, split it. Finished stories get `status: review` and are run through the spec linter.
|
|
107
110
|
|
|
108
111
|
## 6. Review
|
|
109
112
|
Read `.claude/skills/review-stories/SKILL.md` and follow it with the story IDs from step 5 as the arguments (only this feature's stories, not everything in `review`). The spec linter must be green before a story reaches `ready-for-human-review`; the user approves every change and every `ready`. There is no separate review subagent.
|
|
110
113
|
|
|
111
114
|
## 7. Wrap-up
|
|
112
|
-
Summarize: the source and the route taken (idea, ticket, existing story or bug), glossary changes, new/changed events, ADRs proposed, a prototype run and its verdict (if any), stories created and their review outcome (`ready` vs. still open), open hotspots/questions. Point to `docs/stories/BACKLOG.md`, where `ready` stories are queued for the engineering workflow. For a bug, say which documented rule was restored, or that the rule changed and became a feature.
|
|
115
|
+
Summarize: the source and the route taken (idea, ticket, existing story or bug), glossary changes, new/changed events, ADRs proposed, research files created or refreshed and the findings the user adopted or rejected, a prototype run and its verdict (if any), stories created and their review outcome (`ready` vs. still open), open hotspots/questions. Point to `docs/stories/BACKLOG.md`, where `ready` stories are queued for the engineering workflow. For a bug, say which documented rule was restored, or that the rule changed and became a feature.
|
|
@@ -27,4 +27,19 @@ Each round the user answers reshapes the tree: settled decisions push the fronti
|
|
|
27
27
|
|
|
28
28
|
Finding _facts_ is your job, never the user's. When a frontier question needs a fact from the environment (filesystem, tools, etc.), dispatch a sub-agent to find it; don't ask the user for anything you could look up yourself. Don't block on it: a running exploration is an unsettled prerequisite, so only the questions downstream of it wait for the sub-agent to report; ask the rest of the frontier now. The _decisions_ are the user's: put each to them and wait.
|
|
29
29
|
|
|
30
|
+
## Research before a round
|
|
31
|
+
|
|
32
|
+
When a frontier question depends on an external fact – authentication/identity, security, payment, privacy/data
|
|
33
|
+
protection, accessibility, performance, a new library or technology – load the `research` skill before the round. A
|
|
34
|
+
trigger is **not a mandate**: if `CONTEXT.md`, `docs/domain/events.yaml`, an accepted ADR, the UX guidelines, the
|
|
35
|
+
conventions or a valid research file already answers it, run nothing. One run per topic, not per story.
|
|
36
|
+
|
|
37
|
+
- The recommendation cites the finding and its source: `➡️ … (RFC 9700 §2.1.1; F1 in docs/research/oauth-pkce.md)`.
|
|
38
|
+
- Only a confirmed recommendation becomes a Gherkin scenario or checklist item with an observable threshold – never
|
|
39
|
+
"follow best practices". A finding that cannot be tested is context or a guideline, not an acceptance criterion.
|
|
40
|
+
- Rejected options are recorded with the reason in the story's `## Context` / `## Out of Scope` (or the ADR /
|
|
41
|
+
initiative). The research file stays evidence and is not rewritten into a decision log.
|
|
42
|
+
- A finding that contradicts `CONTEXT.md`, `events.yaml`, an accepted ADR or the UX guidelines is a grilling
|
|
43
|
+
question. Never edit the model to match the source silently.
|
|
44
|
+
|
|
30
45
|
The session is done when the frontier is empty: every branch of the design tree visited, nothing left silently assumed. Do not act on it until the user confirms you have reached a shared understanding.
|
|
@@ -40,8 +40,11 @@ Read it instead of assuming `main`, `npm` or GitHub.
|
|
|
40
40
|
2. With a tracker (`backlog.provider` ≠ `local`), pull first (`/backlog-sync pull`) and check that the story has its
|
|
41
41
|
`external` key – an unlinked story never reaches `ready`, so stop and report instead of implementing a stale mirror.
|
|
42
42
|
3. `node .claude/skills/implement/scripts/story-context.ts ST-NNN` – the context pack: story, exact test titles,
|
|
43
|
-
dependencies, events/commands/rules/invariants, glossary, data model, ADRs. Read it completely;
|
|
44
|
-
source, not memory.
|
|
43
|
+
dependencies, events/commands/rules/invariants, glossary, data model, ADRs, research sources. Read it completely;
|
|
44
|
+
it is your source, not memory. When a scenario depends on a cited finding, read the research file listed under
|
|
45
|
+
`## Research sources` – findings are evidence, not a specification. If an aggregate in the pack has
|
|
46
|
+
`status: unresolved`, stop: the consistency boundary is not settled, so the story goes back to `review` instead
|
|
47
|
+
of being decided in code.
|
|
45
48
|
4. `git switch -c st-NNN-<slug>` (slug from the file name), then
|
|
46
49
|
`node .claude/skills/implement/scripts/story-status.ts ST-NNN in-progress` and commit `ST-NNN: start`.
|
|
47
50
|
5. **Pick the lane** and say it in one line: fast lane for `chore`/`task`, domain lane otherwise. The fast lane is
|
|
@@ -85,6 +88,38 @@ Seams from the catalog are the "confirmed seams" the tdd skill requires; the use
|
|
|
85
88
|
cover them; the story's checklist is the acceptance. If it already has scenarios, keep them as a sanity check but do
|
|
86
89
|
not create `ST-NNN:` test titles.
|
|
87
90
|
|
|
91
|
+
## 2a. Characterization net (domain lane, existing behaviour)
|
|
92
|
+
|
|
93
|
+
Before the first red test, find out whether the affected paths already have a net:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
node .claude/skills/implement/scripts/characterize.ts --story ST-NNN
|
|
97
|
+
# or name the paths directly
|
|
98
|
+
node .claude/skills/implement/scripts/characterize.ts --changed <file>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The check names the affected source files, the tests it found and the files with **no test found**. It is a
|
|
102
|
+
test-presence check, not a coverage tool: whether a test covers the behaviour that is about to change is your
|
|
103
|
+
judgement.
|
|
104
|
+
|
|
105
|
+
For every affected path whose behaviour is about to change and has no test:
|
|
106
|
+
|
|
107
|
+
1. Write a **characterization test** that pins the current behaviour (Feathers): assert what the code does today,
|
|
108
|
+
not what the story wants it to do.
|
|
109
|
+
2. Run it on the unchanged code and see it green. A red characterization test is not the story's red step.
|
|
110
|
+
3. Commit it separately: `ST-NNN: characterize <area>`.
|
|
111
|
+
4. Only then start the story's red → green loop (step 3).
|
|
112
|
+
|
|
113
|
+
If the current behaviour contradicts the story, that is a **finding**, not a test to adjust: mark the story `[OPEN]`,
|
|
114
|
+
add a row to `docs/stories/OPEN_QUESTIONS.md`, and send it back with
|
|
115
|
+
`node .claude/skills/implement/scripts/story-status.ts ST-NNN review`. Do not silently make the code fit the story.
|
|
116
|
+
|
|
117
|
+
If the repository has no test runner, stop and report it: setting one up is its own `task` story, not a side effect
|
|
118
|
+
of this one.
|
|
119
|
+
|
|
120
|
+
**Fast lane:** skip this step. The exception is a refactoring – when the task says behaviour stays unchanged (or the
|
|
121
|
+
story carries the label `refactor`), write the characterization net first; a refactor without one is a rewrite.
|
|
122
|
+
|
|
88
123
|
## 3. Red → green (domain) / implement (fast)
|
|
89
124
|
|
|
90
125
|
**Domain lane:**
|
|
@@ -211,7 +246,8 @@ A spike answers its **Question** within its **Timebox** – no test plan, no TDD
|
|
|
211
246
|
|
|
212
247
|
## When the story is wrong
|
|
213
248
|
Never guess. When a scenario contradicts another story, a rule or an ADR, can't be tested as written, or leaves a
|
|
214
|
-
decision open
|
|
249
|
+
decision open – and when a characterization test shows that the current behaviour differs from what the story
|
|
250
|
+
assumes:
|
|
215
251
|
1. Stop implementing that part. Mark it `[OPEN]` in the story and add a row to `docs/stories/OPEN_QUESTIONS.md`.
|
|
216
252
|
2. `node .claude/skills/implement/scripts/story-status.ts ST-NNN review` and tell the user. The fix goes through
|
|
217
253
|
discovery in the main session: revise the story, run the spec linter, and let the user approve it again
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finds the source files a story or a diff touches and checks whether a test exists for each of them.
|
|
3
|
+
*
|
|
4
|
+
* node .claude/skills/implement/scripts/characterize.ts --story ST-012
|
|
5
|
+
* node .claude/skills/implement/scripts/characterize.ts --changed src/services/order-service.ts
|
|
6
|
+
* node .claude/skills/implement/scripts/characterize.ts --base origin/main --json
|
|
7
|
+
*
|
|
8
|
+
* This is a deterministic *test-presence* check, not a coverage tool: it finds tests by file-name convention
|
|
9
|
+
* and by imports/references of the source. Whether a test covers the behaviour that is about to change is the
|
|
10
|
+
* agent's judgement. When no test exists, /implement writes characterization tests first and commits them
|
|
11
|
+
* separately, before the story's red → green loop.
|
|
12
|
+
*
|
|
13
|
+
* Exit 0 unless the arguments are wrong or the story does not exist; a missing test is a finding, not a crash.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
16
|
+
import { basename, extname, join } from "node:path";
|
|
17
|
+
import * as d from "../../../lib/discovery.ts";
|
|
18
|
+
import * as e from "../../../lib/engineering.ts";
|
|
19
|
+
import { readRepoMap, scanRepo, type RepoPackage } from "../../../lib/repo.ts";
|
|
20
|
+
import { packageForFile } from "../../../lib/slices.ts";
|
|
21
|
+
|
|
22
|
+
const args = process.argv.slice(2);
|
|
23
|
+
|
|
24
|
+
function usage(): void {
|
|
25
|
+
console.log(
|
|
26
|
+
[
|
|
27
|
+
"Usage: node .claude/skills/implement/scripts/characterize.ts [options]",
|
|
28
|
+
"",
|
|
29
|
+
" --story <ST-NNN> find source files from the story's IDs, names and terms",
|
|
30
|
+
" --changed <file> a file (repeatable or comma-separated); default: git status",
|
|
31
|
+
" --base <ref> use the files changed against this ref",
|
|
32
|
+
" --json machine-readable result",
|
|
33
|
+
].join("\n"),
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
38
|
+
usage();
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let storyId = "";
|
|
43
|
+
const changed: string[] = [];
|
|
44
|
+
let base = "";
|
|
45
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
46
|
+
const take = (): string | undefined => {
|
|
47
|
+
const value = args[i + 1];
|
|
48
|
+
if (!value || value.startsWith("--")) return undefined;
|
|
49
|
+
i += 1;
|
|
50
|
+
return value;
|
|
51
|
+
};
|
|
52
|
+
if (args[i] === "--story") storyId = (take() ?? "").toUpperCase();
|
|
53
|
+
else if (args[i] === "--changed") {
|
|
54
|
+
const value = take();
|
|
55
|
+
if (value) changed.push(...value.split(",").map((f) => f.trim()).filter(Boolean));
|
|
56
|
+
} else if (args[i] === "--base") base = take() ?? base;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!storyId && !changed.length && !base) {
|
|
60
|
+
usage();
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const list = (v: unknown): string[] => (Array.isArray(v) ? v : []);
|
|
65
|
+
const read = (file: string): string => {
|
|
66
|
+
try {
|
|
67
|
+
return readFileSync(join(d.ROOT, file), "utf8");
|
|
68
|
+
} catch {
|
|
69
|
+
return "";
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const map = readRepoMap(d.ROOT) ?? scanRepo(d.ROOT, { toolGraph: false });
|
|
74
|
+
const pkgFor = (file: string): RepoPackage | undefined => packageForFile(map.packages, file);
|
|
75
|
+
|
|
76
|
+
const testFiles = e.projectFiles().filter(e.isTest);
|
|
77
|
+
const sourceFiles = e.projectFiles().filter((f) => e.isCode(f) && !e.isTestCode(f));
|
|
78
|
+
|
|
79
|
+
function changedFiles(): string[] {
|
|
80
|
+
if (changed.length) return [...new Set(changed)];
|
|
81
|
+
if (base) return (e.git("diff", "--name-only", base) ?? "").split("\n").map((l) => l.trim()).filter(Boolean);
|
|
82
|
+
return (e.git("status", "--porcelain", "--untracked-files=all") ?? "")
|
|
83
|
+
.split("\n")
|
|
84
|
+
.map((line) => line.slice(3).trim().replace(/^.* -> /, ""))
|
|
85
|
+
.filter(Boolean);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function testBase(file: string): string {
|
|
89
|
+
return basename(file, extname(file))
|
|
90
|
+
.replace(/^test_/, "")
|
|
91
|
+
.replace(/(?:[._-](?:test|spec|tests|specs))$/i, "")
|
|
92
|
+
.replace(/(?:Test|Tests|Spec|Specs|IT)$/i, "")
|
|
93
|
+
.toLowerCase();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function testsFor(source: string): { file: string; reason: string }[] {
|
|
97
|
+
const sourceBase = basename(source, extname(source)).toLowerCase();
|
|
98
|
+
const out: { file: string; reason: string }[] = [];
|
|
99
|
+
for (const test of testFiles) {
|
|
100
|
+
const reason: string[] = [];
|
|
101
|
+
if (testBase(test) === sourceBase) reason.push("same name");
|
|
102
|
+
const text = read(test);
|
|
103
|
+
// A reference to the module is enough evidence to look; it is not proof of behavioural coverage.
|
|
104
|
+
if (sourceBase.length >= 3 && text.toLowerCase().includes(sourceBase)) reason.push("mentions the module");
|
|
105
|
+
if (reason.length) out.push({ file: test, reason: reason.join(", ") });
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function storySeedTerms(story: d.Story): { ids: string[]; names: string[]; words: string[] } {
|
|
111
|
+
const ids = new Set<string>();
|
|
112
|
+
const names = new Set<string>();
|
|
113
|
+
const text = `${story.meta.title ?? ""}\n${story.body}`;
|
|
114
|
+
for (const id of text.matchAll(/\b(?:ACT|EXT|BC|AGG|EVT|CMD|POL|RM)-[A-Za-z0-9]+\b/g)) ids.add(id[0]);
|
|
115
|
+
for (const key of ["aggregate", "command", "context"]) {
|
|
116
|
+
if (typeof story.meta[key] === "string") ids.add(story.meta[key]);
|
|
117
|
+
}
|
|
118
|
+
for (const id of list(story.meta.events)) ids.add(id);
|
|
119
|
+
|
|
120
|
+
const model = d.loadEventsModel();
|
|
121
|
+
if (model) {
|
|
122
|
+
const idx = d.indexModel(model);
|
|
123
|
+
for (const id of [...ids]) {
|
|
124
|
+
const item = idx.get(id);
|
|
125
|
+
if (typeof item?.name === "string") names.add(item.name);
|
|
126
|
+
}
|
|
127
|
+
for (const command of (model.commands ?? []) as d.Obj[]) {
|
|
128
|
+
if (list(command.produces).some((event) => list(story.meta.events).includes(event))) {
|
|
129
|
+
if (typeof command.name === "string") names.add(command.name);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const glossary = e.loadGlossary();
|
|
135
|
+
for (const term of glossary.terms) {
|
|
136
|
+
if (new RegExp(`\\b${term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i").test(text)) names.add(term);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const stop = new Set(["story", "want", "that", "with", "from", "this", "when", "then", "given", "should", "must", "can", "the", "and", "for"]);
|
|
140
|
+
const words = [...new Set(
|
|
141
|
+
String(story.meta.title ?? "")
|
|
142
|
+
.toLowerCase()
|
|
143
|
+
.split(/[^a-z0-9]+/)
|
|
144
|
+
.filter((word) => word.length >= 4 && !stop.has(word)),
|
|
145
|
+
)];
|
|
146
|
+
return { ids: [...ids], names: [...names], words };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function scoreSource(file: string, terms: { ids: string[]; names: string[]; words: string[] }): { score: number; reasons: string[] } {
|
|
150
|
+
const text = read(file).toLowerCase();
|
|
151
|
+
const path = file.toLowerCase();
|
|
152
|
+
let score = 0;
|
|
153
|
+
const reasons: string[] = [];
|
|
154
|
+
for (const id of terms.ids) {
|
|
155
|
+
if (text.includes(id.toLowerCase())) {
|
|
156
|
+
score += 3;
|
|
157
|
+
reasons.push(`mentions ${id}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
for (const name of terms.names) {
|
|
161
|
+
const lower = name.toLowerCase();
|
|
162
|
+
if (lower.length >= 3 && new RegExp(`\\b${lower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(text)) {
|
|
163
|
+
score += 2;
|
|
164
|
+
reasons.push(`mentions "${name}"`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const word of terms.words) {
|
|
168
|
+
if (path.includes(word)) {
|
|
169
|
+
score += 1;
|
|
170
|
+
reasons.push(`path contains "${word}"`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return { score, reasons };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let story: d.Story | undefined;
|
|
177
|
+
const seeds = new Map<string, { file: string; score: number; reasons: string[] }>();
|
|
178
|
+
const warnings: string[] = [];
|
|
179
|
+
|
|
180
|
+
if (storyId) {
|
|
181
|
+
try {
|
|
182
|
+
story = e.findStory(storyId);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
const terms = storySeedTerms(story);
|
|
188
|
+
for (const file of sourceFiles) {
|
|
189
|
+
const scored = scoreSource(file, terms);
|
|
190
|
+
if (scored.score > 0) seeds.set(file, { file, ...scored });
|
|
191
|
+
}
|
|
192
|
+
if (!seeds.size) warnings.push("no source file mentions the story's IDs or terms – pass --changed to name the affected paths");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
for (const file of changedFiles()) {
|
|
196
|
+
if (!existsSync(join(d.ROOT, file))) continue;
|
|
197
|
+
if (!sourceFiles.includes(file)) continue;
|
|
198
|
+
seeds.set(file, { file, score: seeds.get(file)?.score ?? 0, reasons: [...(seeds.get(file)?.reasons ?? []), "changed"] });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const sourceRows = [...seeds.values()]
|
|
202
|
+
.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file))
|
|
203
|
+
.slice(0, 40)
|
|
204
|
+
.map((seed) => {
|
|
205
|
+
const tests = testsFor(seed.file);
|
|
206
|
+
const pkg = pkgFor(seed.file);
|
|
207
|
+
return {
|
|
208
|
+
file: seed.file,
|
|
209
|
+
package: pkg?.path,
|
|
210
|
+
score: seed.score,
|
|
211
|
+
reasons: seed.reasons,
|
|
212
|
+
tests,
|
|
213
|
+
covered: tests.length > 0,
|
|
214
|
+
confidence: tests.some((test) => test.reason.includes("same name") || test.reason.includes("mentions the module")) ? "medium" : "low",
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const storyTests = storyId
|
|
219
|
+
? testFiles.filter((file) => read(file).includes(storyId)).map((file) => ({ file, reason: `mentions ${storyId}` }))
|
|
220
|
+
: [];
|
|
221
|
+
if (storyId && !storyTests.length) warnings.push(`no test mentions ${storyId} yet – expected before the story's red → green loop`);
|
|
222
|
+
|
|
223
|
+
const result = {
|
|
224
|
+
story: story ? { id: storyId, title: story.meta.title, status: story.meta.status, type: story.meta.type } : null,
|
|
225
|
+
seeds: [...seeds.values()].sort((a, b) => b.score - a.score || a.file.localeCompare(b.file)).slice(0, 40),
|
|
226
|
+
sources: sourceRows,
|
|
227
|
+
storyTests,
|
|
228
|
+
warnings,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
if (args.includes("--json")) {
|
|
232
|
+
console.log(JSON.stringify(result, null, 2));
|
|
233
|
+
process.exit(0);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
console.log(`Characterization check${story ? ` – ${storyId}: ${story.meta.title}` : ""}`);
|
|
237
|
+
if (!sourceRows.length) {
|
|
238
|
+
console.log("\nNo affected source file found. Name the paths with --changed, or widen the story terms.");
|
|
239
|
+
}
|
|
240
|
+
for (const row of sourceRows) {
|
|
241
|
+
const status = row.covered ? "test found" : "NO TEST FOUND";
|
|
242
|
+
console.log(`\n- ${row.file} (${row.package ?? "unmapped"}) – ${status}`);
|
|
243
|
+
if (row.reasons.length) console.log(` why: ${row.reasons.join("; ")}`);
|
|
244
|
+
for (const test of row.tests) console.log(` test: ${test.file} (${test.reason})`);
|
|
245
|
+
if (!row.covered) console.log(" → write a characterization test first; pin the current behaviour, commit it separately, then start the story tests.");
|
|
246
|
+
}
|
|
247
|
+
if (storyTests.length) {
|
|
248
|
+
console.log(`\nTests that mention ${storyId}:`);
|
|
249
|
+
for (const test of storyTests) console.log(`- ${test.file} (${test.reason})`);
|
|
250
|
+
}
|
|
251
|
+
for (const warning of warnings) console.log(`\nWARNING ${warning}`);
|
|
252
|
+
console.log("\nA test file is evidence, not proof of behavioural coverage. If the current behaviour contradicts the story, the story goes back to review.");
|
|
@@ -19,6 +19,7 @@ import { join } from "node:path";
|
|
|
19
19
|
import { stringify } from "yaml";
|
|
20
20
|
import * as d from "../../../lib/discovery.ts";
|
|
21
21
|
import * as e from "../../../lib/engineering.ts";
|
|
22
|
+
import * as research from "../../../lib/research.ts";
|
|
22
23
|
|
|
23
24
|
const id = process.argv[2]?.toUpperCase();
|
|
24
25
|
if (!id) {
|
|
@@ -57,6 +58,36 @@ if (deps.length) {
|
|
|
57
58
|
out.push("");
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
// Research sources: evidence the story cites. Expiry is advisory, but a missing or invalid file is a finding.
|
|
62
|
+
const sources = research.sourceRefs(story.meta);
|
|
63
|
+
if (sources.length) {
|
|
64
|
+
out.push(
|
|
65
|
+
"## Research sources",
|
|
66
|
+
"",
|
|
67
|
+
"Research is evidence, not a specification. When a scenario depends on a cited finding, read the file before implementing it.",
|
|
68
|
+
"",
|
|
69
|
+
);
|
|
70
|
+
for (const ref of sources) {
|
|
71
|
+
if (!research.isLocalResearchRef(ref)) {
|
|
72
|
+
out.push(`- ${ref}`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const state = research.researchStatus(research.researchPath(ref));
|
|
76
|
+
if (state.status === "missing" || state.status === "invalid") {
|
|
77
|
+
out.push(`- \`${ref}\` – **${state.status.toUpperCase()}**`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const detail = [
|
|
81
|
+
state.question ? `question: ${state.question}` : "",
|
|
82
|
+
state.retrieved ? `retrieved ${state.retrieved}` : "",
|
|
83
|
+
state.expires ? `expires ${state.expires}` : "",
|
|
84
|
+
state.status === "expired" ? "**EXPIRED**" : "",
|
|
85
|
+
].filter(Boolean);
|
|
86
|
+
out.push(`- \`${ref}\`${detail.length ? ` – ${detail.join(" · ")}` : ""}`);
|
|
87
|
+
}
|
|
88
|
+
out.push("");
|
|
89
|
+
}
|
|
90
|
+
|
|
60
91
|
// Domain model
|
|
61
92
|
const model = d.loadEventsModel();
|
|
62
93
|
const aggregates = new Set<string>();
|
|
@@ -100,6 +131,39 @@ if (glossary) {
|
|
|
100
131
|
if (entries.length) out.push("## Glossary (`CONTEXT.md`, terms used by the story)", "", ...entries.flatMap((x) => [x, ""]));
|
|
101
132
|
}
|
|
102
133
|
|
|
134
|
+
// Context maps: the target says where the story belongs, the as-is map says what the code does today.
|
|
135
|
+
const contextIds = new Set<string>();
|
|
136
|
+
if (typeof story.meta.context === "string") contextIds.add(story.meta.context);
|
|
137
|
+
for (const match of storyText.matchAll(/\bBC-[A-Za-z0-9]+\b/g)) contextIds.add(match[0]);
|
|
138
|
+
|
|
139
|
+
function contextBlocks(path: string): string[] {
|
|
140
|
+
const text = read(path);
|
|
141
|
+
if (!text) return [];
|
|
142
|
+
const lines = text.split("\n");
|
|
143
|
+
const out: string[] = [];
|
|
144
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
145
|
+
const match = /^(#{2,4})\s+(BC-[A-Za-z0-9]+)\b/.exec(lines[i]);
|
|
146
|
+
if (!match || !contextIds.has(match[2])) continue;
|
|
147
|
+
const level = match[1].length;
|
|
148
|
+
const block = [lines[i]];
|
|
149
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
150
|
+
const heading = /^(#{1,6})\s+/.exec(lines[j]);
|
|
151
|
+
if (heading && heading[1].length <= level) break;
|
|
152
|
+
block.push(lines[j]);
|
|
153
|
+
}
|
|
154
|
+
out.push(block.join("\n").trim());
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const targetBlocks = contextBlocks(join(d.ROOT, "docs", "domain", "context-map.md"));
|
|
160
|
+
const asIsBlocks = contextBlocks(join(d.ROOT, "docs", "domain", "context-map.as-is.md"));
|
|
161
|
+
if (targetBlocks.length || asIsBlocks.length) {
|
|
162
|
+
out.push("## Context maps", "");
|
|
163
|
+
if (targetBlocks.length) out.push("### Target (`docs/domain/context-map.md`)", "", ...targetBlocks.flatMap((block) => [block, ""]));
|
|
164
|
+
if (asIsBlocks.length) out.push("### As-is (`docs/domain/context-map.as-is.md`)", "", ...asIsBlocks.flatMap((block) => [block, ""]));
|
|
165
|
+
}
|
|
166
|
+
|
|
103
167
|
// Data model
|
|
104
168
|
const dataModel = read(join(d.ROOT, "docs", "architecture", "data-model.md"));
|
|
105
169
|
if (dataModel && aggregates.size) {
|
|
@@ -71,9 +71,12 @@ const node = process.execPath;
|
|
|
71
71
|
const disc = join(here, "..", "..");
|
|
72
72
|
run("events.yaml", node, [join(disc, "event-storming", "scripts", "validate-events.ts")]);
|
|
73
73
|
run("stories + spec linter", node, [join(disc, "user-stories", "scripts", "validate-stories.ts")]);
|
|
74
|
+
// Research is evidence: expired or missing referenced files warn, they never fail the gate.
|
|
75
|
+
run("research sources", node, [join(disc, "research", "scripts", "research-check.ts"), "--referenced"], true);
|
|
74
76
|
// The map is generated; staleness is a warning (adoption may not have run yet), not a failed gate.
|
|
75
77
|
run("repository map", node, [join(disc, "adopt", "scripts", "repo-map.ts"), "--check"], true);
|
|
76
78
|
run("path-scoped rules", node, [join(disc, "adopt", "scripts", "rule-doc.ts"), "--check"], true);
|
|
79
|
+
run("gap register", node, [join(disc, "adopt", "scripts", "gap-register.ts"), "--check"], true);
|
|
77
80
|
run("project checks", node, [join(d.ROOT, ".claude", "hooks", "project-check.ts"), "--phase", "stop"]);
|
|
78
81
|
run("scenario ↔ test traceability", node, [join(here, "check-scenarios.ts")]);
|
|
79
82
|
run("domain ID traceability", node, [join(here, "check-commands.ts")]);
|