@milehimikey/em 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/.claude/skills/event-modeling/SKILL.md +60 -12
  2. package/.claude/skills/event-modeling/reference/conform.md +30 -8
  3. package/.claude/skills/event-modeling/reference/em-dsl.md +145 -23
  4. package/.claude/skills/event-modeling/reference/extract.md +2 -1
  5. package/.claude/skills/event-modeling/reference/implement.md +172 -0
  6. package/.claude/skills/event-modeling/reference/methodology.md +5 -0
  7. package/.claude/skills/event-modeling/templates/conformance-report.md +15 -9
  8. package/.claude/skills/event-modeling/templates/model-readme.md +3 -3
  9. package/.claude/skills/event-modeling/templates/slice.md +70 -4
  10. package/.claude/skills/event-modeling/templates/state.md +6 -5
  11. package/README.md +1 -0
  12. package/dist/catalog/build.d.ts +5 -0
  13. package/dist/catalog/build.js +1 -3
  14. package/dist/catalog/build.js.map +1 -1
  15. package/dist/catalog/docJoin.d.ts +39 -0
  16. package/dist/catalog/docJoin.js +86 -0
  17. package/dist/catalog/docJoin.js.map +1 -0
  18. package/dist/catalog/driftSignal.d.ts +16 -0
  19. package/dist/catalog/driftSignal.js +24 -0
  20. package/dist/catalog/driftSignal.js.map +1 -0
  21. package/dist/catalog/frontmatterCoherenceValidate.d.ts +8 -0
  22. package/dist/catalog/frontmatterCoherenceValidate.js +52 -0
  23. package/dist/catalog/frontmatterCoherenceValidate.js.map +1 -0
  24. package/dist/catalog/lineageValidate.d.ts +9 -0
  25. package/dist/catalog/lineageValidate.js +161 -0
  26. package/dist/catalog/lineageValidate.js.map +1 -0
  27. package/dist/catalog/readSliceDoc.d.ts +2 -0
  28. package/dist/catalog/readSliceDoc.js +21 -0
  29. package/dist/catalog/readSliceDoc.js.map +1 -0
  30. package/dist/catalog/sliceDoc.d.ts +94 -3
  31. package/dist/catalog/sliceDoc.js +157 -7
  32. package/dist/catalog/sliceDoc.js.map +1 -1
  33. package/dist/catalog/sliceReadyValidate.d.ts +11 -0
  34. package/dist/catalog/sliceReadyValidate.js +85 -0
  35. package/dist/catalog/sliceReadyValidate.js.map +1 -0
  36. package/dist/cli/diff-inputs.d.ts +20 -0
  37. package/dist/cli/diff-inputs.js +75 -18
  38. package/dist/cli/diff-inputs.js.map +1 -1
  39. package/dist/cli/ledgerCheck.d.ts +38 -0
  40. package/dist/cli/ledgerCheck.js +116 -0
  41. package/dist/cli/ledgerCheck.js.map +1 -0
  42. package/dist/cli/skillCheck.d.ts +18 -0
  43. package/dist/cli/skillCheck.js +63 -0
  44. package/dist/cli/skillCheck.js.map +1 -0
  45. package/dist/cli/skillSync.d.ts +22 -0
  46. package/dist/cli/skillSync.js +69 -0
  47. package/dist/cli/skillSync.js.map +1 -0
  48. package/dist/cli/skillVersionCheck.d.ts +32 -0
  49. package/dist/cli/skillVersionCheck.js +145 -0
  50. package/dist/cli/skillVersionCheck.js.map +1 -0
  51. package/dist/cli.d.ts +3 -1
  52. package/dist/cli.js +199 -32
  53. package/dist/cli.js.map +1 -1
  54. package/dist/emit/diffJson.d.ts +1 -1
  55. package/dist/emit/diffJson.js +18 -14
  56. package/dist/emit/diffJson.js.map +1 -1
  57. package/dist/emit/json.d.ts +10 -25
  58. package/dist/emit/json.js +70 -103
  59. package/dist/emit/json.js.map +1 -1
  60. package/dist/emit/ledgerJson.d.ts +5 -0
  61. package/dist/emit/ledgerJson.js +25 -0
  62. package/dist/emit/ledgerJson.js.map +1 -0
  63. package/dist/emit/skillCheckJson.d.ts +5 -0
  64. package/dist/emit/skillCheckJson.js +32 -0
  65. package/dist/emit/skillCheckJson.js.map +1 -0
  66. package/dist/model/diff.d.ts +44 -1
  67. package/dist/model/diff.js +51 -9
  68. package/dist/model/diff.js.map +1 -1
  69. package/dist/model/model.d.ts +1 -1
  70. package/dist/model/refs.d.ts +23 -0
  71. package/dist/model/refs.js +75 -0
  72. package/dist/model/refs.js.map +1 -0
  73. package/dist/model/rules.d.ts +252 -0
  74. package/dist/model/rules.js +253 -0
  75. package/dist/model/rules.js.map +1 -0
  76. package/dist/model/validate.d.ts +23 -1
  77. package/dist/model/validate.js +184 -49
  78. package/dist/model/validate.js.map +1 -1
  79. package/dist/parser/ast.d.ts +3 -2
  80. package/dist/parser/parser.js +36 -14
  81. package/dist/parser/parser.js.map +1 -1
  82. package/dist/pipeline.d.ts +9 -0
  83. package/dist/pipeline.js +4 -2
  84. package/dist/pipeline.js.map +1 -1
  85. package/dist/render/sliceStatus.js +1 -1
  86. package/dist/render/sliceStatus.js.map +1 -1
  87. package/dist/util/isMainModule.d.ts +14 -0
  88. package/dist/util/isMainModule.js +33 -0
  89. package/dist/util/isMainModule.js.map +1 -0
  90. package/dist/util/walkDir.d.ts +4 -0
  91. package/dist/util/walkDir.js +28 -0
  92. package/dist/util/walkDir.js.map +1 -0
  93. package/package.json +5 -2
