@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
package/dist/catalog/sliceDoc.js
CHANGED
|
@@ -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
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
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
|
|
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
|
-
|
|
17
|
-
|
|
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,
|
|
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":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SliceDoc } from "../catalog/sliceDoc.js";
|
|
1
2
|
/** Resolved `em diff` invocation, or a user-facing `error` for an invalid form. */
|
|
2
3
|
export type DiffPlan = {
|
|
3
4
|
error: string;
|
|
@@ -42,3 +43,22 @@ export type RevisionResult = {
|
|
|
42
43
|
* unit-testable without a real repository.
|
|
43
44
|
*/
|
|
44
45
|
export declare function resolveRevision(file: string, rev: string, runGit?: GitRunner): RevisionResult;
|
|
46
|
+
/**
|
|
47
|
+
* Resolve a slice's doc (`slices/<sliceKey>.md`, relative to `anchorFile`'s directory — same
|
|
48
|
+
* convention as every other doc/note path in em) at git revision `rev`. Unlike
|
|
49
|
+
* `resolveRevision`, a missing doc at a revision is routine (not every slice had a doc yet, or
|
|
50
|
+
* the doc simply isn't tracked) rather than a user-facing error — so every failure mode
|
|
51
|
+
* collapses to `null`, for `em diff`'s lineage annotation (MIL-84) to treat as "nothing to
|
|
52
|
+
* annotate with" rather than aborting the diff.
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveDocAtRevision(anchorFile: string, sliceKey: string, rev: string, runGit?: GitRunner): SliceDoc | null;
|
|
55
|
+
/**
|
|
56
|
+
* Slice doc keys (filename stems under `slices/`, relative to `anchorFile`'s directory — same
|
|
57
|
+
* anchor convention as `resolveDocAtRevision`) present at git revision `rev`. Used by `em ledger`
|
|
58
|
+
* (MIL-89) to discover the candidate set of docs to compare across two revisions, reusing the
|
|
59
|
+
* same repo-root resolution `showAtRevision` does rather than a second, divergent way of finding
|
|
60
|
+
* "what slice docs exist here." Returns `[]` (not an error) when the repo can't be resolved or
|
|
61
|
+
* `slices/` didn't exist at `rev` — both routine (a model with no slice docs yet), matching
|
|
62
|
+
* `resolveDocAtRevision`'s "missing is routine" stance.
|
|
63
|
+
*/
|
|
64
|
+
export declare function listSliceKeysAtRevision(anchorFile: string, rev: string, runGit?: GitRunner): string[];
|
package/dist/cli/diff-inputs.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// unit-tested without spawning a subprocess or a real git repo (cli.ts keeps the
|
|
5
5
|
// process.exit/console wiring; see test/diff-inputs.test.ts).
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
7
|
-
import { dirname, resolve } from "node:path";
|
|
7
|
+
import { basename, dirname, resolve } from "node:path";
|
|
8
|
+
import { parseSliceDoc } from "../catalog/sliceDoc.js";
|
|
8
9
|
/**
|
|
9
10
|
* Validate the `em diff` argument/flag combination. Pure — no I/O. The two forms
|
|
10
11
|
* (two files, or one file across git revisions) are mutually exclusive.
|
|
@@ -33,30 +34,86 @@ export const realGit = (args) => {
|
|
|
33
34
|
const r = spawnSync("git", args, { encoding: "utf8" });
|
|
34
35
|
return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
35
36
|
};
|
|
37
|
+
/**
|
|
38
|
+
* Read `targetAbs`'s content at git revision `rev`, resolving the repo root from
|
|
39
|
+
* `anchorFile`'s directory (a file known to be somewhere inside the repo — doesn't itself have
|
|
40
|
+
* to be `targetAbs`, e.g. a slice doc resolved relative to its `.em` file's location). The git
|
|
41
|
+
* runner is injectable so every failure branch is unit-testable without a real repository.
|
|
42
|
+
*
|
|
43
|
+
* Existence/path lookup uses `git ls-tree -r --name-only <rev>` — scoped to `rev` itself, not
|
|
44
|
+
* `git ls-files` (which reflects the *current* index/working tree). A doc that existed at
|
|
45
|
+
* `rev` but has since been deleted — the routine case for a `superseded-by`-carrying doc, which
|
|
46
|
+
* gets removed in the very commit that performs the split/merge/removal it records — would
|
|
47
|
+
* otherwise resolve to "not tracked" even though `git show <rev>:<path>` can read it just fine.
|
|
48
|
+
*/
|
|
49
|
+
function showAtRevision(runGit, anchorFile, targetAbs, rev) {
|
|
50
|
+
const toplevel = runGit(["-C", dirname(resolve(anchorFile)), "rev-parse", "--show-toplevel"]);
|
|
51
|
+
if (toplevel.status !== 0)
|
|
52
|
+
return { ok: false, reason: "no-repo" };
|
|
53
|
+
const repoRoot = toplevel.stdout.trim();
|
|
54
|
+
const lsTree = runGit(["-C", repoRoot, "ls-tree", "-r", "--name-only", rev, "--", targetAbs]);
|
|
55
|
+
const relPath = lsTree.stdout.trim().split("\n")[0];
|
|
56
|
+
if (!relPath)
|
|
57
|
+
return { ok: false, reason: "not-tracked", repoRoot };
|
|
58
|
+
const show = runGit(["-C", repoRoot, "show", `${rev}:${relPath}`]);
|
|
59
|
+
if (show.status !== 0) {
|
|
60
|
+
return { ok: false, reason: "show-failed", relPath, detail: (show.stderr || "").trim() || "unknown git error" };
|
|
61
|
+
}
|
|
62
|
+
return { ok: true, content: show.stdout };
|
|
63
|
+
}
|
|
36
64
|
/**
|
|
37
65
|
* Resolve `file`'s content at git revision `rev` via `git show <rev>:<repo-relative-path>`.
|
|
38
66
|
* The git runner is injectable so the not-a-repo / not-tracked / bad-rev branches are
|
|
39
67
|
* unit-testable without a real repository.
|
|
40
68
|
*/
|
|
41
69
|
export function resolveRevision(file, rev, runGit = realGit) {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
70
|
+
const result = showAtRevision(runGit, file, resolve(file), rev);
|
|
71
|
+
if (result.ok)
|
|
72
|
+
return result;
|
|
73
|
+
switch (result.reason) {
|
|
74
|
+
case "no-repo":
|
|
75
|
+
return { ok: false, message: `em diff: ${file} is not inside a git repository (needed for --from/--to)` };
|
|
76
|
+
case "not-tracked":
|
|
77
|
+
return { ok: false, message: `em diff: ${file} is not tracked by git in ${result.repoRoot}` };
|
|
78
|
+
case "show-failed":
|
|
79
|
+
return { ok: false, message: `em diff: cannot read ${result.relPath} at revision "${rev}": ${result.detail}` };
|
|
46
80
|
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolve a slice's doc (`slices/<sliceKey>.md`, relative to `anchorFile`'s directory — same
|
|
84
|
+
* convention as every other doc/note path in em) at git revision `rev`. Unlike
|
|
85
|
+
* `resolveRevision`, a missing doc at a revision is routine (not every slice had a doc yet, or
|
|
86
|
+
* the doc simply isn't tracked) rather than a user-facing error — so every failure mode
|
|
87
|
+
* collapses to `null`, for `em diff`'s lineage annotation (MIL-84) to treat as "nothing to
|
|
88
|
+
* annotate with" rather than aborting the diff.
|
|
89
|
+
*/
|
|
90
|
+
export function resolveDocAtRevision(anchorFile, sliceKey, rev, runGit = realGit) {
|
|
91
|
+
const targetAbs = resolve(dirname(resolve(anchorFile)), "slices", `${sliceKey}.md`);
|
|
92
|
+
const result = showAtRevision(runGit, anchorFile, targetAbs, rev);
|
|
93
|
+
return result.ok ? parseSliceDoc(result.content) : null;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Slice doc keys (filename stems under `slices/`, relative to `anchorFile`'s directory — same
|
|
97
|
+
* anchor convention as `resolveDocAtRevision`) present at git revision `rev`. Used by `em ledger`
|
|
98
|
+
* (MIL-89) to discover the candidate set of docs to compare across two revisions, reusing the
|
|
99
|
+
* same repo-root resolution `showAtRevision` does rather than a second, divergent way of finding
|
|
100
|
+
* "what slice docs exist here." Returns `[]` (not an error) when the repo can't be resolved or
|
|
101
|
+
* `slices/` didn't exist at `rev` — both routine (a model with no slice docs yet), matching
|
|
102
|
+
* `resolveDocAtRevision`'s "missing is routine" stance.
|
|
103
|
+
*/
|
|
104
|
+
export function listSliceKeysAtRevision(anchorFile, rev, runGit = realGit) {
|
|
105
|
+
const toplevel = runGit(["-C", dirname(resolve(anchorFile)), "rev-parse", "--show-toplevel"]);
|
|
106
|
+
if (toplevel.status !== 0)
|
|
107
|
+
return [];
|
|
47
108
|
const repoRoot = toplevel.stdout.trim();
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
if (
|
|
51
|
-
return
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
message: `em diff: cannot read ${relPath} at revision "${rev}": ${(show.stderr || "").trim() || "unknown git error"}`,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
return { ok: true, content: show.stdout };
|
|
109
|
+
const slicesDirAbs = resolve(dirname(resolve(anchorFile)), "slices");
|
|
110
|
+
const lsTree = runGit(["-C", repoRoot, "ls-tree", "-r", "--name-only", rev, "--", slicesDirAbs]);
|
|
111
|
+
if (lsTree.status !== 0)
|
|
112
|
+
return [];
|
|
113
|
+
return lsTree.stdout
|
|
114
|
+
.split("\n")
|
|
115
|
+
.map((line) => line.trim())
|
|
116
|
+
.filter((line) => line.endsWith(".md"))
|
|
117
|
+
.map((line) => basename(line, ".md"));
|
|
61
118
|
}
|
|
62
119
|
//# sourceMappingURL=diff-inputs.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diff-inputs.js","sourceRoot":"","sources":["../../src/cli/diff-inputs.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,sEAAsE;AACtE,gFAAgF;AAChF,iFAAiF;AACjF,8DAA8D;AAE9D,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"diff-inputs.js","sourceRoot":"","sources":["../../src/cli/diff-inputs.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,sEAAsE;AACtE,gFAAgF;AAChF,iFAAiF;AACjF,8DAA8D;AAE9D,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACvD,OAAO,EAAE,aAAa,EAAY,MAAM,wBAAwB,CAAC;AAQjE;;;GAGG;AACH,MAAM,UAAU,YAAY,CAC1B,OAAe,EACf,OAA2B,EAC3B,IAAoC;IAEpC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QACd,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO;gBACL,KAAK,EAAE,yFAAyF;aACjG,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC;IACtE,CAAC;IACD,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC;QACZ,OAAO,EAAE,KAAK,EAAE,+BAA+B,EAAE,CAAC;IACpD,CAAC;IACD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO;YACL,KAAK,EAAE,oFAAoF;SAC5F,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AAC7C,CAAC;AAUD,kFAAkF;AAClF,MAAM,CAAC,MAAM,OAAO,GAAc,CAAC,IAAI,EAAE,EAAE;IACzC,MAAM,CAAC,GAAG,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;IACvD,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC;AAC9E,CAAC,CAAC;AAWF;;;;;;;;;;;GAWG;AACH,SAAS,cAAc,CAAC,MAAiB,EAAE,UAAkB,EAAE,SAAiB,EAAE,GAAW;IAC3F,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,EAAE,WAAW,EAAE,iBAAiB,CAAC,CAAC,CAAC;IAC9F,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IACnE,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IACxC,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC;IAC9F,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IACpD,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC;IACpE,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,GAAG,IAAI,OAAO,EAAE,CAAC,CAAC,CAAC;IACnE,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,mBAAmB,EAAE,CAAC;IAClH,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;AAC5C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,GAAW,EAAE,SAAoB,OAAO;IACpF,MAAM,MAAM,GAAG,cAAc,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC;IAChE,IAAI,MAAM,CAAC,EAAE;QAAE,OAAO,MAAM,CAAC;IAC7B,QAAQ,MAAM,CAAC,MAAM,EAAE,CAAC;QACtB,KAAK,SAAS;YACZ,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,IAAI,0DAA0D,EAAE,CAAC;QAC5G,KAAK,aAAa;YAChB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,IAAI,6BAA6B,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;QAChG,KAAK,aAAa;YAChB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,wBAAwB,MAAM,CAAC,OAAO,iBAAiB,GAAG,MAAM,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC;IACnH,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAAC,UAAkB,EAAE,QAAgB,EAAE,GAAW,EAAE,SAAoB,OAAO;IACjH,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,EAAE,QAAQ,EAAE,GAAG,QAAQ,KAAK,CAAC,CAAC;IACpF,MAAM,MAAM,GAAG,cAAc,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,CAAC,CAAC;IAClE,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAC1D,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,uBAAuB,CAAC,UAAkB,EAAE,GAAW,EAAE,SAAoB,OAAO;IAClG,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,EAAE,WAAW,EAAE,iBAAiB,CAAC,CAAC,CAAC;IAC9F,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACrC,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IACxC,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IACrE,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC;IACjG,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACnC,OAAO,MAAM,CAAC,MAAM;SACjB,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;SAC1B,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;SACtC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;AAC1C,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { GitRunner } from "./diff-inputs.js";
|
|
2
|
+
export type LedgerFindingCode = "ledger-content-without-version-bump" | "ledger-version-without-content-change" | "ledger-version-regression";
|
|
3
|
+
export interface LedgerFinding {
|
|
4
|
+
sliceKey: string;
|
|
5
|
+
code: LedgerFindingCode;
|
|
6
|
+
message: string;
|
|
7
|
+
oldVersion: number;
|
|
8
|
+
newVersion: number;
|
|
9
|
+
bodyChanged: boolean;
|
|
10
|
+
lineageChanged: boolean;
|
|
11
|
+
}
|
|
12
|
+
export type LedgerSkipReason = "no-prior-revision" | "deleted" | "frontmatter-invalid";
|
|
13
|
+
export interface LedgerSkip {
|
|
14
|
+
sliceKey: string;
|
|
15
|
+
reason: LedgerSkipReason;
|
|
16
|
+
}
|
|
17
|
+
export interface LedgerCheckResult {
|
|
18
|
+
findings: LedgerFinding[];
|
|
19
|
+
skipped: LedgerSkip[];
|
|
20
|
+
/** Total slice keys considered (the union of both revisions' `slices/*.md` — every key that
|
|
21
|
+
* produced either a finding, a skip, or a clean pass). Lets a caller report "N checked"
|
|
22
|
+
* without re-deriving it from findings.length + skipped.length + (silent clean count). */
|
|
23
|
+
checkedCount: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Compare every slice doc's version:/content agreement between two revisions of `anchorFile`'s
|
|
27
|
+
* repo. `to` null means "the current working tree" (same convention as `em diff`). Pure aside
|
|
28
|
+
* from the injected GitRunner and fs reads via readSliceDoc — no process.exit, no console.
|
|
29
|
+
*
|
|
30
|
+
* Deliberately excludes `status`/`implementedIn` from the content comparison: those change
|
|
31
|
+
* independently by design during re-ratification (docs/slice-doc-schema.md, "`status` under
|
|
32
|
+
* re-ratification") — including them would false-positive on every ordinary lifecycle
|
|
33
|
+
* transition. Also excludes `pattern`/`swimlane`/`schemaVersion`: decorative, not currently
|
|
34
|
+
* exposed on `SliceDoc` at all. Body comparison is `.trim()`-only — strips leading/trailing
|
|
35
|
+
* whitespace, avoiding a false positive from a trailing-newline/CRLF-only re-save, without
|
|
36
|
+
* risking a false negative on a real prose edit.
|
|
37
|
+
*/
|
|
38
|
+
export declare function checkLedger(anchorFile: string, from: string, to: string | null, runGit?: GitRunner): LedgerCheckResult;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// `em ledger` (MIL-89): checks that a slice doc's `version:` frontmatter field and its content
|
|
3
|
+
// (body + lineage refs) always change together across two git revisions — a version bump with
|
|
4
|
+
// no real content change is a no-op ledger entry, a content change with no version bump is a
|
|
5
|
+
// stale ratification signal, and a version going backwards is a typo/regression. Opt-in,
|
|
6
|
+
// CI-recipe tier — never folded into `em validate`, which stays a fast function of the current
|
|
7
|
+
// tree (see lineageValidate.ts's header comment, which named this exact shape during MIL-84).
|
|
8
|
+
//
|
|
9
|
+
// Sibling to diff-inputs.ts in role: testable input handling / git-revision logic factored out
|
|
10
|
+
// of cli.ts, with an injectable GitRunner and no process.exit/console — see
|
|
11
|
+
// test/ledgerCheck.test.ts. Reuses resolveDocAtRevision/listSliceKeysAtRevision rather than
|
|
12
|
+
// second, divergent git plumbing.
|
|
13
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
14
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
15
|
+
import { hasUsableFrontmatter } from "../catalog/sliceDoc.js";
|
|
16
|
+
import { readSliceDoc } from "../catalog/readSliceDoc.js";
|
|
17
|
+
import { realGit, listSliceKeysAtRevision, resolveDocAtRevision } from "./diff-inputs.js";
|
|
18
|
+
/** Slice keys currently on disk under `<baseDir>/slices/*.md` — the working-tree counterpart to
|
|
19
|
+
* `listSliceKeysAtRevision`, used when `to` is null (compare against the working tree). */
|
|
20
|
+
function listSliceKeysOnDisk(baseDir) {
|
|
21
|
+
const dir = join(baseDir, "slices");
|
|
22
|
+
if (!existsSync(dir))
|
|
23
|
+
return [];
|
|
24
|
+
return readdirSync(dir)
|
|
25
|
+
.filter((name) => name.endsWith(".md"))
|
|
26
|
+
.map((name) => basename(name, ".md"));
|
|
27
|
+
}
|
|
28
|
+
/** True when two lineage refs (or their absence) carry the same declared text — compared on
|
|
29
|
+
* `raw` so a malformed ref (null sliceKey/version) still counts as a real change if its text
|
|
30
|
+
* changes, not silently treated as equal to another malformed ref. */
|
|
31
|
+
function refEqual(a, b) {
|
|
32
|
+
if (a === null || b === null)
|
|
33
|
+
return a === b;
|
|
34
|
+
return a.raw === b.raw;
|
|
35
|
+
}
|
|
36
|
+
function refListEqual(a, b) {
|
|
37
|
+
if (a.length !== b.length)
|
|
38
|
+
return false;
|
|
39
|
+
return a.every((ref, i) => refEqual(ref, b[i]));
|
|
40
|
+
}
|
|
41
|
+
/** True when either side's declared lineage (`split-from`/`merged-from`/`superseded-by`)
|
|
42
|
+
* differs — these describe what the doc *asserts*, same-commit-authoring convention means a
|
|
43
|
+
* legitimate change here always co-occurs with a version bump (docs/slice-doc-schema.md). */
|
|
44
|
+
function lineageChanged(oldDoc, newDoc) {
|
|
45
|
+
return (!refEqual(oldDoc.splitFrom, newDoc.splitFrom) ||
|
|
46
|
+
!refListEqual(oldDoc.mergedFrom, newDoc.mergedFrom) ||
|
|
47
|
+
!refListEqual(oldDoc.supersededBy, newDoc.supersededBy));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Compare every slice doc's version:/content agreement between two revisions of `anchorFile`'s
|
|
51
|
+
* repo. `to` null means "the current working tree" (same convention as `em diff`). Pure aside
|
|
52
|
+
* from the injected GitRunner and fs reads via readSliceDoc — no process.exit, no console.
|
|
53
|
+
*
|
|
54
|
+
* Deliberately excludes `status`/`implementedIn` from the content comparison: those change
|
|
55
|
+
* independently by design during re-ratification (docs/slice-doc-schema.md, "`status` under
|
|
56
|
+
* re-ratification") — including them would false-positive on every ordinary lifecycle
|
|
57
|
+
* transition. Also excludes `pattern`/`swimlane`/`schemaVersion`: decorative, not currently
|
|
58
|
+
* exposed on `SliceDoc` at all. Body comparison is `.trim()`-only — strips leading/trailing
|
|
59
|
+
* whitespace, avoiding a false positive from a trailing-newline/CRLF-only re-save, without
|
|
60
|
+
* risking a false negative on a real prose edit.
|
|
61
|
+
*/
|
|
62
|
+
export function checkLedger(anchorFile, from, to, runGit = realGit) {
|
|
63
|
+
const baseDir = dirname(resolve(anchorFile));
|
|
64
|
+
const oldKeys = listSliceKeysAtRevision(anchorFile, from, runGit);
|
|
65
|
+
const newKeys = to ? listSliceKeysAtRevision(anchorFile, to, runGit) : listSliceKeysOnDisk(baseDir);
|
|
66
|
+
const keys = [...new Set([...oldKeys, ...newKeys])].sort();
|
|
67
|
+
const findings = [];
|
|
68
|
+
const skipped = [];
|
|
69
|
+
for (const sliceKey of keys) {
|
|
70
|
+
const oldDoc = resolveDocAtRevision(anchorFile, sliceKey, from, runGit);
|
|
71
|
+
if (!oldDoc) {
|
|
72
|
+
skipped.push({ sliceKey, reason: "no-prior-revision" });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const newDoc = to ? resolveDocAtRevision(anchorFile, sliceKey, to, runGit) : readSliceDoc(baseDir, sliceKey);
|
|
76
|
+
if (!newDoc) {
|
|
77
|
+
skipped.push({ sliceKey, reason: "deleted" });
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
// hasUsableFrontmatter() only checks that the `version` key is *present*
|
|
81
|
+
// (missingRequiredFields), not that its value parsed to a valid positive integer —
|
|
82
|
+
// `version: abc`/`version: 0`/`version: 1.5` all pass that gate with SliceDoc.version still
|
|
83
|
+
// null (see sliceDoc.ts's parseVersion()). lineageValidate.ts hits the same gap and handles
|
|
84
|
+
// it the same way: an explicit null check, routed to the same skip a missing key gets,
|
|
85
|
+
// rather than a non-null assertion that would corrupt a finding's message/classification.
|
|
86
|
+
if (!hasUsableFrontmatter(oldDoc) || !hasUsableFrontmatter(newDoc) || oldDoc.version === null || newDoc.version === null) {
|
|
87
|
+
skipped.push({ sliceKey, reason: "frontmatter-invalid" });
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const oldVersion = oldDoc.version;
|
|
91
|
+
const newVersion = newDoc.version;
|
|
92
|
+
const bodyChanged = oldDoc.body.trim() !== newDoc.body.trim();
|
|
93
|
+
const docsLineageChanged = lineageChanged(oldDoc, newDoc);
|
|
94
|
+
const contentChanged = bodyChanged || docsLineageChanged;
|
|
95
|
+
let code = null;
|
|
96
|
+
if (newVersion < oldVersion) {
|
|
97
|
+
code = "ledger-version-regression";
|
|
98
|
+
}
|
|
99
|
+
else if (contentChanged && newVersion === oldVersion) {
|
|
100
|
+
code = "ledger-content-without-version-bump";
|
|
101
|
+
}
|
|
102
|
+
else if (!contentChanged && newVersion > oldVersion) {
|
|
103
|
+
code = "ledger-version-without-content-change";
|
|
104
|
+
}
|
|
105
|
+
if (!code)
|
|
106
|
+
continue;
|
|
107
|
+
const message = code === "ledger-version-regression"
|
|
108
|
+
? `slice "${sliceKey}": version went backwards (v${oldVersion} -> v${newVersion})`
|
|
109
|
+
: code === "ledger-content-without-version-bump"
|
|
110
|
+
? `slice "${sliceKey}": doc content changed but version: didn't bump (still v${oldVersion})`
|
|
111
|
+
: `slice "${sliceKey}": version: bumped (v${oldVersion} -> v${newVersion}) but doc content is unchanged`;
|
|
112
|
+
findings.push({ sliceKey, code, message, oldVersion, newVersion, bodyChanged, lineageChanged: docsLineageChanged });
|
|
113
|
+
}
|
|
114
|
+
return { findings, skipped, checkedCount: keys.length };
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=ledgerCheck.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ledgerCheck.js","sourceRoot":"","sources":["../../src/cli/ledgerCheck.ts"],"names":[],"mappings":"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"}
|