@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.
- package/.claude/skills/event-modeling/SKILL.md +94 -49
- package/.claude/skills/event-modeling/reference/conform.md +30 -8
- package/.claude/skills/event-modeling/reference/em-dsl.md +194 -73
- package/.claude/skills/event-modeling/reference/extract.md +14 -11
- package/.claude/skills/event-modeling/reference/implement.md +172 -0
- package/.claude/skills/event-modeling/reference/methodology.md +40 -33
- package/.claude/skills/event-modeling/templates/conformance-report.md +15 -9
- package/.claude/skills/event-modeling/templates/model-readme.md +7 -7
- package/.claude/skills/event-modeling/templates/slice.md +73 -7
- package/.claude/skills/event-modeling/templates/state.md +7 -6
- package/README.md +1 -0
- package/dist/catalog/build.d.ts +5 -0
- package/dist/catalog/build.js +1 -3
- package/dist/catalog/build.js.map +1 -1
- package/dist/catalog/classify.d.ts +7 -8
- package/dist/catalog/classify.js +7 -8
- package/dist/catalog/classify.js.map +1 -1
- package/dist/catalog/docJoin.d.ts +39 -0
- package/dist/catalog/docJoin.js +86 -0
- package/dist/catalog/docJoin.js.map +1 -0
- package/dist/catalog/driftSignal.d.ts +16 -0
- package/dist/catalog/driftSignal.js +24 -0
- package/dist/catalog/driftSignal.js.map +1 -0
- package/dist/catalog/frontmatterCoherenceValidate.d.ts +8 -0
- package/dist/catalog/frontmatterCoherenceValidate.js +52 -0
- package/dist/catalog/frontmatterCoherenceValidate.js.map +1 -0
- package/dist/catalog/lineageValidate.d.ts +9 -0
- package/dist/catalog/lineageValidate.js +161 -0
- package/dist/catalog/lineageValidate.js.map +1 -0
- package/dist/catalog/readSliceDoc.d.ts +2 -0
- package/dist/catalog/readSliceDoc.js +21 -0
- package/dist/catalog/readSliceDoc.js.map +1 -0
- package/dist/catalog/sliceDoc.d.ts +94 -3
- package/dist/catalog/sliceDoc.js +157 -7
- package/dist/catalog/sliceDoc.js.map +1 -1
- package/dist/catalog/sliceReadyValidate.d.ts +11 -0
- package/dist/catalog/sliceReadyValidate.js +85 -0
- package/dist/catalog/sliceReadyValidate.js.map +1 -0
- package/dist/cli/diff-inputs.d.ts +20 -0
- package/dist/cli/diff-inputs.js +75 -18
- package/dist/cli/diff-inputs.js.map +1 -1
- package/dist/cli/ledgerCheck.d.ts +38 -0
- package/dist/cli/ledgerCheck.js +116 -0
- package/dist/cli/ledgerCheck.js.map +1 -0
- package/dist/cli/skillCheck.d.ts +18 -0
- package/dist/cli/skillCheck.js +63 -0
- package/dist/cli/skillCheck.js.map +1 -0
- package/dist/cli/skillSync.d.ts +22 -0
- package/dist/cli/skillSync.js +69 -0
- package/dist/cli/skillSync.js.map +1 -0
- package/dist/cli/skillVersionCheck.d.ts +32 -0
- package/dist/cli/skillVersionCheck.js +145 -0
- package/dist/cli/skillVersionCheck.js.map +1 -0
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +203 -32
- package/dist/cli.js.map +1 -1
- package/dist/emit/diffJson.d.ts +1 -1
- package/dist/emit/diffJson.js +18 -14
- package/dist/emit/diffJson.js.map +1 -1
- package/dist/emit/dot.js +46 -5
- package/dist/emit/dot.js.map +1 -1
- package/dist/emit/json.d.ts +10 -25
- package/dist/emit/json.js +70 -103
- package/dist/emit/json.js.map +1 -1
- package/dist/emit/ledgerJson.d.ts +5 -0
- package/dist/emit/ledgerJson.js +25 -0
- package/dist/emit/ledgerJson.js.map +1 -0
- package/dist/emit/skillCheckJson.d.ts +5 -0
- package/dist/emit/skillCheckJson.js +32 -0
- package/dist/emit/skillCheckJson.js.map +1 -0
- package/dist/model/diff.d.ts +44 -1
- package/dist/model/diff.js +51 -9
- package/dist/model/diff.js.map +1 -1
- package/dist/model/edges.js +17 -17
- package/dist/model/edges.js.map +1 -1
- package/dist/model/model.d.ts +1 -1
- package/dist/model/refs.d.ts +23 -0
- package/dist/model/refs.js +75 -0
- package/dist/model/refs.js.map +1 -0
- package/dist/model/rules.d.ts +253 -0
- package/dist/model/rules.js +254 -0
- package/dist/model/rules.js.map +1 -0
- package/dist/model/validate.d.ts +23 -1
- package/dist/model/validate.js +207 -69
- package/dist/model/validate.js.map +1 -1
- package/dist/parser/ast.d.ts +3 -2
- package/dist/parser/lexer.d.ts +17 -2
- package/dist/parser/lexer.js +119 -13
- package/dist/parser/lexer.js.map +1 -1
- package/dist/parser/parser.js +75 -34
- package/dist/parser/parser.js.map +1 -1
- package/dist/pipeline.d.ts +9 -0
- package/dist/pipeline.js +4 -2
- package/dist/pipeline.js.map +1 -1
- package/dist/render/sliceDiagram.js +33 -35
- package/dist/render/sliceDiagram.js.map +1 -1
- package/dist/render/sliceStatus.js +1 -1
- package/dist/render/sliceStatus.js.map +1 -1
- package/dist/util/isMainModule.d.ts +14 -0
- package/dist/util/isMainModule.js +33 -0
- package/dist/util/isMainModule.js.map +1 -0
- package/dist/util/walkDir.d.ts +4 -0
- package/dist/util/walkDir.js +28 -0
- package/dist/util/walkDir.js.map +1 -0
- 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.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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;
|
package/dist/catalog/classify.js
CHANGED
|
@@ -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.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
|
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":"AAAA,+BAA+B;AAC/B,4FAA4F;AAC5F,6FAA6F;AAC7F,2FAA2F;AAC3F,4FAA4F;AAC5F,uFAAuF;AACvF,6FAA6F;AAC7F,oFAAoF;AACpF,0FAA0F;AAC1F,4FAA4F;AAC5F,EAAE;AACF,oFAAoF;AACpF,6FAA6F;AAC7F,4FAA4F;AAC5F,8FAA8F;AAC9F,wCAAwC;AAIxC,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,2BAA2B,EAAmB,MAAM,kBAAkB,CAAC;AAChF,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,oBAAoB,EAAY,MAAM,eAAe,CAAC;AAgC/D,MAAM,aAAa,GAAG;IACpB,MAAM,EAAE,IAAI;IACZ,OAAO,EAAE,IAAI;IACb,aAAa,EAAE,IAAI;IACnB,SAAS,EAAE,IAAI;IACf,UAAU,EAAE,EAAgB;IAC5B,YAAY,EAAE,EAAgB;IAC9B,WAAW,EAAE,IAA8B;CAC5C,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CACjC,KAAY,EACZ,QAAgB,EAChB,OAAe,EACf,YAAoC;IAEpC,MAAM,IAAI,GAAG,UAAU,QAAQ,KAAK,CAAC;IACrC,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;IAEjE,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG,aAAa,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC;IACpG,CAAC;IAED,MAAM,MAAM,GAAG,YAAY,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAC/C,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO;YACL,GAAG,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,sBAAsB,EAAE,GAAG,aAAa,EAAE;YAC7E,WAAW,EAAE;gBACX,QAAQ,CAAC,sBAAsB,EAAE;oBAC/B,OAAO,EAAE,UAAU,KAAK,CAAC,IAAI,YAAY,IAAI,2BAA2B;oBACxE,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI;oBACtB,IAAI,EAAE,CAAC,QAAQ,EAAE,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;iBAC/D,CAAC;aACH;SACF,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,oBAAoB,CAAC,MAAM,CAAC,EAAE,CAAC;QAClC,OAAO;YACL,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,qBAAqB,EAAE,GAAG,aAAa,EAAE;YAC3E,WAAW,EAAE;gBACX,QAAQ,CAAC,qBAAqB,EAAE;oBAC9B,OAAO,EAAE,MAAM,CAAC,kBAAkB;wBAChC,CAAC,CAAC,cAAc,IAAI,2CAA2C,MAAM,CAAC,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;wBACxG,CAAC,CAAC,cAAc,IAAI,4BAA4B;oBAClD,IAAI,EAAE,KAAK,CAAC,IAAI;oBAChB,IAAI,EAAE,CAAC,QAAQ,CAAC;iBACjB,CAAC;aACH;SACF,CAAC;IACJ,CAAC;IAED,OAAO;QACL,GAAG,EAAE;YACH,KAAK,EAAE,IAAI;YACX,IAAI;YACJ,MAAM,EAAE,IAAI;YACZ,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,aAAa,EAAE,MAAM,CAAC,aAAa;YACnC,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,UAAU,EAAE,MAAM,CAAC,UAAU;YAC7B,YAAY,EAAE,MAAM,CAAC,YAAY;YACjC,WAAW,EAAE,2BAA2B,CAAC,MAAM,CAAC;SACjD;QACD,WAAW,EAAE,EAAE;KAChB,CAAC;AACJ,CAAC"}
|
|
@@ -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,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
|
-
/**
|
|
5
|
-
*
|
|
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
|
-
/**
|
|
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;
|