@@ -0,0 +1,85 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // `em validate --slice-ready <key>` (MIL-87): the handoff gate without the bridge. Gives em
3
+ // itself a native version of the readiness check that used to live only in em-sdd-bridge's
4
+ // `assertReadyToImplement` — status: ready-to-implement AND every Open Question checked —
5
+ // so any toolchain reading slice docs directly (not just ones routed through the bridge) has
6
+ // a gate to enforce.
7
+ //
8
+ // Deliberately single-slice, opt-in — unlike catalog/lineageValidate.ts (MIL-84) and
9
+ // catalog/frontmatterCoherenceValidate.ts (MIL-85), which run unconditionally on every
10
+ // `em validate` because they only fire on genuine anomalies. "Not yet ready-to-implement" /
11
+ // "has unchecked Open Questions" is the NORMAL state of most slices most of the time (drafts,
12
+ // reviewed-but-not-ready-yet, ...) — splicing this into the unconditional diagnostic set would
13
+ // make plain `em validate` permanently noisy on any healthy in-progress model. So this only
14
+ // ever runs for the one slice named by `--slice-ready <key>`.
15
+ //
16
+ // "Note binding" (the ticket's own phrase) means reusing docJoin.ts's resolveSliceDocJoin — the
17
+ // same note-gated resolution `em export` (MIL-91) uses — rather than the filename-only
18
+ // readSliceDoc/existsSync convention `em catalog`/`em render` deliberately use instead (that
19
+ // convention has a tested invariant of ignoring `note` entirely; this module doesn't touch it).
20
+ // A slice with no `note "slices/<key>.md"` element bound to it isn't ready by definition: there's
21
+ // nothing ratified to check status/Open Questions against.
22
+ //
23
+ // version/status/link coherence (MIL-85) is folded in for free at the CLI layer (src/cli.ts):
24
+ // validateFrontmatterCoherence already runs unconditionally and already tags its diagnostics
25
+ // with `refs: [sliceKey]`, so the `--slice-ready` branch filters the full combined diagnostic
26
+ // set by that ref instead of this module re-deriving classifyImplementationDrift.
27
+ import { makeDiag, pushDiag } from "../model/rules.js";
28
+ import { resolveSliceDocJoin } from "./docJoin.js";
29
+ import { readSliceDoc } from "./readSliceDoc.js";
30
+ /**
31
+ * Readiness gate for one slice, named by its export key (`refs.sliceKeys`). `baseDir` is the
32
+ * `.em` file's directory, same convention every other doc/note path in em uses. Always warning-
33
+ * severity (fits validate's existing warning-producer shape) except for a bad `sliceKey` itself,
34
+ * which is an error — a CLI-argument mistake, not a model-quality finding. The `--slice-ready`
35
+ * flag in src/cli.ts is what decides whether these warnings gate the exit code.
36
+ */
37
+ export function validateSliceReady(model, refs, baseDir, sliceKey) {
38
+ const sliceIndex = refs.sliceKeys.indexOf(sliceKey);
39
+ if (sliceIndex === -1) {
40
+ return [
41
+ makeDiag("slice-ready-unknown-slice", {
42
+ message: `no slice with export key "${sliceKey}" in this model`,
43
+ refs: [sliceKey],
44
+ }),
45
+ ];
46
+ }
47
+ const slice = model.slices[sliceIndex];
48
+ const { doc, diagnostics: joinDiagnostics } = resolveSliceDocJoin(slice, sliceKey, baseDir, (id) => refs.refById.get(id));
49
+ if (doc.reason === "no-doc-bound") {
50
+ return [
51
+ makeDiag("slice-ready-no-doc-bound", {
52
+ message: `slice "${sliceKey}" has no doc bound via \`note "slices/${sliceKey}.md"\` — bind a slice doc before it can be ready-to-implement`,
53
+ line: slice.line,
54
+ refs: [sliceKey],
55
+ }),
56
+ ];
57
+ }
58
+ if (doc.reason === "binding-missing-file" || doc.reason === "frontmatter-invalid") {
59
+ // Reuse docJoin's own diagnostics verbatim — don't re-code binding-missing-file/
60
+ // frontmatter-invalid a second time under a slice-ready-specific code.
61
+ return joinDiagnostics;
62
+ }
63
+ // doc.reason === null: found, usable frontmatter.
64
+ const diags = [];
65
+ if (doc.status !== "ready-to-implement") {
66
+ pushDiag(diags, "slice-ready-status-not-ready", {
67
+ message: `slice "${sliceKey}" is status: ${doc.status ?? "(none)"}, not ready-to-implement`,
68
+ line: slice.line,
69
+ refs: [sliceKey],
70
+ });
71
+ }
72
+ // resolveSliceDocJoin's SliceDocExport deliberately never carries Open Questions counts (same
73
+ // hard boundary that keeps doc.html/doc.raw out of it) — re-read via readSliceDoc for the
74
+ // full SliceDoc. Non-null: doc.reason === null already implies the file exists and parses.
75
+ const parsed = readSliceDoc(baseDir, sliceKey);
76
+ if (parsed.openQuestionsUnchecked > 0) {
77
+ pushDiag(diags, "slice-ready-open-questions-unchecked", {
78
+ message: `slice "${sliceKey}" has ${parsed.openQuestionsUnchecked} of ${parsed.openQuestionsTotal} Open Question(s) unchecked`,
79
+ line: slice.line,
80
+ refs: [sliceKey],
81
+ });
82
+ }
83
+ return diags;
84
+ }
85
+ //# sourceMappingURL=sliceReadyValidate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sliceReadyValidate.js","sourceRoot":"","sources":["../../src/catalog/sliceReadyValidate.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { SliceDoc } from "../catalog/sliceDoc.js";
1
2
  /** Resolved `em diff` invocation, or a user-facing `error` for an invalid form. */
