@milehimikey/em 1.6.1 → 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 (92) 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 +139 -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/templates/conformance-report.md +15 -9
  7. package/.claude/skills/event-modeling/templates/model-readme.md +3 -3
  8. package/.claude/skills/event-modeling/templates/slice.md +70 -4
  9. package/.claude/skills/event-modeling/templates/state.md +6 -5
  10. package/README.md +1 -0
  11. package/dist/catalog/build.d.ts +5 -0
  12. package/dist/catalog/build.js +1 -3
  13. package/dist/catalog/build.js.map +1 -1
  14. package/dist/catalog/docJoin.d.ts +39 -0
  15. package/dist/catalog/docJoin.js +86 -0
  16. package/dist/catalog/docJoin.js.map +1 -0
  17. package/dist/catalog/driftSignal.d.ts +16 -0
  18. package/dist/catalog/driftSignal.js +24 -0
  19. package/dist/catalog/driftSignal.js.map +1 -0
  20. package/dist/catalog/frontmatterCoherenceValidate.d.ts +8 -0
  21. package/dist/catalog/frontmatterCoherenceValidate.js +52 -0
  22. package/dist/catalog/frontmatterCoherenceValidate.js.map +1 -0
  23. package/dist/catalog/lineageValidate.d.ts +9 -0
  24. package/dist/catalog/lineageValidate.js +161 -0
  25. package/dist/catalog/lineageValidate.js.map +1 -0
  26. package/dist/catalog/readSliceDoc.d.ts +2 -0
  27. package/dist/catalog/readSliceDoc.js +21 -0
  28. package/dist/catalog/readSliceDoc.js.map +1 -0
  29. package/dist/catalog/sliceDoc.d.ts +94 -3
  30. package/dist/catalog/sliceDoc.js +157 -7
  31. package/dist/catalog/sliceDoc.js.map +1 -1
  32. package/dist/catalog/sliceReadyValidate.d.ts +11 -0
  33. package/dist/catalog/sliceReadyValidate.js +85 -0
  34. package/dist/catalog/sliceReadyValidate.js.map +1 -0
  35. package/dist/cli/diff-inputs.d.ts +20 -0
  36. package/dist/cli/diff-inputs.js +75 -18
  37. package/dist/cli/diff-inputs.js.map +1 -1
  38. package/dist/cli/ledgerCheck.d.ts +38 -0
  39. package/dist/cli/ledgerCheck.js +116 -0
  40. package/dist/cli/ledgerCheck.js.map +1 -0
  41. package/dist/cli/skillCheck.d.ts +18 -0
  42. package/dist/cli/skillCheck.js +63 -0
  43. package/dist/cli/skillCheck.js.map +1 -0
  44. package/dist/cli/skillSync.d.ts +22 -0
  45. package/dist/cli/skillSync.js +69 -0
  46. package/dist/cli/skillSync.js.map +1 -0
  47. package/dist/cli/skillVersionCheck.d.ts +32 -0
  48. package/dist/cli/skillVersionCheck.js +145 -0
  49. package/dist/cli/skillVersionCheck.js.map +1 -0
  50. package/dist/cli.d.ts +3 -1
  51. package/dist/cli.js +199 -32
  52. package/dist/cli.js.map +1 -1
  53. package/dist/emit/diffJson.d.ts +1 -1
  54. package/dist/emit/diffJson.js +18 -14
  55. package/dist/emit/diffJson.js.map +1 -1
  56. package/dist/emit/json.d.ts +10 -25
  57. package/dist/emit/json.js +70 -103
  58. package/dist/emit/json.js.map +1 -1
  59. package/dist/emit/ledgerJson.d.ts +5 -0
  60. package/dist/emit/ledgerJson.js +25 -0
  61. package/dist/emit/ledgerJson.js.map +1 -0
  62. package/dist/emit/skillCheckJson.d.ts +5 -0
  63. package/dist/emit/skillCheckJson.js +32 -0
  64. package/dist/emit/skillCheckJson.js.map +1 -0
  65. package/dist/model/diff.d.ts +44 -1
  66. package/dist/model/diff.js +51 -9
  67. package/dist/model/diff.js.map +1 -1
  68. package/dist/model/model.d.ts +1 -1
  69. package/dist/model/refs.d.ts +23 -0
  70. package/dist/model/refs.js +75 -0
  71. package/dist/model/refs.js.map +1 -0
  72. package/dist/model/rules.d.ts +252 -0
  73. package/dist/model/rules.js +253 -0
  74. package/dist/model/rules.js.map +1 -0
  75. package/dist/model/validate.d.ts +23 -1
  76. package/dist/model/validate.js +168 -47
  77. package/dist/model/validate.js.map +1 -1
  78. package/dist/parser/ast.d.ts +3 -2
  79. package/dist/parser/parser.js +23 -11
  80. package/dist/parser/parser.js.map +1 -1
  81. package/dist/pipeline.d.ts +9 -0
  82. package/dist/pipeline.js +4 -2
  83. package/dist/pipeline.js.map +1 -1
  84. package/dist/render/sliceStatus.js +1 -1
  85. package/dist/render/sliceStatus.js.map +1 -1
  86. package/dist/util/isMainModule.d.ts +14 -0
  87. package/dist/util/isMainModule.js +33 -0
  88. package/dist/util/isMainModule.js.map +1 -0
  89. package/dist/util/walkDir.d.ts +4 -0
  90. package/dist/util/walkDir.js +28 -0
  91. package/dist/util/walkDir.js.map +1 -0
  92. package/package.json +5 -2
