@milehimikey/em 1.6.1 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/.claude/skills/event-modeling/SKILL.md +94 -49
  2. package/.claude/skills/event-modeling/reference/conform.md +30 -8
  3. package/.claude/skills/event-modeling/reference/em-dsl.md +194 -73
  4. package/.claude/skills/event-modeling/reference/extract.md +14 -11
  5. package/.claude/skills/event-modeling/reference/implement.md +172 -0
  6. package/.claude/skills/event-modeling/reference/methodology.md +40 -33
  7. package/.claude/skills/event-modeling/templates/conformance-report.md +15 -9
  8. package/.claude/skills/event-modeling/templates/model-readme.md +7 -7
  9. package/.claude/skills/event-modeling/templates/slice.md +73 -7
  10. package/.claude/skills/event-modeling/templates/state.md +7 -6
  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/classify.d.ts +7 -8
  16. package/dist/catalog/classify.js +7 -8
  17. package/dist/catalog/classify.js.map +1 -1
  18. package/dist/catalog/docJoin.d.ts +39 -0
  19. package/dist/catalog/docJoin.js +86 -0
  20. package/dist/catalog/docJoin.js.map +1 -0
  21. package/dist/catalog/driftSignal.d.ts +16 -0
  22. package/dist/catalog/driftSignal.js +24 -0
  23. package/dist/catalog/driftSignal.js.map +1 -0
  24. package/dist/catalog/frontmatterCoherenceValidate.d.ts +8 -0
  25. package/dist/catalog/frontmatterCoherenceValidate.js +52 -0
  26. package/dist/catalog/frontmatterCoherenceValidate.js.map +1 -0
  27. package/dist/catalog/lineageValidate.d.ts +9 -0
  28. package/dist/catalog/lineageValidate.js +161 -0
  29. package/dist/catalog/lineageValidate.js.map +1 -0
  30. package/dist/catalog/readSliceDoc.d.ts +2 -0
  31. package/dist/catalog/readSliceDoc.js +21 -0
  32. package/dist/catalog/readSliceDoc.js.map +1 -0
  33. package/dist/catalog/sliceDoc.d.ts +94 -3
  34. package/dist/catalog/sliceDoc.js +157 -7
  35. package/dist/catalog/sliceDoc.js.map +1 -1
  36. package/dist/catalog/sliceReadyValidate.d.ts +11 -0
  37. package/dist/catalog/sliceReadyValidate.js +85 -0
  38. package/dist/catalog/sliceReadyValidate.js.map +1 -0
  39. package/dist/cli/diff-inputs.d.ts +20 -0
  40. package/dist/cli/diff-inputs.js +75 -18
  41. package/dist/cli/diff-inputs.js.map +1 -1
  42. package/dist/cli/ledgerCheck.d.ts +38 -0
  43. package/dist/cli/ledgerCheck.js +116 -0
  44. package/dist/cli/ledgerCheck.js.map +1 -0
  45. package/dist/cli/skillCheck.d.ts +18 -0
  46. package/dist/cli/skillCheck.js +63 -0
  47. package/dist/cli/skillCheck.js.map +1 -0
  48. package/dist/cli/skillSync.d.ts +22 -0
  49. package/dist/cli/skillSync.js +69 -0
  50. package/dist/cli/skillSync.js.map +1 -0
  51. package/dist/cli/skillVersionCheck.d.ts +32 -0
  52. package/dist/cli/skillVersionCheck.js +145 -0
  53. package/dist/cli/skillVersionCheck.js.map +1 -0
  54. package/dist/cli.d.ts +3 -1
  55. package/dist/cli.js +203 -32
  56. package/dist/cli.js.map +1 -1
  57. package/dist/emit/diffJson.d.ts +1 -1
  58. package/dist/emit/diffJson.js +18 -14
  59. package/dist/emit/diffJson.js.map +1 -1
  60. package/dist/emit/dot.js +46 -5
  61. package/dist/emit/dot.js.map +1 -1
  62. package/dist/emit/json.d.ts +10 -25
  63. package/dist/emit/json.js +70 -103
  64. package/dist/emit/json.js.map +1 -1
  65. package/dist/emit/ledgerJson.d.ts +5 -0
  66. package/dist/emit/ledgerJson.js +25 -0
  67. package/dist/emit/ledgerJson.js.map +1 -0
  68. package/dist/emit/skillCheckJson.d.ts +5 -0
  69. package/dist/emit/skillCheckJson.js +32 -0
  70. package/dist/emit/skillCheckJson.js.map +1 -0
  71. package/dist/model/diff.d.ts +44 -1
  72. package/dist/model/diff.js +51 -9
  73. package/dist/model/diff.js.map +1 -1
  74. package/dist/model/edges.js +17 -17
  75. package/dist/model/edges.js.map +1 -1
  76. package/dist/model/model.d.ts +1 -1
  77. package/dist/model/refs.d.ts +23 -0
  78. package/dist/model/refs.js +75 -0
  79. package/dist/model/refs.js.map +1 -0
  80. package/dist/model/rules.d.ts +253 -0
  81. package/dist/model/rules.js +254 -0
  82. package/dist/model/rules.js.map +1 -0
  83. package/dist/model/validate.d.ts +23 -1
  84. package/dist/model/validate.js +207 -69
  85. package/dist/model/validate.js.map +1 -1
  86. package/dist/parser/ast.d.ts +3 -2
  87. package/dist/parser/lexer.d.ts +17 -2
  88. package/dist/parser/lexer.js +119 -13
  89. package/dist/parser/lexer.js.map +1 -1
  90. package/dist/parser/parser.js +75 -34
  91. package/dist/parser/parser.js.map +1 -1
  92. package/dist/pipeline.d.ts +9 -0
  93. package/dist/pipeline.js +4 -2
  94. package/dist/pipeline.js.map +1 -1
  95. package/dist/render/sliceDiagram.js +33 -35
  96. package/dist/render/sliceDiagram.js.map +1 -1
  97. package/dist/render/sliceStatus.js +1 -1
  98. package/dist/render/sliceStatus.js.map +1 -1
  99. package/dist/util/isMainModule.d.ts +14 -0
  100. package/dist/util/isMainModule.js +33 -0
  101. package/dist/util/isMainModule.js.map +1 -0
  102. package/dist/util/walkDir.d.ts +4 -0
  103. package/dist/util/walkDir.js +28 -0
  104. package/dist/util/walkDir.js.map +1 -0
  105. package/package.json +5 -2
