@eventmodelers/cli 1.0.45 → 1.0.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/cli.js +53 -1
- package/package.json +2 -2
- package/shared/build-kit/lib/checks/README.md +59 -0
- package/shared/build-kit/lib/ralph.js +110 -28
- package/shared/build-kit/lib/util/find-slice.cjs +59 -0
- package/shared/build-kit/ralph-claude.js +6 -2
- package/shared/skills/learn-eventmodelers-api/SKILL.md +52 -46
- package/shared/skills/request-feedback/SKILL.md +7 -5
- package/stacks/axon/templates/build-kit/CLAUDE.md +1 -1
- package/stacks/blank/templates/build-kit/CLAUDE.md +1 -1
- package/stacks/blank/templates/build-kit/lib/backend-prompt.md +106 -102
- package/stacks/blank/templates/build-kit/lib/prompt.md +102 -106
- package/stacks/cratis-csharp/templates/build-kit/CLAUDE.md +9 -0
- package/stacks/kurrent/templates/.claude/skills/build-automation/SKILL.md +422 -0
- package/stacks/kurrent/templates/.claude/skills/build-automation/references/feature-flag-patterns.md +19 -0
- package/stacks/kurrent/templates/.claude/skills/build-automation/references/idempotent-dispatch-patterns.md +65 -0
- package/stacks/kurrent/templates/.claude/skills/build-state-change/SKILL.md +418 -0
- package/stacks/kurrent/templates/.claude/skills/build-state-change/references/feature-flag-patterns.md +41 -0
- package/stacks/kurrent/templates/.claude/skills/build-state-change/references/integration-test-patterns.md +64 -0
- package/stacks/kurrent/templates/.claude/skills/build-state-view/SKILL.md +391 -0
- package/stacks/kurrent/templates/build-kit/CLAUDE.md +124 -0
- package/stacks/kurrent/templates/build-kit/lib/AGENT.md +73 -0
- package/stacks/kurrent/templates/build-kit/lib/backend-prompt.md +169 -0
- package/stacks/kurrent/templates/build-kit/lib/prompt.md +128 -0
- package/stacks/kurrent/templates/root/README.md +46 -0
- package/stacks/kurrent/templates/root/docker-compose.yml +45 -0
- package/stacks/kurrent/templates/root/mvnw +259 -0
- package/stacks/kurrent/templates/root/mvnw.cmd +149 -0
- package/stacks/kurrent/templates/root/pom.xml +152 -0
- package/stacks/kurrent/templates/root/src/main/java/com/example/quickstart/QuickstartApplication.java +12 -0
- package/stacks/kurrent/templates/root/src/main/java/com/example/quickstart/common/EventStore.java +91 -0
- package/stacks/kurrent/templates/root/src/main/java/com/example/quickstart/config/KurrentConfiguration.java +42 -0
- package/stacks/kurrent/templates/root/src/main/resources/application.properties +14 -0
- package/stacks/kurrent/templates/root/src/main/resources/static/index.html +11 -0
- package/stacks/modeling-kit/templates/.claude/skills/add-next-slice/SKILL.md +2 -2
- package/stacks/modeling-kit/templates/.claude/skills/attributes/SKILL.md +0 -1
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-checking-completeness/SKILL.md +1 -1
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-checking-completeness/references/examples.md +2 -2
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-core-rules/SKILL.md +2 -2
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-outputs/SKILL.md +1 -1
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-interview-protocol/SKILL.md +1 -1
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/SKILL.md +4 -4
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-storyboarding-events/SKILL.md +1 -1
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models/SKILL.md +1 -1
- package/stacks/modeling-kit/templates/.claude/skills/examples/SKILL.md +1 -1
- package/stacks/modeling-kit/templates/.claude/skills/handle-comment/SKILL.md +13 -4
- package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +2 -2
- package/stacks/modeling-kit/templates/.claude/skills/wdyt/SKILL.md +5 -5
- package/stacks/modeling-kit/templates/kit/AGENTS.md +1 -1
- package/stacks/modeling-kit/templates/kit/CLAUDE.md +2 -2
- package/stacks/node/templates/build-kit/CLAUDE.md +23 -1
- package/stacks/node/templates/build-kit/lib/check-commit-scope.cjs +123 -0
- package/stacks/node/templates/build-kit/lib/checks/00-blocked-paths.cjs +28 -0
- package/stacks/node/templates/build-kit/lib/checks/10-slice-scope.cjs +29 -0
- package/stacks/node/templates/build-kit/lib/checks/20-append-only-migrations.cjs +20 -0
- package/stacks/node/templates/build-kit/lib/checks/30-test-file-present.cjs +44 -0
- package/stacks/node/templates/build-kit/lib/checks/40-no-invented-fields.cjs +92 -0
- package/stacks/node/templates/build-kit/lib/checks/50-spec-coverage.cjs +50 -0
- package/stacks/node/templates/build-kit/lib/checks/90-tsc-build.cjs +22 -0
- package/stacks/node/templates/root/.githooks/pre-commit +11 -0
- package/stacks/node/templates/root/package.json +2 -1
- package/stacks/node/templates/root/setup-env.sh +7 -1
- package/stacks/opencqrs/templates/.claude/skills/build-automation/SKILL.md +434 -0
- package/stacks/opencqrs/templates/.claude/skills/build-automation/references/feature-flag-patterns.md +19 -0
- package/stacks/opencqrs/templates/.claude/skills/build-automation/references/idempotent-dispatch-patterns.md +62 -0
- package/stacks/opencqrs/templates/.claude/skills/build-state-change/SKILL.md +413 -0
- package/stacks/opencqrs/templates/.claude/skills/build-state-change/references/feature-flag-patterns.md +46 -0
- package/stacks/opencqrs/templates/.claude/skills/build-state-change/references/rest-api-patterns.md +145 -0
- package/stacks/opencqrs/templates/.claude/skills/build-state-change/references/test-fixture-patterns.md +85 -0
- package/stacks/opencqrs/templates/.claude/skills/build-state-view/SKILL.md +354 -0
- package/stacks/opencqrs/templates/build-kit/CLAUDE.md +103 -0
- package/stacks/opencqrs/templates/build-kit/lib/AGENT.md +58 -0
- package/stacks/opencqrs/templates/build-kit/lib/backend-prompt.md +169 -0
- package/stacks/opencqrs/templates/build-kit/lib/prompt.md +128 -0
- package/stacks/opencqrs/templates/root/README.md +42 -0
- package/stacks/opencqrs/templates/root/docker-compose.yml +37 -0
- package/stacks/opencqrs/templates/root/mvnw +259 -0
- package/stacks/opencqrs/templates/root/mvnw.cmd +149 -0
- package/stacks/opencqrs/templates/root/pom.xml +139 -0
- package/stacks/opencqrs/templates/root/src/main/java/com/example/quickstart/QuickstartApplication.java +12 -0
- package/stacks/opencqrs/templates/root/src/main/java/com/example/quickstart/config/CqrsConfiguration.java +74 -0
- package/stacks/opencqrs/templates/root/src/main/resources/application.properties +23 -0
- package/stacks/opencqrs/templates/root/src/main/resources/schema.sql +19 -0
- package/stacks/opencqrs/templates/root/src/main/resources/static/index.html +11 -0
- package/stacks/supabase/templates/build-kit/CLAUDE.md +26 -1
- package/stacks/supabase/templates/build-kit/lib/check-commit-scope.cjs +126 -0
- package/stacks/supabase/templates/build-kit/lib/checks/00-blocked-paths.cjs +28 -0
- package/stacks/supabase/templates/build-kit/lib/checks/10-slice-scope.cjs +34 -0
- package/stacks/supabase/templates/build-kit/lib/checks/20-append-only-migrations.cjs +21 -0
- package/stacks/supabase/templates/build-kit/lib/checks/30-test-file-present.cjs +44 -0
- package/stacks/supabase/templates/build-kit/lib/checks/40-no-invented-fields.cjs +92 -0
- package/stacks/supabase/templates/build-kit/lib/checks/50-spec-coverage.cjs +50 -0
- package/stacks/supabase/templates/build-kit/lib/checks/90-tsc-build.cjs +22 -0
- package/stacks/supabase/templates/root/.githooks/pre-commit +11 -0
- package/stacks/supabase/templates/root/package.json +2 -1
- package/stacks/supabase/templates/root/setup-env.sh +7 -1
- package/stacks/umadb/templates/.claude/skills/build-automation/SKILL.md +313 -0
- package/stacks/umadb/templates/.claude/skills/build-automation/references/feature-flag-patterns.md +42 -0
- package/stacks/umadb/templates/.claude/skills/build-state-change/SKILL.md +376 -0
- package/stacks/umadb/templates/.claude/skills/build-state-change/references/feature-flag-patterns.md +42 -0
- package/stacks/umadb/templates/.claude/skills/build-state-change/references/umadb-query-patterns.md +78 -0
- package/stacks/umadb/templates/.claude/skills/build-state-view/SKILL.md +338 -0
- package/stacks/umadb/templates/build-kit/CLAUDE.md +94 -0
- package/stacks/umadb/templates/build-kit/lib/AGENT.md +47 -0
- package/stacks/umadb/templates/build-kit/lib/backend-prompt.md +169 -0
- package/stacks/umadb/templates/build-kit/lib/prompt.md +128 -0
- package/stacks/umadb/templates/root/.mvn/wrapper/maven-wrapper.properties +19 -0
- package/stacks/umadb/templates/root/README.md +48 -0
- package/stacks/umadb/templates/root/docker-compose.yml +29 -0
- package/stacks/umadb/templates/root/mvnw +259 -0
- package/stacks/umadb/templates/root/mvnw.cmd +149 -0
- package/stacks/umadb/templates/root/pom.xml +151 -0
- package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/QuickstartApplication.java +12 -0
- package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/config/UmaDbConfig.java +39 -0
- package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/DecisionModelLoader.java +76 -0
- package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/EventCodec.java +40 -0
- package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/EventDispatcher.java +100 -0
- package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/OptimisticConcurrencyException.java +14 -0
- package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/SliceEventListener.java +23 -0
- package/stacks/umadb/templates/root/src/main/resources/application.properties +17 -0
- package/stacks/umadb/templates/root/src/test/java/io/umadb/quickstart/testsupport/InMemoryUmaDbClient.java +135 -0
- package/stacks/umadb/templates/root/src/test/resources/application.properties +8 -0
|
@@ -21,20 +21,29 @@ From `$ARGUMENTS` or the calling skill's context, extract:
|
|
|
21
21
|
| `nodeId` | UUID of the target node | **required** |
|
|
22
22
|
| `text` | Comment text (place) or substring to match (resolve/delete) | required for `place`; used to look up comment when `commentId` is absent |
|
|
23
23
|
| `commentId` | UUID of the comment to resolve/delete | preferred over `text` for resolve/delete |
|
|
24
|
-
| `type` | `COMMENT
|
|
25
|
-
| `author` | Author identifier string | `agent` (place only) |
|
|
24
|
+
| `type` | `COMMENT` or `TASK` | `COMMENT` (place only) |
|
|
25
|
+
| `author` | Author identifier string | `agent-$CLAUDE_CODE_SESSION_ID` (place only) |
|
|
26
26
|
| `boardId` | Board UUID | from `connect` skill (`BOARD_ID`) |
|
|
27
27
|
|
|
28
|
+
Use the session env var, not the literal string `agent` — `CLAUDE_CODE_SESSION_ID` is set by the
|
|
29
|
+
host for every session, so two agents working the same board at once post under distinguishable
|
|
30
|
+
authors instead of an identical one. If the env var is unset (non-Claude-Code host), fall back to
|
|
31
|
+
the literal `agent`.
|
|
32
|
+
|
|
33
|
+
There is no `QUESTION` type at the API level — a question is just a `COMMENT` whose text happens
|
|
34
|
+
to be phrased as a question. Callers that want to flag something as a question should word the
|
|
35
|
+
`text` accordingly, not pass a special `type`.
|
|
36
|
+
|
|
28
37
|
Route to the matching section below based on `action`.
|
|
29
38
|
|
|
30
39
|
---
|
|
31
40
|
|
|
32
41
|
## Action: place
|
|
33
42
|
|
|
34
|
-
**Prefer MCP** — one call, `type` (`COMMENT`/`TASK
|
|
43
|
+
**Prefer MCP** — one call, `type` (`COMMENT`/`TASK`) passed straight through:
|
|
35
44
|
|
|
36
45
|
```
|
|
37
|
-
mcp__eventmodelers__add_comment { "boardId": "$BOARD_ID", "nodeId": "$NODE_ID", "text": "<text>", "type": "<COMMENT|TASK
|
|
46
|
+
mcp__eventmodelers__add_comment { "boardId": "$BOARD_ID", "nodeId": "$NODE_ID", "text": "<text>", "type": "<COMMENT|TASK>", "author": "<author>" }
|
|
38
47
|
```
|
|
39
48
|
|
|
40
49
|
**Fallback (no MCP):** see `references/api-fallback.md` — "Action: place".
|
|
@@ -376,9 +376,9 @@ Count inbound edges where `target === COMMAND_NODE_ID` and the source node is ty
|
|
|
376
376
|
mcp__eventmodelers__set_connection { "boardId": "<BOARD_ID>", "source": "<extra-issuer-node-id>", "target": "<COMMAND_NODE_ID>", "action": "remove" }
|
|
377
377
|
```
|
|
378
378
|
|
|
379
|
-
If it's not clear which edge is the deliberate one (e.g. neither source sits in the COMMAND's own column), do not guess — leave both edges and post a
|
|
379
|
+
If it's not clear which edge is the deliberate one (e.g. neither source sits in the COMMAND's own column), do not guess — leave both edges and post a comment on the COMMAND node via `handle-comment` instead, describing the ambiguity.
|
|
380
380
|
|
|
381
|
-
**Fallback (no MCP)**: there is no documented single-purpose REST endpoint for edge removal outside `/nodes/events`. Connect MCP via the `connect` skill first; if that's genuinely not possible, skip the auto-fix and post a
|
|
381
|
+
**Fallback (no MCP)**: there is no documented single-purpose REST endpoint for edge removal outside `/nodes/events`. Connect MCP via the `connect` skill first; if that's genuinely not possible, skip the auto-fix and post a comment on the COMMAND node flagging the double issuer for manual resolution instead of fabricating a payload.
|
|
382
382
|
|
|
383
383
|
---
|
|
384
384
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wdyt
|
|
3
|
-
description: Business analyst exploration of an event model board. Reads all slices, analyzes them from a business perspective, and posts questions/observations as
|
|
3
|
+
description: Business analyst exploration of an event model board. Reads all slices, analyzes them from a business perspective, and posts questions/observations as comments on relevant nodes. Findings about a relationship between elements or a cluster of elements are always additionally drawn on the canvas (arrows, group loops) — comments carry every textual question, drawings carry every visual/structural hint.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# WDYT — What Do You Think?
|
|
@@ -113,17 +113,17 @@ Each of these four is inherently about a relationship or cluster of elements, so
|
|
|
113
113
|
|
|
114
114
|
The two channels have a strict division of labor, always applied the same way — never swap them:
|
|
115
115
|
|
|
116
|
-
- **Every textual question is a comment.** If the finding is "what happens / who does this / what do we expect" about a single element, it is worded and lives only in a
|
|
116
|
+
- **Every textual question is a comment.** If the finding is "what happens / who does this / what do we expect" about a single element, it is worded and lives only in a comment. Never draw a text callout on the canvas to carry a question — that content belongs in 4.1, full stop.
|
|
117
117
|
- **Every visual/structural hint is a drawing.** If the finding is inherently about *where things are relative to each other* — a relationship between two elements, or a cluster of elements sharing one concern — it is always additionally drawn on the canvas (4.2), not left as text alone. This isn't a selective "top 3" step; it's determined by the shape of the finding itself: relational or clustered → draw it, every time.
|
|
118
118
|
|
|
119
119
|
### 4.1 Comments (every textual question, always)
|
|
120
120
|
|
|
121
|
-
For each question you want to ask, post it as a
|
|
121
|
+
For each question you want to ask, post it as a comment on the most relevant node (the COMMAND, EVENT, SCREEN, or READMODEL the question is about). If a question is about the whole slice rather than a specific element, post it on the first/primary EVENT of the slice.
|
|
122
122
|
|
|
123
123
|
Use the `handle-comment` skill with `action=place` to post each comment. Pass:
|
|
124
124
|
- `nodeId` — the UUID of the element the question is about
|
|
125
125
|
- `text` — your question (one sentence, plain business language)
|
|
126
|
-
- `type` — `
|
|
126
|
+
- `type` — `COMMENT` (there is no separate question type — the text itself carries the question)
|
|
127
127
|
- `author` — `wdyt`
|
|
128
128
|
|
|
129
129
|
The comment API has no batch endpoint — `handle-comment` sends one request per comment. Fire them sequentially.
|
|
@@ -137,7 +137,7 @@ Use `POST /api/org/{orgId}/boards/{boardId}/drawing/draw` (auth headers same as
|
|
|
137
137
|
- **Arrow** (`kind: "path"`, `arrowEnd: true`) — the concern is about a missing or unclear relationship *between two elements* (e.g. "does this event actually reach this automation?"). Draw a straight line from one element's position to the other's. `path` is `M 0 0 L <dx> <dy>` in the box's own local coordinates; `x`/`y`/`width`/`height` describe that box in canvas space (so `width`/`height` = the delta between the two elements' positions). Get element positions from the slice data already loaded in Step 2 (or `GET .../nodes/{nodeId}` if not present).
|
|
138
138
|
- **Group loop** (`kind: "rect"`, drawn around a computed bounding box) — the concern spans a *cluster* of elements together (e.g. "this whole flow assumes nothing ever fails"). There's no dedicated group endpoint — union the elements' own `x`/`y`/`width`/`height` (plus some padding) yourself and draw one `rect` around that box via `.../drawing/draw`. This is a visual grouping only — unrelated to the `MODEL_CONTEXT` node type; never touch a `modelContext` field to satisfy this.
|
|
139
139
|
|
|
140
|
-
Every arrow/group loop is paired with a
|
|
140
|
+
Every arrow/group loop is paired with a comment on the relevant node(s) from 4.1 — the drawing makes the concern visible at a glance on the canvas itself, the comment carries the actual worded question. Post both; neither replaces the other.
|
|
141
141
|
|
|
142
142
|
A finding about a single element with no relational or cluster dimension gets a comment only — don't manufacture an arrow or loop for it just to add a drawing.
|
|
143
143
|
|
|
@@ -4,7 +4,7 @@ Reusable learnings accumulated while processing prompts for this board. Append n
|
|
|
4
4
|
ones in a compressed, reusable form; only add if not already covered here.
|
|
5
5
|
|
|
6
6
|
- `/place-element` requires an existing column — create one via the timeline API if missing.
|
|
7
|
-
- `/wdyt` posts
|
|
7
|
+
- `/wdyt` posts comments onto nodes — use for analysis only, not modifications.
|
|
8
8
|
- The `board_id`, `timeline_id`, and `organization_id` from each prompt provide full context — pass them to skills that need them.
|
|
9
9
|
- If a prompt's `context.timelineId` is present and non-null, it overrules the prompt's own `timeline_id` field — it's the chapter the user was pointing at on the canvas, which can differ from whatever chapter the prompt/voice session was scoped to. Resolve `TIMELINE_ID` from `context.timelineId` first, falling back to `timeline_id` only when it's absent, before passing it to any skill.
|
|
10
10
|
- Same pattern for node references: if a prompt's `context.selectedNodes` array is present and non-empty, its first entry overrules the prompt's own `node_id` field (e.g. for `/handle-comment`'s `nodeId`) — it reflects the actual canvas selection at prompt time, whereas `node_id` is only set when the prompt originated from a specific node/comment.
|
|
@@ -42,9 +42,9 @@ At the start of every session, read `.agent-modeling-kit/AGENTS.md` if it exists
|
|
|
42
42
|
5. **Invoke the matched skill — never substitute direct tool calls for it.** Execute the prompt using the skill matched in the Skill Selection table below, passing the resolved `TIMELINE_ID`, `NODE_ID`, and `CELL_ID` from step 3 as that skill's `timelineId`/node-reference/`cellName` arguments (not the raw `timeline_id`/`node_id` fields, and not a cell reference parsed from the prompt text). For a skill like `/place-element` that accepts a `cellName`, pass the resolved `CELL_ID` as `cellName` whenever it's present — skip parsing the prompt text for a cell reference entirely in that case.
|
|
43
43
|
|
|
44
44
|
`mcp__eventmodelers__*` tools (and the REST fallback) are building blocks a skill calls *internally* once you've invoked it — they are not a substitute for invoking the skill. Being able to see `mcp__eventmodelers__get_node`/`create_slice`/etc. in your tool list does not mean you should reach for them directly to satisfy a prompt that matches a row in the Skill Selection table: e.g. "add the next slice" always goes through `/eventmodeling-slicing-event-models` (falling through to `/add-next-slice` when nothing existing is left to slice) or `/place-element`, even though technically a couple of raw MCP calls could produce something on the board. The skill is what encodes the actual domain reasoning (which node type follows which, naming, field derivation, dependency notes) — a raw tool call skips all of that and produces a shallower result even when it "works." Only call MCP/REST directly when no row in the table matches the prompt's intent at all.
|
|
45
|
-
**Questioning rule**: you are running autonomously — no human is available to answer questions. If you need clarification, do not pause or ask interactively — post a
|
|
45
|
+
**Questioning rule**: you are running autonomously — no human is available to answer questions. If you need clarification, do not pause or ask interactively — post a comment (`/handle-comment` with `action=place`, `type=COMMENT`) on the most relevant node. Then:
|
|
46
46
|
- If a reasonable default interpretation exists, continue with it.
|
|
47
|
-
- If it doesn't — the prompt is ambiguous enough that any guess risks doing the wrong thing — stop instead of guessing. Skip straight to step 6 and mark the prompt `DONE` with a comment explaining what's unclear and pointing to the
|
|
47
|
+
- If it doesn't — the prompt is ambiguous enough that any guess risks doing the wrong thing — stop instead of guessing. Skip straight to step 6 and mark the prompt `DONE` with a comment explaining what's unclear and pointing to the comment you just posted. Never leave a prompt neither progressed nor closed.
|
|
48
48
|
6. **Mark the prompt as finished** — invoke `/update-prompt-status` with this turn's `prompt_id`, `newStatus=DONE`, and a `comment` that summarizes what you actually did (e.g. "Added the OrderPlaced event and wired it to the read model"). Do this once, right after the work is done — not per skill call within the turn.
|
|
49
49
|
7. If this turn has a `comment_id` field, invoke `/handle-comment` with `action=resolve`, `nodeId` from the resolved `NODE_ID` (step 3), `commentId` from `comment_id`.
|
|
50
50
|
8. Append a progress entry to `progress.txt` — see the Progress Entry Format below. Fill in the `Learnings` line with anything reusable noticed this turn (pattern, gotcha, useful context), or "none".
|
|
@@ -51,12 +51,34 @@ When asked to build a slice, always follow this flow:
|
|
|
51
51
|
- **State-view** — `projections` or `queries` array is non-empty → invoke `/build-state-view`
|
|
52
52
|
- **State-change** — default (has `commands` / `events`) → invoke `/build-state-change`
|
|
53
53
|
3. Invoke the matching skill and follow its instructions completely. Do not deviate.
|
|
54
|
-
4. **Verify against slice.json**: After the skill completes, check that every command field, event field, and specification in slice.json appears in the implementation. No invented fields — if it is not in slice.json, it must not be in the code.
|
|
54
|
+
4. **Verify against slice.json**: After the skill completes, check that every command field, event field, and specification in slice.json appears in the implementation. No invented fields — if it is not in slice.json, it must not be in the code. This applies even when the slice was previously `Done` and reappears as `Planned` — never dismiss a mismatch as "already implemented" or harmless drift; diff slice.json against the code field by field and update the code to match every change.
|
|
55
55
|
5. Run quality checks (`npm run build`, then the slice tests only).
|
|
56
56
|
6. If checks pass, commit with `feat: [Slice Name]` and set slice status to `Done`.
|
|
57
57
|
|
|
58
58
|
After you are done, automatically run the tests for the slice that was edited.
|
|
59
59
|
|
|
60
|
+
## Commit Scope Guard
|
|
61
|
+
|
|
62
|
+
A pre-commit hook (`.githooks/pre-commit`, installed via `init --hooks` — see its own project's setup)
|
|
63
|
+
runs `.build-kit/lib/check-commit-scope.cjs` on every commit that touches `src/slices/{context}/{slice}/`.
|
|
64
|
+
It loads every check under `.build-kit/lib/checks/` and rejects the commit if any of them find a problem:
|
|
65
|
+
|
|
66
|
+
- **blocked-paths** — `package.json`/lockfiles and `server.ts` are never touched by slice work
|
|
67
|
+
- **slice-scope** — everything staged must be inside the slice folder or a documented exception:
|
|
68
|
+
`src/slices/{context}/{Context}Events.ts` or `src/common/loadPostgresEventstore.ts`
|
|
69
|
+
- **append-only-migrations** — `migrations/V{n}__*.sql` may only be **added**, never edited
|
|
70
|
+
- **test-file-present** — a changed Command/Projection/processor file needs a sibling `*.test.ts`
|
|
71
|
+
- **no-invented-fields** — heuristic: flags a field used in code that isn't declared anywhere in
|
|
72
|
+
`.build-kit/.slices/{context}/{slice}/slice.json`
|
|
73
|
+
- **spec-coverage** — heuristic: the test file needs at least as many `it(...)` blocks as slice.json
|
|
74
|
+
has `specifications[]` entries
|
|
75
|
+
- **tsc-build** — `npx tsc --noEmit` must still pass
|
|
76
|
+
|
|
77
|
+
If a commit is rejected, split it — commit the out-of-scope file separately from the slice work, or add
|
|
78
|
+
the missing test/fix the field — rather than passing `--no-verify`. Run `npm run check:scope` any time
|
|
79
|
+
you want to check staged files before committing. To add a new check, read
|
|
80
|
+
`.build-kit/lib/checks/README.md` and drop in a file following its interface — no other wiring needed.
|
|
81
|
+
|
|
60
82
|
## Example Slice Structure
|
|
61
83
|
|
|
62
84
|
```
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Runner for the slice commit-scope guard. Loads every check module from
|
|
5
|
+
// ./checks/*.cjs and runs it against the currently staged changeset.
|
|
6
|
+
//
|
|
7
|
+
// Check interface (see ./checks/README.md for the full contract + a template):
|
|
8
|
+
// module.exports = {
|
|
9
|
+
// name: 'my-check', // short id, shown in violation output
|
|
10
|
+
// skipIfAlreadyFailing: false, // optional — skip this check once an earlier
|
|
11
|
+
// // one has already failed (use for slow checks)
|
|
12
|
+
// run(ctx) {
|
|
13
|
+
// return [{ path: 'some/file.ts', reason: 'why this is a problem' }];
|
|
14
|
+
// },
|
|
15
|
+
// };
|
|
16
|
+
// `run()` returns an array of violations (empty array/undefined/null = pass).
|
|
17
|
+
//
|
|
18
|
+
// `ctx` passed to every check:
|
|
19
|
+
// changes [{status, path}] — staged files (git status letter + path)
|
|
20
|
+
// touchesSlice true — this commit touches src/slices/{context}/{slice}/**
|
|
21
|
+
// (the runner already gates on this before loading checks)
|
|
22
|
+
// repoRoot absolute path to the repo root
|
|
23
|
+
// SLICE_PATTERN RegExp matching a path inside a slice's own folder
|
|
24
|
+
//
|
|
25
|
+
// Zero dependencies — plain Node, so it works from git's pre-commit hook
|
|
26
|
+
// (see ../../.githooks/pre-commit), from `npm run check:scope`, or from CI.
|
|
27
|
+
// Invoked as: node .build-kit/lib/check-commit-scope.cjs
|
|
28
|
+
|
|
29
|
+
const { execSync } = require('child_process');
|
|
30
|
+
const fs = require('fs');
|
|
31
|
+
const path = require('path');
|
|
32
|
+
|
|
33
|
+
const SLICE_PATTERN = /^src\/slices\/[^/]+\/[^/]+\//;
|
|
34
|
+
|
|
35
|
+
function stagedChanges() {
|
|
36
|
+
const out = execSync('git diff --cached --name-status --no-renames', { encoding: 'utf8' });
|
|
37
|
+
return out
|
|
38
|
+
.split('\n')
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.map((line) => {
|
|
41
|
+
const [status, ...rest] = line.split('\t');
|
|
42
|
+
return { status: status[0], path: rest.join('\t') };
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function loadChecks() {
|
|
47
|
+
const checksDir = path.join(__dirname, 'checks');
|
|
48
|
+
if (!fs.existsSync(checksDir)) return [];
|
|
49
|
+
return fs
|
|
50
|
+
.readdirSync(checksDir)
|
|
51
|
+
.filter((f) => f.endsWith('.cjs'))
|
|
52
|
+
.sort() // numeric filename prefixes (00-, 10-, ...) control run order
|
|
53
|
+
.map((f) => {
|
|
54
|
+
let mod;
|
|
55
|
+
try {
|
|
56
|
+
mod = require(path.join(checksDir, f));
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error(`check-commit-scope: failed to load checks/${f} — ${err.message}`);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (typeof mod?.run !== 'function') {
|
|
62
|
+
console.error(`check-commit-scope: skipping checks/${f} — does not export { name, run(ctx) }`);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return { file: f, name: mod.name || f, run: mod.run, skipIfAlreadyFailing: !!mod.skipIfAlreadyFailing };
|
|
66
|
+
})
|
|
67
|
+
.filter(Boolean);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function main() {
|
|
71
|
+
let changes;
|
|
72
|
+
try {
|
|
73
|
+
changes = stagedChanges();
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error('check-commit-scope: could not read staged changes —', err.message);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (changes.length === 0) process.exit(0);
|
|
80
|
+
|
|
81
|
+
const touchesSlice = changes.some((c) => SLICE_PATTERN.test(c.path));
|
|
82
|
+
if (!touchesSlice) process.exit(0); // not a slice commit — nothing to enforce
|
|
83
|
+
|
|
84
|
+
const ctx = {
|
|
85
|
+
changes,
|
|
86
|
+
touchesSlice,
|
|
87
|
+
repoRoot: execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim(),
|
|
88
|
+
SLICE_PATTERN,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const checks = loadChecks();
|
|
92
|
+
const violations = [];
|
|
93
|
+
const claimedPaths = new Set(); // first check to flag a path wins — avoids repeat noise
|
|
94
|
+
|
|
95
|
+
for (const check of checks) {
|
|
96
|
+
if (check.skipIfAlreadyFailing && violations.length > 0) continue;
|
|
97
|
+
|
|
98
|
+
let result;
|
|
99
|
+
try {
|
|
100
|
+
result = check.run(ctx) || [];
|
|
101
|
+
} catch (err) {
|
|
102
|
+
violations.push({ path: '(check error)', reason: `[${check.name}] threw: ${err.message}` });
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const v of result) {
|
|
107
|
+
if (claimedPaths.has(v.path)) continue;
|
|
108
|
+
claimedPaths.add(v.path);
|
|
109
|
+
violations.push({ path: v.path, reason: `[${check.name}] ${v.reason}` });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (violations.length > 0) {
|
|
114
|
+
console.error('\n❌ commit blocked — slice commit-scope guard found issues:\n');
|
|
115
|
+
for (const v of violations) console.error(` - ${v.path} — ${v.reason}`);
|
|
116
|
+
console.error('\nSee .build-kit/lib/checks/ for what each check enforces.\n');
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
process.exit(0);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
main();
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Rejects a slice commit that touches shared infra which must never change from
|
|
4
|
+
// slice work: the package manifest/lockfiles (no new/changed dependencies) and
|
|
5
|
+
// server.ts (routes/processors are auto-discovered — never wired there by hand).
|
|
6
|
+
|
|
7
|
+
const BLOCKED = [
|
|
8
|
+
{
|
|
9
|
+
pattern: /^(package(-lock)?\.json|pnpm-lock\.yaml|yarn\.lock|npm-shrinkwrap\.json)$/,
|
|
10
|
+
reason: 'dependency/package manifest changes are not allowed from a slice commit',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
pattern: /^server\.ts$/,
|
|
14
|
+
reason: 'server.ts is shared infra (routes/processors are auto-discovered) — never touched by slice work',
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
name: 'blocked-paths',
|
|
20
|
+
run(ctx) {
|
|
21
|
+
const violations = [];
|
|
22
|
+
for (const { path: p } of ctx.changes) {
|
|
23
|
+
const hit = BLOCKED.find((b) => b.pattern.test(p));
|
|
24
|
+
if (hit) violations.push({ path: p, reason: hit.reason });
|
|
25
|
+
}
|
|
26
|
+
return violations;
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Everything staged in a slice commit must be inside the slice's own folder, or
|
|
4
|
+
// one of the documented shared-infra exceptions a slice legitimately registers
|
|
5
|
+
// *into* (never rewrites wholesale). See build-kit/lib/AGENT.md and the
|
|
6
|
+
// build-state-view/build-automation SKILL.md files for what a compliant edit to
|
|
7
|
+
// an exception file looks like. Migration files have their own dedicated check
|
|
8
|
+
// (20-append-only-migrations.cjs), so they're allowed through here.
|
|
9
|
+
|
|
10
|
+
const ALLOWED_EXCEPTIONS = [
|
|
11
|
+
/^src\/slices\/[^/]+\/[A-Za-z0-9]+Events\.ts$/, // per-context event union (append-only)
|
|
12
|
+
/^src\/common\/loadPostgresEventstore\.ts$/, // projection registration / schema.migrate()
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const MIGRATION_PATTERN = /^migrations\/V\d+__.*\.sql$/;
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
name: 'slice-scope',
|
|
19
|
+
run(ctx) {
|
|
20
|
+
const violations = [];
|
|
21
|
+
for (const { path: p } of ctx.changes) {
|
|
22
|
+
if (ctx.SLICE_PATTERN.test(p)) continue;
|
|
23
|
+
if (MIGRATION_PATTERN.test(p)) continue;
|
|
24
|
+
if (ALLOWED_EXCEPTIONS.some((r) => r.test(p))) continue;
|
|
25
|
+
violations.push({ path: p, reason: 'outside src/slices/{context}/{slice}/ and not a documented exception' });
|
|
26
|
+
}
|
|
27
|
+
return violations;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// migrations/V{n}__*.sql may only be ADDED, never modified or deleted — migrations
|
|
4
|
+
// are append-only; a fix belongs in a new migration, not an edit to an old one.
|
|
5
|
+
|
|
6
|
+
const MIGRATION_PATTERN = /^migrations\/V\d+__.*\.sql$/;
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
name: 'append-only-migrations',
|
|
10
|
+
run(ctx) {
|
|
11
|
+
const violations = [];
|
|
12
|
+
for (const { status, path: p } of ctx.changes) {
|
|
13
|
+
if (!MIGRATION_PATTERN.test(p)) continue;
|
|
14
|
+
if (status !== 'A') {
|
|
15
|
+
violations.push({ path: p, reason: 'existing migrations are append-only; add a new V{n} file instead of editing this one' });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return violations;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// A slice commit that adds/changes a command handler, projection, or processor
|
|
4
|
+
// must also include a *.test.ts for that slice — catches the agent skipping the
|
|
5
|
+
// matching build skill's test step (build-state-change Step 4 / build-state-view
|
|
6
|
+
// Step 5 / build-automation's DeciderSpecification tests).
|
|
7
|
+
|
|
8
|
+
const { execSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
const IMPLEMENTATION_FILE = /^src\/slices\/([^/]+)\/([^/]+)\/(?:[A-Za-z0-9]+Command|[A-Za-z0-9]+Projection|processor(?:-[A-Za-z0-9]+)?)\.ts$/;
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
name: 'test-file-present',
|
|
14
|
+
run(ctx) {
|
|
15
|
+
const sliceDirs = new Set();
|
|
16
|
+
for (const { path: p } of ctx.changes) {
|
|
17
|
+
const m = IMPLEMENTATION_FILE.exec(p);
|
|
18
|
+
if (m) sliceDirs.add(`src/slices/${m[1]}/${m[2]}`);
|
|
19
|
+
}
|
|
20
|
+
if (sliceDirs.size === 0) return [];
|
|
21
|
+
|
|
22
|
+
let tracked = [];
|
|
23
|
+
try {
|
|
24
|
+
tracked = execSync('git ls-files -- src/slices', { cwd: ctx.repoRoot, encoding: 'utf8' })
|
|
25
|
+
.split('\n')
|
|
26
|
+
.filter(Boolean);
|
|
27
|
+
} catch {
|
|
28
|
+
// best-effort — fall through with whatever's staged
|
|
29
|
+
}
|
|
30
|
+
const known = new Set([...tracked, ...ctx.changes.map((c) => c.path)]);
|
|
31
|
+
|
|
32
|
+
const violations = [];
|
|
33
|
+
for (const dir of sliceDirs) {
|
|
34
|
+
const hasTest = [...known].some((f) => f.startsWith(`${dir}/`) && f.endsWith('.test.ts'));
|
|
35
|
+
if (!hasTest) {
|
|
36
|
+
violations.push({
|
|
37
|
+
path: dir,
|
|
38
|
+
reason: 'no *.test.ts found for this slice — command handlers/projections/processors need test coverage',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return violations;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Heuristic: flags a field name used in a slice's Command/ReadModel type literal
|
|
4
|
+
// that doesn't appear anywhere in that slice's own slice.json. Not a real
|
|
5
|
+
// TS/schema-aware check (no parser dependency) — it regex-extracts the fields
|
|
6
|
+
// declared inside `Command<'Name', { ... }, ...>` and `type XReadModel = { ... }`
|
|
7
|
+
// literals, so unusual formatting (nested object/array field types, nonstandard
|
|
8
|
+
// generics) can slip past undetected. It only ever adds violations for fields it
|
|
9
|
+
// is confident about; when slice.json can't be found/parsed for a slice, that
|
|
10
|
+
// slice's files are skipped entirely rather than guessed at.
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { findSliceJson, normalize } = require('../util/find-slice.cjs');
|
|
15
|
+
|
|
16
|
+
const COMMON_ALLOWED = new Set(
|
|
17
|
+
['id', 'userId', 'correlationId', 'causationId', 'streamName', 'type', 'data', 'metadata', 'createdAt', 'updatedAt', 'timestamp'].map(normalize),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const TYPE_LITERALS = [
|
|
21
|
+
/(?:Command|Event)<\s*'[^']+'\s*,\s*{([^}]*)}/g,
|
|
22
|
+
/type\s+[A-Za-z0-9_]+ReadModel\s*=\s*{([^}]*)}/g,
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const FIELD_LINE = /^\s*([A-Za-z_][A-Za-z0-9_]*)\??\s*:/gm;
|
|
26
|
+
|
|
27
|
+
function collectDeclaredFields(node, out) {
|
|
28
|
+
if (Array.isArray(node)) {
|
|
29
|
+
for (const item of node) collectDeclaredFields(item, out);
|
|
30
|
+
} else if (node && typeof node === 'object') {
|
|
31
|
+
if (typeof node.name === 'string' && ('type' in node || 'optional' in node)) {
|
|
32
|
+
out.add(normalize(node.name));
|
|
33
|
+
}
|
|
34
|
+
for (const key of Object.keys(node)) collectDeclaredFields(node[key], out);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = {
|
|
39
|
+
name: 'no-invented-fields',
|
|
40
|
+
run(ctx) {
|
|
41
|
+
const bySlice = new Map(); // "context/SliceName" -> { context, sliceName, files: [] }
|
|
42
|
+
|
|
43
|
+
for (const { path: p } of ctx.changes) {
|
|
44
|
+
if (!p.endsWith('.ts') || p.endsWith('.test.ts')) continue;
|
|
45
|
+
const m = /^src\/slices\/([^/]+)\/([^/]+)\//.exec(p);
|
|
46
|
+
if (!m) continue;
|
|
47
|
+
const key = `${m[1]}/${m[2]}`;
|
|
48
|
+
if (!bySlice.has(key)) bySlice.set(key, { context: m[1], sliceName: m[2], files: [] });
|
|
49
|
+
bySlice.get(key).files.push(p);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const violations = [];
|
|
53
|
+
|
|
54
|
+
for (const { context, sliceName, files } of bySlice.values()) {
|
|
55
|
+
const slice = findSliceJson(ctx.repoRoot, context, sliceName);
|
|
56
|
+
if (!slice) continue; // can't verify — don't block
|
|
57
|
+
|
|
58
|
+
const declared = new Set();
|
|
59
|
+
collectDeclaredFields(slice, declared);
|
|
60
|
+
if (declared.size === 0) continue; // slice.json shape not recognized — don't block
|
|
61
|
+
|
|
62
|
+
for (const file of files) {
|
|
63
|
+
let content;
|
|
64
|
+
try {
|
|
65
|
+
content = fs.readFileSync(path.join(ctx.repoRoot, file), 'utf8');
|
|
66
|
+
} catch {
|
|
67
|
+
continue; // deleted/unreadable — nothing to check
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const pattern of TYPE_LITERALS) {
|
|
71
|
+
pattern.lastIndex = 0;
|
|
72
|
+
let typeMatch;
|
|
73
|
+
while ((typeMatch = pattern.exec(content))) {
|
|
74
|
+
FIELD_LINE.lastIndex = 0;
|
|
75
|
+
let fieldMatch;
|
|
76
|
+
while ((fieldMatch = FIELD_LINE.exec(typeMatch[1]))) {
|
|
77
|
+
const raw = fieldMatch[1];
|
|
78
|
+
const norm = normalize(raw);
|
|
79
|
+
if (COMMON_ALLOWED.has(norm) || declared.has(norm)) continue;
|
|
80
|
+
violations.push({
|
|
81
|
+
path: file,
|
|
82
|
+
reason: `field "${raw}" is not declared anywhere in slice.json for this slice — check for an invented field`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return violations;
|
|
91
|
+
},
|
|
92
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Heuristic: a slice's *.test.ts must have at least as many `it(...)` blocks as
|
|
4
|
+
// slice.json has `specifications[]` entries. Doesn't verify each spec is
|
|
5
|
+
// actually tested (that would need matching scenario content, not just count),
|
|
6
|
+
// just that nobody's silently short a test case. Skipped (not blocked) when
|
|
7
|
+
// slice.json can't be found or has no specifications[] array.
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { findSliceJson } = require('../util/find-slice.cjs');
|
|
12
|
+
|
|
13
|
+
const TEST_FILE = /^src\/slices\/([^/]+)\/([^/]+)\/[^/]+\.test\.ts$/;
|
|
14
|
+
const IT_BLOCK = /\bit(?:\.(?:only|skip))?\s*\(/g;
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
name: 'spec-coverage',
|
|
18
|
+
run(ctx) {
|
|
19
|
+
const violations = [];
|
|
20
|
+
|
|
21
|
+
for (const { path: p } of ctx.changes) {
|
|
22
|
+
TEST_FILE.lastIndex = 0;
|
|
23
|
+
const m = TEST_FILE.exec(p);
|
|
24
|
+
if (!m) continue;
|
|
25
|
+
const [, context, sliceName] = m;
|
|
26
|
+
|
|
27
|
+
const slice = findSliceJson(ctx.repoRoot, context, sliceName);
|
|
28
|
+
if (!slice || !Array.isArray(slice.specifications) || slice.specifications.length === 0) continue;
|
|
29
|
+
|
|
30
|
+
let content;
|
|
31
|
+
try {
|
|
32
|
+
content = fs.readFileSync(path.join(ctx.repoRoot, p), 'utf8');
|
|
33
|
+
} catch {
|
|
34
|
+
continue; // deleted — nothing to check
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const specCount = slice.specifications.length;
|
|
38
|
+
const itCount = (content.match(IT_BLOCK) || []).length;
|
|
39
|
+
|
|
40
|
+
if (itCount < specCount) {
|
|
41
|
+
violations.push({
|
|
42
|
+
path: p,
|
|
43
|
+
reason: `slice.json declares ${specCount} specification(s) but this test file only has ${itCount} it(...) block(s)`,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return violations;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The project must still typecheck after the slice change. Uses the project's
|
|
4
|
+
// own `typescript` devDependency via `npx tsc --noEmit` — no new dependency,
|
|
5
|
+
// but it does mean this check needs node_modules already installed (same
|
|
6
|
+
// prerequisite `npm run build` already has).
|
|
7
|
+
|
|
8
|
+
const { execSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
name: 'tsc-build',
|
|
12
|
+
skipIfAlreadyFailing: true, // slow — don't bother once the commit is rejected already
|
|
13
|
+
run(ctx) {
|
|
14
|
+
try {
|
|
15
|
+
execSync('npx tsc --noEmit', { cwd: ctx.repoRoot, stdio: 'pipe' });
|
|
16
|
+
return [];
|
|
17
|
+
} catch (err) {
|
|
18
|
+
const output = String(err.stdout || err.message || '').trim().split('\n').slice(0, 20).join('\n');
|
|
19
|
+
return [{ path: '(tsc --noEmit)', reason: `TypeScript build failed:\n${output}` }];
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Installed via setup-env.sh (`git config core.hooksPath .githooks`), so this file
|
|
3
|
+
# is versioned and shared by every clone instead of living only in .git/hooks/.
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
repo_root=$(git rev-parse --show-toplevel)
|
|
7
|
+
script="$repo_root/.build-kit/lib/check-commit-scope.cjs"
|
|
8
|
+
|
|
9
|
+
if [ -f "$script" ]; then
|
|
10
|
+
node "$script"
|
|
11
|
+
fi
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
"dev": "node --env-file=.env --require ts-node/register server.ts",
|
|
8
8
|
"build": "tsc",
|
|
9
9
|
"start": "NODE_ENV=production node --env-file=.env --require ts-node/register server.ts",
|
|
10
|
-
"test": "tsx --test 'src/**/*.test.ts'"
|
|
10
|
+
"test": "tsx --test 'src/**/*.test.ts'",
|
|
11
|
+
"check:scope": "node .build-kit/lib/check-commit-scope.cjs"
|
|
11
12
|
},
|
|
12
13
|
"dependencies": {
|
|
13
14
|
"@event-driven-io/emmett": "^0.42.1-alpha.1",
|
|
@@ -50,4 +50,10 @@ FLYWAY_PASSWORD=${DB_PASSWORD}
|
|
|
50
50
|
EOF
|
|
51
51
|
|
|
52
52
|
echo ""
|
|
53
|
-
echo ".env created successfully."
|
|
53
|
+
echo ".env created successfully."
|
|
54
|
+
|
|
55
|
+
if [ -f .githooks/pre-commit ] && git rev-parse --git-dir >/dev/null 2>&1; then
|
|
56
|
+
chmod +x .githooks/pre-commit 2>/dev/null || true
|
|
57
|
+
git config core.hooksPath .githooks
|
|
58
|
+
echo "Configured git to use .githooks/ (slice commit-scope guard)."
|
|
59
|
+
fi
|