@milehimikey/em 1.8.1 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/.claude/skills/event-modeling/SKILL.md +58 -404
  2. package/.claude/skills/event-modeling-conform/SKILL.md +77 -0
  3. package/.claude/skills/{event-modeling → event-modeling-conform}/reference/conform.md +39 -13
  4. package/.claude/skills/event-modeling-design/SKILL.md +139 -0
  5. package/.claude/skills/event-modeling-discover/SKILL.md +89 -0
  6. package/.claude/skills/{event-modeling → event-modeling-discover}/reference/extract.md +3 -2
  7. package/.claude/skills/event-modeling-implement/SKILL.md +40 -0
  8. package/.claude/skills/{event-modeling → event-modeling-implement}/reference/implement.md +3 -2
  9. package/.claude/skills/event-modeling-review/SKILL.md +58 -0
  10. package/.claude/skills/{event-modeling → event-modeling-shared}/reference/em-dsl.md +115 -83
  11. package/.claude/skills/{event-modeling → event-modeling-shared}/reference/methodology.md +6 -5
  12. package/.claude/skills/event-modeling-shared/reference/operating-principles.md +147 -0
  13. package/.claude/skills/{event-modeling → event-modeling-shared}/templates/model-readme.md +2 -2
  14. package/.claude/skills/{event-modeling → event-modeling-shared}/templates/slice.md +51 -8
  15. package/README.md +24 -4
  16. package/dist/catalog/build.js +9 -0
  17. package/dist/catalog/build.js.map +1 -1
  18. package/dist/catalog/docJoin.d.ts +15 -0
  19. package/dist/catalog/docJoin.js +8 -0
  20. package/dist/catalog/docJoin.js.map +1 -1
  21. package/dist/catalog/modelCollisionValidate.d.ts +22 -0
  22. package/dist/catalog/modelCollisionValidate.js +67 -0
  23. package/dist/catalog/modelCollisionValidate.js.map +1 -0
  24. package/dist/catalog/orphanedSliceDocValidate.d.ts +11 -0
  25. package/dist/catalog/orphanedSliceDocValidate.js +98 -0
  26. package/dist/catalog/orphanedSliceDocValidate.js.map +1 -0
  27. package/dist/catalog/sliceDoc.d.ts +15 -0
  28. package/dist/catalog/sliceDoc.js +18 -0
  29. package/dist/catalog/sliceDoc.js.map +1 -1
  30. package/dist/cli/changelogBuild.d.ts +12 -0
  31. package/dist/cli/changelogBuild.js +89 -0
  32. package/dist/cli/changelogBuild.js.map +1 -0
  33. package/dist/cli/ciInit.d.ts +42 -0
  34. package/dist/cli/ciInit.js +298 -0
  35. package/dist/cli/ciInit.js.map +1 -0
  36. package/dist/cli/conformSupersede.d.ts +32 -0
  37. package/dist/cli/conformSupersede.js +102 -0
  38. package/dist/cli/conformSupersede.js.map +1 -0
  39. package/dist/cli/coverage.d.ts +6 -1
  40. package/dist/cli/coverage.js +28 -3
  41. package/dist/cli/coverage.js.map +1 -1
  42. package/dist/cli/frontmatterSurgery.d.ts +28 -0
  43. package/dist/cli/frontmatterSurgery.js +66 -0
  44. package/dist/cli/frontmatterSurgery.js.map +1 -0
  45. package/dist/cli/markImplemented.js +6 -45
  46. package/dist/cli/markImplemented.js.map +1 -1
  47. package/dist/cli/ratify.d.ts +41 -0
  48. package/dist/cli/ratify.js +151 -0
  49. package/dist/cli/ratify.js.map +1 -0
  50. package/dist/cli/reratify.d.ts +33 -0
  51. package/dist/cli/reratify.js +137 -0
  52. package/dist/cli/reratify.js.map +1 -0
  53. package/dist/cli/skillCheck.d.ts +6 -0
  54. package/dist/cli/skillCheck.js +46 -0
  55. package/dist/cli/skillCheck.js.map +1 -1
  56. package/dist/cli/skillDirs.d.ts +15 -0
  57. package/dist/cli/skillDirs.js +34 -0
  58. package/dist/cli/skillDirs.js.map +1 -0
  59. package/dist/cli/skillSync.d.ts +10 -0
  60. package/dist/cli/skillSync.js +15 -0
  61. package/dist/cli/skillSync.js.map +1 -1
  62. package/dist/cli/sliceIndex.d.ts +3 -0
  63. package/dist/cli/sliceIndex.js +6 -2
  64. package/dist/cli/sliceIndex.js.map +1 -1
  65. package/dist/cli/sliceLink.d.ts +52 -0
  66. package/dist/cli/sliceLink.js +152 -0
  67. package/dist/cli/sliceLink.js.map +1 -0
  68. package/dist/cli/status.d.ts +261 -0
  69. package/dist/cli/status.js +536 -0
  70. package/dist/cli/status.js.map +1 -0
  71. package/dist/cli/usageLog.d.ts +75 -0
  72. package/dist/cli/usageLog.js +193 -0
  73. package/dist/cli/usageLog.js.map +1 -0
  74. package/dist/cli.js +501 -130
  75. package/dist/cli.js.map +1 -1
  76. package/dist/emit/freshnessJson.d.ts +6 -0
  77. package/dist/emit/freshnessJson.js +24 -0
  78. package/dist/emit/freshnessJson.js.map +1 -0
  79. package/dist/emit/json.d.ts +62 -8
  80. package/dist/emit/json.js +14 -3
  81. package/dist/emit/json.js.map +1 -1
  82. package/dist/emit/statusJson.d.ts +7 -0
  83. package/dist/emit/statusJson.js +39 -0
  84. package/dist/emit/statusJson.js.map +1 -0
  85. package/dist/emit/typespec.d.ts +26 -0
  86. package/dist/emit/typespec.js +356 -0
  87. package/dist/emit/typespec.js.map +1 -0
  88. package/dist/emit/usageReportJson.d.ts +3 -0
  89. package/dist/emit/usageReportJson.js +23 -0
  90. package/dist/emit/usageReportJson.js.map +1 -0
  91. package/dist/mcp/server.d.ts +1 -1
  92. package/dist/mcp/server.js +301 -11
  93. package/dist/mcp/server.js.map +1 -1
  94. package/dist/model/rules.d.ts +13 -0
  95. package/dist/model/rules.js +20 -0
  96. package/dist/model/rules.js.map +1 -1
  97. package/dist/templates.d.ts +1 -1
  98. package/dist/templates.js +2 -2
  99. package/dist/util/markers.d.ts +16 -4
  100. package/dist/util/markers.js +23 -10
  101. package/dist/util/markers.js.map +1 -1
  102. package/package.json +2 -1
  103. /package/.claude/skills/{event-modeling → event-modeling-shared}/templates/conformance-report.md +0 -0
  104. /package/.claude/skills/{event-modeling → event-modeling-shared}/templates/state.md +0 -0