@@ -1,14 +1,13 @@
1
1
  import { Slice } from "../model/model.js";
2
2
  export type SlicePattern = "translation" | "automation" | "state-change" | "state-view" | "unclassified";
3
3
  /**
4
- * Classify a single slice by its element kinds. Known simplification, not a
5
- * bug: docs/patterns.md describes Automation and Translation as spanning
6
- * *two* slices — the reaction slice, then a following `command`+`event`
7
- * slice. That second slice has the exact same kind-signature as a State
8
- * Change slice, so per-slice classification reads it as State Change. This
9
- * matches "derived from which element kinds appear in the slice" and keeps
10
- * the classifier a pure function of one slice at a time; don't "fix" this by
11
- * trying to look at neighboring slices.
4
+ * Classify a single slice by its element kinds. A reaction (`translation`/
5
+ * `processor`/`automation`/`saga`) shares its slice with the command it
6
+ * triggers — see docs/patterns.md — so checking translation/automation kinds
7
+ * before command/event is what makes that combined slice read as Translation
8
+ * or Automation rather than State Change. A view the reaction reads via
9
+ * `from` lives in its own, earlier slice and classifies as State View on its
10
+ * own. Order matters for this reason; keep the reaction checks first.
12
11
  */
13
12
  export declare function classifySlicePattern(slice: Slice): SlicePattern;
14
13
  export declare function slicePatternLabel(pattern: SlicePattern): string;
@@ -6,14 +6,13 @@
6
6
  // checked in priority order (a slice can carry a translation and a view
7
7
  // together, for instance, so order matters).
