@milehimikey/em 1.8.1 → 1.9.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/skills/event-modeling/SKILL.md +58 -404
- package/.claude/skills/event-modeling-conform/SKILL.md +77 -0
- package/.claude/skills/{event-modeling → event-modeling-conform}/reference/conform.md +40 -14
- package/.claude/skills/event-modeling-design/SKILL.md +139 -0
- package/.claude/skills/event-modeling-discover/SKILL.md +89 -0
- package/.claude/skills/{event-modeling → event-modeling-discover}/reference/extract.md +3 -2
- package/.claude/skills/event-modeling-implement/SKILL.md +40 -0
- package/.claude/skills/{event-modeling → event-modeling-implement}/reference/implement.md +3 -2
- package/.claude/skills/event-modeling-review/SKILL.md +58 -0
- package/.claude/skills/{event-modeling → event-modeling-shared}/reference/em-dsl.md +115 -83
- package/.claude/skills/{event-modeling → event-modeling-shared}/reference/methodology.md +6 -5
- package/.claude/skills/event-modeling-shared/reference/operating-principles.md +150 -0
- package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +381 -0
- package/.claude/skills/{event-modeling → event-modeling-shared}/templates/model-readme.md +2 -2
- package/.claude/skills/{event-modeling → event-modeling-shared}/templates/slice.md +57 -14
- package/README.md +24 -4
- package/dist/catalog/build.js +9 -0
- package/dist/catalog/build.js.map +1 -1
- package/dist/catalog/docJoin.d.ts +15 -0
- package/dist/catalog/docJoin.js +8 -0
- package/dist/catalog/docJoin.js.map +1 -1
- package/dist/catalog/modelCollisionValidate.d.ts +22 -0
- package/dist/catalog/modelCollisionValidate.js +67 -0
- package/dist/catalog/modelCollisionValidate.js.map +1 -0
- package/dist/catalog/orphanedSliceDocValidate.d.ts +11 -0
- package/dist/catalog/orphanedSliceDocValidate.js +98 -0
- package/dist/catalog/orphanedSliceDocValidate.js.map +1 -0
- package/dist/catalog/sliceDoc.d.ts +15 -0
- package/dist/catalog/sliceDoc.js +18 -0
- package/dist/catalog/sliceDoc.js.map +1 -1
- package/dist/cli/changelogBuild.d.ts +12 -0
- package/dist/cli/changelogBuild.js +89 -0
- package/dist/cli/changelogBuild.js.map +1 -0
- package/dist/cli/ciInit.d.ts +42 -0
- package/dist/cli/ciInit.js +298 -0
- package/dist/cli/ciInit.js.map +1 -0
- package/dist/cli/conformSupersede.d.ts +32 -0
- package/dist/cli/conformSupersede.js +102 -0
- package/dist/cli/conformSupersede.js.map +1 -0
- package/dist/cli/coverage.d.ts +6 -1
- package/dist/cli/coverage.js +28 -3
- package/dist/cli/coverage.js.map +1 -1
- package/dist/cli/frontmatterSurgery.d.ts +28 -0
- package/dist/cli/frontmatterSurgery.js +66 -0
- package/dist/cli/frontmatterSurgery.js.map +1 -0
- package/dist/cli/markImplemented.js +6 -45
- package/dist/cli/markImplemented.js.map +1 -1
- package/dist/cli/ratify.d.ts +41 -0
- package/dist/cli/ratify.js +151 -0
- package/dist/cli/ratify.js.map +1 -0
- package/dist/cli/reratify.d.ts +33 -0
- package/dist/cli/reratify.js +137 -0
- package/dist/cli/reratify.js.map +1 -0
- package/dist/cli/skillCheck.d.ts +6 -0
- package/dist/cli/skillCheck.js +46 -0
- package/dist/cli/skillCheck.js.map +1 -1
- package/dist/cli/skillDirs.d.ts +15 -0
- package/dist/cli/skillDirs.js +34 -0
- package/dist/cli/skillDirs.js.map +1 -0
- package/dist/cli/skillSync.d.ts +10 -0
- package/dist/cli/skillSync.js +15 -0
- package/dist/cli/skillSync.js.map +1 -1
- package/dist/cli/sliceIndex.d.ts +3 -0
- package/dist/cli/sliceIndex.js +6 -2
- package/dist/cli/sliceIndex.js.map +1 -1
- package/dist/cli/sliceLink.d.ts +52 -0
- package/dist/cli/sliceLink.js +152 -0
- package/dist/cli/sliceLink.js.map +1 -0
- package/dist/cli/status.d.ts +261 -0
- package/dist/cli/status.js +536 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/cli/usageLog.d.ts +75 -0
- package/dist/cli/usageLog.js +193 -0
- package/dist/cli/usageLog.js.map +1 -0
- package/dist/cli.js +503 -132
- package/dist/cli.js.map +1 -1
- package/dist/emit/freshnessJson.d.ts +6 -0
- package/dist/emit/freshnessJson.js +24 -0
- package/dist/emit/freshnessJson.js.map +1 -0
- package/dist/emit/json.d.ts +62 -8
- package/dist/emit/json.js +14 -3
- package/dist/emit/json.js.map +1 -1
- package/dist/emit/statusJson.d.ts +7 -0
- package/dist/emit/statusJson.js +39 -0
- package/dist/emit/statusJson.js.map +1 -0
- package/dist/emit/typespec.d.ts +26 -0
- package/dist/emit/typespec.js +356 -0
- package/dist/emit/typespec.js.map +1 -0
- package/dist/emit/usageReportJson.d.ts +3 -0
- package/dist/emit/usageReportJson.js +23 -0
- package/dist/emit/usageReportJson.js.map +1 -0
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +301 -11
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/rules.d.ts +13 -0
- package/dist/model/rules.js +20 -0
- package/dist/model/rules.js.map +1 -1
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +2 -2
- package/dist/util/markers.d.ts +16 -4
- package/dist/util/markers.js +23 -10
- package/dist/util/markers.js.map +1 -1
- package/package.json +2 -1
- /package/.claude/skills/{event-modeling → event-modeling-shared}/templates/conformance-report.md +0 -0
- /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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"}
|
package/dist/cli/coverage.d.ts
CHANGED
|
@@ -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 {
|
package/dist/cli/coverage.js
CHANGED
|
@@ -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
|
|
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])) {
|
package/dist/cli/coverage.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coverage.js","sourceRoot":"","sources":["../../src/cli/coverage.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,8FAA8F;AAC9F,iGAAiG;AACjG,6FAA6F;AAC7F,gGAAgG;AAChG,iGAAiG;AACjG,iGAAiG;AACjG,
|
|
1
|
+
{"version":3,"file":"coverage.js","sourceRoot":"","sources":["../../src/cli/coverage.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,8FAA8F;AAC9F,iGAAiG;AACjG,6FAA6F;AAC7F,gGAAgG;AAChG,iGAAiG;AACjG,iGAAiG;AACjG,6FAA6F;AAC7F,+FAA+F;AAC/F,sBAAsB;AACtB,EAAE;AACF,8FAA8F;AAC9F,wFAAwF;AACxF,+FAA+F;AAC/F,+FAA+F;AAC/F,+FAA+F;AAC/F,wFAAwF;AACxF,EAAE;AACF,oGAAoG;AACpG,6FAA6F;AAC7F,kGAAkG;AAClG,uFAAuF;AACvF,+FAA+F;AAC/F,+FAA+F;AAC/F,8FAA8F;AAC9F,kGAAkG;AAClG,iGAAiG;AACjG,qFAAqF;AAErF,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAE7C;;;wCAGwC;AACxC,MAAM,CAAC,MAAM,YAAY,GAAG,yCAAyC,CAAC;AAEtE;;;;;;uEAMuE;AACvE,MAAM,oBAAoB,GAAG,4DAA4D,CAAC;AAE1F;;;;;;;;;;;;;;;;oFAgBoF;AACpF,MAAM,kBAAkB,GAAG,sBAAsB,CAAC;AAElD;;;6FAG6F;AAC7F,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC,CAAC,oBAAoB,EAAE,aAAa,CAAC,CAAC,CAAC;AAEzE;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,IAAI,kBAAkB,GAAG,KAAK,CAAC;IAC/B,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QACvC,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3B,kBAAkB,GAAG,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACrD,SAAS;QACX,CAAC;QACD,IAAI,CAAC,kBAAkB,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,SAAS;QACpE,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;YAC5C,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;gBACpB,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBACf,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACjB,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAUD,iFAAiF;AACjF,6FAA6F;AAC7F,2FAA2F;AAC3F,oFAAoF;AACpF,0FAA0F;AAC1F,sFAAsF;AACtF,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC,CAAC;AAE5D;;uEAEuE;AACvE,SAAS,cAAc,CAAC,GAAW;IACjC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACvC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QAC7B,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;IAChC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAAgB,EAAE,GAAsB;IACxE,MAAM,SAAS,GAAG,IAAI,GAAG,CAAqB,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;IACzE,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACvC,MAAM,UAAU,GAA4B,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAE7F,KAAK,MAAM,GAAG,IAAI,OAAO,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,iBAAiB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;QACxF,IAAI,GAAW,CAAC;QAChB,IAAI,CAAC;YACH,GAAG,GAAG,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC;QAC1C,CAAC;QAAC,MAAM,CAAC;YACP,SAAS,CAAC,8EAA8E;QAC1F,CAAC;QACD,IAAI,cAAc,CAAC,GAAG,CAAC;YAAE,SAAS;QAElC,MAAM,KAAK,GAAG,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAClD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACtC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YACtB,KAAK,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,UAAU,EAAE,CAAC;gBAClC,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC;oBAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAE,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACzE,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAiCD;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CACjC,KAAsB,EACtB,IAAgB,EAChB,OAAe,EACf,QAAgB;IAEhB,MAAM,MAAM,GACV,EAAE,CAAC;IACL,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;IAEjC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC7C,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAC9B,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;QAC9B,MAAM,EAAE,GAAG,EAAE,GAAG,mBAAmB,CAAC,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAE,CAAC,CAAC;QACxF,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,KAAK,IAAI,IAAI,GAAG,CAAC,MAAM,KAAK,IAAI,IAAI,iBAAiB,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAEhG,IAAI,GAAG,GAAa,EAAE,CAAC;QACvB,IAAI,OAAO,EAAE,CAAC;YACZ,yFAAyF;YACzF,wFAAwF;YACxF,+DAA+D;YAC/D,MAAM,QAAQ,GAAG,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;YACxE,MAAM,MAAM,GAAG,YAAY,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YAC/C,IAAI,MAAM;gBAAE,GAAG,GAAG,mBAAmB,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACrD,CAAC;QAED,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;QAC9E,KAAK,MAAM,EAAE,IAAI,GAAG;YAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACvC,CAAC;IAED,MAAM,SAAS,GAAG,iBAAiB,CAAC,QAAQ,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC;IAE3D,IAAI,eAAe,GAAG,CAAC,CAAC;IACxB,IAAI,cAAc,GAAG,CAAC,CAAC;IACvB,MAAM,MAAM,GAAoB,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE;QACtF,MAAM,UAAU,GAAwB,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE;YACrD,MAAM,EAAE,GAAG,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC;YACnC,eAAe,EAAE,CAAC;YAClB,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC;gBAAE,cAAc,EAAE,CAAC;YACtC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;QACrD,CAAC,CAAC,CAAC;QACH,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC;IACzD,CAAC,CAAC,CAAC;IAEH,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,cAAc,EAAE,CAAC;AACrD,CAAC"}
|
|
@@ -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
|