@@ -0,0 +1,161 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // `em validate`'s lineage-ref resolution (MIL-84): checks what the *current tree* can prove
3
+ // wrong about a slice doc's `split-from`/`merged-from`/`superseded-by` frontmatter, and stays
4
+ // silent about what it can't. Ratified ruling (Linear MIL-84, 2026-08-13):
5
+ //
6
+ // Backward lineage refs (`split-from`, `merged-from`) are claims about history; for
7
+ // rename/merge/remove the predecessor is *supposed* to be absent from the current tree —
8
+ // `merged-from: cart@v3` on a doc whose predecessor was legitimately removed references an
9
+ // absent key forever, that's steady state, not a defect. So a backward ref naming a key
10
+ // absent from the current model gets NO diagnostic, not even a warning.
11
+ //
12
+ // What *is* checked (all errors, all current-tree-checkable): malformed ref grammar;
13
+ // self-reference/cycles; a dangling forward ref (`superseded-by` naming an absent key —
14
+ // forward refs point at the *present*, so absence is a real broken reference); impossible
15
+ // version arithmetic when the referenced key resolves in-tree (a ref to a version number
16
+ // higher than the target doc's own current `version:` — a reference to a future that hasn't
17
+ // happened).
18
+ //
19
+ // Deep historical verification (did slice@vN actually, once, exist?) is deliberately out of
20
+ // scope here — a same-commit authoring convention substitutes for it (see
21
+ // docs/slice-doc-schema.md's lineage section): the lineage ref is written in the same
22
+ // ratification commit that performs the operation it records, so the PR diff already contains
23
+ // both sides of the claim. `em ledger` (MIL-89, src/cli/ledgerCheck.ts) is that CI-recipe-tier,
24
+ // two-revision shape for a related but different invariant — version:/content agreement, not
25
+ // lineage-ref resolution — kept opt-in and separate for the same reason: it needs git history,
26
+ // and this command stays a fast function of the current tree.
27
+ import { pushDiag } from "../model/rules.js";
28
+ import { readSliceDoc } from "./readSliceDoc.js";
29
+ function edgesOf(ownerKey, ownerLine, doc) {
30
+ const edges = [];
31
+ if (doc.splitFrom)
32
+ edges.push({ ownerKey, ownerLine, field: "split-from", ref: doc.splitFrom });
33
+ for (const ref of doc.mergedFrom)
34
+ edges.push({ ownerKey, ownerLine, field: "merged-from", ref });
35
+ for (const ref of doc.supersededBy)
36
+ edges.push({ ownerKey, ownerLine, field: "superseded-by", ref });
37
+ return edges;
38
+ }
39
+ /**
40
+ * Resolve every slice's lineage refs against the current tree and report what the current tree
41
+ * can prove wrong. `baseDir` is the `.em` file's directory (doc paths, like every other
42
+ * doc/note path in em, are relative to it).
43
+ */
44
+ export function validateLineage(model, refs, baseDir) {
45
+ const diags = [];
46
+ const inModel = new Set(refs.sliceKeys);
47
+ // Cache doc reads: a slice can be visited both as an edge owner and as another edge's
48
+ // resolved target, and there's no reason to read the same file twice.
49
+ const docCache = new Map();
50
+ const docFor = (sliceKey) => {
51
+ if (!docCache.has(sliceKey))
52
+ docCache.set(sliceKey, readSliceDoc(baseDir, sliceKey));
53
+ return docCache.get(sliceKey);
54
+ };
55
+ const edges = [];
56
+ model.slices.forEach((slice, i) => {
57
+ const sliceKey = refs.sliceKeys[i];
58
+ const doc = docFor(sliceKey);
59
+ if (doc)
60
+ edges.push(...edgesOf(sliceKey, slice.line, doc));
61
+ });
62
+ // Malformed grammar, self-reference, and dangling-forward are each a single edge-local check.
63
+ // Version arithmetic and cycles need every edge whose target resolves in-model, gathered
64
+ // first, so they're handled in a second pass below.
65
+ const resolvableEdges = [];
66
+ for (const edge of edges) {
67
+ if (edge.ref.sliceKey === null) {
68
+ pushDiag(diags, "lineage-ref-malformed", {
69
+ message: `slice "${edge.ownerKey}"'s ${edge.field} value "${edge.ref.raw}" doesn't match the <slice-key>@v<N> grammar`,
70
+ line: edge.ownerLine,
71
+ refs: [edge.ownerKey],
72
+ });
73
+ continue;
74
+ }
75
+ if (edge.ref.sliceKey === edge.ownerKey) {
76
+ pushDiag(diags, "lineage-ref-cycle", {
77
+ message: `slice "${edge.ownerKey}"'s ${edge.field} names itself (${edge.ref.raw})`,
78
+ line: edge.ownerLine,
79
+ refs: [edge.ownerKey],
80
+ });
81
+ continue;
82
+ }
83
+ if (edge.field === "superseded-by" && !inModel.has(edge.ref.sliceKey)) {
84
+ pushDiag(diags, "lineage-forward-dangling", {
85
+ message: `slice "${edge.ownerKey}"'s superseded-by names "${edge.ref.sliceKey}", which isn't a slice in the current model`,
86
+ line: edge.ownerLine,
87
+ refs: [edge.ownerKey],
88
+ });
89
+ continue;
90
+ }
91
+ if (inModel.has(edge.ref.sliceKey))
92
+ resolvableEdges.push(edge);
93
+ // else: a backward ref (split-from/merged-from) naming a key absent from the current
94
+ // model — the expected steady state for a real split/merge/remove. No diagnostic.
95
+ }
96
+ // Version arithmetic: only checkable when the target resolves in-model AND its own doc has a
97
+ // readable numeric version. Silently skip otherwise — can't check what isn't there.
98
+ for (const edge of resolvableEdges) {
99
+ const targetDoc = docFor(edge.ref.sliceKey);
100
+ if (!targetDoc || targetDoc.version === null)
101
+ continue;
102
+ if (edge.ref.version > targetDoc.version) {
103
+ pushDiag(diags, "lineage-version-impossible", {
104
+ message: `slice "${edge.ownerKey}"'s ${edge.field} names "${edge.ref.sliceKey}"@v${edge.ref.version}, but "${edge.ref.sliceKey}" is only at v${targetDoc.version}`,
105
+ line: edge.ownerLine,
106
+ refs: [edge.ownerKey, edge.ref.sliceKey],
107
+ });
108
+ }
109
+ }
110
+ // Multi-node cycles: adjacency over edges whose target resolves in-model (self-loops already
111
+ // reported above, and excluded here so they aren't double-counted as a 1-node "cycle"). 3-color
112
+ // DFS, same shape as model/validate.ts's findTypeCycles(), adapted to string slice keys.
113
+ const adjacency = new Map();
114
+ for (const edge of resolvableEdges) {
115
+ if (edge.ref.sliceKey === edge.ownerKey)
116
+ continue;
117
+ const list = adjacency.get(edge.ownerKey);
118
+ if (list)
119
+ list.push(edge.ref.sliceKey);
120
+ else
121
+ adjacency.set(edge.ownerKey, [edge.ref.sliceKey]);
122
+ }
123
+ const color = new Map(); // 0 = white, 1 = gray (on stack), 2 = black (done)
124
+ const stack = [];
125
+ const reportedCycles = new Set();
126
+ const lineOf = new Map();
127
+ model.slices.forEach((slice, i) => lineOf.set(refs.sliceKeys[i], slice.line));
128
+ function visit(key) {
129
+ color.set(key, 1);
130
+ stack.push(key);
131
+ for (const dep of adjacency.get(key) ?? []) {
132
+ const c = color.get(dep) ?? 0;
133
+ if (c === 0) {
134
+ visit(dep);
135
+ }
136
+ else if (c === 1) {
137
+ const idx = stack.indexOf(dep);
138
+ const cyclePath = [...stack.slice(idx), dep];
139
+ const dedupeKey = [...new Set(cyclePath)].sort().join(",");
140
+ if (!reportedCycles.has(dedupeKey)) {
141
+ reportedCycles.add(dedupeKey);
142
+ const cycleRefs = [...new Set(cyclePath)];
143
+ pushDiag(diags, "lineage-ref-cycle", {
144
+ message: `slice "${dep}" is part of a lineage cycle: ${cyclePath.join(" -> ")}`,
145
+ line: lineOf.get(dep),
146
+ refs: cycleRefs,
147
+ });
148
+ }
149
+ }
150
+ // c === 2 (black): already fully explored via another path — not a new cycle.
151
+ }
152
+ stack.pop();
153
+ color.set(key, 2);
154
+ }
155
+ for (const key of adjacency.keys()) {
156
+ if ((color.get(key) ?? 0) === 0)
157
+ visit(key);
158
+ }
159
+ return diags;
160
+ }
161
+ //# sourceMappingURL=lineageValidate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lineageValidate.js","sourceRoot":"","sources":["../../src/catalog/lineageValidate.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,4FAA4F;AAC5F,8FAA8F;AAC9F,2EAA2E;AAC3E,EAAE;AACF,sFAAsF;AACtF,2FAA2F;AAC3F,6FAA6F;AAC7F,0FAA0F;AAC1F,0EAA0E;AAC1E,EAAE;AACF,uFAAuF;AACvF,0FAA0F;AAC1F,4FAA4F;AAC5F,2FAA2F;AAC3F,8FAA8F;AAC9F,eAAe;AACf,EAAE;AACF,4FAA4F;AAC5F,0EAA0E;AAC1E,sFAAsF;AACtF,8FAA8F;AAC9F,gGAAgG;AAChG,6FAA6F;AAC7F,+FAA+F;AAC/F,8DAA8D;AAK9D,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAWjD,SAAS,OAAO,CAAC,QAAgB,EAAE,SAAiB,EAAE,GAAa;IACjE,MAAM,KAAK,GAAkB,EAAE,CAAC;IAChC,IAAI,GAAG,CAAC,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,EAAE,GAAG,EAAE,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC;IAChG,KAAK,MAAM,GAAG,IAAI,GAAG,CAAC,UAAU;QAAE,KAAK,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,EAAE,CAAC,CAAC;IACjG,KAAK,MAAM,GAAG,IAAI,GAAG,CAAC,YAAY;QAAE,KAAK,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,eAAe,EAAE,GAAG,EAAE,CAAC,CAAC;IACrG,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,KAAsB,EAAE,IAAgB,EAAE,OAAe;IACvF,MAAM,KAAK,GAAiB,EAAE,CAAC;IAC/B,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAExC,sFAAsF;IACtF,sEAAsE;IACtE,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA2B,CAAC;IACpD,MAAM,MAAM,GAAG,CAAC,QAAgB,EAAmB,EAAE;QACnD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC;YAAE,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,YAAY,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;QACrF,OAAO,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAE,CAAC;IACjC,CAAC,CAAC;IAEF,MAAM,KAAK,GAAkB,EAAE,CAAC;IAChC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE;QAChC,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;QACnC,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC7B,IAAI,GAAG;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;IAEH,8FAA8F;IAC9F,yFAAyF;IACzF,oDAAoD;IACpD,MAAM,eAAe,GAAkB,EAAE,CAAC;IAC1C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,GAAG,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;YAC/B,QAAQ,CAAC,KAAK,EAAE,uBAAuB,EAAE;gBACvC,OAAO,EAAE,UAAU,IAAI,CAAC,QAAQ,OAAO,IAAI,CAAC,KAAK,WAAW,IAAI,CAAC,GAAG,CAAC,GAAG,8CAA8C;gBACtH,IAAI,EAAE,IAAI,CAAC,SAAS;gBACpB,IAAI,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;aACtB,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QACD,IAAI,IAAI,CAAC,GAAG,CAAC,QAAQ,KAAK,IAAI,CAAC,QAAQ,EAAE,CAAC;YACxC,QAAQ,CAAC,KAAK,EAAE,mBAAmB,EAAE;gBACnC,OAAO,EAAE,UAAU,IAAI,CAAC,QAAQ,OAAO,IAAI,CAAC,KAAK,kBAAkB,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG;gBAClF,IAAI,EAAE,IAAI,CAAC,SAAS;gBACpB,IAAI,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;aACtB,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QACD,IAAI,IAAI,CAAC,KAAK,KAAK,eAAe,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YACtE,QAAQ,CAAC,KAAK,EAAE,0BAA0B,EAAE;gBAC1C,OAAO,EAAE,UAAU,IAAI,CAAC,QAAQ,4BAA4B,IAAI,CAAC,GAAG,CAAC,QAAQ,6CAA6C;gBAC1H,IAAI,EAAE,IAAI,CAAC,SAAS;gBACpB,IAAI,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;aACtB,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QACD,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC;YAAE,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/D,qFAAqF;QACrF,kFAAkF;IACpF,CAAC;IAED,6FAA6F;IAC7F,oFAAoF;IACpF,KAAK,MAAM,IAAI,IAAI,eAAe,EAAE,CAAC;QACnC,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,QAAS,CAAC,CAAC;QAC7C,IAAI,CAAC,SAAS,IAAI,SAAS,CAAC,OAAO,KAAK,IAAI;YAAE,SAAS;QACvD,IAAI,IAAI,CAAC,GAAG,CAAC,OAAQ,GAAG,SAAS,CAAC,OAAO,EAAE,CAAC;YAC1C,QAAQ,CAAC,KAAK,EAAE,4BAA4B,EAAE;gBAC5C,OAAO,EAAE,UAAU,IAAI,CAAC,QAAQ,OAAO,IAAI,CAAC,KAAK,WAAW,IAAI,CAAC,GAAG,CAAC,QAAQ,MAAM,IAAI,CAAC,GAAG,CAAC,OAAO,UAAU,IAAI,CAAC,GAAG,CAAC,QAAQ,iBAAiB,SAAS,CAAC,OAAO,EAAE;gBAClK,IAAI,EAAE,IAAI,CAAC,SAAS;gBACpB,IAAI,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,QAAS,CAAC;aAC1C,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,6FAA6F;IAC7F,gGAAgG;IAChG,yFAAyF;IACzF,MAAM,SAAS,GAAG,IAAI,GAAG,EAAoB,CAAC;IAC9C,KAAK,MAAM,IAAI,IAAI,eAAe,EAAE,CAAC;QACnC,IAAI,IAAI,CAAC,GAAG,CAAC,QAAQ,KAAK,IAAI,CAAC,QAAQ;YAAE,SAAS;QAClD,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC1C,IAAI,IAAI;YAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAS,CAAC,CAAC;;YACnC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,QAAS,CAAC,CAAC,CAAC;IAC1D,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAqB,CAAC,CAAC,mDAAmD;IAC/F,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,cAAc,GAAG,IAAI,GAAG,EAAU,CAAC;IACzC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IACzC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IAE9E,SAAS,KAAK,CAAC,GAAW;QACxB,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QAClB,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAChB,KAAK,MAAM,GAAG,IAAI,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC;YAC3C,MAAM,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC9B,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;gBACZ,KAAK,CAAC,GAAG,CAAC,CAAC;YACb,CAAC;iBAAM,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;gBACnB,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;gBAC/B,MAAM,SAAS,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;gBAC7C,MAAM,SAAS,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBAC3D,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;oBACnC,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;oBAC9B,MAAM,SAAS,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC;oBAC1C,QAAQ,CAAC,KAAK,EAAE,mBAAmB,EAAE;wBACnC,OAAO,EAAE,UAAU,GAAG,iCAAiC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE;wBAC/E,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC;wBACrB,IAAI,EAAE,SAAS;qBAChB,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;YACD,8EAA8E;QAChF,CAAC;QACD,KAAK,CAAC,GAAG,EAAE,CAAC;QACZ,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IACpB,CAAC;IACD,KAAK,MAAM,GAAG,IAAI,SAAS,CAAC,IAAI,EAAE,EAAE,CAAC;QACnC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC;YAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAC9C,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { SliceDoc } from "./sliceDoc.js";
2
+ export declare function readSliceDoc(baseDir: string, sliceKey: string): SliceDoc | null;
@@ -0,0 +1,21 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Lowest-level slice-doc read: given a base dir and a slice's export key,
3
+ // read+parse `slices/<sliceKey>.md` if it exists. Never throws — a missing
4
+ // file is routine (not every slice has reached specify yet), so this is a
5
+ // plain `null`, not an exception or a diagnostic. Shared by `em export`'s
6
+ // doc join (catalog/docJoin.ts, which additionally gates on a `note` binding
7
+ // and produces export-shaped diagnostics for a *missing-but-bound* doc — a
8
+ // concern this module doesn't have), `em diff`'s lineage annotation
9
+ // (model/diff.ts, via cli.ts), and `em validate`'s lineage-ref resolution
10
+ // (catalog/lineageValidate.ts) — three callers with different reactions to
11
+ // "no doc here," so the read itself stays opinion-free.
12
+ import { existsSync, readFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { parseSliceDoc } from "./sliceDoc.js";
15
+ export function readSliceDoc(baseDir, sliceKey) {
16
+ const path = join(baseDir, "slices", `${sliceKey}.md`);
17
+ if (!existsSync(path))
18
+ return null;
19
+ return parseSliceDoc(readFileSync(path, "utf8"));
20
+ }
21
+ //# sourceMappingURL=readSliceDoc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"readSliceDoc.js","sourceRoot":"","sources":["../../src/catalog/readSliceDoc.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,0EAA0E;AAC1E,2EAA2E;AAC3E,0EAA0E;AAC1E,0EAA0E;AAC1E,6EAA6E;AAC7E,2EAA2E;AAC3E,oEAAoE;AACpE,0EAA0E;AAC1E,2EAA2E;AAC3E,wDAAwD;AAExD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,aAAa,EAAY,MAAM,eAAe,CAAC;AAExD,MAAM,UAAU,YAAY,CAAC,OAAe,EAAE,QAAgB;IAC5D,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,GAAG,QAAQ,KAAK,CAAC,CAAC;IACvD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,OAAO,aAAa,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;AACnD,CAAC"}
@@ -1,10 +1,101 @@
1
+ /** A parsed `<slice-key>@v<N>` lineage reference. */
2
+ export interface SliceRef {
3
+ /** The exact frontmatter value this ref was read from, e.g. "checkout@v4".
4
+ * Always present, even when the grammar didn't match — nothing is silently
5
+ * dropped the way an unrecognized top-level key is. */
6
+ raw: string;
7
+ /** The referenced slice's doc key (its kebab-case filename stem), lowercased,
8
+ * or null if `raw` doesn't match the `<slice-key>@v<N>` grammar. Whether
9
+ * this actually names a real slice/version is NOT checked here — that's
10
+ * MIL-84 (lineage-aware `em diff`/`em validate`). */
11
+ sliceKey: string | null;
12
+ /** The referenced version number, or null if `raw` didn't match the grammar. */
13
+ version: number | null;
14
+ }
1
15
  export interface SliceDoc {
2
16
  /** The doc's raw markdown, as read from disk. */
3
17
  raw: string;
4
- /** Lowercased value of the doc's `- **Status:** ...` line, or null if no such line is found
5
- * (a freeform doc that doesn't follow the slice.md template). */
18
+ /** The doc's body text — everything after the frontmatter fence (or the whole doc, if no
19
+ * fence) — the same text `html`/`openQuestionsTotal` are already derived from. Exposed so a
20
+ * two-revision content-agreement check (MIL-89, `em ledger`) can diff body text across git
21
+ * revisions without frontmatter noise leaking in. */
22
+ body: string;
23
+ /** Lowercased status value from frontmatter `status:` (canonical) or a legacy
24
+ * `- **Status:** ...` bullet line, or null if neither is found (a freeform
25
+ * doc that doesn't follow the slice.md template). */
6
26
  status: string | null;
7
- /** The whole doc rendered to HTML. */
27
+ /** This slice's own ratified-content version, from frontmatter `version:`
28
+ * (e.g. `version: 1`) — a cache of git truth (docset module 06), not
29
+ * derived or validated against git history. Null when absent or
30
+ * non-numeric. Distinct from the frontmatter dialect's own `schemaVersion`,
31
+ * which this parser still doesn't expose. */
32
+ version: number | null;
33
+ /** Lineage: this doc split off exactly one prior slice, from frontmatter
34
+ * `split-from:`. Null when this doc wasn't produced by a split. */
35
+ splitFrom: SliceRef | null;
36
+ /** Lineage: this doc was produced by merging one or more prior slices, from
37
+ * frontmatter `merged-from:` (comma-separated). Empty array when absent —
38
+ * the common case. */
39
+ mergedFrom: SliceRef[];
40
+ /** Lineage: this doc has been superseded by one or more successor slices —
41
+ * a rename, or the retired side of a split/merge — from frontmatter
42
+ * `superseded-by:` (comma-separated). Empty array when absent — the
43
+ * common case; most slices are never superseded. */
44
+ supersededBy: SliceRef[];
45
+ /** Frontmatter `implementedIn:` — free text, typically a PR/commit link — or null
46
+ * when absent. No legacy bullet-line form, same as `version`/lineage. */
47
+ implementedIn: string | null;
48
+ /** True when a well-formed leading `---`/`---` frontmatter fence was found and
49
+ * closed — independent of which keys it contained. False for a legacy
50
+ * status-bullet-only doc, a doc with no frontmatter at all, or an
51
+ * unterminated fence (splitFrontmatter() already treats that as "absent"). */
52
+ frontmatterPresent: boolean;
53
+ /** Which of REQUIRED_FRONTMATTER_KEYS were absent (or present-but-empty, which
54
+ * the parser already treats as absent) from the frontmatter. Empty whenever
55
+ * frontmatterPresent is false too — check frontmatterPresent separately to
56
+ * distinguish "no frontmatter at all" from "every required key present".
57
+ * Presence-only: doesn't validate `pattern`'s/`status`'s enum values, matching
58
+ * this parser's existing lenient, informational treatment of those keys. */
59
+ missingRequiredFields: string[];
60
+ /** The whole doc rendered to HTML, with any leading frontmatter block stripped
61
+ * first (otherwise its `---` fences render as stray <hr>s and the raw
62
+ * `key: value` lines leak into the body). */
8
63
  html: string;
64
+ /** Count of GFM task-list items (`- [ ]` / `- [x]`) found under the `## Open Questions`
65
+ * heading (case-insensitive), up to the next `#`/`##` heading or EOF. Both 0 when the doc
66
+ * has no such heading — nothing for MIL-87's readiness check to block on. See
67
+ * `openQuestionsUnchecked` for how many of these are still unresolved. */
68
+ openQuestionsTotal: number;
69
+ /** Of `openQuestionsTotal`, how many are still `- [ ]` (unchecked). 0 when every Open
70
+ * Question has been checked off, or when there's no Open Questions section at all. */
71
+ openQuestionsUnchecked: number;
9
72
  }
73
+ /** Frontmatter keys docs/slice-doc-schema.md's required-vs-optional table marks
74
+ * required at every `status` — the mechanical basis for `frontmatter-invalid`
75
+ * (MIL-91). `implementedIn` is deliberately excluded: its requirement depends on
76
+ * whether the slice has *ever* reached `implemented`, which is git history this
77
+ * parser has no access to and doesn't attempt to check. */
78
+ export declare const REQUIRED_FRONTMATTER_KEYS: readonly ["schemaversion", "pattern", "swimlane", "status", "version"];
79
+ /** True when a doc's frontmatter is well-formed enough to trust its canonical fields: a closed
80
+ * fence was found AND every REQUIRED_FRONTMATTER_KEYS entry is present. The single gate `em
81
+ * export`'s doc join (MIL-91, `frontmatter-invalid`) and `em validate`'s frontmatter-coherence
82
+ * check (MIL-85) both call, instead of each re-deriving `!frontmatterPresent ||
83
+ * missingRequiredFields.length > 0` and risking the two silently drifting apart on what counts
84
+ * as "usable" as the required-fields list evolves. A doc that fails this — no fence at all
85
+ * (e.g. MIL-86's legacy body-label dialect), or missing any required key — has nothing reliable
86
+ * enough to classify; treat it as unreadable rather than guess. */
87
+ export declare function hasUsableFrontmatter(doc: Pick<SliceDoc, "frontmatterPresent" | "missingRequiredFields">): boolean;
88
+ /**
89
+ * Parses a single `<slice-key>@v<N>` lineage reference (`split-from`, and each
90
+ * comma-separated item of `merged-from`/`superseded-by`). Never throws: a
91
+ * value that doesn't match the grammar still comes back with the original
92
+ * text in `raw` and null `sliceKey`/`version`, so a malformed value stays
93
+ * visible — to a human reading the doc, or to MIL-84's validation — instead
94
+ * of being silently dropped like an unrecognized `status`. Exported so MIL-84
95
+ * (lineage diff/validate) and MIL-91 (export join) share this grammar instead
96
+ * of re-deriving it. `sliceKey` matches `kebabSlug()`'s output
97
+ * (src/util/slug.ts) — lowercase alphanumerics/hyphens, no assumption it
98
+ * starts with a letter.
99
+ */
100
+ export declare function parseSliceRef(raw: string): SliceRef;
10
101
  export declare function parseSliceDoc(markdown: string): SliceDoc;
@@ -3,18 +3,168 @@
3
3
  // .claude/skills/event-modeling/templates/slice.md) for `em catalog`. `em`
4
4
  // has never parsed this markdown structurally before — it only checked the
5
5
  // file exists (cli.ts's warnMissingNotes). This is deliberately shallow: pull
6
- // the one bold-label line the index page needs (`- **Status:** ...`) with a
7
- // regex rather than a full frontmatter parser, and render the whole doc body
8
- // as HTML for the per-slice detail page. Pure — no fs access; the caller
9
- // reads the file and hands us its text.
6
+ // the status/version/lineage values the index page and downstream consumers
7
+ // need, and render the doc body as HTML for the per-slice detail page. Pure —
8
+ // no fs access; the caller reads the file and hands us its text.
9
+ //
10
+ // Two status dialects: a leading YAML frontmatter block (`status: ...`) is
11
+ // canonical (MIL-86); a `- **Status:** ...` bullet line is legacy/accepted
12
+ // input for docs written before the frontmatter dialect existed. Frontmatter
13
+ // wins when both are somehow present. No `yaml` dependency — the 1.1.1 revert
14
+ // (6de0a05) dropped it on purpose; frontmatter is a plain top-level-scalar
15
+ // line-scan, same spirit as the bullet-line regex it now sits alongside.
16
+ //
17
+ // `version` and the three lineage keys (`split-from`/`merged-from`/
18
+ // `superseded-by`) are canonical, frontmatter-only fields (MIL-90) — no
19
+ // legacy bullet-line form exists for them. Full schema: docs/slice-doc-schema.md.
20
+ // `#`-prefixed lines (the template's commented-out optional lineage guidance)
21
+ // are inert: they don't match the key regex below, so they're skipped exactly
22
+ // like any other non-`key: value` line — safe to leave in an unfinished doc.
23
+ //
24
+ // `implementedIn`, `frontmatterPresent`, and `missingRequiredFields` (MIL-91)
25
+ // exist so `em export`'s doc join (src/catalog/docJoin.ts) can mechanically
26
+ // classify a doc as `frontmatter-invalid` — no frontmatter fence at all, or
27
+ // missing one of the keys docs/slice-doc-schema.md says are required at every
28
+ // status — without export re-deriving frontmatter-shape rules of its own.
10
29
  import { marked } from "marked";
30
+ /** Frontmatter keys docs/slice-doc-schema.md's required-vs-optional table marks
31
+ * required at every `status` — the mechanical basis for `frontmatter-invalid`
32
+ * (MIL-91). `implementedIn` is deliberately excluded: its requirement depends on
33
+ * whether the slice has *ever* reached `implemented`, which is git history this
34
+ * parser has no access to and doesn't attempt to check. */
35
+ export const REQUIRED_FRONTMATTER_KEYS = ["schemaversion", "pattern", "swimlane", "status", "version"];
36
+ /** True when a doc's frontmatter is well-formed enough to trust its canonical fields: a closed
37
+ * fence was found AND every REQUIRED_FRONTMATTER_KEYS entry is present. The single gate `em
38
+ * export`'s doc join (MIL-91, `frontmatter-invalid`) and `em validate`'s frontmatter-coherence
39
+ * check (MIL-85) both call, instead of each re-deriving `!frontmatterPresent ||
40
+ * missingRequiredFields.length > 0` and risking the two silently drifting apart on what counts
41
+ * as "usable" as the required-fields list evolves. A doc that fails this — no fence at all
42
+ * (e.g. MIL-86's legacy body-label dialect), or missing any required key — has nothing reliable
43
+ * enough to classify; treat it as unreadable rather than guess. */
44
+ export function hasUsableFrontmatter(doc) {
45
+ return doc.frontmatterPresent && doc.missingRequiredFields.length === 0;
46
+ }
11
47
  const STATUS_LINE = /^-\s*\*\*Status:\*\*\s*(.+?)\s*$/im;
48
+ const SLICE_REF = /^([a-z0-9]+(?:-[a-z0-9]+)*)@v(\d+)$/i;
49
+ const OPEN_QUESTIONS_HEADING = /^##\s+Open Questions\s*$/i;
50
+ const TASK_ITEM = /^[ \t]*[-*]\s*\[([ xX])\]/;
51
+ /**
52
+ * Counts GFM task-list items under a `## Open Questions` heading (MIL-87), scanning `body`
53
+ * (post-frontmatter, pre-HTML-render text — the same text `html` below is built from) rather
54
+ * than the rendered HTML, which would be needlessly fragile to scrape. The section ends at the
55
+ * next `#`/`##` heading or EOF; a doc with no such heading counts as zero/zero, not an error —
56
+ * most slice docs won't have open questions left by the time they're read.
57
+ */
58
+ function countOpenQuestions(body) {
59
+ const lines = body.split(/\r?\n/);
60
+ const start = lines.findIndex((l) => OPEN_QUESTIONS_HEADING.test(l));
61
+ if (start === -1)
62
+ return { openQuestionsTotal: 0, openQuestionsUnchecked: 0 };
63
+ let openQuestionsTotal = 0;
64
+ let openQuestionsUnchecked = 0;
65
+ for (let i = start + 1; i < lines.length; i++) {
66
+ if (/^#{1,2}\s/.test(lines[i]))
67
+ break;
68
+ const m = lines[i].match(TASK_ITEM);
69
+ if (!m)
70
+ continue;
71
+ openQuestionsTotal++;
72
+ if (m[1] === " ")
73
+ openQuestionsUnchecked++;
74
+ }
75
+ return { openQuestionsTotal, openQuestionsUnchecked };
76
+ }
77
+ /**
78
+ * Parses a single `<slice-key>@v<N>` lineage reference (`split-from`, and each
79
+ * comma-separated item of `merged-from`/`superseded-by`). Never throws: a
80
+ * value that doesn't match the grammar still comes back with the original
81
+ * text in `raw` and null `sliceKey`/`version`, so a malformed value stays
82
+ * visible — to a human reading the doc, or to MIL-84's validation — instead
83
+ * of being silently dropped like an unrecognized `status`. Exported so MIL-84
84
+ * (lineage diff/validate) and MIL-91 (export join) share this grammar instead
85
+ * of re-deriving it. `sliceKey` matches `kebabSlug()`'s output
86
+ * (src/util/slug.ts) — lowercase alphanumerics/hyphens, no assumption it
87
+ * starts with a letter.
88
+ */
89
+ export function parseSliceRef(raw) {
90
+ const trimmed = raw.trim();
91
+ const m = trimmed.match(SLICE_REF);
92
+ return m
93
+ ? { raw: trimmed, sliceKey: m[1].toLowerCase(), version: Number(m[2]) }
94
+ : { raw: trimmed, sliceKey: null, version: null };
95
+ }
96
+ function parseSliceRefList(raw) {
97
+ if (!raw)
98
+ return [];
99
+ return raw
100
+ .split(",")
101
+ .map((s) => s.trim())
102
+ .filter(Boolean)
103
+ .map(parseSliceRef);
104
+ }
105
+ function parseVersion(raw) {
106
+ if (!raw)
107
+ return null;
108
+ const n = Number(raw);
109
+ return Number.isInteger(n) && n > 0 ? n : null;
110
+ }
111
+ /**
112
+ * Splits a leading YAML frontmatter block (`---` ... `---`) off the doc, if
113
+ * present. Deliberately minimal: top-level scalar `key: value` lines only —
114
+ * no lists, no nesting. Returns the scalar fields (lowercased keys) and the
115
+ * remaining body with the frontmatter fences removed. An unterminated `---`
116
+ * fence is treated as "no frontmatter" (the whole doc is the body). Like
117
+ * every other frontmatter convention (Jekyll, Hugo, ...), the fence must be
118
+ * the literal first thing in the file — the template's own guidance comment
119
+ * must be deleted before the frontmatter counts as present.
120
+ */
121
+ function splitFrontmatter(markdown) {
122
+ if (!/^---\s*\r?\n/.test(markdown))
123
+ return { fields: new Map(), body: markdown, frontmatterPresent: false };
124
+ const lines = markdown.split(/\r?\n/);
125
+ const fields = new Map();
126
+ let closeIndex = -1;
127
+ for (let i = 1; i < lines.length; i++) {
128
+ if (/^---\s*$/.test(lines[i])) {
129
+ closeIndex = i;
130
+ break;
131
+ }
132
+ const m = lines[i].match(/^([A-Za-z][\w-]*):\s*(.*)$/);
133
+ if (!m)
134
+ continue;
135
+ const value = m[2].trim().replace(/^["']|["']$/g, "");
136
+ if (value)
137
+ fields.set(m[1].toLowerCase(), value);
138
+ }
139
+ if (closeIndex === -1)
140
+ return { fields: new Map(), body: markdown, frontmatterPresent: false };
141
+ return { fields, body: lines.slice(closeIndex + 1).join("\n"), frontmatterPresent: true };
142
+ }
12
143
  export function parseSliceDoc(markdown) {
13
- const match = markdown.match(STATUS_LINE);
144
+ const { fields, body, frontmatterPresent } = splitFrontmatter(markdown);
145
+ const legacyMatch = body.match(STATUS_LINE);
146
+ const frontmatterStatus = fields.get("status");
147
+ const status = frontmatterStatus
148
+ ? frontmatterStatus.toLowerCase()
149
+ : legacyMatch
150
+ ? legacyMatch[1].trim().toLowerCase()
151
+ : null;
152
+ const splitFromRaw = fields.get("split-from");
153
+ const { openQuestionsTotal, openQuestionsUnchecked } = countOpenQuestions(body);
14
154
  return {
15
155
  raw: markdown,
16
- status: match ? match[1].trim().toLowerCase() : null,
17
- html: marked.parse(markdown, { async: false }),
156
+ body,
157
+ status,
158
+ version: parseVersion(fields.get("version")),
159
+ splitFrom: splitFromRaw ? parseSliceRef(splitFromRaw) : null,
160
+ mergedFrom: parseSliceRefList(fields.get("merged-from")),
161
+ supersededBy: parseSliceRefList(fields.get("superseded-by")),
162
+ implementedIn: fields.get("implementedin") ?? null,
163
+ frontmatterPresent,
164
+ missingRequiredFields: REQUIRED_FRONTMATTER_KEYS.filter((k) => !fields.has(k)),
165
+ html: marked.parse(body, { async: false }),
166
+ openQuestionsTotal,
167
+ openQuestionsUnchecked,
18
168
  };
19
169
  }
20
170
  //# sourceMappingURL=sliceDoc.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sliceDoc.js","sourceRoot":"","sources":["../../src/catalog/sliceDoc.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,yDAAyD;AACzD,2EAA2E;AAC3E,2EAA2E;AAC3E,8EAA8E;AAC9E,4EAA4E;AAC5E,6EAA6E;AAC7E,yEAAyE;AACzE,wCAAwC;AAExC,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAYhC,MAAM,WAAW,GAAG,oCAAoC,CAAC;AAEzD,MAAM,UAAU,aAAa,CAAC,QAAgB;IAC5C,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;IAC1C,OAAO;QACL,GAAG,EAAE,QAAQ;QACb,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI;QACpD,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAW;KACzD,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"sliceDoc.js","sourceRoot":"","sources":["../../src/catalog/sliceDoc.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import { NormalizedModel } from "../model/model.js";
2
+ import { RefsResult } from "../model/refs.js";
3
+ import { Diagnostic } from "../model/validate.js";
4
+ /**
5
+ * Readiness gate for one slice, named by its export key (`refs.sliceKeys`). `baseDir` is the
6
+ * `.em` file's directory, same convention every other doc/note path in em uses. Always warning-
7
+ * severity (fits validate's existing warning-producer shape) except for a bad `sliceKey` itself,
8
+ * which is an error — a CLI-argument mistake, not a model-quality finding. The `--slice-ready`
9
+ * flag in src/cli.ts is what decides whether these warnings gate the exit code.
10
+ */
11
+ export declare function validateSliceReady(model: NormalizedModel, refs: RefsResult, baseDir: string, sliceKey: string): Diagnostic[];
@@ -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":"AAAA,+BAA+B;AAC/B,4FAA4F;AAC5F,2FAA2F;AAC3F,0FAA0F;AAC1F,6FAA6F;AAC7F,qBAAqB;AACrB,EAAE;AACF,qFAAqF;AACrF,uFAAuF;AACvF,4FAA4F;AAC5F,8FAA8F;AAC9F,+FAA+F;AAC/F,4FAA4F;AAC5F,8DAA8D;AAC9D,EAAE;AACF,gGAAgG;AAChG,uFAAuF;AACvF,6FAA6F;AAC7F,gGAAgG;AAChG,kGAAkG;AAClG,2DAA2D;AAC3D,EAAE;AACF,8FAA8F;AAC9F,6FAA6F;AAC7F,8FAA8F;AAC9F,kFAAkF;AAKlF,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAChC,KAAsB,EACtB,IAAgB,EAChB,OAAe,EACf,QAAgB;IAEhB,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACpD,IAAI,UAAU,KAAK,CAAC,CAAC,EAAE,CAAC;QACtB,OAAO;YACL,QAAQ,CAAC,2BAA2B,EAAE;gBACpC,OAAO,EAAE,6BAA6B,QAAQ,iBAAiB;gBAC/D,IAAI,EAAE,CAAC,QAAQ,CAAC;aACjB,CAAC;SACH,CAAC;IACJ,CAAC;IACD,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IACvC,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,eAAe,EAAE,GAAG,mBAAmB,CAC/D,KAAK,EACL,QAAQ,EACR,OAAO,EACP,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAE,CAC9B,CAAC;IAEF,IAAI,GAAG,CAAC,MAAM,KAAK,cAAc,EAAE,CAAC;QAClC,OAAO;YACL,QAAQ,CAAC,0BAA0B,EAAE;gBACnC,OAAO,EAAE,UAAU,QAAQ,yCAAyC,QAAQ,+DAA+D;gBAC3I,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,IAAI,EAAE,CAAC,QAAQ,CAAC;aACjB,CAAC;SACH,CAAC;IACJ,CAAC;IACD,IAAI,GAAG,CAAC,MAAM,KAAK,sBAAsB,IAAI,GAAG,CAAC,MAAM,KAAK,qBAAqB,EAAE,CAAC;QAClF,iFAAiF;QACjF,uEAAuE;QACvE,OAAO,eAAe,CAAC;IACzB,CAAC;IAED,kDAAkD;IAClD,MAAM,KAAK,GAAiB,EAAE,CAAC;IAC/B,IAAI,GAAG,CAAC,MAAM,KAAK,oBAAoB,EAAE,CAAC;QACxC,QAAQ,CAAC,KAAK,EAAE,8BAA8B,EAAE;YAC9C,OAAO,EAAE,UAAU,QAAQ,gBAAgB,GAAG,CAAC,MAAM,IAAI,QAAQ,0BAA0B;YAC3F,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,IAAI,EAAE,CAAC,QAAQ,CAAC;SACjB,CAAC,CAAC;IACL,CAAC;IAED,8FAA8F;IAC9F,0FAA0F;IAC1F,2FAA2F;IAC3F,MAAM,MAAM,GAAG,YAAY,CAAC,OAAO,EAAE,QAAQ,CAAE,CAAC;IAChD,IAAI,MAAM,CAAC,sBAAsB,GAAG,CAAC,EAAE,CAAC;QACtC,QAAQ,CAAC,KAAK,EAAE,sCAAsC,EAAE;YACtD,OAAO,EAAE,UAAU,QAAQ,SAAS,MAAM,CAAC,sBAAsB,OAAO,MAAM,CAAC,kBAAkB,6BAA6B;YAC9H,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,IAAI,EAAE,CAAC,QAAQ,CAAC;SACjB,CAAC,CAAC;IACL,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -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[];