8
8
  /**
9
- * Classify a single slice by its element kinds. Known simplification, not a
10
- * bug: docs/patterns.md describes Automation and Translation as spanning
11
- * *two* slices — the reaction slice, then a following `command`+`event`
12
- * slice. That second slice has the exact same kind-signature as a State
13
- * Change slice, so per-slice classification reads it as State Change. This
14
- * matches "derived from which element kinds appear in the slice" and keeps
15
- * the classifier a pure function of one slice at a time; don't "fix" this by
16
- * trying to look at neighboring slices.
9
+ * Classify a single slice by its element kinds. A reaction (`translation`/
10
+ * `processor`/`automation`/`saga`) shares its slice with the command it
11
+ * triggers — see docs/patterns.md — so checking translation/automation kinds
12
+ * before command/event is what makes that combined slice read as Translation
13
+ * or Automation rather than State Change. A view the reaction reads via
14
+ * `from` lives in its own, earlier slice and classifies as State View on its
15
+ * own. Order matters for this reason; keep the reaction checks first.
17
16
  */
18
17
  export function classifySlicePattern(slice) {
19
18
  const kinds = new Set(slice.elements.map((el) => el.kind));
@@ -1 +1 @@
1
- {"version":3,"file":"classify.js","sourceRoot":"","sources":["../../src/catalog/classify.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,yEAAyE;AACzE,sEAAsE;AACtE,yEAAyE;AACzE,0EAA0E;AAC1E,wEAAwE;AACxE,6CAA6C;AAM7C;;;;;;;;;GASG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAY;IAC/C,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3D,IAAI,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC;QAAE,OAAO,aAAa,CAAC;IACnD,IAAI,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC;QAAE,OAAO,YAAY,CAAC;IAChG,IAAI,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC;QAAE,OAAO,cAAc,CAAC;IACtE,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC;QAAE,OAAO,YAAY,CAAC;IAC3C,OAAO,cAAc,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,OAAqB;IACrD,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,aAAa;YAChB,OAAO,aAAa,CAAC;QACvB,KAAK,YAAY;YACf,OAAO,YAAY,CAAC;QACtB,KAAK,cAAc;YACjB,OAAO,cAAc,CAAC;QACxB,KAAK,YAAY;YACf,OAAO,YAAY,CAAC;QACtB,KAAK,cAAc;YACjB,OAAO,cAAc,CAAC;IAC1B,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"classify.js","sourceRoot":"","sources":["../../src/catalog/classify.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,yEAAyE;AACzE,sEAAsE;AACtE,yEAAyE;AACzE,0EAA0E;AAC1E,wEAAwE;AACxE,6CAA6C;AAM7C;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAY;IAC/C,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3D,IAAI,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC;QAAE,OAAO,aAAa,CAAC;IACnD,IAAI,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC;QAAE,OAAO,YAAY,CAAC;IAChG,IAAI,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC;QAAE,OAAO,cAAc,CAAC;IACtE,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC;QAAE,OAAO,YAAY,CAAC;IAC3C,OAAO,cAAc,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,OAAqB;IACrD,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,aAAa;YAChB,OAAO,aAAa,CAAC;QACvB,KAAK,YAAY;YACf,OAAO,YAAY,CAAC;QACtB,KAAK,cAAc;YACjB,OAAO,cAAc,CAAC;QACxB,KAAK,YAAY;YACf,OAAO,YAAY,CAAC;QACtB,KAAK,cAAc;YACjB,OAAO,cAAc,CAAC;IAC1B,CAAC;AACH,CAAC"}
@@ -0,0 +1,39 @@
1
+ import { Slice } from "../model/model.js";
2
+ import { Diagnostic } from "../model/validate.js";
3
+ import { DriftSignalKind } from "./driftSignal.js";
4
+ import { SliceRef } from "./sliceDoc.js";
5
+ export type DocReason = "no-doc-bound" | "binding-missing-file" | "frontmatter-invalid" | null;
6
+ /** A slice's export-facing doc join — always a non-null object (never JSON `null`), matching
7
+ * every other optional field in `em export`'s style: explicit-null-on-a-stable-key, never a
8
+ * nullable wrapper. `reason` is null exactly when `found` is true and the frontmatter parsed
9
+ * cleanly; otherwise it names which of the three broken states applies. */
10
+ export interface SliceDocExport {
11
+ found: boolean;
12
+ /** The conventional path (`slices/<sliceKey>.md`), always present regardless of `found` —
13
+ * the path a doc would need to exist at, and (when `found`) the path it was read from. */
14
+ path: string;
15
+ reason: DocReason;
16
+ status: string | null;
17
+ version: number | null;
18
+ implementedIn: string | null;
19
+ splitFrom: SliceRef | null;
20
+ mergedFrom: SliceRef[];
21
+ supersededBy: SliceRef[];
22
+ /** Implementation-drift classification (MIL-85), from `status`+`implementedIn` alone — see
23
+ * catalog/driftSignal.ts. Null only when `found` is false (nothing to classify); paired with
24
+ * `version` above from the same doc parse — a finding built from `driftSignal` should always
25
+ * cite `version` alongside it, never cache one without the other. */
26
+ driftSignal: DriftSignalKind | null;
27
+ }
28
+ export interface SliceDocJoinResult {
29
+ doc: SliceDocExport;
30
+ diagnostics: Diagnostic[];
31
+ }
32
+ /**
33
+ * Resolve one slice's doc join. `baseDir` is the `.em` file's directory (doc paths, like note
34
+ * paths everywhere else in em, are relative to it). `sliceRef`/`elementRefOf` supply the
35
+ * export-stable refs (from the same `computeRefs()` result `em export`'s per-element/slice
36
+ * entries use) so the returned diagnostics point at the same identities every other export
37
+ * diagnostic does — never a second identifier scheme.
38
+ */
39
+ export declare function resolveSliceDocJoin(slice: Slice, sliceKey: string, baseDir: string, elementRefOf: (id: string) => string): SliceDocJoinResult;
@@ -0,0 +1,86 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // The `em export` slice-doc join (MIL-91): for each slice, resolve whether a design doc was
3
+ // declared, found, and well-formed, and if so pull its canonical frontmatter fields into the
4
+ // export JSON. Used ONLY by `em export` (src/emit/json.ts) — `em catalog`'s own doc lookup
5
+ // (catalog/build.ts) deliberately keeps its simpler existsSync+parseSliceDoc convention: it
6
+ // has a standing tested invariant that catalog's doc discovery ignores `note` entirely
7
+ // (test/catalog.e2e.test.ts), and doesn't need this module's 3-state reason granularity. The
8
+ // underlying read (`readSliceDoc.ts`) is shared with `em diff`'s lineage annotation
9
+ // (model/diff.ts) and `em validate`'s lineage-ref resolution (catalog/lineageValidate.ts,
10
+ // MIL-84) — this module's `note`-binding gate and 3-state `DocReason` stay export-specific.
11
+ //
12
+ // Three states (MIL-91): the `note "slices/<name>.md"` binding on an element is the
13
+ // load-bearing fact — binding a note IS the ratified declaration that a doc should exist. No
14
+ // note binding is a normal, unremarkable state (a slice that hasn't reached specify yet); a
15
+ // binding pointing at a missing file, or a doc whose frontmatter is missing or malformed, are
16
+ // both broken-contract states and warn.
17
+ import { makeDiag } from "../model/rules.js";
18
+ import { classifyImplementationDrift } from "./driftSignal.js";
19
+ import { readSliceDoc } from "./readSliceDoc.js";
20
+ import { hasUsableFrontmatter } from "./sliceDoc.js";
21
+ const EMPTY_CONTENT = {
22
+ status: null,
23
+ version: null,
24
+ implementedIn: null,
25
+ splitFrom: null,
26
+ mergedFrom: [],
27
+ supersededBy: [],
28
+ driftSignal: null,
29
+ };
30
+ /**
31
+ * Resolve one slice's doc join. `baseDir` is the `.em` file's directory (doc paths, like note
32
+ * paths everywhere else in em, are relative to it). `sliceRef`/`elementRefOf` supply the
33
+ * export-stable refs (from the same `computeRefs()` result `em export`'s per-element/slice
34
+ * entries use) so the returned diagnostics point at the same identities every other export
35
+ * diagnostic does — never a second identifier scheme.
36
+ */
37
+ export function resolveSliceDocJoin(slice, sliceKey, baseDir, elementRefOf) {
38
+ const path = `slices/${sliceKey}.md`;
39
+ const boundEls = slice.elements.filter((el) => el.note === path);
40
+ if (boundEls.length === 0) {
41
+ return { doc: { found: false, path, reason: "no-doc-bound", ...EMPTY_CONTENT }, diagnostics: [] };
42
+ }
43
+ const parsed = readSliceDoc(baseDir, sliceKey);
44
+ if (!parsed) {
45
+ return {
46
+ doc: { found: false, path, reason: "binding-missing-file", ...EMPTY_CONTENT },
47
+ diagnostics: [
48
+ makeDiag("binding-missing-file", {
49
+ message: `slice "${slice.name}" notes "${path}" but no such file exists`,
50
+ line: boundEls[0].line,
51
+ refs: [sliceKey, ...boundEls.map((el) => elementRefOf(el.id))],
52
+ }),
53
+ ],
54
+ };
55
+ }
56
+ if (!hasUsableFrontmatter(parsed)) {
57
+ return {
58
+ doc: { found: true, path, reason: "frontmatter-invalid", ...EMPTY_CONTENT },
59
+ diagnostics: [
60
+ makeDiag("frontmatter-invalid", {
61
+ message: parsed.frontmatterPresent
62
+ ? `slice doc "${path}" is missing required frontmatter keys: ${parsed.missingRequiredFields.join(", ")}`
63
+ : `slice doc "${path}" has no frontmatter block`,
64
+ line: slice.line,
65
+ refs: [sliceKey],
66
+ }),
67
+ ],
68
+ };
69
+ }
70
+ return {
71
+ doc: {
72
+ found: true,
73
+ path,
74
+ reason: null,
75
+ status: parsed.status,
76
+ version: parsed.version,
77
+ implementedIn: parsed.implementedIn,
78
+ splitFrom: parsed.splitFrom,
79
+ mergedFrom: parsed.mergedFrom,
80
+ supersededBy: parsed.supersededBy,
81
+ driftSignal: classifyImplementationDrift(parsed),
82
+ },
83
+ diagnostics: [],
84
+ };
85
+ }
86
+ //# sourceMappingURL=docJoin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docJoin.js","sourceRoot":"","sources":["../../src/catalog/docJoin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import { SliceDoc } from "./sliceDoc.js";
2
+ export type DriftSignalKind =
3
+ /** status: implemented, implementedIn set — normal shipped state. */
4
+ "in-sync"
5
+ /** status not implemented, implementedIn absent — normal pre-ship state. */
6
+ | "never-implemented"
7
+ /** status not implemented, implementedIn still set — a ratified delta hasn't shipped yet.
8
+ * Expected, not a defect: never surface this as fresh drift or a validate diagnostic. */
9
+ | "unpropagated-delta"
10
+ /** status: implemented, implementedIn absent — genuine incoherence (em validate warns). */
11
+ | "implemented-without-link";
12
+ /**
13
+ * Classify a doc's status/implementedIn pair. Takes a `Pick` rather than the full `SliceDoc` so
14
+ * callers building a partial/synthetic doc (tests, future callers) don't need every field.
15
+ */
16
+ export declare function classifyImplementationDrift(doc: Pick<SliceDoc, "status" | "implementedIn">): DriftSignalKind;
@@ -0,0 +1,24 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Classifies a slice doc's implementation-drift state from `status` + `implementedIn` alone
3
+ // (MIL-85). Pure — same spirit as sliceDoc.ts itself: no fs, no model, no baseDir — so
4
+ // `em export`'s doc join (catalog/docJoin.ts), `em validate`'s frontmatter-coherence check
5
+ // (catalog/frontmatterCoherenceValidate.ts), and the conform skill (reading `em export --json`'s
6
+ // `slice.doc.driftSignal`) all consume the exact same answer instead of re-deriving this
7
+ // predicate three different ways.
8
+ //
9
+ // The load-bearing distinction (docs/slice-doc-schema.md, "`status` under re-ratification"):
10
+ // re-ratifying a shipped slice flips `status` back off `implemented` while `implementedIn` keeps
11
+ // naming the prior version's PR. That combination — `unpropagated-delta` — is the EXPECTED drift
12
+ // signal, not staleness, and must never be flagged as incoherence. Only `implemented-without-link`
13
+ // (status: implemented, no link at all) is a genuine coherence problem worth a diagnostic.
14
+ /**
15
+ * Classify a doc's status/implementedIn pair. Takes a `Pick` rather than the full `SliceDoc` so
16
+ * callers building a partial/synthetic doc (tests, future callers) don't need every field.
17
+ */
18
+ export function classifyImplementationDrift(doc) {
19
+ const hasLink = typeof doc.implementedIn === "string" && doc.implementedIn.trim().length > 0;
20
+ if (doc.status === "implemented")
21
+ return hasLink ? "in-sync" : "implemented-without-link";
22
+ return hasLink ? "unpropagated-delta" : "never-implemented";
23
+ }
24
+ //# sourceMappingURL=driftSignal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"driftSignal.js","sourceRoot":"","sources":["../../src/catalog/driftSignal.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,4FAA4F;AAC5F,uFAAuF;AACvF,2FAA2F;AAC3F,iGAAiG;AACjG,yFAAyF;AACzF,kCAAkC;AAClC,EAAE;AACF,6FAA6F;AAC7F,iGAAiG;AACjG,iGAAiG;AACjG,mGAAmG;AACnG,2FAA2F;AAe3F;;;GAGG;AACH,MAAM,UAAU,2BAA2B,CAAC,GAA+C;IACzF,MAAM,OAAO,GAAG,OAAO,GAAG,CAAC,aAAa,KAAK,QAAQ,IAAI,GAAG,CAAC,aAAa,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;IAC7F,IAAI,GAAG,CAAC,MAAM,KAAK,aAAa;QAAE,OAAO,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,0BAA0B,CAAC;IAC1F,OAAO,OAAO,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,mBAAmB,CAAC;AAC9D,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { NormalizedModel } from "../model/model.js";
2
+ import { RefsResult } from "../model/refs.js";
3
+ import { Diagnostic } from "../model/validate.js";
4
+ /**
5
+ * Resolve every slice's doc and flag genuine status/implementedIn incoherence. `baseDir` is the
6
+ * `.em` file's directory (doc paths, like every other doc/note path in em, are relative to it).
7
+ */
8
+ export declare function validateFrontmatterCoherence(model: NormalizedModel, refs: RefsResult, baseDir: string): Diagnostic[];
@@ -0,0 +1,52 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // `em validate`'s frontmatter-coherence check (MIL-85): warns when a slice doc's `status` and
3
+ // `implementedIn` are genuinely incoherent — `status: implemented` with no `implementedIn` link
4
+ // at all. Deliberately narrow: re-ratifying a shipped slice flips `status` back off
5
+ // `implemented` while `implementedIn` keeps naming the prior version's PR (docs/slice-doc-
6
+ // schema.md, "`status` under re-ratification") — that combination is the EXPECTED drift signal
7
+ // of an unpropagated delta, not incoherence, and must stay silent here. See
8
+ // catalog/driftSignal.ts for the shared classification this check (and `em export`'s
9
+ // `slice.doc.driftSignal`) both read.
10
+ //
11
+ // Same fs-aware shape as catalog/lineageValidate.ts (MIL-84): a sibling module to
12
+ // model/validate.ts, not folded into it, because resolving a slice's doc needs `baseDir`/fs
13
+ // access that the rest of validate deliberately never touches.
14
+ //
15
+ // Requires usable frontmatter (`hasUsableFrontmatter()`, sliceDoc.ts) — the exact same gate
16
+ // `em export`'s docJoin.ts uses for `frontmatter-invalid`, shared as one function precisely so
17
+ // the two can't silently disagree on what counts as a readable doc. `implementedIn` is a
18
+ // frontmatter-only key (MIL-90/91) with no legacy body-label form (MIL-86's accepted-input
19
+ // dialect: `- **Status:** ...`, no frontmatter block), and every canonical field this check
20
+ // reads is itself frontmatter-only — so a doc with no fence, or one missing any required key,
21
+ // has nothing reliable enough to classify. Flagging it as incoherent would be a false positive
22
+ // against a doc that's merely unreadable, not one that's actually incoherent; stay silent
23
+ // rather than guess, matching export's classification of the same doc.
24
+ import { pushDiag } from "../model/rules.js";
25
+ import { classifyImplementationDrift } from "./driftSignal.js";
26
+ import { readSliceDoc } from "./readSliceDoc.js";
27
+ import { hasUsableFrontmatter } from "./sliceDoc.js";
28
+ /**
29
+ * Resolve every slice's doc and flag genuine status/implementedIn incoherence. `baseDir` is the
30
+ * `.em` file's directory (doc paths, like every other doc/note path in em, are relative to it).
31
+ */
32
+ export function validateFrontmatterCoherence(model, refs, baseDir) {
33
+ const diags = [];
34
+ model.slices.forEach((slice, i) => {
35
+ const sliceKey = refs.sliceKeys[i];
36
+ const doc = readSliceDoc(baseDir, sliceKey);
37
+ if (!doc || !hasUsableFrontmatter(doc))
38
+ return;
39
+ const drift = classifyImplementationDrift(doc);
40
+ if (drift !== "implemented-without-link")
41
+ return;
42
+ // "unpropagated-delta" falls through here silently — that's the point: a re-ratified slice
43
+ // whose implementedIn still names prior work is expected, not a defect.
44
+ pushDiag(diags, "frontmatter-coherence-implemented-without-link", {
45
+ message: `slice "${sliceKey}" has status: implemented but no implementedIn link (v${doc.version ?? "?"})`,
46
+ line: slice.line,
47
+ refs: [sliceKey],
48
+ });
49
+ });
50
+ return diags;
51
+ }
52
+ //# sourceMappingURL=frontmatterCoherenceValidate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frontmatterCoherenceValidate.js","sourceRoot":"","sources":["../../src/catalog/frontmatterCoherenceValidate.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,8FAA8F;AAC9F,gGAAgG;AAChG,oFAAoF;AACpF,2FAA2F;AAC3F,+FAA+F;AAC/F,4EAA4E;AAC5E,qFAAqF;AACrF,sCAAsC;AACtC,EAAE;AACF,kFAAkF;AAClF,4FAA4F;AAC5F,+DAA+D;AAC/D,EAAE;AACF,4FAA4F;AAC5F,+FAA+F;AAC/F,yFAAyF;AACzF,2FAA2F;AAC3F,4FAA4F;AAC5F,8FAA8F;AAC9F,+FAA+F;AAC/F,0FAA0F;AAC1F,uEAAuE;AAKvE,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,2BAA2B,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAErD;;;GAGG;AACH,MAAM,UAAU,4BAA4B,CAAC,KAAsB,EAAE,IAAgB,EAAE,OAAe;IACpG,MAAM,KAAK,GAAiB,EAAE,CAAC;IAE/B,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,YAAY,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QAC5C,IAAI,CAAC,GAAG,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC;YAAE,OAAO;QAE/C,MAAM,KAAK,GAAG,2BAA2B,CAAC,GAAG,CAAC,CAAC;QAC/C,IAAI,KAAK,KAAK,0BAA0B;YAAE,OAAO;QACjD,2FAA2F;QAC3F,wEAAwE;QAExE,QAAQ,CAAC,KAAK,EAAE,gDAAgD,EAAE;YAChE,OAAO,EAAE,UAAU,QAAQ,yDAAyD,GAAG,CAAC,OAAO,IAAI,GAAG,GAAG;YACzG,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,IAAI,EAAE,CAAC,QAAQ,CAAC;SACjB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { NormalizedModel } from "../model/model.js";
2
+ import { RefsResult } from "../model/refs.js";
3
+ import { Diagnostic } from "../model/validate.js";
4
+ /**
5
+ * Resolve every slice's lineage refs against the current tree and report what the current tree
6
+ * can prove wrong. `baseDir` is the `.em` file's directory (doc paths, like every other
7
+ * doc/note path in em, are relative to it).
8
+ */
9
+ export declare function validateLineage(model: NormalizedModel, refs: RefsResult, baseDir: string): Diagnostic[];
@@ -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":"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"}
@@ -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;