@milehimikey/em 1.8.0 → 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.
- 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 +39 -13
- 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 +128 -84
- package/.claude/skills/{event-modeling → event-modeling-shared}/reference/methodology.md +6 -5
- package/.claude/skills/event-modeling-shared/reference/operating-principles.md +147 -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 +51 -8
- 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 +17 -10
- package/dist/cli/coverage.js +61 -16
- 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 +501 -130
- package/dist/cli.js.map +1 -1
- package/dist/emit/diffJson.d.ts +9 -1
- package/dist/emit/diffJson.js +44 -1
- package/dist/emit/diffJson.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 +67 -8
- package/dist/emit/json.js +19 -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/model/validate.js +9 -1
- package/dist/model/validate.js.map +1 -1
- package/dist/parser/ast.d.ts +6 -0
- package/dist/parser/parser.js +20 -6
- package/dist/parser/parser.js.map +1 -1
- package/dist/render/drawNotes.d.ts +11 -1
- package/dist/render/drawNotes.js +20 -2
- package/dist/render/drawNotes.js.map +1 -1
- package/dist/render/render.js +31 -6
- package/dist/render/render.js.map +1 -1
- package/dist/render/serve.js +4 -0
- package/dist/render/serve.js.map +1 -1
- package/dist/render/sliceOverlay.d.ts +7 -2
- package/dist/render/sliceOverlay.js +19 -7
- package/dist/render/sliceOverlay.js.map +1 -1
- package/dist/render/sliceStatus.d.ts +7 -3
- package/dist/render/sliceStatus.js +21 -14
- package/dist/render/sliceStatus.js.map +1 -1
- package/dist/render/viewerHtml.js +1 -1
- package/dist/render/viewerHtml.js.map +1 -1
- package/dist/templates.d.ts +2 -2
- package/dist/templates.js +5 -5
- 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,89 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// `em changelog`'s document builder (MIL-100), extracted out of cli.ts (MIL-167) so the MCP
|
|
3
|
+
// server's `changelog` tool can call the exact same function the CLI command does — same
|
|
4
|
+
// "one builder, two callers" pattern as buildDiffJson/buildGlossaryJson, just markdown instead
|
|
5
|
+
// of JSON (the CLI command itself has no `--json` flag; the document IS the markdown text).
|
|
6
|
+
//
|
|
7
|
+
// Already pure enough to share as-is: no process.exit, no console — a per-revision compile
|
|
8
|
+
// failure becomes that entry's `error` field, never a crash (see the docstring below). The only
|
|
9
|
+
// impure bit (`readFileAtCommit`, a `git show`) is injected in, same convention as
|
|
10
|
+
// changelog-git.ts/diff-inputs.ts's GitRunner.
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
13
|
+
import { compile } from "../pipeline.js";
|
|
14
|
+
import { ParseError } from "../parser/parser.js";
|
|
15
|
+
import { hasErrors } from "../model/validate.js";
|
|
16
|
+
import { diffModels } from "../model/diff.js";
|
|
17
|
+
import { buildChangelog, parseDecisionsLog } from "../emit/changelog.js";
|
|
18
|
+
import { readFileAtCommit } from "./changelog-git.js";
|
|
19
|
+
import { STATE_FILE_NAME } from "./stateFile.js";
|
|
20
|
+
/**
|
|
21
|
+
* Build the `em changelog` markdown document for an already-resolved commit list
|
|
22
|
+
* (oldest -> newest, see `listModelCommits`). Compiles every revision once; per-revision
|
|
23
|
+
* warnings are deliberately never surfaced (historical revisions can be noisy) — only a compile
|
|
24
|
+
* *failure* (parse error or validation error, same threshold `em diff` uses) surfaces, as that
|
|
25
|
+
* entry's error note, never a crash. Diffs are computed against the previous *parseable*
|
|
26
|
+
* revision, so a single bad revision in the middle of the walk doesn't break every entry after
|
|
27
|
+
* it. Content is read at each commit's own path (`readFileAtCommit`), so the walk survives
|
|
28
|
+
* renames.
|
|
29
|
+
*/
|
|
30
|
+
export function buildChangelogDoc(file, repoRoot, commits) {
|
|
31
|
+
const models = [];
|
|
32
|
+
const refsList = [];
|
|
33
|
+
const errors = [];
|
|
34
|
+
for (const c of commits) {
|
|
35
|
+
const rev = readFileAtCommit(repoRoot, c);
|
|
36
|
+
if (!rev.ok) {
|
|
37
|
+
models.push(null);
|
|
38
|
+
refsList.push(null);
|
|
39
|
+
errors.push(rev.message);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const { model, refs, diagnostics } = compile(rev.content);
|
|
44
|
+
if (hasErrors(diagnostics)) {
|
|
45
|
+
models.push(null);
|
|
46
|
+
refsList.push(null);
|
|
47
|
+
errors.push(`validation errors at ${c.shortHash} — fix with \`em validate\` at that revision`);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
models.push(model);
|
|
51
|
+
refsList.push(refs);
|
|
52
|
+
errors.push(null);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
models.push(null);
|
|
57
|
+
refsList.push(null);
|
|
58
|
+
errors.push(e instanceof ParseError ? `parse error: ${e.message}` : e instanceof Error ? e.message : String(e));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const entries = [];
|
|
62
|
+
let prevParseable = -1;
|
|
63
|
+
commits.forEach((c, i) => {
|
|
64
|
+
const base = { shortHash: c.shortHash, date: c.date, subject: c.subject };
|
|
65
|
+
if (i === 0) {
|
|
66
|
+
entries.push({ ...base, diff: null });
|
|
67
|
+
}
|
|
68
|
+
else if (!models[i]) {
|
|
69
|
+
entries.push({ ...base, diff: null, error: errors[i] });
|
|
70
|
+
}
|
|
71
|
+
else if (prevParseable === -1) {
|
|
72
|
+
entries.push({ ...base, diff: null, error: `no earlier parseable revision to diff against (${commits[0].shortHash}: ${errors[0]})` });
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
entries.push({
|
|
76
|
+
...base,
|
|
77
|
+
diff: diffModels(models[prevParseable], models[i], refsList[prevParseable], refsList[i]),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (models[i])
|
|
81
|
+
prevParseable = i;
|
|
82
|
+
});
|
|
83
|
+
const intro = models[0] ? { slices: models[0].slices.length, elements: models[0].elements.length } : null;
|
|
84
|
+
const introError = models[0] ? undefined : (errors[0] ?? undefined);
|
|
85
|
+
const stateFile = join(dirname(file), STATE_FILE_NAME);
|
|
86
|
+
const decisions = existsSync(stateFile) ? parseDecisionsLog(readFileSync(stateFile, "utf8")) : [];
|
|
87
|
+
return buildChangelog(entries, decisions, { file, intro, introError });
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=changelogBuild.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changelogBuild.js","sourceRoot":"","sources":["../../src/cli/changelogBuild.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,4FAA4F;AAC5F,yFAAyF;AACzF,+FAA+F;AAC/F,4FAA4F;AAC5F,EAAE;AACF,2FAA2F;AAC3F,gGAAgG;AAChG,mFAAmF;AACnF,+CAA+C;AAE/C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAGjD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAkC,MAAM,sBAAsB,CAAC;AACzG,OAAO,EAAE,gBAAgB,EAAc,MAAM,oBAAoB,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,QAAgB,EAAE,OAAqB;IACrF,MAAM,MAAM,GAA+B,EAAE,CAAC;IAC9C,MAAM,QAAQ,GAA0B,EAAE,CAAC;IAC3C,MAAM,MAAM,GAAsB,EAAE,CAAC;IAErC,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,GAAG,GAAG,gBAAgB,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;QAC1C,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAClB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACpB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YACzB,SAAS;QACX,CAAC;QACD,IAAI,CAAC;YACH,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YAC1D,IAAI,SAAS,CAAC,WAAW,CAAC,EAAE,CAAC;gBAC3B,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAClB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACpB,MAAM,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAC,SAAS,8CAA8C,CAAC,CAAC;YACjG,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBACnB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACpB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACpB,CAAC;QACH,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAClB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACpB,MAAM,CAAC,IAAI,CAAC,CAAC,YAAY,UAAU,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAClH,CAAC;IACH,CAAC;IAED,MAAM,OAAO,GAAqB,EAAE,CAAC;IACrC,IAAI,aAAa,GAAG,CAAC,CAAC,CAAC;IACvB,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACvB,MAAM,IAAI,GAAG,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;QAC1E,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YACZ,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACxC,CAAC;aAAM,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;YACtB,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAE,EAAE,CAAC,CAAC;QAC3D,CAAC;aAAM,IAAI,aAAa,KAAK,CAAC,CAAC,EAAE,CAAC;YAChC,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,kDAAkD,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;QACxI,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,IAAI,CAAC;gBACX,GAAG,IAAI;gBACP,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC,aAAa,CAAE,EAAE,MAAM,CAAC,CAAC,CAAE,EAAE,QAAQ,CAAC,aAAa,CAAE,EAAE,QAAQ,CAAC,CAAC,CAAE,CAAC;aAC7F,CAAC,CAAC;QACL,CAAC;QACD,IAAI,MAAM,CAAC,CAAC,CAAC;YAAE,aAAa,GAAG,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;IAEH,MAAM,KAAK,GAA0B,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACjI,MAAM,UAAU,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC;IAEpE,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,eAAe,CAAC,CAAC;IACvD,MAAM,SAAS,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAElG,OAAO,cAAc,CAAC,OAAO,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAC;AACzE,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export declare const CI_WORKFLOW_MARKER = "em-ci";
|
|
2
|
+
export declare const CONFORM_WORKFLOW_MARKER = "em-conform";
|
|
3
|
+
export declare function ciWorkflowPath(repoRoot: string): string;
|
|
4
|
+
export declare function conformWorkflowPath(repoRoot: string): string;
|
|
5
|
+
export declare function findUnsafeCiInitArg(value: string): string | null;
|
|
6
|
+
/** The managed block only (no header/`on:`/`jobs:` scaffolding) — what gets written between
|
|
7
|
+
* the marker pair, both for a fresh file and to patch an existing marked one in place. */
|
|
8
|
+
export declare function ciManagedBody(model: string, testsDir: string): string;
|
|
9
|
+
export declare function buildCiWorkflowFile(model: string, testsDir: string, emVersion: string): string;
|
|
10
|
+
/** The managed block only (no header/`on:`/`permissions:`/`jobs:` scaffolding) — what gets
|
|
11
|
+
* written between the marker pair, both for a fresh file and to patch an existing marked one
|
|
12
|
+
* in place. */
|
|
13
|
+
export declare function conformManagedBody(model: string): string;
|
|
14
|
+
export declare function buildConformWorkflowFile(model: string, emVersion: string): string;
|
|
15
|
+
export type CiFileStatus = {
|
|
16
|
+
kind: "create";
|
|
17
|
+
content: string;
|
|
18
|
+
} | {
|
|
19
|
+
kind: "ok";
|
|
20
|
+
content: string;
|
|
21
|
+
} | {
|
|
22
|
+
kind: "stale";
|
|
23
|
+
content: string;
|
|
24
|
+
current: string;
|
|
25
|
+
} | {
|
|
26
|
+
kind: "missing-markers";
|
|
27
|
+
} | {
|
|
28
|
+
kind: "would-replace";
|
|
29
|
+
content: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Decide what to do with one generated file: create it if missing, patch the managed block if
|
|
33
|
+
* the marker pair is present, or (without `force`) leave an existing unmarked file alone. Pure
|
|
34
|
+
* — never touches disk; `applyCiFile` below does the actual write.
|
|
35
|
+
*
|
|
36
|
+
* `generated` is the full from-scratch file content (header + `on:`/`jobs:` scaffolding +
|
|
37
|
+
* marker-wrapped body) — written verbatim for `create`/`would-replace`. `managedBody` is just
|
|
38
|
+
* the text between the markers, reused to patch an existing marked file in place without
|
|
39
|
+
* touching whatever a repo added around it.
|
|
40
|
+
*/
|
|
41
|
+
export declare function planCiFile(path: string, generated: string, managedBody: string, markerName: string, force: boolean): CiFileStatus;
|
|
42
|
+
export declare function applyCiFile(path: string, status: CiFileStatus): void;
|
|
@@ -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
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
import { NormalizedModel } from "../model/model.js";
|
|
2
2
|
import { RefsResult } from "../model/refs.js";
|
|
3
|
-
/** The stable INV token format
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* and `INV-CHK-4`/`INV-CHK-3a` (the per-slice mnemonic style reference/conform.md's own example
|
|
8
|
-
* uses). Exported so tests can assert against the exact grammar. */
|
|
3
|
+
/** The stable INV token format. `INV-` then one or more alphanumeric/hyphen segments — matches
|
|
4
|
+
* both `INV-1`/`INV-2` (the template's bare numbering) and `INV-CHK-4`/`INV-CHK-3a` (the
|
|
5
|
+
* per-slice mnemonic style reference/conform.md's own example uses). Exported so tests can
|
|
6
|
+
* assert against the exact grammar. */
|
|
9
7
|
export declare const INV_TOKEN_RE: RegExp;
|
|
10
8
|
/**
|
|
11
|
-
* Extract every distinct `INV-*` token
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* Extract every distinct `INV-*` token this slice doc *defines*, in first-occurrence order (a
|
|
10
|
+
* rule stated once in `## Invariants` and re-declared in a later `## Delta` section dedupes to
|
|
11
|
+
* its first appearance — order is for stable, readable output, not a claim about which mention is
|
|
12
|
+
* canonical). Scoped to `OWNERSHIP_HEADING_RE`'s sections only, so a doc's prose narratively
|
|
13
|
+
* citing another slice's ID elsewhere (Dependencies, Open Questions, ...) doesn't cross-credit
|
|
14
|
+
* that ID as this slice's own (MIL-149) — the section ends at the next `#`/`##` heading or EOF,
|
|
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. 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.
|
|
15
22
|
*/
|
|
16
23
|
export declare function extractInvariantIds(body: string): string[];
|
|
17
24
|
export interface Citation {
|