kataster 0.0.0-stage → 0.2.1
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/agents/acceptance-tester.md +53 -0
- package/.claude/agents/code-reviewer.md +56 -0
- package/.claude/agents/domain-architect.md +40 -0
- package/.claude/agents/lead-dev.md +50 -0
- package/.claude/agents/product-owner.md +39 -0
- package/.claude/agents/repo-analyst.md +78 -0
- package/.claude/agents/requirements-engineer.md +44 -0
- package/.claude/agents/ux-designer.md +64 -0
- package/.claude/hooks/bash-guard.ts +92 -0
- package/.claude/hooks/code-check.ts +37 -0
- package/.claude/hooks/path-guard.ts +34 -0
- package/.claude/hooks/path-rules.ts +66 -0
- package/.claude/hooks/project-check.ts +108 -0
- package/.claude/hooks/protect-files.ts +55 -0
- package/.claude/hooks/session-start.ts +213 -0
- package/.claude/hooks/stop-gate.ts +56 -0
- package/.claude/hooks/validate-docs.ts +58 -0
- package/.claude/lib/architecture.ts +736 -0
- package/.claude/lib/backlog-github.ts +454 -0
- package/.claude/lib/backlog-jira.ts +347 -0
- package/.claude/lib/backlog-sync.ts +42 -0
- package/.claude/lib/backlog.ts +396 -0
- package/.claude/lib/bazel.ts +212 -0
- package/.claude/lib/config.ts +122 -0
- package/.claude/lib/discovery.ts +821 -0
- package/.claude/lib/engineering.ts +406 -0
- package/.claude/lib/nx.ts +129 -0
- package/.claude/lib/repo.ts +1194 -0
- package/.claude/lib/toolgraph.ts +74 -0
- package/.claude/lib/turbo.ts +166 -0
- package/.claude/package-lock.json +427 -0
- package/.claude/package.json +21 -0
- package/.claude/settings.json +82 -0
- package/.claude/skills/adopt/SKILL.md +283 -0
- package/.claude/skills/adopt/scripts/boundary-check.ts +156 -0
- package/.claude/skills/adopt/scripts/package-doc.ts +71 -0
- package/.claude/skills/adopt/scripts/repo-map.ts +123 -0
- package/.claude/skills/adopt/scripts/rule-doc.ts +80 -0
- package/.claude/skills/architect/ADR-FORMAT.md +97 -0
- package/.claude/skills/architect/SKILL.md +64 -0
- package/.claude/skills/architect/TECH-STACK.md +21 -0
- package/.claude/skills/backlog/SKILL.md +198 -0
- package/.claude/skills/backlog/scripts/backlog-pull.ts +45 -0
- package/.claude/skills/backlog/scripts/backlog-push.ts +38 -0
- package/.claude/skills/backlog/scripts/backlog-status.ts +32 -0
- package/.claude/skills/backlog/scripts/jira-import.ts +40 -0
- package/.claude/skills/backlog/scripts/jira-plan.ts +26 -0
- package/.claude/skills/backlog/scripts/jira-record.ts +41 -0
- package/.claude/skills/bootstrap/SKILL.md +119 -0
- package/.claude/skills/codebase-design/DEEPENING.md +37 -0
- package/.claude/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/.claude/skills/codebase-design/SKILL.md +114 -0
- package/.claude/skills/domain-model/CONTEXT-FORMAT.md +47 -0
- package/.claude/skills/domain-model/SKILL.md +64 -0
- package/.claude/skills/domain-model/templates/data-model.md +29 -0
- package/.claude/skills/engineering-conventions/SKILL.md +31 -0
- package/.claude/skills/event-storming/SKILL.md +44 -0
- package/.claude/skills/event-storming/schema/events.schema.json +124 -0
- package/.claude/skills/event-storming/scripts/render-event-storming.ts +11 -0
- package/.claude/skills/event-storming/scripts/validate-events.ts +11 -0
- package/.claude/skills/event-storming/templates/events.example.yaml +91 -0
- package/.claude/skills/feature/SKILL.md +86 -0
- package/.claude/skills/feature/scripts/intake.ts +356 -0
- package/.claude/skills/grill-me/SKILL.md +7 -0
- package/.claude/skills/grill-with-docs/SKILL.md +7 -0
- package/.claude/skills/grilling/SKILL.md +30 -0
- package/.claude/skills/groom-backlog/SKILL.md +93 -0
- package/.claude/skills/groom-backlog/scripts/backlog-health.ts +238 -0
- package/.claude/skills/implement/SKILL.md +129 -0
- package/.claude/skills/implement/scripts/affected.ts +245 -0
- package/.claude/skills/implement/scripts/check-commands.ts +37 -0
- package/.claude/skills/implement/scripts/check-language.ts +15 -0
- package/.claude/skills/implement/scripts/check-scenarios.ts +59 -0
- package/.claude/skills/implement/scripts/next-story.ts +23 -0
- package/.claude/skills/implement/scripts/story-context.ts +135 -0
- package/.claude/skills/implement/scripts/story-status.ts +47 -0
- package/.claude/skills/implement/scripts/verify.ts +92 -0
- package/.claude/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/.claude/skills/improve-codebase-architecture/SKILL.md +71 -0
- package/.claude/skills/retro/SKILL.md +53 -0
- package/.claude/skills/review-stories/SKILL.md +31 -0
- package/.claude/skills/shape/SKILL.md +193 -0
- package/.claude/skills/tdd/SKILL.md +38 -0
- package/.claude/skills/tdd/mocking.md +59 -0
- package/.claude/skills/tdd/tests.md +77 -0
- package/.claude/skills/user-stories/SKILL.md +138 -0
- package/.claude/skills/user-stories/schema/story.schema.json +39 -0
- package/.claude/skills/user-stories/scripts/render-backlog.ts +4 -0
- package/.claude/skills/user-stories/scripts/validate-stories.ts +13 -0
- package/.claude/skills/user-stories/templates/story-template.md +38 -0
- package/.claude/skills/ux-design/SKILL.md +107 -0
- package/.claude/skills/writing-for-agents/SKILL-MECHANICS.md +22 -0
- package/.claude/skills/writing-for-agents/SKILL.md +81 -0
- package/.claude/tsconfig.json +15 -0
- package/.opencode/agents/acceptance-tester.md +64 -0
- package/.opencode/agents/code-reviewer.md +69 -0
- package/.opencode/agents/domain-architect.md +59 -0
- package/.opencode/agents/lead-dev.md +67 -0
- package/.opencode/agents/product-owner.md +59 -0
- package/.opencode/agents/repo-analyst.md +86 -0
- package/.opencode/agents/requirements-engineer.md +58 -0
- package/.opencode/agents/ux-designer.md +83 -0
- package/.opencode/commands/adopt.md +7 -0
- package/.opencode/commands/backlog-sync.md +7 -0
- package/.opencode/commands/bootstrap.md +7 -0
- package/.opencode/commands/event-storming.md +7 -0
- package/.opencode/commands/feature.md +7 -0
- package/.opencode/commands/grill-me.md +7 -0
- package/.opencode/commands/grill-with-docs.md +7 -0
- package/.opencode/commands/groom-backlog.md +7 -0
- package/.opencode/commands/implement.md +7 -0
- package/.opencode/commands/improve-codebase-architecture.md +7 -0
- package/.opencode/commands/retro.md +7 -0
- package/.opencode/commands/review-stories.md +7 -0
- package/.opencode/commands/shape.md +7 -0
- package/.opencode/opencode.jsonc +15 -0
- package/.opencode/plugins/kataster/index.ts +283 -0
- package/AGENTS.md +21 -0
- package/CHANGELOG.md +91 -0
- package/LICENSE +21 -0
- package/README.md +331 -2
- package/bin/kataster.mjs +7 -0
- package/extras/skills/shadcn/SKILL.md +277 -0
- package/extras/skills/shadcn/agents/openai.yml +5 -0
- package/extras/skills/shadcn/assets/shadcn-small.png +0 -0
- package/extras/skills/shadcn/assets/shadcn.png +0 -0
- package/extras/skills/shadcn/cli.md +290 -0
- package/extras/skills/shadcn/customization.md +209 -0
- package/extras/skills/shadcn/evals/evals.json +77 -0
- package/extras/skills/shadcn/mcp.md +105 -0
- package/extras/skills/shadcn/registry.md +277 -0
- package/extras/skills/shadcn/rules/base-vs-radix.md +306 -0
- package/extras/skills/shadcn/rules/chat.md +250 -0
- package/extras/skills/shadcn/rules/composition.md +213 -0
- package/extras/skills/shadcn/rules/forms.md +192 -0
- package/extras/skills/shadcn/rules/icons.md +101 -0
- package/extras/skills/shadcn/rules/styling.md +185 -0
- package/guide/README.md +9 -0
- package/guide/brownfield.md +242 -0
- package/guide/concepts.md +109 -0
- package/guide/greenfield.md +84 -0
- package/guide/install.md +144 -0
- package/guide/release.md +92 -0
- package/package.json +56 -4
- package/src/cli.mjs +302 -0
- package/src/detect.mjs +237 -0
- package/src/install.mjs +427 -0
- package/src/repo.mjs +129 -0
- package/src/util.mjs +110 -0
- package/templates/CONTEXT.md +21 -0
- package/templates/conventions.md +63 -0
- package/templates/docs/adr/README.md +7 -0
- package/templates/docs/architecture/data-model.md +31 -0
- package/templates/docs/domain/context-map.md +21 -0
- package/templates/docs/domain/events.yaml +23 -0
- package/templates/docs/product/initiatives/README.md +41 -0
- package/templates/docs/product/ux-guidelines.md +27 -0
- package/templates/docs/product/vision.md +36 -0
- package/templates/docs/reviews/.gitkeep +0 -0
- package/templates/docs/stories/OPEN_QUESTIONS.md +8 -0
- package/templates/docs/stories/README.md +26 -0
- package/templates/docs/workflow.md +135 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mechanical health check of the open backlog – step 1 of `/groom-backlog`.
|
|
3
|
+
*
|
|
4
|
+
* Finds what nobody spots by reading 60 story files, and what `validate-stories.ts` deliberately does not check
|
|
5
|
+
* (that one answers "is this file well-formed?", this one answers "does this backlog still make sense?"):
|
|
6
|
+
*
|
|
7
|
+
* FINDING dependency cycle – the stories involved can never start
|
|
8
|
+
* FINDING a story depends on one that is dropped (`wont`) or does not exist
|
|
9
|
+
* FINDING priority inversion – a `must` waits for a story of lower priority
|
|
10
|
+
* FINDING a ready story is blocked by a story that is still draft/review
|
|
11
|
+
* FINDING a dead domain reference – an event or an ID in the body that events.yaml no longer has
|
|
12
|
+
* FINDING a story that is not done but already has tests for its scenarios (left in progress, or built elsewhere)
|
|
13
|
+
* FINDING an [OPEN] marker, or an open row in OPEN_QUESTIONS.md older than the threshold
|
|
14
|
+
* FINDING a story with a screen that has not decided its empty case, its rejection or its permission (G7/G8/G11)
|
|
15
|
+
* INFO depth of the dependency chain per story, and what is reachable at all
|
|
16
|
+
*
|
|
17
|
+
* Findings are material for the grooming conversation, not errors: this script never fails a build.
|
|
18
|
+
* Exit code is always 0 unless the stories cannot be read at all.
|
|
19
|
+
*
|
|
20
|
+
* Usage: node backlog-health.ts [BC-Name | ST-NNN ...] (empty = every story that is not done)
|
|
21
|
+
*/
|
|
22
|
+
import * as d from "../../../lib/discovery.ts";
|
|
23
|
+
import * as e from "../../../lib/engineering.ts";
|
|
24
|
+
|
|
25
|
+
const STALE_DAYS = 30;
|
|
26
|
+
|
|
27
|
+
const args = process.argv.slice(2).map((a) => a.toUpperCase());
|
|
28
|
+
const wantedIds = new Set(args.filter((a) => /^ST-\d+$/.test(a)));
|
|
29
|
+
const wantedContexts = new Set(args.filter((a) => a.startsWith("BC-")));
|
|
30
|
+
|
|
31
|
+
const { parsed, failures } = d.loadStories();
|
|
32
|
+
if (!parsed.length) {
|
|
33
|
+
console.error("No stories in docs/stories/ – nothing to groom.");
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
for (const [path, why] of failures) console.log(`FINDING ${d.rel(path)}: unreadable frontmatter – ${why}`);
|
|
37
|
+
|
|
38
|
+
const byId = new Map(parsed.map((s) => [d.storyId(s), s]));
|
|
39
|
+
const meta = (s: d.Story, key: string): unknown => s.meta[key];
|
|
40
|
+
const str = (s: d.Story, key: string): string => (typeof meta(s, key) === "string" ? String(meta(s, key)) : "");
|
|
41
|
+
const arr = (s: d.Story, key: string): string[] => (Array.isArray(meta(s, key)) ? (meta(s, key) as string[]) : []);
|
|
42
|
+
const open = parsed.filter((s) => str(s, "status") !== "done");
|
|
43
|
+
|
|
44
|
+
function selected(s: d.Story): boolean {
|
|
45
|
+
if (!wantedIds.size && !wantedContexts.size) return true;
|
|
46
|
+
return wantedIds.has(d.storyId(s)) || wantedContexts.has(str(s, "context").toUpperCase());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const scope = open.filter(selected);
|
|
50
|
+
if (!scope.length) {
|
|
51
|
+
console.log(`No open stories match ${args.join(", ")}.`);
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const findings: string[] = [];
|
|
56
|
+
const info: string[] = [];
|
|
57
|
+
const find = (id: string, msg: string): void => void findings.push(`${id}: ${msg}`);
|
|
58
|
+
|
|
59
|
+
// --------------------------------------------------------------------------
|
|
60
|
+
// Dependencies
|
|
61
|
+
// --------------------------------------------------------------------------
|
|
62
|
+
const PRIORITY_RANK: Record<string, number> = { must: 0, should: 1, could: 2, wont: 3 };
|
|
63
|
+
const rank = (s: d.Story): number => PRIORITY_RANK[str(s, "priority")] ?? 9;
|
|
64
|
+
|
|
65
|
+
/** The cycle through `id`, as a list of IDs, or null. Depth-first over depends_on. */
|
|
66
|
+
function cycleThrough(id: string): string[] | null {
|
|
67
|
+
const seen = new Map<string, number>();
|
|
68
|
+
const stack: string[] = [];
|
|
69
|
+
const walk = (current: string): string[] | null => {
|
|
70
|
+
const at = seen.get(current);
|
|
71
|
+
if (at !== undefined) return at >= 0 ? [...stack.slice(at), current] : null;
|
|
72
|
+
seen.set(current, stack.length);
|
|
73
|
+
stack.push(current);
|
|
74
|
+
for (const next of arr(byId.get(current) ?? ({ meta: {} } as d.Story), "depends_on")) {
|
|
75
|
+
const found = walk(next);
|
|
76
|
+
if (found) return found;
|
|
77
|
+
}
|
|
78
|
+
stack.pop();
|
|
79
|
+
seen.set(current, -1);
|
|
80
|
+
return null;
|
|
81
|
+
};
|
|
82
|
+
return walk(id);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const reported = new Set<string>();
|
|
86
|
+
for (const s of scope) {
|
|
87
|
+
const id = d.storyId(s);
|
|
88
|
+
const cycle = cycleThrough(id);
|
|
89
|
+
if (cycle && !cycle.some((c) => reported.has(c))) {
|
|
90
|
+
cycle.forEach((c) => reported.add(c));
|
|
91
|
+
find(id, `dependency cycle – ${cycle.join(" → ")}. None of these can ever start.`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const s of scope) {
|
|
96
|
+
const id = d.storyId(s);
|
|
97
|
+
for (const dep of arr(s, "depends_on")) {
|
|
98
|
+
const target = byId.get(dep);
|
|
99
|
+
if (!target) {
|
|
100
|
+
find(id, `depends on ${dep}, which does not exist`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const depStatus = str(target, "status");
|
|
104
|
+
if (str(target, "priority") === "wont") {
|
|
105
|
+
find(id, `depends on ${dep}, which is dropped (priority wont) – ${id} can never start`);
|
|
106
|
+
}
|
|
107
|
+
if (depStatus === "done") continue;
|
|
108
|
+
if (rank(target) > rank(s)) {
|
|
109
|
+
find(
|
|
110
|
+
id,
|
|
111
|
+
`priority inversion – ${str(s, "priority")} story waits for ${dep} (${str(target, "priority")}): ` +
|
|
112
|
+
`either ${dep} is more important than its priority says, or ${id} is less.`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
if (str(s, "status") === "ready" && ["draft", "review"].includes(depStatus)) {
|
|
116
|
+
find(id, `is ready but waits for ${dep}, which is still ${depStatus} – it cannot be implemented yet`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** How many stories have to be done before this one can start (dependencies that are not done). */
|
|
122
|
+
function chainDepth(id: string, seen = new Set<string>()): number {
|
|
123
|
+
if (seen.has(id)) return 0;
|
|
124
|
+
seen.add(id);
|
|
125
|
+
const s = byId.get(id);
|
|
126
|
+
if (!s || str(s, "status") === "done") return 0;
|
|
127
|
+
const deps = arr(s, "depends_on").filter((dep) => str(byId.get(dep) ?? ({ meta: {} } as d.Story), "status") !== "done");
|
|
128
|
+
return deps.length ? 1 + Math.max(...deps.map((dep) => chainDepth(dep, seen))) : 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const deepest = scope
|
|
132
|
+
.map((s) => ({ id: d.storyId(s), title: str(s, "title"), depth: chainDepth(d.storyId(s)) }))
|
|
133
|
+
.sort((a, b) => b.depth - a.depth)
|
|
134
|
+
.slice(0, 5)
|
|
135
|
+
.filter((x) => x.depth > 1);
|
|
136
|
+
for (const x of deepest) info.push(`${x.id} sits ${x.depth} stories deep in the dependency chain – "${x.title}"`);
|
|
137
|
+
|
|
138
|
+
// --------------------------------------------------------------------------
|
|
139
|
+
// Domain references that no longer exist
|
|
140
|
+
// --------------------------------------------------------------------------
|
|
141
|
+
const model = d.loadEventsModel();
|
|
142
|
+
if (model) {
|
|
143
|
+
const known = d.indexModel(model);
|
|
144
|
+
const idRe = /\b(?:ACT|EXT|BC|AGG|EVT|CMD|POL|RM|HS|FLOW)-[A-Z][A-Za-z0-9]+\b/g;
|
|
145
|
+
for (const s of scope) {
|
|
146
|
+
const id = d.storyId(s);
|
|
147
|
+
const dead = new Set<string>();
|
|
148
|
+
for (const ev of arr(s, "events")) if (!known.has(ev)) dead.add(`${ev} (frontmatter)`);
|
|
149
|
+
for (const m of s.body.matchAll(idRe)) if (!known.has(m[0])) dead.add(m[0]);
|
|
150
|
+
if (dead.size) find(id, `refers to IDs that events.yaml no longer has: ${[...dead].join(", ")}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// --------------------------------------------------------------------------
|
|
155
|
+
// Already built, although the story is not done
|
|
156
|
+
// --------------------------------------------------------------------------
|
|
157
|
+
const titles = e.testTitles();
|
|
158
|
+
for (const s of scope) {
|
|
159
|
+
if (str(s, "type") !== "story") continue;
|
|
160
|
+
const id = d.storyId(s);
|
|
161
|
+
const { covered, missing } = e.scenarioCoverage(s, titles);
|
|
162
|
+
if (covered.length && missing.length) {
|
|
163
|
+
find(id, `${covered.length} of ${covered.length + missing.length} scenarios already have a test – partly built`);
|
|
164
|
+
} else if (covered.length && !missing.length) {
|
|
165
|
+
find(id, `every scenario already has a test, although the story is ${str(s, "status")} – is it done?`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// --------------------------------------------------------------------------
|
|
170
|
+
// Stories with a screen that have not decided their states (UX guidelines G7, G8, G11)
|
|
171
|
+
// --------------------------------------------------------------------------
|
|
172
|
+
// `validate-stories.ts` warns about these while a story is draft or review. Here every open story is checked,
|
|
173
|
+
// including the `ready` ones approved under the older rules – that debt belongs in a grooming conversation, not
|
|
174
|
+
// in the output of every single write. Grouped per guideline, because forty separate lines drown everything else.
|
|
175
|
+
const perGuideline = new Map<string, string[]>();
|
|
176
|
+
for (const s of scope) {
|
|
177
|
+
for (const w of d.completenessWarnings(s)) {
|
|
178
|
+
const guideline = /UX guideline (G\d+[a-z]?)/.exec(w)?.[1] ?? "G?";
|
|
179
|
+
perGuideline.set(guideline, [...(perGuideline.get(guideline) ?? []), d.storyId(s)]);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const GUIDELINE_GAP: Record<string, string> = {
|
|
183
|
+
G7: "no scenario for the empty case",
|
|
184
|
+
G8: "a rejection that never says what happens to what was typed",
|
|
185
|
+
G11: "a permission rejection without the \"is not offered\" half",
|
|
186
|
+
};
|
|
187
|
+
for (const [guideline, ids] of [...perGuideline].sort()) {
|
|
188
|
+
findings.push(
|
|
189
|
+
`worth a look – ${ids.length} stories with a screen may have ${GUIDELINE_GAP[guideline] ?? guideline} ` +
|
|
190
|
+
`(${guideline}); the wording check cannot tell whether the guideline applies, the ux-designer can: ${ids.join(", ")}`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// --------------------------------------------------------------------------
|
|
195
|
+
// Decisions that were parked
|
|
196
|
+
// --------------------------------------------------------------------------
|
|
197
|
+
for (const s of scope) {
|
|
198
|
+
const marks = (s.body.match(/\[OPEN\]/g) ?? []).length;
|
|
199
|
+
if (marks) find(d.storyId(s), `${marks} [OPEN] marker(s) – a decision is still missing`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const questions = d.rel(`${d.STORIES_DIR}/OPEN_QUESTIONS.md`);
|
|
203
|
+
const openRows: string[] = [];
|
|
204
|
+
try {
|
|
205
|
+
const text = (await import("node:fs")).readFileSync(`${d.STORIES_DIR}/OPEN_QUESTIONS.md`, "utf8");
|
|
206
|
+
const today = Date.now();
|
|
207
|
+
for (const line of text.split("\n")) {
|
|
208
|
+
const cells = line.split("|").map((c) => c.trim());
|
|
209
|
+
if (cells.length < 6 || !/^\d{4}-\d{2}-\d{2}$/.test(cells[3] ?? "")) continue;
|
|
210
|
+
if (cells[4]) continue; // already answered
|
|
211
|
+
const days = Math.round((today - Date.parse(cells[3])) / 86_400_000);
|
|
212
|
+
if (days >= STALE_DAYS) openRows.push(`${cells[1]} – unanswered for ${days} days: ${cells[2].slice(0, 90)}…`);
|
|
213
|
+
}
|
|
214
|
+
} catch {
|
|
215
|
+
/* no open questions file */
|
|
216
|
+
}
|
|
217
|
+
for (const row of openRows) findings.push(`${questions}: ${row}`);
|
|
218
|
+
|
|
219
|
+
// --------------------------------------------------------------------------
|
|
220
|
+
// Report
|
|
221
|
+
// --------------------------------------------------------------------------
|
|
222
|
+
const counts = new Map<string, number>();
|
|
223
|
+
for (const s of scope) {
|
|
224
|
+
const key = `${str(s, "status")}/${str(s, "priority")}`;
|
|
225
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
226
|
+
}
|
|
227
|
+
console.log(
|
|
228
|
+
`INFO ${scope.length} open stories in scope` +
|
|
229
|
+
(args.length ? ` (${args.join(", ")})` : "") +
|
|
230
|
+
` – ${[...counts].sort().map(([k, n]) => `${k}: ${n}`).join(", ")}`,
|
|
231
|
+
);
|
|
232
|
+
for (const line of info) console.log(`INFO ${line}`);
|
|
233
|
+
for (const line of findings) console.log(`FINDING ${line}`);
|
|
234
|
+
console.log(
|
|
235
|
+
findings.length
|
|
236
|
+
? `\n${findings.length} finding(s) – material for the grooming conversation, not errors.`
|
|
237
|
+
: "\nBacklog health OK – nothing mechanical to challenge.",
|
|
238
|
+
);
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: implement
|
|
3
|
+
description: Engineering workflow – implements one story from docs/stories/ test-first on its own branch, verifies, has it reviewed by code-reviewer and acceptance-tester, refactors, and sets it `done` on the acceptance tester's verdict and hands the pull request to the user to merge.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
argument-hint: "[ST-NNN – empty = next ready story]"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Implement a Story
|
|
9
|
+
|
|
10
|
+
Story: the arguments from the invocation (empty → `node .claude/skills/implement/scripts/next-story.ts`; a story already `in-progress` is finished first).
|
|
11
|
+
|
|
12
|
+
The main session writes the code; subagents only review. One checkpoint belongs to the user: the **merge** (step 7). Everything before it runs without asking – a `ready` story is good enough to build up to the pull request, and the acceptance tester accepts the story, not the user – unless the story turns out to be wrong (see *When the story is wrong*) or a seam is missing from the catalog (step 2).
|
|
13
|
+
|
|
14
|
+
Engineering conventions: `docs/engineering/conventions.md` – read it if it exists; it describes the module layout, how commands and read models are built, and the **seam catalog**.
|
|
15
|
+
|
|
16
|
+
Project profile: `.claude/project.json` – default branch, code roots, commands, preview provider, pull-request tool. Read it instead of assuming `main`, `npm` or GitHub.
|
|
17
|
+
|
|
18
|
+
## 1. Prepare
|
|
19
|
+
1. Working tree clean, on the default branch (`repo.defaultBranch`), up to date (`git pull`). Otherwise stop and tell the user.
|
|
20
|
+
2. With a tracker (`backlog.provider` ≠ `local`), pull first (`/backlog-sync pull`) and check that the story has its
|
|
21
|
+
`external` key – an unlinked story never reaches `ready`, so stop and report instead of implementing a stale mirror.
|
|
22
|
+
3. `node .claude/skills/implement/scripts/story-context.ts ST-NNN` – the context pack: story, exact test titles, dependencies, events/commands/rules/invariants, glossary, data model, ADRs. Read it completely; it is your source, not memory.
|
|
23
|
+
4. `git switch -c st-NNN-<slug>` (slug from the file name), then `node .claude/skills/implement/scripts/story-status.ts ST-NNN in-progress` and commit `ST-NNN: start`.
|
|
24
|
+
|
|
25
|
+
## 2. Test plan
|
|
26
|
+
Call the `skill` tool for `tdd` (and `codebase-design` when a new module or interface is involved).
|
|
27
|
+
Show one table and go on to step 3 without waiting:
|
|
28
|
+
|
|
29
|
+
| Scenario (exact test title) | Seam | Test type | Test file |
|
|
30
|
+
|---|---|---|---|
|
|
31
|
+
|
|
32
|
+
- **Story**: one row per Gherkin scenario. **Tech task**: one row per checklist item that can be tested; the rest gets its evidence named (setting, document). **Spike**: no table – see *Spikes*.
|
|
33
|
+
- Seams come from the seam catalog in the engineering conventions. A seam not in the catalog is a decision for the user – the one case where you stop here and ask.
|
|
34
|
+
- Name anything the story leaves open. Don't guess – see *When the story is wrong*.
|
|
35
|
+
|
|
36
|
+
Seams from the catalog are the "confirmed seams" the tdd skill requires; the user approved the catalog.
|
|
37
|
+
|
|
38
|
+
## 3. Red → green, one scenario at a time
|
|
39
|
+
- Per row: write the test (title exactly `ST-NNN: <scenario title>`), run it and see it fail for the right reason, write the minimal code to pass, run it green. Commit: `ST-NNN: <scenario title>`.
|
|
40
|
+
- Domain IDs in the code: commands, events, read models and policies carry their ID from `events.yaml` (`CMD-ReportProblem`, `EVT-ProblemReported`, …). New code uses the words of `CONTEXT.md`; a new term goes into `CONTEXT.md` first (domain-model skill).
|
|
41
|
+
- Never weaken or delete an existing test to get green. If a test from an earlier story blocks you, its story and this one contradict each other → *When the story is wrong*.
|
|
42
|
+
- Hooks help on the way: formatting and lint after every write, the quick gate (type check + related tests) before you finish a turn.
|
|
43
|
+
- In a monorepo, run the affected set for the packages you touched before the full gate:
|
|
44
|
+
`node .claude/skills/implement/scripts/affected.ts --changed <files>` (or `--base <defaultBranch>`), then
|
|
45
|
+
execute the printed command. It follows shared-library dependents; `verify.ts` still runs everything at the end.
|
|
46
|
+
|
|
47
|
+
## 4. Verify
|
|
48
|
+
`node .claude/skills/implement/scripts/verify.ts` (with `--e2e` when browser tests exist) must be green: lint incl. module boundaries, type check, tests, events and stories valid, scenario ↔ test and domain ID traceability. Language warnings: fix or justify.
|
|
49
|
+
|
|
50
|
+
The full `--e2e` run happens **once, before the push**. During red → green run only the spec you are working on; after the refactor (step 6) run `verify.ts` without `--e2e` plus the specs the refactor touched – CI runs every browser test on the pull request anyway (check `e2e`).
|
|
51
|
+
|
|
52
|
+
## 5. Review (in parallel, scaled to the story)
|
|
53
|
+
Push the branch (`git push -u origin st-NNN-<slug>`) so a preview deployment exists, when the project has one
|
|
54
|
+
(`review.preview` in `.claude/project.json`: `vercel`, `netlify`, … – use the provider's MCP or CLI, or the pull
|
|
55
|
+
request's preview comment). Otherwise there is no preview and the reviewers work from the code.
|
|
56
|
+
|
|
57
|
+
Pick the lane first – review effort follows the risk of the diff, not a fixed list:
|
|
58
|
+
|
|
59
|
+
**Light lane** – when **all** of these hold: size **S**, `events: []`, no command, no migration, no input handling
|
|
60
|
+
(no request handler, form action or route added or changed), no new dependency. Typical: static pages, texts, a
|
|
61
|
+
link, layout.
|
|
62
|
+
- Only `acceptance-tester` – task: story ID, base ref, the preview URL (if known), and: *also check the diff briefly against the engineering conventions (texts from the catalogs, the shared page frames, domain language) and list deviations as minors.* Writes `docs/reviews/ST-NNN-acceptance.md`.
|
|
63
|
+
- No `code-reviewer`, no extra code or security pass.
|
|
64
|
+
|
|
65
|
+
**Normal lane** – everything else. Start in **one** message:
|
|
66
|
+
- `code-reviewer` – task: story ID, base ref, the context pack command. Writes `docs/reviews/ST-NNN-code-review.md`.
|
|
67
|
+
- `acceptance-tester` – task: story ID, base ref, the preview URL (if known). Writes `docs/reviews/ST-NNN-acceptance.md`.
|
|
68
|
+
- Only for size **L/XL** or a story that changes the command layer or other shared platform code: ask the `code-reviewer` for a second, bug-focused pass over the diff.
|
|
69
|
+
- Only when the **diff** touches input handling, login/roles/session, uploads or files, or security headers/CSP: ask the `code-reviewer` for a security-focused pass.
|
|
70
|
+
|
|
71
|
+
Say which lane you picked and why in one line.
|
|
72
|
+
|
|
73
|
+
Tell the reviewers the follow-up hurdle below, so they grade findings with it in mind.
|
|
74
|
+
|
|
75
|
+
## 6. Refactor
|
|
76
|
+
- Fix all blockers and majors **inside this story**, and the minors you agree with; say which minors you skip and why. Where a finding offers options, apply the recommended one without asking; ask only when no option is recommended or a follow-up story is at stake.
|
|
77
|
+
- **Follow-up hurdle** – a finding becomes a new story only if all of these hold:
|
|
78
|
+
1. it is a security or data-loss risk, or it blocks a *named* upcoming story;
|
|
79
|
+
2. it cannot be fixed in this story within about an hour;
|
|
80
|
+
3. no existing story can take it (check the stories that will first touch that code – prefer adding a checklist item there, via `requirements-engineer`, over a new story).
|
|
81
|
+
Otherwise it stays a line in the review file ("skipped minor" or "revisit when …") – no story. Say for each follow-up why it passes the hurdle.
|
|
82
|
+
- Follow-ups that pass: delegate to `requirements-engineer` as new tech-task stories (`status: draft`, label `follow-up`), referencing the review file. They go through `/review-stories` like any story.
|
|
83
|
+
- **Foundation just in time**: technical groundwork goes into the first story that needs it, not into a separate tech task ahead of the domain stories – a `must` tech task with met dependencies jumps ahead of every domain story in the backlog order.
|
|
84
|
+
- Refactor only with green tests; the tests must not change unless they were wrong (then say so). Run
|
|
85
|
+
`affected.ts` for the touched packages, then `node .claude/skills/implement/scripts/verify.ts` again (without
|
|
86
|
+
`--e2e`) plus the browser specs the refactor touched – see step 4.
|
|
87
|
+
|
|
88
|
+
## 7. Acceptance and merge → user checkpoint
|
|
89
|
+
The **acceptance tester's verdict is the acceptance**: the story is `done` in the pull request, so the user only merges.
|
|
90
|
+
1. After the refactor, check the verdict in `docs/reviews/ST-NNN-acceptance.md`. `accepted` or `accepted with remarks` → continue. `not accepted` → fix the gaps in the story and run the `acceptance-tester` again, until it accepts. Only when something goes badly wrong – the verdict stays `not accepted` after a second round, a scenario can't be met as written, the preview shows a defect the tester couldn't see – ask the user, the way *When the story is wrong* describes.
|
|
91
|
+
2. `node .claude/skills/implement/scripts/story-status.ts ST-NNN done` – refuses unless every scenario has its test (story) or every checklist item is ticked (spike/tech task) and all dependencies are done. Commit `ST-NNN: done`, push.
|
|
92
|
+
- GitHub: the status change closes the linked issue.
|
|
93
|
+
- Jira: `story-status.ts` prints the transition it could not make (MCP is agent-only). Run `jira_get_transitions` +
|
|
94
|
+
`jira_transition_issue` on the printed key, then push the story body once (`/backlog-sync push`).
|
|
95
|
+
3. Open a pull request: title `ST-NNN: <story title>`, body = the scenarios as a checklist, links to the story and both review files, the acceptance verdict, the preview URL, remaining minors, and anything the user must approve (a conventions change). (No `gh` CLI, or it isn't authenticated: push and give the user the compare URL `git push` prints.)
|
|
96
|
+
4. **The user merges** – on the preview if they want to look first. Afterwards: back to the default branch and `git pull`.
|
|
97
|
+
|
|
98
|
+
## 8. Wrap-up
|
|
99
|
+
Summarize in a few lines: scenarios implemented, review findings fixed/skipped, follow-ups created, glossary/ADR changes. Then:
|
|
100
|
+
- Suggest `/improve-codebase-architecture` after the foundation stories, when a bounded context is complete, or about every 8–10 done stories.
|
|
101
|
+
- Name the next story (`node .claude/skills/implement/scripts/next-story.ts`) and tell the user to start it in a fresh session (`/new`, then `/implement`): every turn re-reads the whole session, so a second story in the same one costs several times the first.
|
|
102
|
+
|
|
103
|
+
## Spikes
|
|
104
|
+
A spike answers its **Question** within its **Timebox** – no test plan, no TDD for throwaway experiments.
|
|
105
|
+
- Before starting, tell the user the timebox and what will be kept (skeleton code that becomes the application gets tests and passes verify).
|
|
106
|
+
- Tick each checklist item in the story file as it is answered; write results where the item says (an ADR's consequences, `OPEN_QUESTIONS.md`). A new decision → ADR with `status: proposed` (architect skill); the user accepts.
|
|
107
|
+
- Timebox exhausted → stop, document what is known, and ask the user how to continue.
|
|
108
|
+
- Review: `code-reviewer` only for code that is kept; `acceptance-tester` checks the checklist evidence.
|
|
109
|
+
|
|
110
|
+
## When the story is wrong
|
|
111
|
+
Never guess. When a scenario contradicts another story, a rule or an ADR, can't be tested as written, or leaves a decision open:
|
|
112
|
+
1. Stop implementing that part. Mark it `[OPEN]` in the story and add a row to `docs/stories/OPEN_QUESTIONS.md`.
|
|
113
|
+
2. `node .claude/skills/implement/scripts/story-status.ts ST-NNN review` and tell the user. The fix goes through discovery: `requirements-engineer` revises the story, `/review-stories` approves it again. Work already done on the branch stays.
|
|
114
|
+
3. New domain term → `CONTEXT.md` (domain-model skill). Architecture deviation → a `proposed` ADR (architect skill). An accepted ADR is never edited; the hooks block it.
|
|
115
|
+
4. A `done` story is never reopened – a change to finished behaviour is a new story.
|
|
116
|
+
|
|
117
|
+
## Foundation stories
|
|
118
|
+
The first stories build what all others rely on, so they also set up the workflow's tooling:
|
|
119
|
+
- A single quality command in the repository's task runner (e.g. `npm run verify`) that runs
|
|
120
|
+
`node .claude/skills/implement/scripts/verify.ts`. CI runs the same command.
|
|
121
|
+
- Scripts for `lint` (including module-boundary rules), `typecheck`, `test` and, when there are browser tests,
|
|
122
|
+
`test:e2e`. Record them under `commands` in `.claude/project.json` – verify and the hooks read them there.
|
|
123
|
+
- The application's lint and type-check configs exclude `.claude/` and `docs/`; the tooling has its own tsconfig.
|
|
124
|
+
- The hooks use Prettier, ESLint, `tsc` and Vitest when they are installed in the app's `node_modules`. With a
|
|
125
|
+
different toolchain, adapt `.claude/hooks/stop-gate.ts` and `code-check.ts` – they are plain Node.
|
|
126
|
+
- **After the foundation exists** write `docs/engineering/conventions.md` from what now exists (not before):
|
|
127
|
+
module layout, how a command runs (transaction, journal, authorization, version check), a read model, a
|
|
128
|
+
time-based rule, UI texts, test data builders and the fixed clock, and the **seam catalog** – which seam each
|
|
129
|
+
kind of code is tested at. The user approves it; then re-check the stories that depend on the foundation's results.
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which packages are affected by a change, and the command that tests exactly them.
|
|
3
|
+
*
|
|
4
|
+
* node .claude/skills/implement/scripts/affected.ts --changed apps/billing/src/index.ts
|
|
5
|
+
* node .claude/skills/implement/scripts/affected.ts --base origin/main
|
|
6
|
+
* node .claude/skills/implement/scripts/affected.ts --task lint --run
|
|
7
|
+
* node .claude/skills/implement/scripts/affected.ts --json
|
|
8
|
+
*
|
|
9
|
+
* The package graph comes from `docs/architecture/repo-map.json` (manifests) plus the optional build-tool
|
|
10
|
+
* `toolGraph` (Turbo, Nx, Bazel). Dependents are followed transitively: touching a shared library selects the
|
|
11
|
+
* packages that depend on it. The command is emitted in the repository's own tool (Turbo filters, Nx
|
|
12
|
+
* run-many, Bazel, …); `--run` executes it.
|
|
13
|
+
*/
|
|
14
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
15
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
16
|
+
import { join, relative, resolve } from "node:path";
|
|
17
|
+
import { ROOT, readRepoMap, scanRepo, type RepoMap, type RepoPackage } from "../../../lib/repo.ts";
|
|
18
|
+
|
|
19
|
+
const args = process.argv.slice(2);
|
|
20
|
+
|
|
21
|
+
function usage(): void {
|
|
22
|
+
console.log(
|
|
23
|
+
[
|
|
24
|
+
"Usage: node .claude/skills/implement/scripts/affected.ts [options]",
|
|
25
|
+
"",
|
|
26
|
+
" --changed <file> a changed file (repeatable or comma-separated; default: git status)",
|
|
27
|
+
" --base <ref> use git diff against this ref (e.g. origin/main)",
|
|
28
|
+
" --task <name> task to run (default: test)",
|
|
29
|
+
" --run execute the emitted command",
|
|
30
|
+
" --json machine-readable result",
|
|
31
|
+
].join("\n"),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
36
|
+
usage();
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const json = args.includes("--json");
|
|
41
|
+
const run = args.includes("--run");
|
|
42
|
+
|
|
43
|
+
const changed: string[] = [];
|
|
44
|
+
let base = "";
|
|
45
|
+
let task = "test";
|
|
46
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
47
|
+
if (args[i] === "--changed") {
|
|
48
|
+
const value = args[i + 1];
|
|
49
|
+
if (!value || value.startsWith("--")) continue;
|
|
50
|
+
changed.push(...value.split(",").map((f) => f.trim()).filter(Boolean));
|
|
51
|
+
i += 1;
|
|
52
|
+
} else if (args[i] === "--base") {
|
|
53
|
+
const value = args[i + 1];
|
|
54
|
+
if (value && !value.startsWith("--")) {
|
|
55
|
+
base = value;
|
|
56
|
+
i += 1;
|
|
57
|
+
}
|
|
58
|
+
} else if (args[i] === "--task") {
|
|
59
|
+
const value = args[i + 1];
|
|
60
|
+
if (value && !value.startsWith("--")) {
|
|
61
|
+
task = value;
|
|
62
|
+
i += 1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (changed.length && base) {
|
|
68
|
+
console.error("Use either --changed or --base, not both.");
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function git(args: string[]): string[] {
|
|
73
|
+
try {
|
|
74
|
+
return execFileSync("git", args, { cwd: ROOT, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] })
|
|
75
|
+
.split("\n")
|
|
76
|
+
.map((line) => line.trim())
|
|
77
|
+
.filter(Boolean);
|
|
78
|
+
} catch {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function changedFiles(): string[] {
|
|
84
|
+
if (changed.length) return [...new Set(changed.map((f) => relative(ROOT, resolve(ROOT, f)).split("\\").join("/")))];
|
|
85
|
+
if (base) return git(["diff", "--name-only", base]);
|
|
86
|
+
return git(["status", "--porcelain", "--untracked-files=all"])
|
|
87
|
+
.map((line) => line.slice(3).trim().replace(/^.* -> /, ""))
|
|
88
|
+
.filter(Boolean);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function packageForFile(map: RepoMap, file: string): RepoPackage | undefined {
|
|
92
|
+
return map.packages
|
|
93
|
+
.filter((p) => p.path === "." || file === p.path || file.startsWith(`${p.path}/`))
|
|
94
|
+
.sort((a, b) => b.path.length - a.path.length)[0];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function dependentsOf(map: RepoMap): Map<string, Set<string>> {
|
|
98
|
+
const byName = new Map(map.packages.map((p) => [p.name, p]));
|
|
99
|
+
const dependents = new Map<string, Set<string>>();
|
|
100
|
+
const add = (from: string, to: string): void => {
|
|
101
|
+
if (!byName.has(from) || !byName.has(to) || from === to) return;
|
|
102
|
+
dependents.set(to, new Set([...(dependents.get(to) ?? []), from]));
|
|
103
|
+
};
|
|
104
|
+
for (const pkg of map.packages) for (const dep of pkg.internalDeps) add(pkg.name, dep);
|
|
105
|
+
for (const edge of map.toolGraph?.edges ?? []) add(edge.from, edge.to);
|
|
106
|
+
return dependents;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Turbo/pnpm/npm only run a task where the package defines the script; Nx/Bazel may infer targets. */
|
|
110
|
+
function runnableForTask(map: RepoMap, packages: RepoPackage[], task: string): RepoPackage[] {
|
|
111
|
+
switch (map.tooling.kind) {
|
|
112
|
+
case "turbo":
|
|
113
|
+
case "pnpm":
|
|
114
|
+
case "workspaces":
|
|
115
|
+
case "none":
|
|
116
|
+
return packages.filter((p) => Boolean(p.scripts[task]));
|
|
117
|
+
default:
|
|
118
|
+
return packages;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function commandFor(map: RepoMap, packages: RepoPackage[], all: boolean): string {
|
|
123
|
+
const names = packages.map((p) => p.name);
|
|
124
|
+
const paths = packages.map((p) => p.path);
|
|
125
|
+
if (all) {
|
|
126
|
+
switch (map.tooling.kind) {
|
|
127
|
+
case "turbo": return `turbo run ${task}`;
|
|
128
|
+
case "nx": return `nx run-many -t ${task}`;
|
|
129
|
+
case "bazel": return `bazel ${task} //...`;
|
|
130
|
+
case "pnpm": return `pnpm -r ${task}`;
|
|
131
|
+
case "workspaces":
|
|
132
|
+
if (map.tooling.manager === "yarn") return `yarn workspaces run ${task}`;
|
|
133
|
+
if (map.tooling.manager === "bun") return `bun run --filter '*' ${task}`;
|
|
134
|
+
return `npm run ${task} --workspaces`;
|
|
135
|
+
case "go-work": return `go ${task} ./...`;
|
|
136
|
+
case "cargo-workspace": return `cargo ${task} --workspace`;
|
|
137
|
+
case "gradle": return `./gradlew ${task}`;
|
|
138
|
+
case "maven": return `mvn ${task}`;
|
|
139
|
+
default: return existsSync(join(ROOT, "package.json")) ? `npm run ${task}` : "";
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
switch (map.tooling.kind) {
|
|
144
|
+
case "turbo":
|
|
145
|
+
if (base) return `turbo run ${task} --filter='...[${base}]'`;
|
|
146
|
+
return names.length ? `turbo run ${task} ${names.map((n) => `--filter=${n}`).join(" ")}` : "";
|
|
147
|
+
case "nx": {
|
|
148
|
+
if (base) return `nx affected -t ${task} --base=${base}`;
|
|
149
|
+
const projects = packages.map(
|
|
150
|
+
(p) => map.toolGraph?.projectNames?.[p.name] ?? p.projectName ?? p.name.replace(/^@[^/]+\//, ""),
|
|
151
|
+
);
|
|
152
|
+
return projects.length ? `nx run-many -t ${task} -p ${projects.join(",")}` : "";
|
|
153
|
+
}
|
|
154
|
+
case "bazel":
|
|
155
|
+
return paths.length ? `bazel ${task} ${paths.map((p) => `//${p}/...`).join(" ")}` : "";
|
|
156
|
+
case "pnpm":
|
|
157
|
+
return names.length ? `pnpm ${names.map((n) => `--filter=${n}`).join(" ")} ${task}` : "";
|
|
158
|
+
case "workspaces":
|
|
159
|
+
if (map.tooling.manager === "yarn") return names.map((n) => `yarn workspace ${n} ${task}`).join(" && ");
|
|
160
|
+
if (map.tooling.manager === "bun") return names.length ? `bun run ${names.map((n) => `--filter=${n}`).join(" ")} ${task}` : "";
|
|
161
|
+
return names.length ? `npm run ${task} ${names.map((n) => `--workspace=${n}`).join(" ")}` : "";
|
|
162
|
+
case "go-work":
|
|
163
|
+
return paths.length ? `go ${task} ${paths.map((p) => `./${p}/...`).join(" ")}` : "";
|
|
164
|
+
case "cargo-workspace":
|
|
165
|
+
return names.length ? `cargo ${task} ${names.map((n) => `-p ${n}`).join(" ")}` : "";
|
|
166
|
+
case "gradle":
|
|
167
|
+
return paths.length ? `./gradlew ${paths.map((p) => `:${p.split("/").join(":")}:${task}`).join(" ")}` : "";
|
|
168
|
+
case "maven":
|
|
169
|
+
return paths.length ? `mvn -pl ${paths.join(",")} ${task}` : "";
|
|
170
|
+
default:
|
|
171
|
+
return packages.length === 1 && packages[0].scripts[task]
|
|
172
|
+
? `npm --prefix ${packages[0].path} run ${task}`
|
|
173
|
+
: "";
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const map = readRepoMap(ROOT) ?? scanRepo(ROOT, { toolGraph: false });
|
|
178
|
+
const files = changedFiles();
|
|
179
|
+
const changedPackages = new Map<string, RepoPackage>();
|
|
180
|
+
const IGNORED_CHANGED = /^(docs|\.claude|\.opencode|\.github)\//;
|
|
181
|
+
let all = false;
|
|
182
|
+
for (const file of files) {
|
|
183
|
+
const pkg = packageForFile(map, file);
|
|
184
|
+
if (pkg) changedPackages.set(pkg.name, pkg);
|
|
185
|
+
else if (!IGNORED_CHANGED.test(file)) all = true; // root config or an unmapped file can affect everything
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const dependents = dependentsOf(map);
|
|
189
|
+
const affected = new Map(changedPackages);
|
|
190
|
+
const queue = [...changedPackages.keys()];
|
|
191
|
+
while (queue.length) {
|
|
192
|
+
const name = queue.shift()!;
|
|
193
|
+
for (const dependent of dependents.get(name) ?? []) {
|
|
194
|
+
if (affected.has(dependent)) continue;
|
|
195
|
+
const pkg = map.packages.find((p) => p.name === dependent);
|
|
196
|
+
if (pkg) {
|
|
197
|
+
affected.set(dependent, pkg);
|
|
198
|
+
queue.push(dependent);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const packages = (all ? map.packages : [...affected.values()]).sort((a, b) => a.path.localeCompare(b.path));
|
|
204
|
+
const runnable = runnableForTask(map, packages, task);
|
|
205
|
+
const command = commandFor(map, runnable, all);
|
|
206
|
+
const source = map.toolGraph
|
|
207
|
+
? `${map.toolGraph.source}:${map.toolGraph.task ?? "graph"}+manifests`
|
|
208
|
+
: "manifests";
|
|
209
|
+
|
|
210
|
+
if (json) {
|
|
211
|
+
console.log(JSON.stringify({
|
|
212
|
+
changed: files,
|
|
213
|
+
changedPackages: [...changedPackages.values()].map((p) => p.path),
|
|
214
|
+
affected: packages.map((p) => ({ name: p.name, path: p.path })),
|
|
215
|
+
all,
|
|
216
|
+
task,
|
|
217
|
+
base: base || null,
|
|
218
|
+
source,
|
|
219
|
+
command,
|
|
220
|
+
}, null, 2));
|
|
221
|
+
process.exit(0);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
console.log(`Changed files: ${files.length}`);
|
|
225
|
+
if (!files.length) {
|
|
226
|
+
console.log("No changed files found – nothing to do.");
|
|
227
|
+
process.exit(0);
|
|
228
|
+
}
|
|
229
|
+
console.log(`Affected packages (${source}): ${packages.length}${all ? " (all – a root file changed)" : ""}`);
|
|
230
|
+
for (const pkg of packages) console.log(`- ${pkg.path} (${pkg.name})${changedPackages.has(pkg.name) ? " – changed" : ""}`);
|
|
231
|
+
if (runnable.length !== packages.length) {
|
|
232
|
+
console.log(`Only ${runnable.length} of them define a '${task}' task.`);
|
|
233
|
+
}
|
|
234
|
+
if (!packages.length) {
|
|
235
|
+
console.log("No package matched the changed files – run the full gate (verify.ts).");
|
|
236
|
+
process.exit(0);
|
|
237
|
+
}
|
|
238
|
+
console.log("");
|
|
239
|
+
console.log(`Command:\n ${command || "(no command for this tooling – run verify.ts)"}`);
|
|
240
|
+
|
|
241
|
+
if (run) {
|
|
242
|
+
if (!command) process.exit(1);
|
|
243
|
+
const result = spawnSync(command, { cwd: ROOT, shell: true, stdio: "inherit" });
|
|
244
|
+
process.exit(result.status ?? 1);
|
|
245
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Traceability between the event storming model and the code.
|
|
3
|
+
* Commands, events, read models etc. are referenced in the code by their ID from docs/domain/events.yaml
|
|
4
|
+
* (e.g. a command is registered as "CMD-ReportProblem", a journal entry has type "EVT-ProblemReported").
|
|
5
|
+
*
|
|
6
|
+
* ERROR an ID in the code that does not exist in events.yaml (renamed or invented)
|
|
7
|
+
* INFO which commands and read models are implemented
|
|
8
|
+
*
|
|
9
|
+
* Usage: node check-commands.ts [--verbose]
|
|
10
|
+
*/
|
|
11
|
+
import * as d from "../../../lib/discovery.ts";
|
|
12
|
+
import * as e from "../../../lib/engineering.ts";
|
|
13
|
+
|
|
14
|
+
const model = d.loadEventsModel();
|
|
15
|
+
if (model === null) {
|
|
16
|
+
console.error("docs/domain/events.yaml is missing or invalid.");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const idx = d.indexModel(model);
|
|
20
|
+
const used = e.domainIdsInCode();
|
|
21
|
+
const report = new d.Report();
|
|
22
|
+
for (const [id, files] of used) {
|
|
23
|
+
if (!idx.has(id)) report.error(files.join(", "), `'${id}' does not exist in docs/domain/events.yaml`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const coverage = (key: string): string => {
|
|
27
|
+
const all = (model[key] ?? []) as d.Obj[];
|
|
28
|
+
const done = all.filter((x) => used.has(x.id));
|
|
29
|
+
return `${done.length}/${all.length} ${key.replace("_", " ")} in code`;
|
|
30
|
+
};
|
|
31
|
+
console.log(`INFO ${coverage("commands")} · ${coverage("read_models")} · ${coverage("policies")}`);
|
|
32
|
+
if (process.argv.includes("--verbose")) {
|
|
33
|
+
for (const c of (model.commands ?? []) as d.Obj[]) console.log(`INFO ${used.has(c.id) ? "✓" : " "} ${c.id}`);
|
|
34
|
+
}
|
|
35
|
+
for (const line of report.lines()) console.log(line);
|
|
36
|
+
console.log(report.ok ? "Domain ID traceability OK." : `${report.errors.length} error(s).`);
|
|
37
|
+
process.exit(report.ok ? 0 : 1);
|