@milehimikey/em 1.7.1 → 1.8.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 +92 -65
- package/.claude/skills/event-modeling/reference/conform.md +45 -20
- package/.claude/skills/event-modeling/reference/em-dsl.md +120 -55
- package/.claude/skills/event-modeling/reference/extract.md +5 -4
- package/.claude/skills/event-modeling/reference/implement.md +34 -22
- package/.claude/skills/event-modeling/templates/model-readme.md +8 -5
- package/.claude/skills/event-modeling/templates/slice.md +19 -5
- package/README.md +6 -4
- package/dist/catalog/docJoin.d.ts +28 -3
- package/dist/catalog/docJoin.js +82 -13
- package/dist/catalog/docJoin.js.map +1 -1
- package/dist/catalog/docModelConsistencyValidate.d.ts +8 -0
- package/dist/catalog/docModelConsistencyValidate.js +335 -0
- package/dist/catalog/docModelConsistencyValidate.js.map +1 -0
- package/dist/catalog/noteBindingValidate.d.ts +8 -0
- package/dist/catalog/noteBindingValidate.js +133 -0
- package/dist/catalog/noteBindingValidate.js.map +1 -0
- package/dist/catalog/pages.js +3 -3
- package/dist/catalog/pages.js.map +1 -1
- package/dist/catalog/sliceDoc.d.ts +14 -0
- package/dist/catalog/sliceDoc.js +22 -0
- package/dist/catalog/sliceDoc.js.map +1 -1
- package/dist/catalog/sliceReadyValidate.d.ts +18 -0
- package/dist/catalog/sliceReadyValidate.js +25 -1
- package/dist/catalog/sliceReadyValidate.js.map +1 -1
- package/dist/cli/agentsMd.d.ts +15 -0
- package/dist/cli/agentsMd.js +72 -0
- package/dist/cli/agentsMd.js.map +1 -0
- package/dist/cli/conformScope.d.ts +86 -0
- package/dist/cli/conformScope.js +156 -0
- package/dist/cli/conformScope.js.map +1 -0
- package/dist/cli/contract.d.ts +7 -0
- package/dist/cli/contract.js +27 -0
- package/dist/cli/contract.js.map +1 -0
- package/dist/cli/coverage.d.ts +69 -0
- package/dist/cli/coverage.js +157 -0
- package/dist/cli/coverage.js.map +1 -0
- package/dist/cli/markImplemented.d.ts +34 -0
- package/dist/cli/markImplemented.js +180 -0
- package/dist/cli/markImplemented.js.map +1 -0
- package/dist/cli/migrateReactionShape.d.ts +62 -0
- package/dist/cli/migrateReactionShape.js +283 -0
- package/dist/cli/migrateReactionShape.js.map +1 -0
- package/dist/cli/sliceIndex.d.ts +49 -0
- package/dist/cli/sliceIndex.js +115 -0
- package/dist/cli/sliceIndex.js.map +1 -0
- package/dist/cli/sliceNew.d.ts +18 -0
- package/dist/cli/sliceNew.js +53 -0
- package/dist/cli/sliceNew.js.map +1 -0
- package/dist/cli/stateFile.d.ts +74 -0
- package/dist/cli/stateFile.js +191 -0
- package/dist/cli/stateFile.js.map +1 -0
- package/dist/cli.js +535 -43
- package/dist/cli.js.map +1 -1
- package/dist/emit/coverageJson.d.ts +8 -0
- package/dist/emit/coverageJson.js +30 -0
- package/dist/emit/coverageJson.js.map +1 -0
- package/dist/emit/dot.js +41 -10
- package/dist/emit/dot.js.map +1 -1
- package/dist/emit/json.d.ts +72 -4
- package/dist/emit/json.js +73 -4
- package/dist/emit/json.js.map +1 -1
- package/dist/emit/validateJson.d.ts +52 -0
- package/dist/emit/validateJson.js +120 -0
- package/dist/emit/validateJson.js.map +1 -0
- package/dist/mcp/main.d.ts +2 -0
- package/dist/mcp/main.js +20 -0
- package/dist/mcp/main.js.map +1 -0
- package/dist/mcp/server.d.ts +11 -0
- package/dist/mcp/server.js +260 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/model/model.d.ts +30 -1
- package/dist/model/model.js +26 -0
- package/dist/model/model.js.map +1 -1
- package/dist/model/rules.d.ts +114 -0
- package/dist/model/rules.js +116 -4
- package/dist/model/rules.js.map +1 -1
- package/dist/model/validate.js +83 -7
- package/dist/model/validate.js.map +1 -1
- package/dist/parser/ast.d.ts +41 -0
- package/dist/parser/lexer.d.ts +16 -0
- package/dist/parser/lexer.js +44 -2
- package/dist/parser/lexer.js.map +1 -1
- package/dist/parser/parser.js +340 -20
- package/dist/parser/parser.js.map +1 -1
- package/dist/render/drawNotes.js +8 -1
- package/dist/render/drawNotes.js.map +1 -1
- package/dist/render/render.js +85 -17
- package/dist/render/render.js.map +1 -1
- package/dist/render/serve.d.ts +4 -0
- package/dist/render/serve.js +23 -227
- package/dist/render/serve.js.map +1 -1
- package/dist/render/sliceStatus.d.ts +1 -1
- package/dist/render/sliceStatus.js +35 -6
- package/dist/render/sliceStatus.js.map +1 -1
- package/dist/render/viewerHtml.d.ts +1 -0
- package/dist/render/viewerHtml.js +5 -0
- package/dist/render/viewerHtml.js.map +1 -0
- package/dist/render/watch.d.ts +10 -0
- package/dist/render/watch.js +29 -0
- package/dist/render/watch.js.map +1 -1
- package/dist/templates.d.ts +19 -0
- package/dist/templates.js +169 -0
- package/dist/templates.js.map +1 -1
- package/dist/util/markers.d.ts +13 -0
- package/dist/util/markers.js +30 -0
- package/dist/util/markers.js.map +1 -0
- package/dist/util/walkDir.d.ts +10 -1
- package/dist/util/walkDir.js +11 -7
- package/dist/util/walkDir.js.map +1 -1
- package/package.json +12 -3
- package/.claude/skills/event-modeling/templates/live.html +0 -105
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// `em conform-scope` (MIL-100): mechanizes the mechanical half of reference/conform.md step 1
|
|
3
|
+
// ("Scope") — everything that was previously prose telling the agent to shell out to git and
|
|
4
|
+
// eyeball `implementedIn:` links by hand.
|
|
5
|
+
//
|
|
6
|
+
// Three pieces, each reusing an existing module rather than re-deriving it:
|
|
7
|
+
// - `Last conformance:` comes from `stateFile.ts`'s `parseState` (MIL-99) — this module never
|
|
8
|
+
// parses the state file's bullets itself.
|
|
9
|
+
// - Changed paths come from `git diff --name-only <revision>..HEAD` in the target repo, via
|
|
10
|
+
// the same injectable `GitRunner`/`realGit` convention diff-inputs.ts/changelog-git.ts use
|
|
11
|
+
// (so every failure branch — no repo, unknown revision — is unit-testable without a real
|
|
12
|
+
// repository).
|
|
13
|
+
// - A changed path maps to a slice only when its doc's `implementedIn` (from
|
|
14
|
+
// catalog/docJoin.ts's join — the same one `em export`/`em slice index` use, never a second
|
|
15
|
+
// parse) is textually, deterministically a repo-relative path that's a prefix of (or equal
|
|
16
|
+
// to) that changed path. A URL (PR/commit link) carries no path information we can trust, so
|
|
17
|
+
// it deterministically matches nothing. Anything left over is `unmappedPaths` — the agent's
|
|
18
|
+
// judgment queue (grep fallback, evidence walks); this module never guesses.
|
|
19
|
+
//
|
|
20
|
+
// `--seed-asis` mechanizes the OTHER mechanical convention step 1 describes: seeding
|
|
21
|
+
// `<model>-asis.em` as a byte copy of the canonical model, and making sure `*-asis.em` is
|
|
22
|
+
// gitignored (idempotent).
|
|
23
|
+
import { copyFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
25
|
+
import { resolveSliceDocJoin } from "../catalog/docJoin.js";
|
|
26
|
+
import { realGit } from "./diff-inputs.js";
|
|
27
|
+
const URL_LIKE = /^[a-z][a-z0-9+.-]*:\/\//i; // scheme://... (http, https, git, ssh, ...)
|
|
28
|
+
const SCP_LIKE = /^[\w.-]+@[\w.-]+:/; // git@host:owner/repo.git style — also carries no path
|
|
29
|
+
/** `implementedIn` -> the repo-relative path text worth prefix-matching against changed paths,
|
|
30
|
+
* or null when it's a URL/SCP-style link (a PR/commit/host reference) that carries no path
|
|
31
|
+
* information we can trust. Deliberately conservative — see this module's header comment. */
|
|
32
|
+
function pathCandidate(implementedIn) {
|
|
33
|
+
const trimmed = implementedIn.trim();
|
|
34
|
+
if (!trimmed || URL_LIKE.test(trimmed) || SCP_LIKE.test(trimmed))
|
|
35
|
+
return null;
|
|
36
|
+
return trimmed.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
37
|
+
}
|
|
38
|
+
/** The changed paths one slice's `implementedIn` textually, deterministically names: equal to
|
|
39
|
+
* the candidate path, or nested under it (the candidate names a directory). No fuzzy matching,
|
|
40
|
+
* no grep — that judgment call stays with the agent (reference/conform.md step 1). */
|
|
41
|
+
export function matchImplementedInToPaths(implementedIn, changedPaths) {
|
|
42
|
+
if (!implementedIn)
|
|
43
|
+
return [];
|
|
44
|
+
const candidate = pathCandidate(implementedIn);
|
|
45
|
+
if (!candidate)
|
|
46
|
+
return [];
|
|
47
|
+
return changedPaths.filter((p) => p === candidate || p.startsWith(`${candidate}/`));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Build the `em conform-scope` JSON body — pure, no fs/git. `full` covers `--full`; a `null`
|
|
51
|
+
* `lastConformance` (the state file's `never` marker, first run) forces the same behavior
|
|
52
|
+
* regardless of `full`, matching reference/conform.md step 1 ("If it's `null` (first run) or
|
|
53
|
+
* the user passes `--full`, scope is every `implemented` slice"). In that mode `changedPaths`
|
|
54
|
+
* is `[]` (documented choice, not omitted — keeps the JSON shape stable across modes) and every
|
|
55
|
+
* `implemented`-status slice is a candidate with `matchedBy: "full"` and empty `paths`.
|
|
56
|
+
*/
|
|
57
|
+
export function buildConformScope(slices, lastConformance, changedPaths, full) {
|
|
58
|
+
const lastConformanceOut = lastConformance ? { date: lastConformance.date, revision: lastConformance.revision } : null;
|
|
59
|
+
if (full || lastConformance === null) {
|
|
60
|
+
const candidateSlices = slices
|
|
61
|
+
.filter((s) => s.status === "implemented")
|
|
62
|
+
.map((s) => ({ key: s.key, matchedBy: "full", paths: [] }));
|
|
63
|
+
return { lastConformance: lastConformanceOut, changedPaths: [], candidateSlices, unmappedPaths: [] };
|
|
64
|
+
}
|
|
65
|
+
const matched = new Set();
|
|
66
|
+
const candidateSlices = [];
|
|
67
|
+
for (const s of slices) {
|
|
68
|
+
const paths = matchImplementedInToPaths(s.implementedIn, changedPaths);
|
|
69
|
+
if (paths.length === 0)
|
|
70
|
+
continue;
|
|
71
|
+
candidateSlices.push({ key: s.key, matchedBy: "implementedIn", paths });
|
|
72
|
+
for (const p of paths)
|
|
73
|
+
matched.add(p);
|
|
74
|
+
}
|
|
75
|
+
const unmappedPaths = changedPaths.filter((p) => !matched.has(p));
|
|
76
|
+
return { lastConformance: lastConformanceOut, changedPaths, candidateSlices, unmappedPaths };
|
|
77
|
+
}
|
|
78
|
+
/** Resolve every slice's doc-join facts in declaration order — exactly the two inputs
|
|
79
|
+
* `buildConformScope` needs (`status`, `implementedIn`), via the same `resolveSliceDocJoin`
|
|
80
|
+
* call `em export`/`em slice index` make, never a second doc parse. `diagnostics` carries any
|
|
81
|
+
* `binding-missing-file`/`frontmatter-invalid` warnings, for the caller to print the same way
|
|
82
|
+
* `em slice index` does. */
|
|
83
|
+
export function resolveSliceDocFacts(model, refs, baseDir) {
|
|
84
|
+
const diagnostics = [];
|
|
85
|
+
const facts = model.slices.map((slice, i) => {
|
|
86
|
+
const key = refs.sliceKeys[i];
|
|
87
|
+
const { doc, diagnostics: docDiags } = resolveSliceDocJoin(slice, key, baseDir, (id) => refs.refById.get(id));
|
|
88
|
+
diagnostics.push(...docDiags);
|
|
89
|
+
return { key, status: doc.status, implementedIn: doc.implementedIn };
|
|
90
|
+
});
|
|
91
|
+
return { facts, diagnostics };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* `git diff --name-only <revision>..HEAD`, run from `repo` (any directory inside the target
|
|
95
|
+
* repo, not necessarily its root — same "anchor dir, resolve toplevel" convention
|
|
96
|
+
* diff-inputs.ts/changelog-git.ts use). Paths come back repo-root-relative — git's own default
|
|
97
|
+
* for `--name-only` without `--relative`.
|
|
98
|
+
*/
|
|
99
|
+
export function changedPathsSince(repo, revision, runGit = realGit) {
|
|
100
|
+
const toplevel = runGit(["-C", repo, "rev-parse", "--show-toplevel"]);
|
|
101
|
+
if (toplevel.status !== 0) {
|
|
102
|
+
return { ok: false, message: `em conform-scope: ${repo} is not a git repository` };
|
|
103
|
+
}
|
|
104
|
+
const repoRoot = toplevel.stdout.trim();
|
|
105
|
+
const diff = runGit(["-C", repoRoot, "diff", "--name-only", `${revision}..HEAD`]);
|
|
106
|
+
if (diff.status !== 0) {
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
message: `em conform-scope: git diff failed: ${(diff.stderr || "").trim() || `unknown revision "${revision}"`}`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
ok: true,
|
|
114
|
+
paths: diff.stdout
|
|
115
|
+
.split("\n")
|
|
116
|
+
.map((s) => s.trim())
|
|
117
|
+
.filter(Boolean),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const ASIS_GITIGNORE_PATTERN = "*-asis.em";
|
|
121
|
+
/**
|
|
122
|
+
* `--seed-asis`: byte-copy `modelFile` to `<dirname(modelFile)>/<stem>-asis.em`, and make sure
|
|
123
|
+
* `*-asis.em` is gitignored. reference/conform.md's Conventions section says to add the pattern
|
|
124
|
+
* to "the repository's `.gitignore`" — read here as the git repo the model itself lives in
|
|
125
|
+
* (resolved the same "anchor dir, `rev-parse --show-toplevel`" way as `changedPathsSince`
|
|
126
|
+
* above, from the model's own directory — a different repo entirely from the `--repo` target
|
|
127
|
+
* codebase conform is checking), so the entry lands once, at that repo's root, covering every
|
|
128
|
+
* model the repo holds. Falls back to a `.gitignore` right next to the model when the model
|
|
129
|
+
* isn't inside a git repo at all. Idempotent: never appends a second `*-asis.em` line if one
|
|
130
|
+
* (exactly, on its own line) is already present anywhere in the file.
|
|
131
|
+
*/
|
|
132
|
+
export function seedAsisModel(modelFile, runGit = realGit) {
|
|
133
|
+
const dir = dirname(modelFile);
|
|
134
|
+
const stem = basename(modelFile, extname(modelFile));
|
|
135
|
+
const asisPath = join(dir, `${stem}-asis.em`);
|
|
136
|
+
copyFileSync(modelFile, asisPath);
|
|
137
|
+
const toplevel = runGit(["-C", resolve(dir), "rev-parse", "--show-toplevel"]);
|
|
138
|
+
const gitignoreDir = toplevel.status === 0 ? toplevel.stdout.trim() : dir;
|
|
139
|
+
const gitignorePath = join(gitignoreDir, ".gitignore");
|
|
140
|
+
let gitignoreUpdated = false;
|
|
141
|
+
if (existsSync(gitignorePath)) {
|
|
142
|
+
const text = readFileSync(gitignorePath, "utf8");
|
|
143
|
+
const hasLine = text.split(/\r?\n/).some((l) => l.trim() === ASIS_GITIGNORE_PATTERN);
|
|
144
|
+
if (!hasLine) {
|
|
145
|
+
const sep = text.length === 0 || text.endsWith("\n") ? "" : "\n";
|
|
146
|
+
writeFileSync(gitignorePath, `${text}${sep}${ASIS_GITIGNORE_PATTERN}\n`);
|
|
147
|
+
gitignoreUpdated = true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
writeFileSync(gitignorePath, `${ASIS_GITIGNORE_PATTERN}\n`);
|
|
152
|
+
gitignoreUpdated = true;
|
|
153
|
+
}
|
|
154
|
+
return { asisPath, gitignoreUpdated };
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=conformScope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conformScope.js","sourceRoot":"","sources":["../../src/cli/conformScope.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Path to the packaged contract inside `packagedSkillDir` — the same directory `em skill
|
|
2
|
+
* install`/`sync`/`check` read from (src/cli.ts's packagedSkillDir()). */
|
|
3
|
+
export declare function contractPath(packagedSkillDir: string): string;
|
|
4
|
+
/** Pure: reads the packaged contract's exact bytes. No process.exit/console — src/cli.ts's
|
|
5
|
+
* action wraps fs errors (a corrupt/incomplete package install) the same way every other
|
|
6
|
+
* command's file reads do. */
|
|
7
|
+
export declare function readContract(packagedSkillDir: string): string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// `em contract` (MIL-129): prints the packaged implementation contract — reference/implement.md
|
|
3
|
+
// inside the skill directory bundled with whatever em package is actually running (see
|
|
4
|
+
// src/cli.ts's packagedSkillDir) — to stdout, verbatim.
|
|
5
|
+
//
|
|
6
|
+
// Why this exists: reference/implement.md opens by claiming the contract applies to "any
|
|
7
|
+
// implementing agent, whether or not the session started from `/event-modeling`" — but until
|
|
8
|
+
// this command, its only distribution channel was `em skill install` copying it into
|
|
9
|
+
// `.claude/skills/event-modeling/`, a path only Claude Code discovers. An agent that can run a
|
|
10
|
+
// shell but has no `.claude/` awareness had no route to the contract at all (Slicewright story
|
|
11
|
+
// gap G5). `em contract` needs no vendored skill copy, no repo scaffolding, nothing but the em
|
|
12
|
+
// binary itself — the file always comes from the installed package, not from any project's
|
|
13
|
+
// working tree.
|
|
14
|
+
import { readFileSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
/** Path to the packaged contract inside `packagedSkillDir` — the same directory `em skill
|
|
17
|
+
* install`/`sync`/`check` read from (src/cli.ts's packagedSkillDir()). */
|
|
18
|
+
export function contractPath(packagedSkillDir) {
|
|
19
|
+
return join(packagedSkillDir, "reference", "implement.md");
|
|
20
|
+
}
|
|
21
|
+
/** Pure: reads the packaged contract's exact bytes. No process.exit/console — src/cli.ts's
|
|
22
|
+
* action wraps fs errors (a corrupt/incomplete package install) the same way every other
|
|
23
|
+
* command's file reads do. */
|
|
24
|
+
export function readContract(packagedSkillDir) {
|
|
25
|
+
return readFileSync(contractPath(packagedSkillDir), "utf8");
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../../src/cli/contract.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,gGAAgG;AAChG,uFAAuF;AACvF,wDAAwD;AACxD,EAAE;AACF,yFAAyF;AACzF,6FAA6F;AAC7F,qFAAqF;AACrF,+FAA+F;AAC/F,+FAA+F;AAC/F,+FAA+F;AAC/F,2FAA2F;AAC3F,gBAAgB;AAEhB,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC;2EAC2E;AAC3E,MAAM,UAAU,YAAY,CAAC,gBAAwB;IACnD,OAAO,IAAI,CAAC,gBAAgB,EAAE,WAAW,EAAE,cAAc,CAAC,CAAC;AAC7D,CAAC;AAED;;+BAE+B;AAC/B,MAAM,UAAU,YAAY,CAAC,gBAAwB;IACnD,OAAO,YAAY,CAAC,YAAY,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC,CAAC;AAC9D,CAAC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { NormalizedModel } from "../model/model.js";
|
|
2
|
+
import { RefsResult } from "../model/refs.js";
|
|
3
|
+
/** The stable INV token format, scanned across a slice doc's whole body (not just the
|
|
4
|
+
* `## Invariants / Business Rules` section — a `## Delta` section's Added/Modified/Renamed
|
|
5
|
+
* requirement IDs are just as citable, see templates/slice.md). `INV-` then one or more
|
|
6
|
+
* alphanumeric/hyphen segments — matches both `INV-1`/`INV-2` (the template's bare numbering)
|
|
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. */
|
|
9
|
+
export declare const INV_TOKEN_RE: RegExp;
|
|
10
|
+
/**
|
|
11
|
+
* Extract every distinct `INV-*` token from a slice doc's body text, in first-occurrence order
|
|
12
|
+
* (a rule stated once in `## Invariants` and mentioned again in a later `## Delta` section dedupes
|
|
13
|
+
* to its first appearance — order is for stable, readable output, not a claim about which mention
|
|
14
|
+
* is canonical).
|
|
15
|
+
*/
|
|
16
|
+
export declare function extractInvariantIds(body: string): string[];
|
|
17
|
+
export interface Citation {
|
|
18
|
+
/** Path relative to the `--tests <dir>` root, POSIX forward slashes — same convention
|
|
19
|
+
* walkDir() already returns. */
|
|
20
|
+
file: string;
|
|
21
|
+
/** 1-based line number within `file`. */
|
|
22
|
+
line: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Scan every text file under `testsDir` for lines citing any of `ids`, word-boundary-anchored so
|
|
26
|
+
* `INV-KEY-1` doesn't match inside `INV-KEY-12` (both boundary sides land on a hyphen/alnum
|
|
27
|
+
* transition since the ID alphabet is exactly `[A-Za-z0-9-]`, so no escaping is needed to build
|
|
28
|
+
* the per-ID regex). Returns every citing `{file, line}`, in walk order — a line citing more than
|
|
29
|
+
* one ID is recorded under each. `ids` with zero test-dir mentions still get a `[]` entry, never
|
|
30
|
+
* an absent map key, so a caller never has to guard a lookup with `?? []`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function scanTestCitations(testsDir: string, ids: readonly string[]): Map<string, Citation[]>;
|
|
33
|
+
export interface InvariantCoverage {
|
|
34
|
+
id: string;
|
|
35
|
+
cited: boolean;
|
|
36
|
+
citations: Citation[];
|
|
37
|
+
}
|
|
38
|
+
export interface SliceCoverage {
|
|
39
|
+
key: string;
|
|
40
|
+
/** The joined doc's `status`, or null when no usable doc was found — carried even for an
|
|
41
|
+
* out-of-scope slice, for transparency (a reader can see *why* it was skipped). */
|
|
42
|
+
status: string | null;
|
|
43
|
+
/** The resolveSliceDocJoin() `DocReason` behind `status: null` — `"no-doc-bound"`,
|
|
44
|
+
* `"binding-missing-file"`, or `"frontmatter-invalid"` — or null when the doc joined cleanly
|
|
45
|
+
* (whether or not the slice ended up in scope; a draft slice with a perfectly good doc still
|
|
46
|
+
* has `docReason: null`). Lets an out-of-scope reader tell "nothing bound yet" apart from
|
|
47
|
+
* "bound but broken" without re-running the join themselves. */
|
|
48
|
+
docReason: string | null;
|
|
49
|
+
/** True when this slice's doc was found, its frontmatter usable, and `status` is
|
|
50
|
+
* `ready-to-implement` or `implemented` — the only slices `invariants` is populated for. */
|
|
51
|
+
inScope: boolean;
|
|
52
|
+
invariants: InvariantCoverage[];
|
|
53
|
+
}
|
|
54
|
+
export interface CoverageReport {
|
|
55
|
+
slices: SliceCoverage[];
|
|
56
|
+
/** Total invariant IDs across every in-scope slice (sum of `slices[*].invariants.length`). */
|
|
57
|
+
totalInvariants: number;
|
|
58
|
+
/** Of `totalInvariants`, how many have zero citations. */
|
|
59
|
+
uncoveredCount: number;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Assemble the full coverage report: for every slice in the model, resolve its doc via the same
|
|
63
|
+
* note-binding join `em export`/`--slice-ready` use (resolveSliceDocJoin), decide in-scope status,
|
|
64
|
+
* extract invariant IDs for in-scope slices, then scan `testsDir` once for every ID across the
|
|
65
|
+
* whole model (cheaper than one scan per slice, and citations are looked up per-slice from the
|
|
66
|
+
* single resulting map). `baseDir` is the `.em` file's directory, same convention every doc/note
|
|
67
|
+
* path in `em` uses.
|
|
68
|
+
*/
|
|
69
|
+
export declare function buildCoverageReport(model: NormalizedModel, refs: RefsResult, baseDir: string, testsDir: string): CoverageReport;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// `em coverage` (MIL-130): a mechanical check for reference/implement.md's definition-of-done
|
|
3
|
+
// line "every INV-n has a test that cites its ID" — until now a pure honor system, since no `em`
|
|
4
|
+
// command related a slice doc's invariant IDs to a test suite. Both sides are stable strings
|
|
5
|
+
// (the doc's `INV-*` tokens, a test file's source text), so the citation check is deterministic
|
|
6
|
+
// and grep-shaped: extract every invariant ID a slice doc's body mentions, then scan a test tree
|
|
7
|
+
// for lines that cite it.
|
|
8
|
+
//
|
|
9
|
+
// Deliberately a *second*, independent reader of doc bodies, alongside `em export`'s doc join
|
|
10
|
+
// (docJoin.ts, MIL-91) — not a change to it. `em export`'s frontmatter-only contract is
|
|
11
|
+
// untouched: this module never adds a frontmatter field, and it reads `SliceDoc.body` (already
|
|
12
|
+
// exposed for `em ledger`'s content comparison, catalog/sliceDoc.ts) rather than reshaping the
|
|
13
|
+
// export document. Checks that an ID is *cited*, nothing about whether the citing test is good
|
|
14
|
+
// or passing — that stays with review and CI respectively; no judgment mechanized here.
|
|
15
|
+
//
|
|
16
|
+
// Token format: `INV-<KEY>-n` where `<KEY>` is present in nothing textually — the actual convention
|
|
17
|
+
// found in the codebase (templates/slice.md's own Invariants section, reference/conform.md's
|
|
18
|
+
// worked example `INV-CHK-4`) uses either a bare running number (`INV-1`, `INV-2`) or a per-slice
|
|
19
|
+
// mnemonic prefix (`INV-CHK-3`, `INV-CHK-3a` for a rename) — IDs are hand-authored per
|
|
20
|
+
// docs/slice-doc-schema.md, "give each a stable ID", not machine-generated, so no single fixed
|
|
21
|
+
// shape is enforced by any `em` command. INV_TOKEN_RE below matches both: `INV-` followed by a
|
|
22
|
+
// greedy run of alphanumeric/hyphen segments. Greedy is what keeps `INV-KEY-1` from partially
|
|
23
|
+
// matching inside `INV-KEY-12` during *extraction* (`\d+` inside the run always consumes the full
|
|
24
|
+
// digit sequence); citation *scanning* below re-anchors with `\b` on the exact ID string for the
|
|
25
|
+
// same reason, since that direction is a substring search rather than a token match.
|
|
26
|
+
import { readFileSync } from "node:fs";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
import { resolveSliceDocJoin } from "../catalog/docJoin.js";
|
|
29
|
+
import { readSliceDoc } from "../catalog/readSliceDoc.js";
|
|
30
|
+
import { walkDir } from "../util/walkDir.js";
|
|
31
|
+
/** The stable INV token format, scanned across a slice doc's whole body (not just the
|
|
32
|
+
* `## Invariants / Business Rules` section — a `## Delta` section's Added/Modified/Renamed
|
|
33
|
+
* requirement IDs are just as citable, see templates/slice.md). `INV-` then one or more
|
|
34
|
+
* alphanumeric/hyphen segments — matches both `INV-1`/`INV-2` (the template's bare numbering)
|
|
35
|
+
* and `INV-CHK-4`/`INV-CHK-3a` (the per-slice mnemonic style reference/conform.md's own example
|
|
36
|
+
* uses). Exported so tests can assert against the exact grammar. */
|
|
37
|
+
export const INV_TOKEN_RE = /\bINV-[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*\b/g;
|
|
38
|
+
/** Statuses `reference/implement.md`'s coverage gate applies to — a slice hasn't reached the
|
|
39
|
+
* point of needing test citations before `ready-to-implement`, and stays in scope forever after
|
|
40
|
+
* once `implemented` (tests shouldn't regress away from citing an invariant just because the
|
|
41
|
+
* slice shipped). Anything else (`draft`, `reviewed`, or no doc at all) is out of scope. */
|
|
42
|
+
const IN_SCOPE_STATUSES = new Set(["ready-to-implement", "implemented"]);
|
|
43
|
+
/**
|
|
44
|
+
* Extract every distinct `INV-*` token from a slice doc's body text, in first-occurrence order
|
|
45
|
+
* (a rule stated once in `## Invariants` and mentioned again in a later `## Delta` section dedupes
|
|
46
|
+
* to its first appearance — order is for stable, readable output, not a claim about which mention
|
|
47
|
+
* is canonical).
|
|
48
|
+
*/
|
|
49
|
+
export function extractInvariantIds(body) {
|
|
50
|
+
const seen = new Set();
|
|
51
|
+
const ids = [];
|
|
52
|
+
for (const m of body.matchAll(INV_TOKEN_RE)) {
|
|
53
|
+
if (!seen.has(m[0])) {
|
|
54
|
+
seen.add(m[0]);
|
|
55
|
+
ids.push(m[0]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return ids;
|
|
59
|
+
}
|
|
60
|
+
// Directory segments never worth descending into for citations: dependency trees
|
|
61
|
+
// (node_modules) and VCS metadata (.git) can be enormous and never contain a test file worth
|
|
62
|
+
// scanning. Passed to walkDir() as a skipDir predicate so these subtrees are pruned during
|
|
63
|
+
// descent rather than walked in full and filtered out afterward — the skill walkers
|
|
64
|
+
// (skillSync.ts/skillCheck.ts) never encounter this problem in the first place, since the
|
|
65
|
+
// bundled skill directory they walk never contains a node_modules or .git of its own.
|
|
66
|
+
const SKIP_DIR_SEGMENTS = new Set(["node_modules", ".git"]);
|
|
67
|
+
/** Cheap binary sniff: a NUL byte anywhere in the first few KB is not valid UTF-8/ASCII text —
|
|
68
|
+
* every text source file this check cares about (and every test fixture worth citing an
|
|
69
|
+
* invariant from) will fail this in exactly zero legitimate cases. */
|
|
70
|
+
function isLikelyBinary(buf) {
|
|
71
|
+
const len = Math.min(buf.length, 8000);
|
|
72
|
+
for (let i = 0; i < len; i++) {
|
|
73
|
+
if (buf[i] === 0)
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Scan every text file under `testsDir` for lines citing any of `ids`, word-boundary-anchored so
|
|
80
|
+
* `INV-KEY-1` doesn't match inside `INV-KEY-12` (both boundary sides land on a hyphen/alnum
|
|
81
|
+
* transition since the ID alphabet is exactly `[A-Za-z0-9-]`, so no escaping is needed to build
|
|
82
|
+
* the per-ID regex). Returns every citing `{file, line}`, in walk order — a line citing more than
|
|
83
|
+
* one ID is recorded under each. `ids` with zero test-dir mentions still get a `[]` entry, never
|
|
84
|
+
* an absent map key, so a caller never has to guard a lookup with `?? []`.
|
|
85
|
+
*/
|
|
86
|
+
export function scanTestCitations(testsDir, ids) {
|
|
87
|
+
const citations = new Map(ids.map((id) => [id, []]));
|
|
88
|
+
if (ids.length === 0)
|
|
89
|
+
return citations;
|
|
90
|
+
const idPatterns = ids.map((id) => [id, new RegExp(`\\b${id}\\b`)]);
|
|
91
|
+
for (const rel of walkDir(testsDir, { skipDir: (name) => SKIP_DIR_SEGMENTS.has(name) })) {
|
|
92
|
+
let buf;
|
|
93
|
+
try {
|
|
94
|
+
buf = readFileSync(join(testsDir, rel));
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
continue; // unreadable (permissions, broken symlink target, ...) — not this check's job
|
|
98
|
+
}
|
|
99
|
+
if (isLikelyBinary(buf))
|
|
100
|
+
continue;
|
|
101
|
+
const lines = buf.toString("utf8").split(/\r?\n/);
|
|
102
|
+
for (let i = 0; i < lines.length; i++) {
|
|
103
|
+
const line = lines[i];
|
|
104
|
+
for (const [id, re] of idPatterns) {
|
|
105
|
+
if (re.test(line))
|
|
106
|
+
citations.get(id).push({ file: rel, line: i + 1 });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return citations;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Assemble the full coverage report: for every slice in the model, resolve its doc via the same
|
|
114
|
+
* note-binding join `em export`/`--slice-ready` use (resolveSliceDocJoin), decide in-scope status,
|
|
115
|
+
* extract invariant IDs for in-scope slices, then scan `testsDir` once for every ID across the
|
|
116
|
+
* whole model (cheaper than one scan per slice, and citations are looked up per-slice from the
|
|
117
|
+
* single resulting map). `baseDir` is the `.em` file's directory, same convention every doc/note
|
|
118
|
+
* path in `em` uses.
|
|
119
|
+
*/
|
|
120
|
+
export function buildCoverageReport(model, refs, baseDir, testsDir) {
|
|
121
|
+
const scoped = [];
|
|
122
|
+
const allIds = new Set();
|
|
123
|
+
for (let i = 0; i < model.slices.length; i++) {
|
|
124
|
+
const slice = model.slices[i];
|
|
125
|
+
const key = refs.sliceKeys[i];
|
|
126
|
+
const { doc } = resolveSliceDocJoin(slice, key, baseDir, (id) => refs.refById.get(id));
|
|
127
|
+
const inScope = doc.reason === null && doc.status !== null && IN_SCOPE_STATUSES.has(doc.status);
|
|
128
|
+
let ids = [];
|
|
129
|
+
if (inScope) {
|
|
130
|
+
// Re-derive the bound doc's key from doc.path rather than assuming it's this slice's own
|
|
131
|
+
// — MIL-121's ratified cross-binding can resolve `doc` to a DIFFERENT slice's doc, same
|
|
132
|
+
// re-derivation sliceReadyValidate.ts uses for Open Questions.
|
|
133
|
+
const boundKey = doc.path.replace(/^slices\//, "").replace(/\.md$/, "");
|
|
134
|
+
const parsed = readSliceDoc(baseDir, boundKey);
|
|
135
|
+
if (parsed)
|
|
136
|
+
ids = extractInvariantIds(parsed.body);
|
|
137
|
+
}
|
|
138
|
+
scoped.push({ key, status: doc.status, docReason: doc.reason, inScope, ids });
|
|
139
|
+
for (const id of ids)
|
|
140
|
+
allIds.add(id);
|
|
141
|
+
}
|
|
142
|
+
const citations = scanTestCitations(testsDir, [...allIds]);
|
|
143
|
+
let totalInvariants = 0;
|
|
144
|
+
let uncoveredCount = 0;
|
|
145
|
+
const slices = scoped.map(({ key, status, docReason, inScope, ids }) => {
|
|
146
|
+
const invariants = ids.map((id) => {
|
|
147
|
+
const cs = citations.get(id) ?? [];
|
|
148
|
+
totalInvariants++;
|
|
149
|
+
if (cs.length === 0)
|
|
150
|
+
uncoveredCount++;
|
|
151
|
+
return { id, cited: cs.length > 0, citations: cs };
|
|
152
|
+
});
|
|
153
|
+
return { key, status, docReason, inScope, invariants };
|
|
154
|
+
});
|
|
155
|
+
return { slices, totalInvariants, uncoveredCount };
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=coverage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coverage.js","sourceRoot":"","sources":["../../src/cli/coverage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { NormalizedModel } from "../model/model.js";
|
|
2
|
+
import { RefsResult } from "../model/refs.js";
|
|
3
|
+
export type ApplyFrontmatterResult = {
|
|
4
|
+
ok: true;
|
|
5
|
+
content: string;
|
|
6
|
+
changed: boolean;
|
|
7
|
+
} | {
|
|
8
|
+
ok: false;
|
|
9
|
+
message: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Pure text transform: flips `status:` to `implemented` and sets `implementedIn:` to `prUrl` in
|
|
13
|
+
* `raw`'s frontmatter block. Idempotent (re-applying the same URL is a no-op, `changed: false`,
|
|
14
|
+
* `content` returned byte-identical to `raw`); refuses (`ok: false`) rather than overwrite
|
|
15
|
+
* provenance when the doc is already `status: implemented` with a *different* `implementedIn`.
|
|
16
|
+
* No fs access — the caller reads/writes; see `runMarkImplemented` below.
|
|
17
|
+
*/
|
|
18
|
+
export declare function applyImplementedFrontmatter(raw: string, prUrl: string): ApplyFrontmatterResult;
|
|
19
|
+
export type RunMarkImplementedResult = {
|
|
20
|
+
ok: true;
|
|
21
|
+
path: string;
|
|
22
|
+
changed: boolean;
|
|
23
|
+
} | {
|
|
24
|
+
ok: false;
|
|
25
|
+
message: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Resolves `sliceKey` to its bound doc via the same note-binding join `--slice-ready`/`em
|
|
29
|
+
* export` use (MIL-121 cross-binding included — the doc actually edited is whichever one the
|
|
30
|
+
* join resolved to, exactly like `sliceReadyValidate.ts` re-deriving its own read from
|
|
31
|
+
* `doc.path`), then reads/applies/writes it. `baseDir` is the `.em` file's directory, same
|
|
32
|
+
* convention every doc/note path in `em` uses.
|
|
33
|
+
*/
|
|
34
|
+
export declare function runMarkImplemented(model: NormalizedModel, refs: RefsResult, baseDir: string, sliceKey: string, prUrl: string): RunMarkImplementedResult;
|