@@ -0,0 +1,298 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // `em ci init` (MIL-166): scaffolds the CI enforcement preset docs/ci.md describes as a
3
+ // copy-paste cookbook into two installed, plain GitHub Actions workflow files — converting
4
+ // "machines check this" from available (a recipe someone has to know to copy) to default (a
5
+ // command that installs it). Every check the preset wires already exists as its own `em`
6
+ // command (docs/ci.md); this module only owns the YAML wiring around them.
7
+ //
8
+ // Same install discipline as `em skill install`/`em slice index` (see their own modules):
9
+ // - marker-delimited (src/util/markers.ts's "hash" style, since YAML has no `<!-- -->`
10
+ // comment syntax) — the managed job block sits between `# GENERATED:em-ci:start` /
11
+ // `# GENERATED:em-ci:end`, so a repo can add its own jobs above or below the markers, at
12
+ // the same indent under `jobs:`, and keep them across a future re-run.
13
+ // - idempotent — re-running with nothing changed leaves both files byte-identical; a file
14
+ // that already exists without our markers is left alone unless `--force` says to replace
15
+ // it wholesale (matching `em skill install`: existing-and-no-force is reported, not an
16
+ // error).
17
+ // - `--check` for CI self-verification — never writes; reports whether the managed block in
18
+ // each file still matches what the current preset would generate (missing / no-markers /
19
+ // stale / ok), CI-ready the same way `em slice index --check` is.
20
+ //
21
+ // The generated files are handed to the repo, not owned by `em` forever: past the initial
22
+ // `em ci init`, edit them freely (see each file's own header comment). `--check` is there for a
23
+ // team that would rather pin the vanilla preset and gate on drift — opt-in, the same posture
24
+ // docs/ci.md already recommends for `em skill check` ("if you'd rather pin ... add `em skill
25
+ // check` as its own gate instead of silently overwriting").
26
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
27
+ import { dirname, join } from "node:path";
28
+ import { applyMarker, markerPair } from "../util/markers.js";
29
+ export const CI_WORKFLOW_MARKER = "em-ci";
30
+ export const CONFORM_WORKFLOW_MARKER = "em-conform";
31
+ const CI_WORKFLOW_RELPATH = join(".github", "workflows", "em-ci.yml");
32
+ const CONFORM_WORKFLOW_RELPATH = join(".github", "workflows", "em-conform.yml");
33
+ export function ciWorkflowPath(repoRoot) {
34
+ return join(repoRoot, CI_WORKFLOW_RELPATH);
35
+ }
36
+ export function conformWorkflowPath(repoRoot) {
37
+ return join(repoRoot, CONFORM_WORKFLOW_RELPATH);
38
+ }
39
+ /** Characters that would break out of the double-quoted shell strings the generated workflow
40
+ * embeds `model`/`testsDir` into (`"model.em"`, `"test"`) — same validate-the-input-not-the-
41
+ * output posture as `em scaffold`'s name check (src/cli.ts). */
42
+ const UNSAFE_ARG_CHARS = /["`$\n]/;
43
+ export function findUnsafeCiInitArg(value) {
44
+ return UNSAFE_ARG_CHARS.test(value) ? value : null;
45
+ }
46
+ // ---- em-ci.yml (PR gates + push-triggered badge rebuild) ----
47
+ /** The managed block only (no header/`on:`/`jobs:` scaffolding) — what gets written between
48
+ * the marker pair, both for a fresh file and to patch an existing marked one in place. */
49
+ export function ciManagedBody(model, testsDir) {
50
+ return ` validate:
51
+ name: em validate (changed .em files)
52
+ if: github.event_name == 'pull_request'
53
+ runs-on: ubuntu-latest
54
+ steps:
55
+ - uses: actions/checkout@v4
56
+ with:
57
+ fetch-depth: 0
58
+ - uses: actions/setup-node@v4
59
+ with:
60
+ node-version: 20
61
+ - name: Validate changed models
62
+ run: |
63
+ set -e
64
+ base="\${{ github.event.pull_request.base.sha }}"
65
+ head="\${{ github.event.pull_request.head.sha }}"
66
+ changed=$(git diff --name-only "$base" "$head" -- '*.em')
67
+ if [ -z "$changed" ]; then
68
+ echo "no .em files changed"
69
+ exit 0
70
+ fi
71
+ status=0
72
+ for f in $changed; do
73
+ echo "::group::em validate $f"
74
+ npx @milehimikey/em validate "$f" || status=1
75
+ npx @milehimikey/em validate "$f" --list-issues
76
+ echo "::endgroup::"
77
+ done
78
+ exit $status
79
+
80
+ slice-index:
81
+ name: "em slice index --check (README table drift)"
82
+ if: github.event_name == 'pull_request'
83
+ runs-on: ubuntu-latest
84
+ steps:
85
+ - uses: actions/checkout@v4
86
+ - uses: actions/setup-node@v4
87
+ with:
88
+ node-version: 20
89
+ - name: Check README Slices table is current
90
+ run: npx @milehimikey/em slice index "${model}" --check
91
+
92
+ coverage:
93
+ name: "em coverage --strict (invariant citations)"
94
+ if: github.event_name == 'pull_request'
95
+ runs-on: ubuntu-latest
96
+ steps:
97
+ - uses: actions/checkout@v4
98
+ - uses: actions/setup-node@v4
99
+ with:
100
+ node-version: 20
101
+ - name: Check invariant test coverage
102
+ run: npx @milehimikey/em coverage "${model}" --tests "${testsDir}" --strict
103
+
104
+ ledger:
105
+ name: em ledger (slice doc version/content agreement)
106
+ if: github.event_name == 'pull_request'
107
+ runs-on: ubuntu-latest
108
+ steps:
109
+ - uses: actions/checkout@v4
110
+ with:
111
+ fetch-depth: 0
112
+ - uses: actions/setup-node@v4
113
+ with:
114
+ node-version: 20
115
+ - name: Check slice doc version/content agreement
116
+ run: npx @milehimikey/em ledger "${model}" --from "\${{ github.event.pull_request.base.sha }}"
117
+
118
+ skill-check:
119
+ name: em skill check (vendored skill drift)
120
+ if: github.event_name == 'pull_request'
121
+ runs-on: ubuntu-latest
122
+ steps:
123
+ - uses: actions/checkout@v4
124
+ - uses: actions/setup-node@v4
125
+ with:
126
+ node-version: 20
127
+ - name: Check vendored skill matches installed em
128
+ run: |
129
+ if [ -d .claude/skills/event-modeling ]; then
130
+ npx @milehimikey/em skill check
131
+ else
132
+ echo "no vendored skill at .claude/skills/event-modeling — skipping (run \`em skill install\` to opt in)"
133
+ fi
134
+
135
+ glossary:
136
+ name: "em glossary --fail-on-conflicts (cross-model vocabulary)"
137
+ if: github.event_name == 'pull_request'
138
+ runs-on: ubuntu-latest
139
+ steps:
140
+ - uses: actions/checkout@v4
141
+ - uses: actions/setup-node@v4
142
+ with:
143
+ node-version: 20
144
+ - name: Check glossary consistency across models
145
+ run: npx @milehimikey/em glossary $(git ls-files '*.em') --fail-on-conflicts
146
+
147
+ status-badge:
148
+ name: rebuild status badge (advisory — publish only, never a gate)
149
+ if: github.event_name == 'push'
150
+ runs-on: ubuntu-latest
151
+ permissions:
152
+ contents: write
153
+ steps:
154
+ - uses: actions/checkout@v4
155
+ - uses: actions/setup-node@v4
156
+ with:
157
+ node-version: 20
158
+ - name: Rebuild status badge
159
+ run: npx @milehimikey/em status "${model}" --tests "${testsDir}" --badge -o status-badge.svg
160
+ - name: Commit badge if changed
161
+ run: |
162
+ if git diff --quiet -- status-badge.svg; then
163
+ echo "status-badge.svg unchanged"
164
+ exit 0
165
+ fi
166
+ git config user.name "em-ci"
167
+ git config user.email "em-ci@users.noreply.github.com"
168
+ git add status-badge.svg
169
+ git commit -m "em ci: rebuild status badge [skip ci]"
170
+ git push`;
171
+ }
172
+ function ciWorkflowHeader(model, emVersion) {
173
+ return `# Generated once by \`em ci init ${model}\` (em ${emVersion}) — docs/ci.md
174
+ #
175
+ # This file is yours from here: edit it, add jobs, remove ones you don't want. The content
176
+ # between the GENERATED:${CI_WORKFLOW_MARKER} markers below is what a future \`em ci init\`
177
+ # (e.g. after upgrading em) refreshes in place — add your own jobs above or below the markers,
178
+ # at the same indent under \`jobs:\`, and they survive a re-run untouched. Every gate here fails
179
+ # the PR the same way any other required check does; the status-badge job only ever publishes.
180
+ # \`em ci init ${model} --check\` reports drift in the managed block — advisory unless you wire
181
+ # it into a gate yourself (docs/ci.md).
182
+ name: em ci
183
+
184
+ on:
185
+ pull_request:
186
+ paths:
187
+ - "**/*.em"
188
+ - "**/slices/**"
189
+ - "**/README.md"
190
+ push:
191
+ branches: [main]
192
+
193
+ jobs:
194
+ `;
195
+ }
196
+ // Marker comment lines sit at column 0, not indented to the 2-space job-key column: YAML
197
+ // comments are whitespace-agnostic (indentation only matters for actual content nodes), and
198
+ // `applyMarker`'s regex only ever recognizes the marker text itself, not any indentation before
199
+ // it on the same line — a fixed indent here would get silently swallowed into the discarded
200
+ // middle capture group on the very first re-patch (was a real bug during development: the
201
+ // closing marker's leading spaces vanished on a second `em ci init` run).
202
+ export function buildCiWorkflowFile(model, testsDir, emVersion) {
203
+ const { start, end } = markerPair(CI_WORKFLOW_MARKER, "hash");
204
+ return `${ciWorkflowHeader(model, emVersion)}${start}\n${ciManagedBody(model, testsDir)}\n${end}\n`;
205
+ }
206
+ // ---- em-conform.yml (scheduled, advisory-only conformance cadence) ----
207
+ /** The managed block only (no header/`on:`/`permissions:`/`jobs:` scaffolding) — what gets
208
+ * written between the marker pair, both for a fresh file and to patch an existing marked one
209
+ * in place. */
210
+ export function conformManagedBody(model) {
211
+ const modelDir = dirname(model) === "." ? "." : dirname(model);
212
+ return ` conform:
213
+ runs-on: ubuntu-latest
214
+ steps:
215
+ - uses: actions/checkout@v4
216
+ - uses: actions/setup-node@v4
217
+ with:
218
+ node-version: 20
219
+
220
+ - name: Note reports already present
221
+ run: ls "$MODEL_DIR"/conformance/*-report.md 2>/dev/null | sort > /tmp/reports-before
222
+
223
+ - name: Run conform phase
224
+ run: |
225
+ npm i -g @milehimikey/em@1 @anthropic-ai/claude-code
226
+ em skill install --force
227
+ claude -p "/event-modeling conform" \\
228
+ --allowedTools "Bash(em:*),Bash(git:*),Read,Grep,Glob,Write,Edit"
229
+ env:
230
+ ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
231
+
232
+ - name: Post report
233
+ run: |
234
+ ls "$MODEL_DIR"/conformance/*-report.md 2>/dev/null | sort > /tmp/reports-after
235
+ report=$(comm -13 /tmp/reports-before /tmp/reports-after | tail -1)
236
+ if [ -z "$report" ]; then
237
+ echo "the run produced no new report — nothing to post"
238
+ exit 0
239
+ fi
240
+ gh issue create --title "Model conformance report $(date +%F)" \\
241
+ --body-file "$report"
242
+ env:
243
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
244
+ env:
245
+ MODEL_DIR: ${modelDir}`;
246
+ }
247
+ function conformWorkflowHeader(model, emVersion) {
248
+ return `# Generated once by \`em ci init ${model}\` (em ${emVersion}) — docs/ci.md#conformance-cadence-advisory
249
+ #
250
+ # This file is yours from here: edit it freely. It is advisory-only by construction — the job
251
+ # never fails the build on drift (findings become a GitHub issue for a human to ratify), and
252
+ # the state file's \`Last conformance:\` marker only advances when a human ratifies the run's
253
+ # outcome locally and commits that update. Resist wiring this to every push; cadence, not
254
+ # trigger, is the intended shape (docs/ci.md).
255
+ name: model-conformance
256
+
257
+ on:
258
+ schedule:
259
+ - cron: "0 6 * * 1" # weekly, Monday 06:00 UTC
260
+ workflow_dispatch: {} # and on demand
261
+
262
+ permissions:
263
+ contents: read
264
+ issues: write # the Post report step opens an issue
265
+
266
+ jobs:
267
+ `;
268
+ }
269
+ export function buildConformWorkflowFile(model, emVersion) {
270
+ const { start, end } = markerPair(CONFORM_WORKFLOW_MARKER, "hash");
271
+ return `${conformWorkflowHeader(model, emVersion)}${start}\n${conformManagedBody(model)}\n${end}\n`;
272
+ }
273
+ /**
274
+ * Decide what to do with one generated file: create it if missing, patch the managed block if
275
+ * the marker pair is present, or (without `force`) leave an existing unmarked file alone. Pure
276
+ * — never touches disk; `applyCiFile` below does the actual write.
277
+ *
278
+ * `generated` is the full from-scratch file content (header + `on:`/`jobs:` scaffolding +
279
+ * marker-wrapped body) — written verbatim for `create`/`would-replace`. `managedBody` is just
280
+ * the text between the markers, reused to patch an existing marked file in place without
281
+ * touching whatever a repo added around it.
282
+ */
283
+ export function planCiFile(path, generated, managedBody, markerName, force) {
284
+ if (!existsSync(path))
285
+ return { kind: "create", content: generated };
286
+ const original = readFileSync(path, "utf8");
287
+ const updated = applyMarker(original, markerName, managedBody, "hash");
288
+ if (updated === null) {
289
+ return force ? { kind: "would-replace", content: generated } : { kind: "missing-markers" };
290
+ }
291
+ return updated === original ? { kind: "ok", content: original } : { kind: "stale", content: updated, current: original };
292
+ }
293
+ export function applyCiFile(path, status) {
294
+ if (status.kind === "create" || status.kind === "stale" || status.kind === "would-replace") {
295
+ writeFileSync(path, status.content, "utf8");
296
+ }
297
+ }
298
+ //# sourceMappingURL=ciInit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ciInit.js","sourceRoot":"","sources":["../../src/cli/ciInit.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ export type ApplySupersedeResult = {
2
+ ok: true;
3
+ content: string;
4
+ changed: boolean;
5
+ } | {
6
+ ok: false;
7
+ message: string;
8
+ };
9
+ /**
10
+ * Pure text transform: inserts (or, if already present, no-ops on) a `> **Superseded as of
11
+ * `<revision>`** — findings <findings> since ruled (<on>).` banner line directly under `raw`'s
12
+ * first line. Refuses when `raw` has no first line to anchor under, or when any input carries
13
+ * control characters / an unsafe `findings` value that could corrupt the report or smuggle
14
+ * unintended markdown into it. No fs access — the caller reads/writes; see `runConformSupersede`
15
+ * below.
16
+ */
17
+ export declare function applySupersededBanner(raw: string, revision: string, findings: string, on: string): ApplySupersedeResult;
18
+ export type RunConformSupersedeResult = {
19
+ ok: true;
20
+ path: string;
21
+ changed: boolean;
22
+ } | {
23
+ ok: false;
24
+ message: string;
25
+ };
26
+ /**
27
+ * Resolves `reportPath` (relative to `baseDir` — the `.em` file's directory, same convention
28
+ * every doc/note/report path in `em` uses) and applies the banner. Refuses cleanly when the
29
+ * report doesn't exist rather than creating one — `em conform-supersede` stamps an existing
30
+ * report, it never authors one.
31
+ */
32
+ export declare function runConformSupersede(baseDir: string, reportPath: string, revision: string, findings: string, on: string): RunConformSupersedeResult;
@@ -0,0 +1,102 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // `em conform-supersede` (MIL-164): stamps a conformance report with a "this describes an
3
+ // ancestor" banner once its findings have been ruled on (docs/process.md's "Ratifying
4
+ // conformance findings" — stage 7 of workflow.md, the companion step to run alongside/after
5
+ // `em slice ratify` once a report's findings are ruled). A reader following a superseded
6
+ // report's file:line citations needs to know, before trusting them, that the report was
7
+ // written against an older revision — the exact problem a stale-but-uncaptioned report causes
8
+ // (Meridian Goods' 2026-08-23 report citing a file a later ruling renamed: correct as history,
9
+ // misleading as a current view).
10
+ //
11
+ // Write strategy: additive-only text splice, never a parse+re-serialize of the report — same
12
+ // surgical discipline ratify.ts/markImplemented.ts hold for slice-doc frontmatter, applied here
13
+ // to a plain-markdown report instead. The banner is inserted as a blockquote line directly under
14
+ // the report's title (its first line); every byte after that — the metadata bullets, Summary,
15
+ // Findings, everything — is copied through verbatim. Repeated calls (a report ruled on
16
+ // incrementally, across more than one session) accumulate one banner line per call rather than
17
+ // overwriting the last — each stamp names its own revision/findings/date, so the accumulated
18
+ // block reads as a small history of rulings, not a single mutable fact. Calling again with the
19
+ // EXACT same revision/findings/date is a no-op (`changed: false`), same idempotency convention
20
+ // every other `em` write command holds.
21
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
22
+ import { join } from "node:path";
23
+ import { isValidDateString } from "./stateFile.js";
24
+ const BANNER_PREFIX = "> **Superseded as of `";
25
+ /** Conservative charset for `--findings`: digits, commas, spaces, and either dash style (ASCII
26
+ * hyphen or an en/em dash pasted from a report) — enough to write "1-3", "1, 2, 4", or "1–3",
27
+ * nothing that could smuggle markdown/HTML into the banner it gets spliced into. */
28
+ const FINDINGS_RE = /^[\d,\s\-–—]+$/;
29
+ const CONTROL_CHARS_RE = /[\x00-\x1f\x7f]/;
30
+ /**
31
+ * Pure text transform: inserts (or, if already present, no-ops on) a `> **Superseded as of
32
+ * `<revision>`** — findings <findings> since ruled (<on>).` banner line directly under `raw`'s
33
+ * first line. Refuses when `raw` has no first line to anchor under, or when any input carries
34
+ * control characters / an unsafe `findings` value that could corrupt the report or smuggle
35
+ * unintended markdown into it. No fs access — the caller reads/writes; see `runConformSupersede`
36
+ * below.
37
+ */
38
+ export function applySupersededBanner(raw, revision, findings, on) {
39
+ const trimmedRev = revision.trim();
40
+ if (!trimmedRev)
41
+ return { ok: false, message: "a revision is required (--as-of)" };
42
+ if (CONTROL_CHARS_RE.test(trimmedRev) || trimmedRev.includes("`")) {
43
+ return { ok: false, message: "revision must not contain control characters or a backtick" };
44
+ }
45
+ const trimmedFindings = findings.trim();
46
+ if (!trimmedFindings)
47
+ return { ok: false, message: "a findings spec is required (--findings)" };
48
+ if (!FINDINGS_RE.test(trimmedFindings)) {
49
+ return {
50
+ ok: false,
51
+ message: `--findings "${findings}" must be a plain list/range of numbers (e.g. "1-3" or "1, 2, 4")`,
52
+ };
53
+ }
54
+ if (!isValidDateString(on)) {
55
+ return { ok: false, message: `invalid date "${on}" — expected YYYY-MM-DD` };
56
+ }
57
+ const firstNl = raw.indexOf("\n");
58
+ if (firstNl === -1) {
59
+ return { ok: false, message: "report has no title line to anchor the banner after" };
60
+ }
61
+ const eol = raw[firstNl - 1] === "\r" ? "\r\n" : "\n";
62
+ const titleEnd = firstNl - (eol === "\r\n" ? 1 : 0);
63
+ const titleLine = raw.slice(0, titleEnd);
64
+ const rest = raw.slice(firstNl + 1);
65
+ const newLine = `${BANNER_PREFIX}${trimmedRev}\`** — findings ${trimmedFindings} since ruled (${on}). This report describes an ancestor of the current model; verify file:line citations against the current code before relying on them.`;
66
+ const restLines = rest.split(eol);
67
+ const bannerLines = [];
68
+ let i = 0;
69
+ while (i < restLines.length && restLines[i].startsWith(BANNER_PREFIX)) {
70
+ bannerLines.push(restLines[i]);
71
+ i++;
72
+ }
73
+ if (bannerLines.includes(newLine)) {
74
+ return { ok: true, content: raw, changed: false }; // idempotent no-op — exact stamp already recorded
75
+ }
76
+ const remainder = restLines.slice(i).join(eol);
77
+ const newBannerBlock = [...bannerLines, newLine];
78
+ const content = `${titleLine}${eol}${newBannerBlock.join(eol)}${eol}${remainder}`;
79
+ return { ok: true, content, changed: true };
80
+ }
81
+ /**
82
+ * Resolves `reportPath` (relative to `baseDir` — the `.em` file's directory, same convention
83
+ * every doc/note/report path in `em` uses) and applies the banner. Refuses cleanly when the
84
+ * report doesn't exist rather than creating one — `em conform-supersede` stamps an existing
85
+ * report, it never authors one.
86
+ */
87
+ export function runConformSupersede(baseDir, reportPath, revision, findings, on) {
88
+ const absPath = join(baseDir, reportPath);
89
+ if (!existsSync(absPath)) {
90
+ return { ok: false, message: `no such report: ${reportPath}` };
91
+ }
92
+ const raw = readFileSync(absPath, "utf8");
93
+ const result = applySupersededBanner(raw, revision, findings, on);
94
+ if (!result.ok) {
95
+ return { ok: false, message: `${reportPath}: ${result.message}` };
96
+ }
97
+ if (result.changed) {
98
+ writeFileSync(absPath, result.content, "utf8");
99
+ }
100
+ return { ok: true, path: reportPath, changed: result.changed };
101
+ }
102
+ //# sourceMappingURL=conformSupersede.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conformSupersede.js","sourceRoot":"","sources":["../../src/cli/conformSupersede.ts"],"names":[],"mappings":"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"}
@@ -13,7 +13,12 @@ export declare const INV_TOKEN_RE: RegExp;
13
13
  * citing another slice's ID elsewhere (Dependencies, Open Questions, ...) doesn't cross-credit