2
3
  export type DiffPlan = {
3
4
  error: string;
@@ -42,3 +43,22 @@ export type RevisionResult = {
42
43
  * unit-testable without a real repository.
43
44
  */
44
45
  export declare function resolveRevision(file: string, rev: string, runGit?: GitRunner): RevisionResult;
46
+ /**
47
+ * Resolve a slice's doc (`slices/<sliceKey>.md`, relative to `anchorFile`'s directory — same
48
+ * convention as every other doc/note path in em) at git revision `rev`. Unlike
49
+ * `resolveRevision`, a missing doc at a revision is routine (not every slice had a doc yet, or
50
+ * the doc simply isn't tracked) rather than a user-facing error — so every failure mode
51
+ * collapses to `null`, for `em diff`'s lineage annotation (MIL-84) to treat as "nothing to
52
+ * annotate with" rather than aborting the diff.
53
+ */
54
+ export declare function resolveDocAtRevision(anchorFile: string, sliceKey: string, rev: string, runGit?: GitRunner): SliceDoc | null;
55
+ /**
56
+ * Slice doc keys (filename stems under `slices/`, relative to `anchorFile`'s directory — same
57
+ * anchor convention as `resolveDocAtRevision`) present at git revision `rev`. Used by `em ledger`
58
+ * (MIL-89) to discover the candidate set of docs to compare across two revisions, reusing the
59
+ * same repo-root resolution `showAtRevision` does rather than a second, divergent way of finding
60
+ * "what slice docs exist here." Returns `[]` (not an error) when the repo can't be resolved or
61
+ * `slices/` didn't exist at `rev` — both routine (a model with no slice docs yet), matching
62
+ * `resolveDocAtRevision`'s "missing is routine" stance.
63
+ */
64
+ export declare function listSliceKeysAtRevision(anchorFile: string, rev: string, runGit?: GitRunner): string[];
@@ -4,7 +4,8 @@
4
4
  // unit-tested without spawning a subprocess or a real git repo (cli.ts keeps the
5
5
  // process.exit/console wiring; see test/diff-inputs.test.ts).
6
6
  import { spawnSync } from "node:child_process";
7
- import { dirname, resolve } from "node:path";
7
+ import { basename, dirname, resolve } from "node:path";
8
+ import { parseSliceDoc } from "../catalog/sliceDoc.js";
8
9
  /**
9
10
  * Validate the `em diff` argument/flag combination. Pure — no I/O. The two forms
10
11
  * (two files, or one file across git revisions) are mutually exclusive.
@@ -33,30 +34,86 @@ export const realGit = (args) => {
33
34
  const r = spawnSync("git", args, { encoding: "utf8" });
34
35
  return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
35
36
  };
37
+ /**
38
+ * Read `targetAbs`'s content at git revision `rev`, resolving the repo root from
39
+ * `anchorFile`'s directory (a file known to be somewhere inside the repo — doesn't itself have
40
+ * to be `targetAbs`, e.g. a slice doc resolved relative to its `.em` file's location). The git
41
+ * runner is injectable so every failure branch is unit-testable without a real repository.
42
+ *
43
+ * Existence/path lookup uses `git ls-tree -r --name-only <rev>` — scoped to `rev` itself, not
44
+ * `git ls-files` (which reflects the *current* index/working tree). A doc that existed at
45
+ * `rev` but has since been deleted — the routine case for a `superseded-by`-carrying doc, which
46
+ * gets removed in the very commit that performs the split/merge/removal it records — would
47
+ * otherwise resolve to "not tracked" even though `git show <rev>:<path>` can read it just fine.
48
+ */
49
+ function showAtRevision(runGit, anchorFile, targetAbs, rev) {
50
+ const toplevel = runGit(["-C", dirname(resolve(anchorFile)), "rev-parse", "--show-toplevel"]);
51
+ if (toplevel.status !== 0)
52
+ return { ok: false, reason: "no-repo" };
53
+ const repoRoot = toplevel.stdout.trim();
54
+ const lsTree = runGit(["-C", repoRoot, "ls-tree", "-r", "--name-only", rev, "--", targetAbs]);
55
+ const relPath = lsTree.stdout.trim().split("\n")[0];
56
+ if (!relPath)
57
+ return { ok: false, reason: "not-tracked", repoRoot };
58
+ const show = runGit(["-C", repoRoot, "show", `${rev}:${relPath}`]);
59
+ if (show.status !== 0) {
60
+ return { ok: false, reason: "show-failed", relPath, detail: (show.stderr || "").trim() || "unknown git error" };
61
+ }
62
+ return { ok: true, content: show.stdout };
63
+ }
36
64
  /**
37
65
  * Resolve `file`'s content at git revision `rev` via `git show <rev>:<repo-relative-path>`.
38
66
  * The git runner is injectable so the not-a-repo / not-tracked / bad-rev branches are
39
67
  * unit-testable without a real repository.
40
68
  */
41
69
  export function resolveRevision(file, rev, runGit = realGit) {
42
- const abs = resolve(file);
43
- const toplevel = runGit(["-C", dirname(abs), "rev-parse", "--show-toplevel"]);
44
- if (toplevel.status !== 0) {
45
- return { ok: false, message: `em diff: ${file} is not inside a git repository (needed for --from/--to)` };
70
+ const result = showAtRevision(runGit, file, resolve(file), rev);
71
+ if (result.ok)
72
+ return result;
73
+ switch (result.reason) {
74
+ case "no-repo":
75
+ return { ok: false, message: `em diff: ${file} is not inside a git repository (needed for --from/--to)` };
76
+ case "not-tracked":
77
+ return { ok: false, message: `em diff: ${file} is not tracked by git in ${result.repoRoot}` };
78
+ case "show-failed":
79
+ return { ok: false, message: `em diff: cannot read ${result.relPath} at revision "${rev}": ${result.detail}` };
46
80
  }
81
+ }
82
+ /**
83
+ * Resolve a slice's doc (`slices/<sliceKey>.md`, relative to `anchorFile`'s directory — same
84
+ * convention as every other doc/note path in em) at git revision `rev`. Unlike
85
+ * `resolveRevision`, a missing doc at a revision is routine (not every slice had a doc yet, or
86
+ * the doc simply isn't tracked) rather than a user-facing error — so every failure mode
87
+ * collapses to `null`, for `em diff`'s lineage annotation (MIL-84) to treat as "nothing to
88
+ * annotate with" rather than aborting the diff.
89
+ */
90
+ export function resolveDocAtRevision(anchorFile, sliceKey, rev, runGit = realGit) {
91
+ const targetAbs = resolve(dirname(resolve(anchorFile)), "slices", `${sliceKey}.md`);
92
+ const result = showAtRevision(runGit, anchorFile, targetAbs, rev);
93
+ return result.ok ? parseSliceDoc(result.content) : null;
94
+ }
95
+ /**
96
+ * Slice doc keys (filename stems under `slices/`, relative to `anchorFile`'s directory — same
97
+ * anchor convention as `resolveDocAtRevision`) present at git revision `rev`. Used by `em ledger`
98
+ * (MIL-89) to discover the candidate set of docs to compare across two revisions, reusing the
99
+ * same repo-root resolution `showAtRevision` does rather than a second, divergent way of finding
100
+ * "what slice docs exist here." Returns `[]` (not an error) when the repo can't be resolved or
101
+ * `slices/` didn't exist at `rev` — both routine (a model with no slice docs yet), matching
102
+ * `resolveDocAtRevision`'s "missing is routine" stance.
103
+ */
104
+ export function listSliceKeysAtRevision(anchorFile, rev, runGit = realGit) {
105
+ const toplevel = runGit(["-C", dirname(resolve(anchorFile)), "rev-parse", "--show-toplevel"]);
106
+ if (toplevel.status !== 0)
107
+ return [];
47
108
  const repoRoot = toplevel.stdout.trim();
48
- const lsFiles = runGit(["-C", repoRoot, "ls-files", "--full-name", "--", abs]);
49
- const relPath = lsFiles.stdout.trim().split("\n")[0];
50
- if (!relPath) {
51
- return { ok: false, message: `em diff: ${file} is not tracked by git in ${repoRoot}` };
52
- }
53
- const show = runGit(["-C", repoRoot, "show", `${rev}:${relPath}`]);
54
- if (show.status !== 0) {
55
- return {
56
- ok: false,
57
- message: `em diff: cannot read ${relPath} at revision "${rev}": ${(show.stderr || "").trim() || "unknown git error"}`,
58
- };
59
- }
60
- return { ok: true, content: show.stdout };
109
+ const slicesDirAbs = resolve(dirname(resolve(anchorFile)), "slices");
110
+ const lsTree = runGit(["-C", repoRoot, "ls-tree", "-r", "--name-only", rev, "--", slicesDirAbs]);
111
+ if (lsTree.status !== 0)
112
+ return [];
113
+ return lsTree.stdout
114
+ .split("\n")
115
+ .map((line) => line.trim())
116
+ .filter((line) => line.endsWith(".md"))
117
+ .map((line) => basename(line, ".md"));
61
118
  }
62
119
  //# sourceMappingURL=diff-inputs.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"diff-inputs.js","sourceRoot":"","sources":["../../src/cli/diff-inputs.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"diff-inputs.js","sourceRoot":"","sources":["../../src/cli/diff-inputs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import { GitRunner } from "./diff-inputs.js";
2
+ export type LedgerFindingCode = "ledger-content-without-version-bump" | "ledger-version-without-content-change" | "ledger-version-regression";
3
+ export interface LedgerFinding {
4
+ sliceKey: string;
5
+ code: LedgerFindingCode;
6
+ message: string;
7
+ oldVersion: number;
8
+ newVersion: number;
9
+ bodyChanged: boolean;
10
+ lineageChanged: boolean;
11
+ }
12
+ export type LedgerSkipReason = "no-prior-revision" | "deleted" | "frontmatter-invalid";
13
+ export interface LedgerSkip {
14
+ sliceKey: string;
15
+ reason: LedgerSkipReason;
16
+ }
17
+ export interface LedgerCheckResult {
18
+ findings: LedgerFinding[];
19
+ skipped: LedgerSkip[];
20
+ /** Total slice keys considered (the union of both revisions' `slices/*.md` — every key that
21
+ * produced either a finding, a skip, or a clean pass). Lets a caller report "N checked"
22
+ * without re-deriving it from findings.length + skipped.length + (silent clean count). */
23
+ checkedCount: number;
24
+ }
25
+ /**
26
+ * Compare every slice doc's version:/content agreement between two revisions of `anchorFile`'s
27
+ * repo. `to` null means "the current working tree" (same convention as `em diff`). Pure aside
28
+ * from the injected GitRunner and fs reads via readSliceDoc — no process.exit, no console.
29
+ *
30
+ * Deliberately excludes `status`/`implementedIn` from the content comparison: those change
31
+ * independently by design during re-ratification (docs/slice-doc-schema.md, "`status` under
32
+ * re-ratification") — including them would false-positive on every ordinary lifecycle
33
+ * transition. Also excludes `pattern`/`swimlane`/`schemaVersion`: decorative, not currently
34
+ * exposed on `SliceDoc` at all. Body comparison is `.trim()`-only — strips leading/trailing
35
+ * whitespace, avoiding a false positive from a trailing-newline/CRLF-only re-save, without
36
+ * risking a false negative on a real prose edit.
37
+ */
38
+ export declare function checkLedger(anchorFile: string, from: string, to: string | null, runGit?: GitRunner): LedgerCheckResult;
@@ -0,0 +1,116 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // `em ledger` (MIL-89): checks that a slice doc's `version:` frontmatter field and its content
3
+ // (body + lineage refs) always change together across two git revisions — a version bump with
4
+ // no real content change is a no-op ledger entry, a content change with no version bump is a
5
+ // stale ratification signal, and a version going backwards is a typo/regression. Opt-in,
6
+ // CI-recipe tier — never folded into `em validate`, which stays a fast function of the current
7
+ // tree (see lineageValidate.ts's header comment, which named this exact shape during MIL-84).
8
+ //
9
+ // Sibling to diff-inputs.ts in role: testable input handling / git-revision logic factored out
10
+ // of cli.ts, with an injectable GitRunner and no process.exit/console — see
11
+ // test/ledgerCheck.test.ts. Reuses resolveDocAtRevision/listSliceKeysAtRevision rather than
12
+ // second, divergent git plumbing.
13
+ import { existsSync, readdirSync } from "node:fs";
14
+ import { basename, dirname, join, resolve } from "node:path";
15
+ import { hasUsableFrontmatter } from "../catalog/sliceDoc.js";
16
+ import { readSliceDoc } from "../catalog/readSliceDoc.js";
17
+ import { realGit, listSliceKeysAtRevision, resolveDocAtRevision } from "./diff-inputs.js";
18
+ /** Slice keys currently on disk under `<baseDir>/slices/*.md` — the working-tree counterpart to
19
+ * `listSliceKeysAtRevision`, used when `to` is null (compare against the working tree). */
20
+ function listSliceKeysOnDisk(baseDir) {
21
+ const dir = join(baseDir, "slices");
22
+ if (!existsSync(dir))
23
+ return [];
24
+ return readdirSync(dir)
25
+ .filter((name) => name.endsWith(".md"))
26
+ .map((name) => basename(name, ".md"));
27
+ }
28
+ /** True when two lineage refs (or their absence) carry the same declared text — compared on
29
+ * `raw` so a malformed ref (null sliceKey/version) still counts as a real change if its text
30
+ * changes, not silently treated as equal to another malformed ref. */
31
+ function refEqual(a, b) {
32
+ if (a === null || b === null)
33
+ return a === b;
34
+ return a.raw === b.raw;
35
+ }
36
+ function refListEqual(a, b) {
37
+ if (a.length !== b.length)
38
+ return false;
39
+ return a.every((ref, i) => refEqual(ref, b[i]));
40
+ }
41
+ /** True when either side's declared lineage (`split-from`/`merged-from`/`superseded-by`)
42
+ * differs — these describe what the doc *asserts*, same-commit-authoring convention means a
43
+ * legitimate change here always co-occurs with a version bump (docs/slice-doc-schema.md). */
44
+ function lineageChanged(oldDoc, newDoc) {
45
+ return (!refEqual(oldDoc.splitFrom, newDoc.splitFrom) ||
46
+ !refListEqual(oldDoc.mergedFrom, newDoc.mergedFrom) ||
47
+ !refListEqual(oldDoc.supersededBy, newDoc.supersededBy));
48
+ }
49
+ /**
50
+ * Compare every slice doc's version:/content agreement between two revisions of `anchorFile`'s
51
+ * repo. `to` null means "the current working tree" (same convention as `em diff`). Pure aside
52
+ * from the injected GitRunner and fs reads via readSliceDoc — no process.exit, no console.
53
+ *
54
+ * Deliberately excludes `status`/`implementedIn` from the content comparison: those change
55
+ * independently by design during re-ratification (docs/slice-doc-schema.md, "`status` under
56
+ * re-ratification") — including them would false-positive on every ordinary lifecycle
57
+ * transition. Also excludes `pattern`/`swimlane`/`schemaVersion`: decorative, not currently
58
+ * exposed on `SliceDoc` at all. Body comparison is `.trim()`-only — strips leading/trailing
59
+ * whitespace, avoiding a false positive from a trailing-newline/CRLF-only re-save, without
60
+ * risking a false negative on a real prose edit.
61
+ */
62
+ export function checkLedger(anchorFile, from, to, runGit = realGit) {
63
+ const baseDir = dirname(resolve(anchorFile));
64
+ const oldKeys = listSliceKeysAtRevision(anchorFile, from, runGit);
65
+ const newKeys = to ? listSliceKeysAtRevision(anchorFile, to, runGit) : listSliceKeysOnDisk(baseDir);
66
+ const keys = [...new Set([...oldKeys, ...newKeys])].sort();
67
+ const findings = [];
68
+ const skipped = [];
69
+ for (const sliceKey of keys) {
70
+ const oldDoc = resolveDocAtRevision(anchorFile, sliceKey, from, runGit);
71
+ if (!oldDoc) {
72
+ skipped.push({ sliceKey, reason: "no-prior-revision" });
73
+ continue;
74
+ }
75
+ const newDoc = to ? resolveDocAtRevision(anchorFile, sliceKey, to, runGit) : readSliceDoc(baseDir, sliceKey);
76
+ if (!newDoc) {
77
+ skipped.push({ sliceKey, reason: "deleted" });
78
+ continue;
79
+ }
80
+ // hasUsableFrontmatter() only checks that the `version` key is *present*
81
+ // (missingRequiredFields), not that its value parsed to a valid positive integer —
82
+ // `version: abc`/`version: 0`/`version: 1.5` all pass that gate with SliceDoc.version still
83
+ // null (see sliceDoc.ts's parseVersion()). lineageValidate.ts hits the same gap and handles
84
+ // it the same way: an explicit null check, routed to the same skip a missing key gets,
85
+ // rather than a non-null assertion that would corrupt a finding's message/classification.
86
+ if (!hasUsableFrontmatter(oldDoc) || !hasUsableFrontmatter(newDoc) || oldDoc.version === null || newDoc.version === null) {
87
+ skipped.push({ sliceKey, reason: "frontmatter-invalid" });
88
+ continue;
89
+ }
90
+ const oldVersion = oldDoc.version;
91
+ const newVersion = newDoc.version;
92
+ const bodyChanged = oldDoc.body.trim() !== newDoc.body.trim();
93
+ const docsLineageChanged = lineageChanged(oldDoc, newDoc);
94
+ const contentChanged = bodyChanged || docsLineageChanged;
95
+ let code = null;
96
+ if (newVersion < oldVersion) {
97
+ code = "ledger-version-regression";
98
+ }
99
+ else if (contentChanged && newVersion === oldVersion) {
100
+ code = "ledger-content-without-version-bump";
101
+ }
102
+ else if (!contentChanged && newVersion > oldVersion) {
103
+ code = "ledger-version-without-content-change";
104
+ }
105
+ if (!code)
106
+ continue;
107
+ const message = code === "ledger-version-regression"
108
+ ? `slice "${sliceKey}": version went backwards (v${oldVersion} -> v${newVersion})`
109
+ : code === "ledger-content-without-version-bump"
110
+ ? `slice "${sliceKey}": doc content changed but version: didn't bump (still v${oldVersion})`
111
+ : `slice "${sliceKey}": version: bumped (v${oldVersion} -> v${newVersion}) but doc content is unchanged`;
112
+ findings.push({ sliceKey, code, message, oldVersion, newVersion, bodyChanged, lineageChanged: docsLineageChanged });
113
+ }
114
+ return { findings, skipped, checkedCount: keys.length };
115
+ }
116
+ //# sourceMappingURL=ledgerCheck.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ledgerCheck.js","sourceRoot":"","sources":["../../src/cli/ledgerCheck.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ export type SkillCheckFindingCode = "skill-check-not-installed" | "skill-check-stamp-missing" | "skill-check-stamp-mismatch" | "skill-check-content-drift";
2
+ export interface SkillCheckFinding {
3
+ code: SkillCheckFindingCode;
4
+ message: string;
5
+ vendoredStamp?: string | null;
6
+ installedVersion?: string;
7
+ driftedFiles?: string[];
8
+ }
9
+ export interface SkillCheckResult {
10
+ findings: SkillCheckFinding[];
11
+ ok: boolean;
12
+ }
13
+ /**
14
+ * Pure: compares vendoredDir against packagedDir (content) and installedVersion (em --version
15
+ * — injected, never re-derived; see src/cli.ts's PKG_VERSION) against the vendored SKILL.md's
16
+ * em-version: stamp. No process.exit, no console.
17
+ */
18
+ export declare function checkSkillSync(packagedDir: string, vendoredDir: string, installedVersion: string): SkillCheckResult;
@@ -0,0 +1,63 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // `em skill check` (MIL-93): checks a consuming repo's vendored skill copy for drift against
3
+ // the em package actually installed — the downstream half of the skill-drift problem (the
4
+ // in-repo half is MIL-92's skillVersionCheck.ts, which checks *this* repo's own package.json
5
+ // against *its own* bundled skill across two git revisions). This check has no git revisions
6
+ // at all: both sides are directories on disk right now, so unlike ledgerCheck.ts/
7
+ // skillVersionCheck.ts there's no GitRunner to inject.
8
+ //
9
+ // Two independent signals, both reported (never short-circuited on the first):
10
+ // - the vendored SKILL.md's em-version: stamp vs. the installed em's own version
11
+ // - a full content diff against the packaged skill (reusing planSkillSync's hash-walk), which
12
+ // catches a hand-edited vendored file even when its stamp still happens to match — the
13
+ // ticket's explicit "stamp says current, content isn't" case.
14
+ import { existsSync, readFileSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { extractEmVersionStamp } from "./skillVersionCheck.js";
17
+ import { planSkillSync } from "./skillSync.js";
18
+ /**
19
+ * Pure: compares vendoredDir against packagedDir (content) and installedVersion (em --version
20
+ * — injected, never re-derived; see src/cli.ts's PKG_VERSION) against the vendored SKILL.md's
21
+ * em-version: stamp. No process.exit, no console.
22
+ */
23
+ export function checkSkillSync(packagedDir, vendoredDir, installedVersion) {
24
+ if (!existsSync(vendoredDir)) {
25
+ return {
26
+ ok: false,
27
+ findings: [
28
+ {
29
+ code: "skill-check-not-installed",
30
+ message: `no vendored skill found at ${vendoredDir} — run \`em skill sync\` first`,
31
+ },
32
+ ],
33
+ };
34
+ }
35
+ const findings = [];
36
+ const skillMdPath = join(vendoredDir, "SKILL.md");
37
+ const vendoredStamp = existsSync(skillMdPath) ? extractEmVersionStamp(readFileSync(skillMdPath, "utf8")) : null;
38
+ if (vendoredStamp === null) {
39
+ findings.push({
40
+ code: "skill-check-stamp-missing",
41
+ message: `${skillMdPath} has no em-version: stamp — vendored copy predates MIL-92 or was hand-edited`,
42
+ });
43
+ }
44
+ else if (vendoredStamp !== installedVersion) {
45
+ findings.push({
46
+ code: "skill-check-stamp-mismatch",
47
+ message: `vendored skill's em-version: stamp (${vendoredStamp}) doesn't match installed em (${installedVersion}) — run \`em skill sync\``,
48
+ vendoredStamp,
49
+ installedVersion,
50
+ });
51
+ }
52
+ const plan = planSkillSync(packagedDir, vendoredDir);
53
+ if (plan.changes.length > 0) {
54
+ const driftedFiles = plan.changes.map((c) => c.relPath).sort();
55
+ findings.push({
56
+ code: "skill-check-content-drift",
57
+ message: `vendored skill differs from the packaged skill in ${driftedFiles.length} file(s) — run \`em skill sync\``,
58
+ driftedFiles,
59
+ });
60
+ }
61
+ return { findings, ok: findings.length === 0 };
62
+ }
63
+ //# sourceMappingURL=skillCheck.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skillCheck.js","sourceRoot":"","sources":["../../src/cli/skillCheck.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,6FAA6F;AAC7F,0FAA0F;AAC1F,6FAA6F;AAC7F,6FAA6F;AAC7F,kFAAkF;AAClF,uDAAuD;AACvD,EAAE;AACF,+EAA+E;AAC/E,kFAAkF;AAClF,+FAA+F;AAC/F,0FAA0F;AAC1F,iEAAiE;AAEjE,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAqB/C;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,WAAmB,EAAE,WAAmB,EAAE,gBAAwB;IAC/F,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAC7B,OAAO;YACL,EAAE,EAAE,KAAK;YACT,QAAQ,EAAE;gBACR;oBACE,IAAI,EAAE,2BAA2B;oBACjC,OAAO,EAAE,8BAA8B,WAAW,gCAAgC;iBACnF;aACF;SACF,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAwB,EAAE,CAAC;IAEzC,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;IAClD,MAAM,aAAa,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,qBAAqB,CAAC,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAEhH,IAAI,aAAa,KAAK,IAAI,EAAE,CAAC;QAC3B,QAAQ,CAAC,IAAI,CAAC;YACZ,IAAI,EAAE,2BAA2B;YACjC,OAAO,EAAE,GAAG,WAAW,8EAA8E;SACtG,CAAC,CAAC;IACL,CAAC;SAAM,IAAI,aAAa,KAAK,gBAAgB,EAAE,CAAC;QAC9C,QAAQ,CAAC,IAAI,CAAC;YACZ,IAAI,EAAE,4BAA4B;YAClC,OAAO,EAAE,uCAAuC,aAAa,iCAAiC,gBAAgB,2BAA2B;YACzI,aAAa;YACb,gBAAgB;SACjB,CAAC,CAAC;IACL,CAAC;IAED,MAAM,IAAI,GAAG,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;IACrD,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5B,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;QAC/D,QAAQ,CAAC,IAAI,CAAC;YACZ,IAAI,EAAE,2BAA2B;YACjC,OAAO,EAAE,qDAAqD,YAAY,CAAC,MAAM,kCAAkC;YACnH,YAAY;SACb,CAAC,CAAC;IACL,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;AACjD,CAAC"}
@@ -0,0 +1,22 @@
1
+ export type SkillSyncFileChangeKind = "added" | "modified" | "removed";
2
+ export interface SkillSyncFileChange {
3
+ relPath: string;
4
+ kind: SkillSyncFileChangeKind;
5
+ }
6
+ export interface SkillSyncPlan {
7
+ /** Sorted by relPath. Empty means vendoredDir already matches packagedDir exactly. */
8
+ changes: SkillSyncFileChange[];
9
+ /** Files present, identical, in both dirs — not listed in `changes`. */
10
+ unchangedCount: number;
11
+ }
12
+ /**
13
+ * Pure: compares packagedDir (source of truth) against vendoredDir (which may not exist yet —
14
+ * that's the fresh-sync case, not an error) by per-file sha256. No fs writes.
15
+ */
16
+ export declare function planSkillSync(packagedDir: string, vendoredDir: string): SkillSyncPlan;
17
+ /**
18
+ * Shell: applies a plan produced by planSkillSync — copies added/modified files from
19
+ * packagedDir into vendoredDir, deletes removed files from vendoredDir. Idempotent; a plan
20
+ * with no changes is a no-op. Creates vendoredDir (and any needed subdirectories) as needed.
21
+ */
22
+ export declare function applySkillSync(plan: SkillSyncPlan, packagedDir: string, vendoredDir: string): void;
@@ -0,0 +1,69 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // `em skill sync` (MIL-93): materializes/updates a consuming repo's vendored copy of the
3
+ // event-modeling skill from the em package actually installed. Unlike `em ledger`/
4
+ // skillVersionCheck.ts (which diff two *git revisions* of the same repo), this diffs two
5
+ // *directories on disk right now* — the packaged skill bundled with the running em, and a
6
+ // vendored copy in some other repo — so there's no GitRunner here at all.
7
+ //
8
+ // Contract: the vendored copy is read-only from the consuming repo's point of view. Local
9
+ // edits are never merged — sync always makes vendoredDir byte-identical to packagedDir, the
10
+ // same "you don't own this copy" stance rlm-blueprint's hand-rolled sync guard already
11
+ // enforced by hand. That's why, unlike `em skill install`, there's no --force/confirmation
12
+ // gate: overwriting is the entire point.
13
+ //
14
+ // Split into a pure plan (walk + hash, no writes) and a thin apply (the actual fs mutation) so
15
+ // `em skill check` (src/cli/skillCheck.ts) can reuse planSkillSync's diff for its own
16
+ // content-drift detection without performing any writes.
17
+ import { existsSync, mkdirSync, copyFileSync, readFileSync, rmSync } from "node:fs";
18
+ import { createHash } from "node:crypto";
19
+ import { dirname, join } from "node:path";
20
+ import { walkDir } from "../util/walkDir.js";
21
+ function sha256(path) {
22
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
23
+ }
24
+ /**
25
+ * Pure: compares packagedDir (source of truth) against vendoredDir (which may not exist yet —
26
+ * that's the fresh-sync case, not an error) by per-file sha256. No fs writes.
27
+ */
28
+ export function planSkillSync(packagedDir, vendoredDir) {
29
+ const packagedFiles = new Set(walkDir(packagedDir));
30
+ const vendoredFiles = new Set(existsSync(vendoredDir) ? walkDir(vendoredDir) : []);
31
+ const allPaths = [...new Set([...packagedFiles, ...vendoredFiles])].sort();
32
+ const changes = [];
33
+ let unchangedCount = 0;
34
+ for (const relPath of allPaths) {
35
+ const inPackaged = packagedFiles.has(relPath);
36
+ const inVendored = vendoredFiles.has(relPath);
37
+ if (inPackaged && !inVendored) {
38
+ changes.push({ relPath, kind: "added" });
39
+ }
40
+ else if (!inPackaged && inVendored) {
41
+ changes.push({ relPath, kind: "removed" });
42
+ }
43
+ else if (sha256(join(packagedDir, relPath)) !== sha256(join(vendoredDir, relPath))) {
44
+ changes.push({ relPath, kind: "modified" });
45
+ }
46
+ else {
47
+ unchangedCount++;
48
+ }
49
+ }
50
+ return { changes, unchangedCount };
51
+ }
52
+ /**
53
+ * Shell: applies a plan produced by planSkillSync — copies added/modified files from
54
+ * packagedDir into vendoredDir, deletes removed files from vendoredDir. Idempotent; a plan
55
+ * with no changes is a no-op. Creates vendoredDir (and any needed subdirectories) as needed.
56
+ */
57
+ export function applySkillSync(plan, packagedDir, vendoredDir) {
58
+ for (const change of plan.changes) {
59
+ const dest = join(vendoredDir, change.relPath);
60
+ if (change.kind === "removed") {
61
+ rmSync(dest);
62
+ }
63
+ else {
64
+ mkdirSync(dirname(dest), { recursive: true });
65
+ copyFileSync(join(packagedDir, change.relPath), dest);
66
+ }
67
+ }
68
+ }
69
+ //# sourceMappingURL=skillSync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skillSync.js","sourceRoot":"","sources":["../../src/cli/skillSync.ts"],"names":[],"mappings":"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"}