14
14
  * that ID as this slice's own (MIL-149) — the section ends at the next `#`/`##` heading or EOF,
15
15
  * same boundary rule sliceDoc.ts's countOpenQuestions() uses, so a `### Added`/`#### Requirement`
16
- * subheading under `## Delta` stays in scope while a sibling `## Scenarios` does not.
16
+ * subheading under `## Delta` stays in scope while a sibling `## Scenarios` does not. Within that
17
+ * scope, only `STRUCTURAL_LINE_RE`'s bullet/subheading lines contribute — MIL-155 — so a wrapped
18
+ * continuation line that legitimately *cites* a sibling slice's ID while explaining this doc's own
19
+ * (e.g. an Invariants bullet's exactly-once rule wrapping onto a line naming another slice's
20
+ * queue-draining ID) doesn't cross-credit that citation just because it sits inside the ownership
21
+ * section too.
17
22
  */
18
23
  export declare function extractInvariantIds(body: string): string[];
19
24
  export interface Citation {
@@ -5,7 +5,9 @@
5
5
  // (the doc's `INV-*` tokens, a test file's source text), so the citation check is deterministic
6
6
  // and grep-shaped: extract every invariant ID a slice doc's own Invariants/Delta sections define
7
7
  // (MIL-149: not every ID its prose merely mentions — a doc narratively citing another slice's ID
8
- // doesn't own it), then scan a test tree for lines that cite it.
8
+ // doesn't own it; MIL-155: including a citation embedded inside those very sections, e.g. an
9
+ // Invariants bullet's explanation pointing at a sibling slice's ID), then scan a test tree for
10
+ // lines that cite it.
9
11
  //
10
12
  // Deliberately a *second*, independent reader of doc bodies, alongside `em export`'s doc join
11
13
  // (docJoin.ts, MIL-91) — not a change to it. `em export`'s frontmatter-only contract is
@@ -42,6 +44,24 @@ export const INV_TOKEN_RE = /\bINV-[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*\b/g;
42
44
  * narratively *cite* another slice's ID without this doc owning it (MIL-149: such a mention was
43
45
  * being cross-credited to the citing slice's own coverage ledger). */
44
46
  const OWNERSHIP_HEADING_RE = /^##\s+(?:Invariants(?:\s*\/\s*Business Rules)?|Delta)\s*$/i;
47
+ /** A line contributes INV tokens only when it's *structural* — a top-level bullet/list item
48
+ * (`- ...` / `* ...`, no leading indentation) or a subheading (`### ...` and deeper, since only
49
+ * `#`/`##` toggles `inOwnershipSection` above) — never an indented continuation line wrapping a
50
+ * multi-line bullet, nor a standalone prose paragraph with no bullet marker (MIL-155). Every
51
+ * cross-credit MIL-149 missed turned out to have exactly this shape: a slice's own Invariants
52
+ * bullet legitimately explains its rule across several wrapped lines, and a later wrapped line
53
+ * names a sibling slice's ID for context — e.g. `request-payment`'s `INV-RP-1` bullet wraps onto
54
+ * a line citing `payments-to-request`'s `INV-PTR-2` by way of explaining the exactly-once
55
+ * mechanism. The bullet's own *opening* line is structural and trusted for every ID it mentions
56
+ * (this also covers `## Delta`'s Renamed bullets, which legitimately declare two IDs — old and
57
+ * new — on one line, and its `**MODIFIED (...):** INV-X — ...` bullets, whose bold label isn't
58
+ * itself an ID); each wrapped continuation line is prose and contributes nothing. The indentation
59
+ * test also covers MIL-156's blessed nested-elaboration-bullet shape: `templates/slice.md`'s
60
+ * Invariants section now suggests a rule statement's optional rationale/edge-case detail go on
61
+ * its own indented sub-bullet (` - {{...}}`) rather than run on into the rule sentence, purely
62
+ * for HTML-render readability — that sub-bullet is indented, so it's already excluded here the
63
+ * same as any other continuation line, and never contributes (or steals) an ID. */
64
+ const STRUCTURAL_LINE_RE = /^(?:[-*]\s|#{3,6}\s)/;
45
65
  /** Statuses `reference/implement.md`'s coverage gate applies to — a slice hasn't reached the
46
66
  * point of needing test citations before `ready-to-implement`, and stays in scope forever after
47
67
  * once `implemented` (tests shouldn't regress away from citing an invariant just because the
@@ -55,7 +75,12 @@ const IN_SCOPE_STATUSES = new Set(["ready-to-implement", "implemented"]);
55
75
  * citing another slice's ID elsewhere (Dependencies, Open Questions, ...) doesn't cross-credit
56
76
  * that ID as this slice's own (MIL-149) — the section ends at the next `#`/`##` heading or EOF,
57
77
  * same boundary rule sliceDoc.ts's countOpenQuestions() uses, so a `### Added`/`#### Requirement`
58
- * subheading under `## Delta` stays in scope while a sibling `## Scenarios` does not.
78
+ * subheading under `## Delta` stays in scope while a sibling `## Scenarios` does not. Within that
79
+ * scope, only `STRUCTURAL_LINE_RE`'s bullet/subheading lines contribute — MIL-155 — so a wrapped
80
+ * continuation line that legitimately *cites* a sibling slice's ID while explaining this doc's own
81
+ * (e.g. an Invariants bullet's exactly-once rule wrapping onto a line naming another slice's
82
+ * queue-draining ID) doesn't cross-credit that citation just because it sits inside the ownership
83
+ * section too.
59
84
  */
60
85
  export function extractInvariantIds(body) {
61
86
  const seen = new Set();
@@ -66,7 +91,7 @@ export function extractInvariantIds(body) {
66
91
  inOwnershipSection = OWNERSHIP_HEADING_RE.test(line);
67
92
  continue;
68
93
  }
69
- if (!inOwnershipSection)
94
+ if (!inOwnershipSection || !STRUCTURAL_LINE_RE.test(line))
70
95
  continue;
71
96
  for (const m of line.matchAll(INV_TOKEN_RE)) {
72
97
  if (!seen.has(m[0])) {
@@ -1 +1 @@
1
- {"version":3,"file":"coverage.js","sourceRoot":"","sources":["../../src/cli/coverage.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"coverage.js","sourceRoot":"","sources":["../../src/cli/coverage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /** A field line's matched span within a frontmatter `inner` string: `key[:]<ws>` prefix
2
+ * (preserved verbatim on write) plus the value text that follows, lowercase-key-matched the
3
+ * same way sliceDoc.ts's own parser folds keys for lookup. Column-0 only — same grammar as
4
+ * sliceDoc.ts's `^([A-Za-z][\w-]*):\s*(.*)$`, so this never matches an indented/list-style line
5
+ * the parser wouldn't treat as a field either. */
6
+ export declare function fieldLineRegex(key: string): RegExp;
7
+ /** Strip a value the same way sliceDoc.ts's splitFrontmatter does when reading (surrounding
8
+ * quotes, then blank-after-trim collapses to "absent") — used here only to classify a *current*
9
+ * field value for an idempotent/refusal guard, matching what the parser would have reported for
10
+ * the same text. */
11
+ export declare function normalizeFieldValue(raw: string | undefined): string | null;
12
+ /** Same anchor as `fieldLineRegex`, but captures through the line's own trailing EOL (or
13
+ * end-of-string, for a field that happens to be the frontmatter's last line) so a match can be
14
+ * spliced out ENTIRELY — key, value, and terminator — rather than just replaced. Used by
15
+ * `em slice reratify` (reratify.ts) to drop a now-stale `ratifiedBy:`/`ratifiedOn:` pair: those
16
+ * fields describe who signed off the PRIOR version, and silently leaving them in place would
17
+ * make a fresh version look already ratified. */
18
+ export declare function fieldLineWithEolRegex(key: string): RegExp;
19
+ /** Byte offsets of the frontmatter block's inner lines (between the two `---` fences), located
20
+ * by walking lines with `indexOf` rather than `split(/\r?\n/)` — a rewrite can't afford the
21
+ * line-ending normalization a split/join would silently perform across the WHOLE file the
22
+ * moment it's written back (fine for a read-only parse, not for a surgical rewrite that must
23
+ * preserve the body byte-for-byte). `null` when there's no well-formed leading `---`/`---`
24
+ * fence, mirroring sliceDoc.ts's own "unterminated fence = no frontmatter" treatment. */
25
+ export declare function locateFrontmatterInner(markdown: string): {
26
+ innerStart: number;
27
+ innerEnd: number;
28
+ } | null;
@@ -0,0 +1,66 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Shared primitives for surgical in-place slice-doc frontmatter edits — extracted from
3
+ // `markImplemented.ts` (MIL-103) when `ratify.ts` (MIL-165) needed the exact same "find one
4
+ // field line's byte span, replace only that span, copy everything else through verbatim"
5
+ // discipline. Both commands share the same non-negotiable contract: a parse+re-serialize
6
+ // through sliceDoc.ts's own (deliberately read-only) parser would silently drop comments, key
7
+ // order, spacing, and every field the command isn't allowed to touch — see markImplemented.ts's
8
+ // module header for the full rationale, which applies unchanged here.
9
+ //
10
+ // Pure, fs-free — callers own reading/writing the file, same split as everywhere else in `em`.
11
+ /** A field line's matched span within a frontmatter `inner` string: `key[:]<ws>` prefix
12
+ * (preserved verbatim on write) plus the value text that follows, lowercase-key-matched the
13
+ * same way sliceDoc.ts's own parser folds keys for lookup. Column-0 only — same grammar as
14
+ * sliceDoc.ts's `^([A-Za-z][\w-]*):\s*(.*)$`, so this never matches an indented/list-style line
15
+ * the parser wouldn't treat as a field either. */
16
+ export function fieldLineRegex(key) {
17
+ // `[^\r\n]*` (not a lazy `.*?` + `\r?$`) deliberately -- JS regex `$`/multiline line-terminator
18
+ // semantics treat a lone `\r` as its own line boundary, which makes a trailing-`\r?$` anchor
19
+ // ambiguous on CRLF input. Excluding \r/\n from the value class sidesteps that entirely: the
20
+ // match always stops at the first \r or \n, full stop, no boundary-matching subtlety involved.
21
+ return new RegExp(`^(${key}[ \\t]*:[ \\t]*)([^\\r\\n]*)`, "im");
22
+ }
23
+ /** Strip a value the same way sliceDoc.ts's splitFrontmatter does when reading (surrounding
24
+ * quotes, then blank-after-trim collapses to "absent") — used here only to classify a *current*
25
+ * field value for an idempotent/refusal guard, matching what the parser would have reported for
26
+ * the same text. */
27
+ export function normalizeFieldValue(raw) {
28
+ if (raw === undefined)
29
+ return null;
30
+ const v = raw.trim().replace(/^["']|["']$/g, "");
31
+ return v === "" ? null : v;
32
+ }
33
+ /** Same anchor as `fieldLineRegex`, but captures through the line's own trailing EOL (or
34
+ * end-of-string, for a field that happens to be the frontmatter's last line) so a match can be
35
+ * spliced out ENTIRELY — key, value, and terminator — rather than just replaced. Used by
36
+ * `em slice reratify` (reratify.ts) to drop a now-stale `ratifiedBy:`/`ratifiedOn:` pair: those
37
+ * fields describe who signed off the PRIOR version, and silently leaving them in place would
38
+ * make a fresh version look already ratified. */
39
+ export function fieldLineWithEolRegex(key) {
40
+ return new RegExp(`^${key}[ \\t]*:[ \\t]*[^\\r\\n]*\\r?\\n?`, "im");
41
+ }
42
+ /** Byte offsets of the frontmatter block's inner lines (between the two `---` fences), located
43
+ * by walking lines with `indexOf` rather than `split(/\r?\n/)` — a rewrite can't afford the
44
+ * line-ending normalization a split/join would silently perform across the WHOLE file the
45
+ * moment it's written back (fine for a read-only parse, not for a surgical rewrite that must
46
+ * preserve the body byte-for-byte). `null` when there's no well-formed leading `---`/`---`
47
+ * fence, mirroring sliceDoc.ts's own "unterminated fence = no frontmatter" treatment. */
48
+ export function locateFrontmatterInner(markdown) {
49
+ if (!/^---[ \t]*\r?\n/.test(markdown))
50
+ return null;
51
+ const innerStart = markdown.indexOf("\n") + 1;
52
+ let pos = innerStart;
53
+ while (pos <= markdown.length) {
54
+ const nl = markdown.indexOf("\n", pos);
55
+ const lineEnd = nl === -1 ? markdown.length : nl;
56
+ const line = markdown.slice(pos, lineEnd);
57
+ if (/^---[ \t]*\r?$/.test(line)) {
58
+ return { innerStart, innerEnd: pos };
59
+ }
60
+ if (nl === -1)
61
+ break;
62
+ pos = nl + 1;
63
+ }
64
+ return null;
65
+ }
66
+ //# sourceMappingURL=frontmatterSurgery.js.map