@milehimikey/em 1.12.0 → 1.13.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 +5 -2
- package/.claude/skills/event-modeling-conform/SKILL.md +20 -9
- package/.claude/skills/event-modeling-conform/reference/conform.md +107 -10
- package/.claude/skills/event-modeling-design/SKILL.md +26 -4
- package/.claude/skills/event-modeling-discover/SKILL.md +1 -1
- package/.claude/skills/event-modeling-implement/SKILL.md +7 -4
- package/.claude/skills/event-modeling-implement/reference/implement.md +26 -8
- package/.claude/skills/event-modeling-review/SKILL.md +5 -1
- package/.claude/skills/event-modeling-shared/reference/em-dsl.md +45 -6
- package/.claude/skills/event-modeling-shared/reference/operating-principles.md +13 -3
- package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +39 -0
- package/.claude/skills/event-modeling-shared/templates/conformance-report.md +8 -1
- package/.claude/skills/event-modeling-shared/templates/state.md +3 -0
- package/README.md +1 -0
- package/dist/catalog/docJoin.d.ts +9 -0
- package/dist/catalog/docJoin.js +6 -0
- package/dist/catalog/docJoin.js.map +1 -1
- package/dist/catalog/driftSignal.d.ts +11 -5
- package/dist/catalog/driftSignal.js +18 -4
- package/dist/catalog/driftSignal.js.map +1 -1
- package/dist/catalog/modelVersionValidate.d.ts +9 -0
- package/dist/catalog/modelVersionValidate.js +38 -0
- package/dist/catalog/modelVersionValidate.js.map +1 -0
- package/dist/catalog/sliceDoc.d.ts +12 -0
- package/dist/catalog/sliceDoc.js +15 -0
- package/dist/catalog/sliceDoc.js.map +1 -1
- package/dist/cli/ciInit.d.ts +24 -0
- package/dist/cli/ciInit.js +85 -15
- package/dist/cli/ciInit.js.map +1 -1
- package/dist/cli/conformScope.d.ts +1 -0
- package/dist/cli/conformScope.js +3 -1
- package/dist/cli/conformScope.js.map +1 -1
- package/dist/cli/conformSupersede.d.ts +24 -2
- package/dist/cli/conformSupersede.js +44 -3
- package/dist/cli/conformSupersede.js.map +1 -1
- package/dist/cli/coverage.d.ts +16 -2
- package/dist/cli/coverage.js +26 -4
- package/dist/cli/coverage.js.map +1 -1
- package/dist/cli/findings.d.ts +134 -0
- package/dist/cli/findings.js +355 -0
- package/dist/cli/findings.js.map +1 -0
- package/dist/cli/metrics.d.ts +93 -0
- package/dist/cli/metrics.js +331 -0
- package/dist/cli/metrics.js.map +1 -0
- package/dist/cli/modelVersion.d.ts +139 -0
- package/dist/cli/modelVersion.js +256 -0
- package/dist/cli/modelVersion.js.map +1 -0
- package/dist/cli/reratify.d.ts +24 -1
- package/dist/cli/reratify.js +73 -17
- package/dist/cli/reratify.js.map +1 -1
- package/dist/cli/skillCheck.js +2 -2
- package/dist/cli/skillCheck.js.map +1 -1
- package/dist/cli/sliceConform.d.ts +50 -0
- package/dist/cli/sliceConform.js +206 -0
- package/dist/cli/sliceConform.js.map +1 -0
- package/dist/cli/sliceIndex.d.ts +2 -0
- package/dist/cli/sliceIndex.js +15 -3
- package/dist/cli/sliceIndex.js.map +1 -1
- package/dist/cli/sliceNew.d.ts +12 -0
- package/dist/cli/sliceNew.js +23 -0
- package/dist/cli/sliceNew.js.map +1 -1
- package/dist/cli/sliceStubAll.d.ts +76 -0
- package/dist/cli/sliceStubAll.js +189 -0
- package/dist/cli/sliceStubAll.js.map +1 -0
- package/dist/cli/stateFile.d.ts +63 -7
- package/dist/cli/stateFile.js +179 -18
- package/dist/cli/stateFile.js.map +1 -1
- package/dist/cli/status.d.ts +79 -1
- package/dist/cli/status.js +134 -15
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/upgrade.d.ts +132 -0
- package/dist/cli/upgrade.js +524 -0
- package/dist/cli/upgrade.js.map +1 -0
- package/dist/cli.js +609 -50
- package/dist/cli.js.map +1 -1
- package/dist/emit/coverageJson.d.ts +5 -2
- package/dist/emit/coverageJson.js +6 -2
- package/dist/emit/coverageJson.js.map +1 -1
- package/dist/emit/freshnessJson.d.ts +1 -1
- package/dist/emit/freshnessJson.js +3 -1
- package/dist/emit/freshnessJson.js.map +1 -1
- package/dist/emit/json.d.ts +11 -1
- package/dist/emit/json.js +35 -1
- package/dist/emit/json.js.map +1 -1
- package/dist/emit/metricsJson.d.ts +7 -0
- package/dist/emit/metricsJson.js +27 -0
- package/dist/emit/metricsJson.js.map +1 -0
- package/dist/emit/statusJson.d.ts +1 -1
- package/dist/emit/statusJson.js +17 -1
- package/dist/emit/statusJson.js.map +1 -1
- package/dist/emit/upgradeJson.d.ts +5 -0
- package/dist/emit/upgradeJson.js +29 -0
- package/dist/emit/upgradeJson.js.map +1 -0
- package/dist/emit/validateJson.d.ts +6 -2
- package/dist/emit/validateJson.js +12 -5
- package/dist/emit/validateJson.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +137 -9
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/rules.d.ts +7 -0
- package/dist/model/rules.js +12 -0
- package/dist/model/rules.js.map +1 -1
- package/dist/model/validate.d.ts +20 -0
- package/dist/model/validate.js +94 -45
- package/dist/model/validate.js.map +1 -1
- package/dist/templates.d.ts +2 -2
- package/dist/templates.js +7 -1
- package/dist/templates.js.map +1 -1
- package/dist/util/semver.d.ts +19 -0
- package/dist/util/semver.js +54 -0
- package/dist/util/semver.js.map +1 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -15,8 +15,8 @@ import { buildSliceDiagram } from "./render/sliceDiagram.js";
|
|
|
15
15
|
import { resolveSliceArg, defaultSliceOut } from "./cli/render-inputs.js";
|
|
16
16
|
import { serializeBuilds, watchFile } from "./render/watch.js";
|
|
17
17
|
import { startLiveServer } from "./render/serve.js";
|
|
18
|
-
import { formatDiagnostic, hasErrors } from "./model/validate.js";
|
|
19
|
-
import { buildExport, buildSliceExport } from "./emit/json.js";
|
|
18
|
+
import { formatDiagnostic, hasErrors, publicViewsWithoutInModelReader } from "./model/validate.js";
|
|
19
|
+
import { buildExport, buildSliceExport, GENERATOR_VERSION } from "./emit/json.js";
|
|
20
20
|
import { buildTypeSpec } from "./emit/typespec.js";
|
|
21
21
|
import { buildValidateJson, buildSliceReadyJson, buildValidateListJson, collectMarkers } from "./emit/validateJson.js";
|
|
22
22
|
import { buildDiffJson } from "./emit/diffJson.js";
|
|
@@ -28,16 +28,19 @@ import { validateFrontmatterCoherence } from "./catalog/frontmatterCoherenceVali
|
|
|
28
28
|
import { validateNoteBindings } from "./catalog/noteBindingValidate.js";
|
|
29
29
|
import { validateDocModelConsistency } from "./catalog/docModelConsistencyValidate.js";
|
|
30
30
|
import { validateOrphanedSliceDocs } from "./catalog/orphanedSliceDocValidate.js";
|
|
31
|
+
import { validateModelVersionStale } from "./catalog/modelVersionValidate.js";
|
|
31
32
|
import { validateSliceReady, computeSliceReadyGates } from "./catalog/sliceReadyValidate.js";
|
|
32
33
|
import { detectSliceDocCollisions } from "./catalog/modelCollisionValidate.js";
|
|
33
34
|
import { checkLedger, readLedgerWaiverTrailers, applyLedgerWaivers } from "./cli/ledgerCheck.js";
|
|
34
35
|
import { planMigration, verifyMigration } from "./cli/migrateReactionShape.js";
|
|
35
36
|
import { buildLedgerJson } from "./emit/ledgerJson.js";
|
|
36
|
-
import { buildCoverageReport, resolveScopedSlices } from "./cli/coverage.js";
|
|
37
|
+
import { buildCoverageReport, resolveScopedSlices, resolveCoverageSliceKey } from "./cli/coverage.js";
|
|
37
38
|
import { buildCoverageJson } from "./emit/coverageJson.js";
|
|
38
|
-
import { resolveSliceStatusFacts, countOpenIssues, resolveConformanceEntry, aggregateInvariantTotals, buildStatusReport, formatStatusText, formatStatusMarkdown, buildStatusBadge, formatConformancePart, findSpecifyRoot, } from "./cli/status.js";
|
|
39
|
+
import { resolveSliceStatusFacts, countOpenIssues, resolveConformanceEntry, aggregateInvariantTotals, buildStatusReport, formatStatusText, formatStatusMarkdown, buildStatusBadge, formatConformancePart, findSpecifyRoot, resolveModelVersionStatusEntry, resolveEmVersionStatusEntry, } from "./cli/status.js";
|
|
39
40
|
import { buildStatusJson } from "./emit/statusJson.js";
|
|
40
41
|
import { buildFreshnessJson } from "./emit/freshnessJson.js";
|
|
42
|
+
import { computeMetrics, formatMetricsText } from "./cli/metrics.js";
|
|
43
|
+
import { buildMetricsJson } from "./emit/metricsJson.js";
|
|
41
44
|
import { planSkillSyncBundle, applySkillSyncBundle } from "./cli/skillSync.js";
|
|
42
45
|
import { checkSkillSyncBundle } from "./cli/skillCheck.js";
|
|
43
46
|
import { buildSkillCheckJson } from "./emit/skillCheckJson.js";
|
|
@@ -57,15 +60,22 @@ import { runRatify } from "./cli/ratify.js";
|
|
|
57
60
|
import { runReview } from "./cli/review.js";
|
|
58
61
|
import { runConformSupersede } from "./cli/conformSupersede.js";
|
|
59
62
|
import { buildConformScope, changedPathsSince, resolveSliceDocFacts, seedAsisModel } from "./cli/conformScope.js";
|
|
60
|
-
import { buildSliceDocContent, isSlicePattern, sliceDocKey, SLICE_PATTERNS } from "./cli/sliceNew.js";
|
|
63
|
+
import { buildSliceDocContent, buildStubDocContent, isSlicePattern, sliceDocKey, SLICE_PATTERNS } from "./cli/sliceNew.js";
|
|
61
64
|
import { wireSliceNote } from "./cli/sliceLink.js";
|
|
65
|
+
import { isStubStatus, runStubAll, STUB_STATUSES } from "./cli/sliceStubAll.js";
|
|
62
66
|
import { listModelCommits } from "./cli/changelog-git.js";
|
|
63
67
|
import { buildChangelogDoc } from "./cli/changelogBuild.js";
|
|
64
68
|
import { runReratify } from "./cli/reratify.js";
|
|
65
|
-
import {
|
|
69
|
+
import { runSliceConform } from "./cli/sliceConform.js";
|
|
70
|
+
import { checkFindingsFile, buildCheckFindingsJson, lookupFindingsBesideReport, unruledFindingsInScope, FINDING_LOCI } from "./cli/findings.js";
|
|
71
|
+
import { STATE_FILE_NAME, PHASES, isPhase, loadStateFile, parseState, setPhase, setConformance, setModelVersion, setCertified, setEmVersion, setReview, isValidDateString, modelPathMismatch, } from "./cli/stateFile.js";
|
|
72
|
+
import { findCertifiedVersion, latestModelVersionNumber, modelVersionDrift, runCertifyModelVersion, runModelVersionBump, } from "./cli/modelVersion.js";
|
|
66
73
|
import { STARTER_EM, starterEmFor, scaffoldReadme, scaffoldStateFile, scaffoldConstitution } from "./templates.js";
|
|
67
74
|
import { kebabSlug } from "./util/slug.js";
|
|
68
75
|
import { ciWorkflowPath, conformWorkflowPath, buildCiWorkflowFile, buildConformWorkflowFile, ciManagedBody, conformManagedBody, planCiFile, applyCiFile, findUnsafeCiInitArg, CI_WORKFLOW_MARKER, CONFORM_WORKFLOW_MARKER, } from "./cli/ciInit.js";
|
|
76
|
+
import { resolveRepoRoot, detectUpgrade, applyUpgrade, checkUpgrade } from "./cli/upgrade.js";
|
|
77
|
+
import { buildUpgradeJson } from "./emit/upgradeJson.js";
|
|
78
|
+
import { isOlder } from "./util/semver.js";
|
|
69
79
|
import { RULES } from "./model/rules.js";
|
|
70
80
|
import { USAGE_PHASES, isUsagePhase, sortUsagePhases, appendUsageLogEntry, findStateFiles, aggregateUsageReport, formatUsageReportText, } from "./cli/usageLog.js";
|
|
71
81
|
import { buildUsageReportJson } from "./emit/usageReportJson.js";
|
|
@@ -126,7 +136,7 @@ program
|
|
|
126
136
|
const today = localIsoDate();
|
|
127
137
|
writeFileSync(join(dirPath, `${slugName}.em`), starterEmFor(name));
|
|
128
138
|
writeFileSync(join(dirPath, "README.md"), scaffoldReadme(name, slugName));
|
|
129
|
-
writeFileSync(join(dirPath, STATE_FILE_NAME), scaffoldStateFile(name, slugName, today));
|
|
139
|
+
writeFileSync(join(dirPath, STATE_FILE_NAME), scaffoldStateFile(name, slugName, today, PKG_VERSION));
|
|
130
140
|
// MIL-202: the implementation constitution. One document per project, never two — in a
|
|
131
141
|
// spec-kit project `.specify/memory/constitution.md` already IS that slot, so em defers to it
|
|
132
142
|
// and writes nothing rather than seeding a second, competing copy beside the model. Same
|
|
@@ -437,6 +447,9 @@ slice
|
|
|
437
447
|
.option("-f, --force", "overwrite the file if it already exists")
|
|
438
448
|
.option("--wire <model-file>", "also insert the `note \"slices/<key>.md\"` line onto the slice's primary element in this " +
|
|
439
449
|
".em file (matched by export key), instead of just printing it to paste by hand (MIL-161)")
|
|
450
|
+
.option("--stub", "write a near-free stub instead: same 5 frontmatter keys, but a one-line placeholder body " +
|
|
451
|
+
"instead of the diagram-image stub and every judgment section (MIL-184) — deepen it later " +
|
|
452
|
+
"by re-running without --stub and -f")
|
|
440
453
|
.action((name, opts) => {
|
|
441
454
|
if (!isSlicePattern(opts.pattern)) {
|
|
442
455
|
console.error(`em slice new: invalid --pattern "${opts.pattern}" — expected one of: ${SLICE_PATTERNS.join(", ")}`);
|
|
@@ -468,7 +481,10 @@ slice
|
|
|
468
481
|
wired = result;
|
|
469
482
|
}
|
|
470
483
|
mkdirSync(dir, { recursive: true });
|
|
471
|
-
|
|
484
|
+
const content = opts.stub
|
|
485
|
+
? buildStubDocContent(name, opts.pattern, opts.swimlane)
|
|
486
|
+
: buildSliceDocContent(name, key, opts.pattern, opts.swimlane);
|
|
487
|
+
writeFileSync(path, content);
|
|
472
488
|
console.log(`wrote ${path}`);
|
|
473
489
|
if (wired) {
|
|
474
490
|
writeFileSync(opts.wire, wired.content);
|
|
@@ -479,6 +495,59 @@ slice
|
|
|
479
495
|
console.log(` note "${path}"`);
|
|
480
496
|
}
|
|
481
497
|
});
|
|
498
|
+
slice
|
|
499
|
+
.command("stub-all")
|
|
500
|
+
.description("scaffold + wire a near-free stub (`em slice new --stub`'s content, MIL-184) for every " +
|
|
501
|
+
"slice in <file> with no resolvable doc — the fast path to status coloring for an " +
|
|
502
|
+
"exploratory/backbone model without hand-running `slice new` per slice. Skips a " +
|
|
503
|
+
"continuation slice (MIL-208, it has no doc of its own), an already-documented slice, and " +
|
|
504
|
+
"a slice whose pattern can't be classified")
|
|
505
|
+
.argument("<file>", "input .em file")
|
|
506
|
+
.option("--status <status>", `target status for every stub: ${STUB_STATUSES.join(" | ")}`, "draft")
|
|
507
|
+
.option("--by <name>", "identity for reviewedBy/ratifiedBy — required unless --status draft")
|
|
508
|
+
.option("--implemented-in <url>", "PR/commit URL for implementedIn — required with --status implemented")
|
|
509
|
+
.option("--dry-run", "list what would be stubbed/wired without writing anything")
|
|
510
|
+
.action((file, opts) => {
|
|
511
|
+
if (!isStubStatus(opts.status)) {
|
|
512
|
+
console.error(`em slice stub-all: invalid --status "${opts.status}" — expected one of: ${STUB_STATUSES.join(", ")}`);
|
|
513
|
+
process.exit(1);
|
|
514
|
+
}
|
|
515
|
+
if (opts.status !== "draft" && !opts.by) {
|
|
516
|
+
console.error(`em slice stub-all: --status ${opts.status} requires --by <name>`);
|
|
517
|
+
process.exit(1);
|
|
518
|
+
}
|
|
519
|
+
if (opts.status === "implemented" && !opts.implementedIn) {
|
|
520
|
+
console.error("em slice stub-all: --status implemented requires --implemented-in <url>");
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
const { model, refs, diagnostics, source } = compileFile(file);
|
|
524
|
+
printDiagnostics(diagnostics);
|
|
525
|
+
if (hasErrors(diagnostics)) {
|
|
526
|
+
console.error("em slice stub-all: not stubbing — fix the errors above");
|
|
527
|
+
process.exit(1);
|
|
528
|
+
}
|
|
529
|
+
mkdirSync(join(dirname(file), "slices"), { recursive: true });
|
|
530
|
+
const result = runStubAll(model, refs, dirname(file), source, {
|
|
531
|
+
status: opts.status,
|
|
532
|
+
by: opts.by ?? null,
|
|
533
|
+
on: localIsoDate(),
|
|
534
|
+
implementedInUrl: opts.implementedIn ?? null,
|
|
535
|
+
dryRun: opts.dryRun === true,
|
|
536
|
+
});
|
|
537
|
+
for (const outcome of result.outcomes) {
|
|
538
|
+
if (outcome.kind === "skip") {
|
|
539
|
+
console.log(`skip ${outcome.sliceKey} — ${outcome.reason}`);
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
const verb = opts.dryRun ? "would stub" : "stubbed";
|
|
543
|
+
const wireNote = outcome.wired ? `wired onto ${outcome.elementName}` : "already wired";
|
|
544
|
+
console.log(`${verb} ${outcome.path} (${outcome.sliceKey}, pattern: ${outcome.pattern}, swimlane: "${outcome.swimlane}", ` +
|
|
545
|
+
`status: ${outcome.status}, ${wireNote})`);
|
|
546
|
+
}
|
|
547
|
+
if (!opts.dryRun && result.source !== source) {
|
|
548
|
+
writeFileSync(file, result.source);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
482
551
|
slice
|
|
483
552
|
.command("index")
|
|
484
553
|
.description("rewrite the model's sibling README.md's GENERATED Slices table from `em export`'s slice " +
|
|
@@ -577,8 +646,12 @@ slice
|
|
|
577
646
|
.command("ratify")
|
|
578
647
|
.description("flip a slice doc's frontmatter to `status: ready-to-implement` and record `ratifiedBy:`/" +
|
|
579
648
|
"`ratifiedOn:` — the handoff sign-off (MIL-165, docs/process.md#the-slice-lifecycle-gates) " +
|
|
580
|
-
"that makes who ratified, and when, a first-class recorded fact.
|
|
581
|
-
"
|
|
649
|
+
"that makes who ratified, and when, a first-class recorded fact. The review gate is a check " +
|
|
650
|
+
"on the doc's current `status:`, not on whether `reviewedBy:`/`reviewedOn:` are populated: " +
|
|
651
|
+
"a doc at `reviewed` or `ready-to-implement` passes (the latter is where `em slice " +
|
|
652
|
+
"reratify` leaves a doc, so the follow-up ratify needs no --skip-review); any other status " +
|
|
653
|
+
"— a `draft` that never went through `em slice review` — is refused unless --skip-review " +
|
|
654
|
+
"is passed. Idempotent on " +
|
|
582
655
|
"the same --by/--on pair; refuses to overwrite a different one already recorded; never " +
|
|
583
656
|
"touches `version:` or the doc body")
|
|
584
657
|
.argument("<file>", "input .em file")
|
|
@@ -587,7 +660,7 @@ slice
|
|
|
587
660
|
.option("--on <date>", "ratification date, YYYY-MM-DD (default: today, local date)")
|
|
588
661
|
.option("--skip-review", "ratify without a recorded review — prints a loud notice on stderr")
|
|
589
662
|
.action((file, sliceKey, opts) => {
|
|
590
|
-
const { model, refs, diagnostics } = compileFile(file);
|
|
663
|
+
const { model, refs, diagnostics, source } = compileFile(file);
|
|
591
664
|
printDiagnostics(diagnostics);
|
|
592
665
|
// Scoped the same way `em slice mark-implemented`/`em export --slice`/`em validate
|
|
593
666
|
// --slice-ready` are: only an error concerning THIS slice refuses.
|
|
@@ -622,19 +695,23 @@ slice
|
|
|
622
695
|
console.log(result.changed
|
|
623
696
|
? `ratified: ${result.path} (ratifiedBy: ${opts.by}, ratifiedOn: ${ratifiedOn})`
|
|
624
697
|
: `already ratified (no-op): ${result.path}`);
|
|
698
|
+
warnModelVersionDrift(dirname(file), model, refs, source, `ratifying "${sliceKey}"`);
|
|
625
699
|
});
|
|
626
700
|
slice
|
|
627
701
|
.command("reratify")
|
|
628
702
|
.description("bump `version:` and flip a shipped slice doc's frontmatter back to `status: " +
|
|
629
703
|
"ready-to-implement` — the re-ratification mechanical edit (MIL-161, mirrors `em slice " +
|
|
630
|
-
"mark-implemented`).
|
|
704
|
+
"mark-implemented`). Applies to a doc at `status: implemented`, or (MIL-258) to a " +
|
|
705
|
+
"`ready-to-implement` doc that is ratified but not yet shipped — there it bumps `version:` " +
|
|
706
|
+
"and clears the sign-off but leaves `status:` alone; a `ready-to-implement` doc with no " +
|
|
707
|
+
"`ratifiedBy:` refuses (awaiting `em slice ratify --by`). Clears any stale " +
|
|
631
708
|
"`ratifiedBy:`/`ratifiedOn:`/`reviewedBy:`/`reviewedOn:` (they describe the PRIOR version's " +
|
|
632
709
|
"review and sign-off) so a follow-up `em slice ratify --by` applies cleanly — and needs no " +
|
|
633
710
|
"fresh review; never touches `implementedIn:` or the doc body")
|
|
634
711
|
.argument("<file>", "input .em file")
|
|
635
712
|
.argument("<slice-key>", "slice export key (kebab-case)")
|
|
636
713
|
.action((file, sliceKey) => {
|
|
637
|
-
const { model, refs, diagnostics } = compileFile(file);
|
|
714
|
+
const { model, refs, diagnostics, source } = compileFile(file);
|
|
638
715
|
printDiagnostics(diagnostics);
|
|
639
716
|
// Scoped the same way `em slice ratify`/`em slice mark-implemented`/`em export --slice`/
|
|
640
717
|
// `em validate --slice-ready` are: only an error concerning THIS slice refuses.
|
|
@@ -648,7 +725,67 @@ slice
|
|
|
648
725
|
console.error(`em slice reratify: ${result.message}`);
|
|
649
726
|
process.exit(1);
|
|
650
727
|
}
|
|
651
|
-
|
|
728
|
+
// MIL-214: advisory only, never refuses — see reratify.ts's reratifyAdvisory. Both warnings
|
|
729
|
+
// can fire together (a version that was never certified AND still carries unruled findings).
|
|
730
|
+
if (result.advisory?.neverCertified) {
|
|
731
|
+
console.error(`warn: reratifying "${sliceKey}" whose v${result.newVersion - 1} was never certified`);
|
|
732
|
+
}
|
|
733
|
+
if (result.advisory && result.advisory.unruledFindingsCount > 0) {
|
|
734
|
+
console.error(`warn: reratifying "${sliceKey}" has ${result.advisory.unruledFindingsCount} unruled conformance finding(s)`);
|
|
735
|
+
}
|
|
736
|
+
if (result.kind === "unshipped") {
|
|
737
|
+
// MIL-258: nothing was flipped — say what DID happen, and that the new version is unsigned.
|
|
738
|
+
console.log(`reratified: ${result.path} (version: ${result.newVersion - 1} -> ${result.newVersion}, sign-off cleared, ` +
|
|
739
|
+
`status unchanged: ready-to-implement) — not ratified until \`em slice ratify --by <name>\` records the new sign-off`);
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
console.log(`reratified: ${result.path} (version: ${result.newVersion}, status: ready-to-implement)`);
|
|
743
|
+
}
|
|
744
|
+
warnModelVersionDrift(dirname(file), model, refs, source, `reratifying "${sliceKey}"`);
|
|
745
|
+
});
|
|
746
|
+
slice
|
|
747
|
+
.command("conform")
|
|
748
|
+
.description("record per-slice-per-version conformance certification: sets `conformedVersion:`/" +
|
|
749
|
+
"`conformedAt:`/`conformedOn:` on a slice doc (MIL-214) — the fact `driftSignal: in-sync` " +
|
|
750
|
+
"now depends on. Legal only for `status: implemented` with an `implementedIn:` link. " +
|
|
751
|
+
"Idempotent on the same (version, --at) pair; a different --at simply overwrites (a " +
|
|
752
|
+
"later re-certification is normal — there's no way for em to tell 'later' from 'earlier' " +
|
|
753
|
+
"for an arbitrary revision string, so there's no --force to reach for). Refuses when the " +
|
|
754
|
+
"newest conformance/*-findings.json matching --at still has an unruled finding in scope " +
|
|
755
|
+
"for this slice; --skip-findings-check overrides with a loud notice.")
|
|
756
|
+
.argument("<file>", "input .em file")
|
|
757
|
+
.argument("<slice-key>", "slice export key (kebab-case)")
|
|
758
|
+
.requiredOption("--at <rev>", "the target-repo revision this certification sweep diffed against")
|
|
759
|
+
.option("--on <date>", "certification date, YYYY-MM-DD (default: today, local date)")
|
|
760
|
+
.option("--skip-findings-check", "certify even with unruled findings in scope — prints a loud notice on stderr")
|
|
761
|
+
.action((file, sliceKey, opts) => {
|
|
762
|
+
const { model, refs, diagnostics } = compileFile(file);
|
|
763
|
+
printDiagnostics(diagnostics);
|
|
764
|
+
// Scoped the same way `em slice ratify`/`em slice reratify`/`em slice mark-implemented` are:
|
|
765
|
+
// only an error concerning THIS slice refuses.
|
|
766
|
+
const scopedErrors = diagnostics.filter((d) => d.severity === "error" && d.refs?.some((r) => r === sliceKey || r.startsWith(`${sliceKey}/`)));
|
|
767
|
+
if (scopedErrors.length > 0) {
|
|
768
|
+
console.error(`em slice conform: slice "${sliceKey}" has errors — fix them first`);
|
|
769
|
+
process.exit(1);
|
|
770
|
+
}
|
|
771
|
+
const today = localIsoDate();
|
|
772
|
+
const on = opts.on ?? today;
|
|
773
|
+
if (opts.on !== undefined && !isValidDateString(opts.on)) {
|
|
774
|
+
console.error(`em slice conform: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
|
|
775
|
+
process.exit(1);
|
|
776
|
+
}
|
|
777
|
+
const result = runSliceConform(model, refs, dirname(file), sliceKey, opts.at, on, opts.skipFindingsCheck === true);
|
|
778
|
+
if (!result.ok) {
|
|
779
|
+
console.error(`em slice conform: ${result.message}`);
|
|
780
|
+
process.exit(1);
|
|
781
|
+
}
|
|
782
|
+
if (result.skippedFindingsCheck !== null) {
|
|
783
|
+
console.error(`notice: --skip-findings-check — certifying "${sliceKey}" with ${result.skippedFindingsCheck.length} ` +
|
|
784
|
+
`unruled conformance finding(s) still in scope (id ${result.skippedFindingsCheck.join(", ")})`);
|
|
785
|
+
}
|
|
786
|
+
console.log(result.changed
|
|
787
|
+
? `certified: ${result.path} (conformedVersion: ${result.version}, conformedAt: ${opts.at}, conformedOn: ${on})`
|
|
788
|
+
: `already certified (no-op): ${result.path}`);
|
|
652
789
|
});
|
|
653
790
|
program
|
|
654
791
|
.command("changelog")
|
|
@@ -673,23 +810,45 @@ program
|
|
|
673
810
|
}
|
|
674
811
|
});
|
|
675
812
|
const STATE_DIR_HELP = "model directory containing .event-modeling.md, or a direct path to that file (default: current directory)";
|
|
676
|
-
/** Shared by every `em state` writer: load, apply the pure patch,
|
|
677
|
-
*
|
|
678
|
-
|
|
813
|
+
/** Shared by every `em state` writer: load, apply the pure patch, stamp `Em version:` to the
|
|
814
|
+
* running em (MIL-219 ruling A — every state-file writer refreshes it, since each is already
|
|
815
|
+
* rewriting the file), write, report — the only difference between set-phase/set-conformance/
|
|
816
|
+
* set-review/set-model-version is which `mutate` closure they pass. `today` defaults to
|
|
817
|
+
* `localIsoDate()`; a caller that also needs to stamp a SECOND artifact (MIL-218: `em state
|
|
818
|
+
* set-conformance`'s model-version certify step) with the exact same date passes it in
|
|
819
|
+
* explicitly, computed once, rather than each call independently reading the clock and risking
|
|
820
|
+
* a midnight-rollover mismatch between the two. The `Em version:` stamp is best-effort — a
|
|
821
|
+
* failure there (should never happen: `Model file:` is one of the six bullets a successful
|
|
822
|
+
* `loadStateFile`+`mutate` already proves is present) falls back to `mutate`'s own output rather
|
|
823
|
+
* than losing the write the caller actually asked for. */
|
|
824
|
+
function writeStateUpdate(dirOrFile, cmdLabel, mutate, today = localIsoDate()) {
|
|
679
825
|
const loaded = loadStateFile(dirOrFile);
|
|
680
826
|
if (!loaded.ok) {
|
|
681
827
|
console.error(loaded.message);
|
|
682
828
|
process.exit(1);
|
|
683
829
|
}
|
|
684
|
-
const today = localIsoDate();
|
|
685
830
|
const result = mutate(loaded.text, today);
|
|
686
831
|
if (!result.ok) {
|
|
687
832
|
console.error(`${cmdLabel}: ${result.message}`);
|
|
688
833
|
process.exit(1);
|
|
689
834
|
}
|
|
690
|
-
|
|
835
|
+
const stamped = setEmVersion(result.text, PKG_VERSION, today);
|
|
836
|
+
writeFileSync(loaded.path, stamped.ok ? stamped.text : result.text);
|
|
691
837
|
console.log(`wrote ${loaded.path}`);
|
|
692
838
|
}
|
|
839
|
+
/** MIL-218 ruling C: the one-line advisory `em slice ratify`/`reratify` print when the write
|
|
840
|
+
* they just made pushed the slice-version vector (or, in principle, the `.em` content hash —
|
|
841
|
+
* never true for these two commands, which only ever edit a slice doc's frontmatter, but
|
|
842
|
+
* checked anyway so this helper stays the one true predicate) past the last bumped design
|
|
843
|
+
* version. Silent when no manifest exists at all (`drift.current === null`) — a repo that
|
|
844
|
+
* never bumped a design version has nothing to be stale against, same silence
|
|
845
|
+
* `model-version-stale` (rules.ts) holds. Never refuses; advisory only. */
|
|
846
|
+
function warnModelVersionDrift(baseDir, model, refs, source, cmdLabel) {
|
|
847
|
+
const drift = modelVersionDrift(baseDir, model, refs, source);
|
|
848
|
+
if (drift.current === null || (!drift.hashChanged && drift.slicesChanged.length === 0))
|
|
849
|
+
return;
|
|
850
|
+
console.error(`warn: ${cmdLabel} moved the model past v${drift.current} — run \`em model version bump\` to record it`);
|
|
851
|
+
}
|
|
693
852
|
const state = program
|
|
694
853
|
.command("state")
|
|
695
854
|
.description("read/write the state file's mechanical fields deterministically (see docs/cli.md)");
|
|
@@ -725,12 +884,95 @@ state
|
|
|
725
884
|
});
|
|
726
885
|
state
|
|
727
886
|
.command("set-conformance")
|
|
728
|
-
.description("rewrite Last conformance: (and Last updated:) in the exact format reference/conform.md
|
|
887
|
+
.description("rewrite Last conformance: (and Last updated:) in the exact format reference/conform.md " +
|
|
888
|
+
"parses. Refuses (MIL-214) while any `implemented` slice has an unruled conformance " +
|
|
889
|
+
"finding in the findings JSON beside --report (a `slice: null` finding blocks every " +
|
|
890
|
+
"slice); --partial records the marker with a ` (partial)` suffix instead, with a loud " +
|
|
891
|
+
"notice. Without a findings JSON beside --report: warns once and records the marker as " +
|
|
892
|
+
"given (migration path for a report predating `em conform-findings`)")
|
|
729
893
|
.argument("<revision>", "target-repo revision just diffed against")
|
|
730
894
|
.argument("[dir]", STATE_DIR_HELP, ".")
|
|
731
895
|
.requiredOption("--report <path>", "path to the conformance report just written")
|
|
896
|
+
.option("--partial", "record the marker as PARTIAL even though some in-scope findings are still unruled")
|
|
732
897
|
.action((revision, dir, opts) => {
|
|
733
|
-
|
|
898
|
+
const loaded = loadStateFile(dir);
|
|
899
|
+
if (!loaded.ok) {
|
|
900
|
+
console.error(loaded.message);
|
|
901
|
+
process.exit(1);
|
|
902
|
+
}
|
|
903
|
+
const parsed = parseState(loaded.text);
|
|
904
|
+
if (!parsed.ok) {
|
|
905
|
+
console.error(`em state set-conformance: ${parsed.message}`);
|
|
906
|
+
process.exit(1);
|
|
907
|
+
}
|
|
908
|
+
const modelDir = dirname(loaded.path);
|
|
909
|
+
const modelFile = join(modelDir, parsed.state.modelPath);
|
|
910
|
+
// The "in scope" set for the findings gate below — every `implemented` slice, per the
|
|
911
|
+
// ruling. Best-effort: a model that doesn't exist or doesn't compile cleanly can't tell us
|
|
912
|
+
// its implemented slices, so the gate is skipped (not refused) in that case rather than
|
|
913
|
+
// blocking a state-file operation on an unrelated model problem — `em validate` is where
|
|
914
|
+
// that problem gets its own diagnostic. Also the compiled model MIL-218's certify step
|
|
915
|
+
// needs below (kept, not just the derived `inScope` set, since certify needs the model/refs
|
|
916
|
+
// themselves — never re-compiled a second time).
|
|
917
|
+
let inScope = new Set();
|
|
918
|
+
let compiledForCertify = null;
|
|
919
|
+
if (existsSync(modelFile)) {
|
|
920
|
+
const compiled = compileFile(modelFile);
|
|
921
|
+
if (!hasErrors(compiled.diagnostics)) {
|
|
922
|
+
const { facts } = resolveSliceDocFacts(compiled.model, compiled.refs, modelDir);
|
|
923
|
+
inScope = new Set(facts.filter((f) => f.status === "implemented").map((f) => f.key));
|
|
924
|
+
compiledForCertify = { model: compiled.model, refs: compiled.refs, source: compiled.source };
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
let unruled = [];
|
|
928
|
+
const lookup = lookupFindingsBesideReport(modelDir, opts.report);
|
|
929
|
+
if (lookup.kind === "invalid") {
|
|
930
|
+
console.error(`em state set-conformance: ${lookup.message}`);
|
|
931
|
+
process.exit(1);
|
|
932
|
+
}
|
|
933
|
+
else if (lookup.kind === "absent") {
|
|
934
|
+
console.error(`warn: no findings JSON found beside ${opts.report} — recording the marker without verifying every ` +
|
|
935
|
+
"finding is ruled (migration path for a report written before `em conform-findings`)");
|
|
936
|
+
}
|
|
937
|
+
else {
|
|
938
|
+
unruled = unruledFindingsInScope(lookup.doc.findings, inScope);
|
|
939
|
+
}
|
|
940
|
+
const partialFlag = opts.partial === true;
|
|
941
|
+
if (unruled.length > 0 && !partialFlag) {
|
|
942
|
+
console.error(`em state set-conformance: ${unruled.length} unruled conformance finding(s) among implemented slices ` +
|
|
943
|
+
`(id ${unruled.map((f) => f.id).join(", ")}) — rule on them (\`em conform-supersede --locus --by\`) or pass --partial`);
|
|
944
|
+
process.exit(1);
|
|
945
|
+
}
|
|
946
|
+
const recordPartial = partialFlag && unruled.length > 0;
|
|
947
|
+
if (recordPartial) {
|
|
948
|
+
console.error(`notice: conformance marker recorded as PARTIAL — ${unruled.length} finding(s) unruled`);
|
|
949
|
+
}
|
|
950
|
+
const today = localIsoDate();
|
|
951
|
+
// MIL-218 ruling D: a FULL (non-`--partial`) marker also certifies the current design
|
|
952
|
+
// version's model-versions/*.json manifest. Checked (and, on success, WRITTEN) before the
|
|
953
|
+
// `Last conformance:` bullet below — a refusal here (no manifest yet, or the vector has
|
|
954
|
+
// drifted since it was bumped) must leave the state file untouched, same "refuse before any
|
|
955
|
+
// write" discipline every other em command holds; never leaves `Last conformance:` updated
|
|
956
|
+
// with no matching `Certified:` to show for it. Best-effort, same posture as the `inScope`
|
|
957
|
+
// computation above, for exactly one case: a model that doesn't compile cleanly can't be
|
|
958
|
+
// certified against, so certification is silently SKIPPED (not a `set-conformance` failure)
|
|
959
|
+
// rather than blocking the conformance marker on an unrelated model problem — `em validate`
|
|
960
|
+
// is where that problem gets its own diagnostic.
|
|
961
|
+
let certifiedVersion = null;
|
|
962
|
+
if (!recordPartial && compiledForCertify) {
|
|
963
|
+
const findingsPath = lookup.kind === "found" ? lookup.path : null;
|
|
964
|
+
const certifyResult = runCertifyModelVersion(modelDir, compiledForCertify.model, compiledForCertify.refs, compiledForCertify.source, revision, today, opts.report, findingsPath);
|
|
965
|
+
if (!certifyResult.ok) {
|
|
966
|
+
console.error(`em state set-conformance: ${certifyResult.message}`);
|
|
967
|
+
process.exit(1);
|
|
968
|
+
}
|
|
969
|
+
certifiedVersion = certifyResult.version;
|
|
970
|
+
console.log(`certified: ${certifyResult.path} (v${certifyResult.version} @ ${revision})`);
|
|
971
|
+
}
|
|
972
|
+
writeStateUpdate(dir, "em state set-conformance", (text, t) => setConformance(text, revision, opts.report, t, recordPartial), today);
|
|
973
|
+
if (certifiedVersion !== null) {
|
|
974
|
+
writeStateUpdate(dir, "em state set-conformance", (text, t) => setCertified(text, certifiedVersion, revision, t, t), today);
|
|
975
|
+
}
|
|
734
976
|
});
|
|
735
977
|
state
|
|
736
978
|
.command("set-review")
|
|
@@ -767,8 +1009,8 @@ state
|
|
|
767
1009
|
process.exit(1);
|
|
768
1010
|
}
|
|
769
1011
|
const phases = sortUsagePhases(rawPhases);
|
|
770
|
-
const { model, diagnostics, refs } = compileFile(file);
|
|
771
|
-
const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics);
|
|
1012
|
+
const { model, diagnostics, refs, source } = compileFile(file);
|
|
1013
|
+
const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics, source);
|
|
772
1014
|
const categorySet = new Set(allDiagnostics.map((d) => RULES[d.code].usageCategory));
|
|
773
1015
|
const categories = categorySet.size > 0 ? [...categorySet].sort() : ["none"];
|
|
774
1016
|
const loaded = loadStateFile(dirname(file));
|
|
@@ -785,6 +1027,67 @@ state
|
|
|
785
1027
|
writeFileSync(loaded.path, result.text);
|
|
786
1028
|
console.log(`wrote ${loaded.path} — phases: ${phases.join(", ")} — validate: ${categories.join(", ")}`);
|
|
787
1029
|
});
|
|
1030
|
+
const model = program.command("model").description("model-level version: design version + certified version (see docs/model-versions.md)");
|
|
1031
|
+
const modelVersionCmd = model.command("version").description("bump/inspect the model's design version (MIL-218)");
|
|
1032
|
+
modelVersionCmd
|
|
1033
|
+
.command("bump")
|
|
1034
|
+
.description("bump the model's design version: write model-versions/v<N+1>.json (sibling of slices/, " +
|
|
1035
|
+
"conformance/) and rewrite the state file's `Model version:` bullet (MIL-218). Refuses " +
|
|
1036
|
+
"when the state file is missing (run `em scaffold` first), when --by is empty, and when " +
|
|
1037
|
+
"nothing has changed since the current version (same model content hash AND same " +
|
|
1038
|
+
"slice-version vector) unless --force — a no-op bump is the one case --force is right for.")
|
|
1039
|
+
.argument("<file>", "input .em file")
|
|
1040
|
+
.requiredOption("--by <name>", "the bumper's name")
|
|
1041
|
+
.option("--on <date>", "bump date, YYYY-MM-DD (default: today, local date)")
|
|
1042
|
+
.option("--force", "bump even though nothing has changed since the current version")
|
|
1043
|
+
.action((file, opts) => {
|
|
1044
|
+
const baseDir = dirname(file);
|
|
1045
|
+
const loaded = loadStateFile(baseDir);
|
|
1046
|
+
if (!loaded.ok) {
|
|
1047
|
+
console.error(`em model version bump: no state file — run \`em scaffold\` first (${loaded.message})`);
|
|
1048
|
+
process.exit(1);
|
|
1049
|
+
}
|
|
1050
|
+
if (opts.on !== undefined && !isValidDateString(opts.on)) {
|
|
1051
|
+
console.error(`em model version bump: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
|
|
1052
|
+
process.exit(1);
|
|
1053
|
+
}
|
|
1054
|
+
const today = localIsoDate();
|
|
1055
|
+
const on = opts.on ?? today;
|
|
1056
|
+
const { model: compiledModel, refs, diagnostics, source } = compileFile(file);
|
|
1057
|
+
printDiagnostics(diagnostics);
|
|
1058
|
+
if (hasErrors(diagnostics)) {
|
|
1059
|
+
console.error(`em model version bump: "${file}" has errors — fix them first`);
|
|
1060
|
+
process.exit(1);
|
|
1061
|
+
}
|
|
1062
|
+
const result = runModelVersionBump(baseDir, basename(file), compiledModel, refs, source, opts.by, on, GENERATOR_VERSION, opts.force === true);
|
|
1063
|
+
if (!result.ok) {
|
|
1064
|
+
console.error(`em model version bump: ${result.message}`);
|
|
1065
|
+
process.exit(1);
|
|
1066
|
+
}
|
|
1067
|
+
writeStateUpdate(baseDir, "em model version bump", (text, t) => setModelVersion(text, result.version, t), today);
|
|
1068
|
+
console.log(`bumped: ${result.path} (v${result.version})`);
|
|
1069
|
+
});
|
|
1070
|
+
modelVersionCmd
|
|
1071
|
+
.command("show")
|
|
1072
|
+
.description("print the model's current design version and the most recently certified version, if any " +
|
|
1073
|
+
"(model-versions/*.json — MIL-218)")
|
|
1074
|
+
.argument("<file>", "input .em file")
|
|
1075
|
+
.option("--json", "print a JSON document instead of text")
|
|
1076
|
+
.action((file, opts) => {
|
|
1077
|
+
const baseDir = dirname(file);
|
|
1078
|
+
const design = latestModelVersionNumber(baseDir);
|
|
1079
|
+
const certifiedFound = findCertifiedVersion(baseDir);
|
|
1080
|
+
const certified = certifiedFound
|
|
1081
|
+
? { version: certifiedFound.version, at: certifiedFound.certified.at, on: certifiedFound.certified.on }
|
|
1082
|
+
: null;
|
|
1083
|
+
if (opts.json) {
|
|
1084
|
+
console.log(JSON.stringify({ file, design, certified }, null, 2));
|
|
1085
|
+
}
|
|
1086
|
+
else {
|
|
1087
|
+
console.log(`design version: ${design ?? "none"}`);
|
|
1088
|
+
console.log(`certified: ${certified ? `v${certified.version} @ ${certified.at} (${certified.on})` : "never"}`);
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
788
1091
|
program
|
|
789
1092
|
.command("usage-report")
|
|
790
1093
|
.description("aggregate every .event-modeling.md's Usage log under [root] into phase/diagnostic-category " +
|
|
@@ -870,12 +1173,17 @@ program
|
|
|
870
1173
|
"at ratification time so a reader following the report's file:line citations knows they " +
|
|
871
1174
|
"describe an ancestor of the current model. Additive-only splice, never a rewrite of the " +
|
|
872
1175
|
"report; idempotent on the same --as-of/--findings/--on stamp; refuses if the report " +
|
|
873
|
-
"doesn't exist"
|
|
1176
|
+
"doesn't exist. --locus/--by (MIL-214, both required together) additionally record the " +
|
|
1177
|
+
"ruling itself — locus/resolvedBy/resolvedOn — on the findings named by --findings in the " +
|
|
1178
|
+
"sibling conformance/<date>-findings.json, when one exists (warns once and stamps the " +
|
|
1179
|
+
"banner only when it doesn't — the migration path for a report predating em conform-findings)")
|
|
874
1180
|
.argument("<file>", "input .em file (used only to resolve the report path's base directory)")
|
|
875
1181
|
.argument("<report-path>", "path to the conformance report, relative to the model's directory")
|
|
876
1182
|
.requiredOption("--as-of <rev>", "the revision this ruling was made against — same value passed to `em state set-conformance`")
|
|
877
1183
|
.requiredOption("--findings <spec>", 'which finding number(s) this stamps as ruled, e.g. "1-3" or "1,2,4"')
|
|
878
1184
|
.option("--on <date>", "ruling date, YYYY-MM-DD (default: today, local date)")
|
|
1185
|
+
.option("--locus <locus>", `who/what the named finding(s) say is wrong: ${FINDING_LOCI.join(" | ")} (requires --by)`)
|
|
1186
|
+
.option("--by <name>", "who ruled on the named finding(s) (requires --locus)")
|
|
879
1187
|
.action((file, reportPath, opts) => {
|
|
880
1188
|
const today = localIsoDate();
|
|
881
1189
|
const on = opts.on ?? today;
|
|
@@ -883,15 +1191,57 @@ program
|
|
|
883
1191
|
console.error(`em conform-supersede: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
|
|
884
1192
|
process.exit(1);
|
|
885
1193
|
}
|
|
886
|
-
|
|
1194
|
+
if ((opts.locus !== undefined) !== (opts.by !== undefined)) {
|
|
1195
|
+
console.error("em conform-supersede: --locus and --by must be given together");
|
|
1196
|
+
process.exit(1);
|
|
1197
|
+
}
|
|
1198
|
+
if (opts.locus !== undefined && !FINDING_LOCI.includes(opts.locus)) {
|
|
1199
|
+
console.error(`em conform-supersede: invalid --locus "${opts.locus}" — expected one of: ${FINDING_LOCI.join(", ")}`);
|
|
1200
|
+
process.exit(1);
|
|
1201
|
+
}
|
|
1202
|
+
const ruling = opts.locus !== undefined && opts.by !== undefined ? { locus: opts.locus, by: opts.by } : undefined;
|
|
1203
|
+
const result = runConformSupersede(dirname(file), reportPath, opts.asOf, opts.findings, on, ruling);
|
|
887
1204
|
if (!result.ok) {
|
|
888
1205
|
console.error(`em conform-supersede: ${result.message}`);
|
|
889
1206
|
process.exit(1);
|
|
890
1207
|
}
|
|
1208
|
+
if (result.findingsRuling.kind === "no-findings-file") {
|
|
1209
|
+
console.error(`warn: ${result.findingsRuling.warning}`);
|
|
1210
|
+
}
|
|
1211
|
+
else if (result.findingsRuling.kind === "applied") {
|
|
1212
|
+
console.log(result.findingsRuling.changed
|
|
1213
|
+
? `ruled: ${result.findingsRuling.path} (finding(s) ${result.findingsRuling.ids.join(", ")} -> locus: ${opts.locus}, by: ${opts.by})`
|
|
1214
|
+
: `already ruled (no-op): ${result.findingsRuling.path}`);
|
|
1215
|
+
}
|
|
891
1216
|
console.log(result.changed
|
|
892
1217
|
? `stamped superseded: ${result.path} (as of ${opts.asOf}, findings ${opts.findings}, on ${on})`
|
|
893
1218
|
: `already stamped (no-op): ${result.path}`);
|
|
894
1219
|
});
|
|
1220
|
+
const conformFindings = program.command("conform-findings").description("read-only checks for the conformance findings record (MIL-214)");
|
|
1221
|
+
conformFindings
|
|
1222
|
+
.command("check")
|
|
1223
|
+
.description("shape-validate a conformance/<date>-findings.json file (findingsSchemaVersion, required " +
|
|
1224
|
+
"fields, enum values, ids sorted+unique, a ruled finding carries resolvedBy+resolvedOn) — " +
|
|
1225
|
+
"exit 1 on shape errors. The conform skill writes this file directly alongside its report; " +
|
|
1226
|
+
"this is how a headless run verifies what it just wrote.")
|
|
1227
|
+
.argument("<path>", "path to a conformance/<date>-findings.json file")
|
|
1228
|
+
.option("--json", "print a JSON document instead of the text report")
|
|
1229
|
+
.action((path, opts) => {
|
|
1230
|
+
const result = checkFindingsFile(path);
|
|
1231
|
+
if (opts.json) {
|
|
1232
|
+
console.log(buildCheckFindingsJson(result));
|
|
1233
|
+
}
|
|
1234
|
+
else if (result.ok) {
|
|
1235
|
+
console.log(`ok — ${result.path}: ${result.findingsCount} finding(s), shape valid`);
|
|
1236
|
+
}
|
|
1237
|
+
else {
|
|
1238
|
+
console.error(`em conform-findings check: ${result.path} — shape errors:`);
|
|
1239
|
+
for (const e of result.errors)
|
|
1240
|
+
console.error(` - ${e}`);
|
|
1241
|
+
}
|
|
1242
|
+
if (!result.ok)
|
|
1243
|
+
process.exit(1);
|
|
1244
|
+
});
|
|
895
1245
|
program
|
|
896
1246
|
.command("watch")
|
|
897
1247
|
.description("re-render on every save")
|
|
@@ -980,8 +1330,8 @@ program
|
|
|
980
1330
|
.option("--json", "print a JSON document instead of text — works on a model WITH errors, unlike `em export` " +
|
|
981
1331
|
"(MIL-128, see docs/cli.md); exit codes are unchanged")
|
|
982
1332
|
.action((file, opts) => {
|
|
983
|
-
const { model, diagnostics, refs } = compileFile(file);
|
|
984
|
-
const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics);
|
|
1333
|
+
const { model, diagnostics, refs, source } = compileFile(file);
|
|
1334
|
+
const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics, source);
|
|
985
1335
|
if (opts.sliceReady) {
|
|
986
1336
|
// MIL-87: a targeted, single-slice readiness gate, not part of the unconditional
|
|
987
1337
|
// diagnostic set above (see sliceReadyValidate.ts's header for why). Folds in MIL-85's
|
|
@@ -1173,9 +1523,13 @@ program
|
|
|
1173
1523
|
"definition-of-done citation check; advisory by default, --strict for CI; " +
|
|
1174
1524
|
"--include-ready also counts ready-to-implement docs (forward-looking report); a " +
|
|
1175
1525
|
"continuation slice (an `again` view instance with no doc of its own, MIL-208) is excluded " +
|
|
1176
|
-
"from the report — its invariants, if any, live in the originating slice's own doc"
|
|
1526
|
+
"from the report — its invariants, if any, live in the originating slice's own doc; " +
|
|
1527
|
+
"--slice <key> scopes the report to one slice whatever its status (MIL-255), the " +
|
|
1528
|
+
"pre-merge definition-of-done check")
|
|
1177
1529
|
.argument("<file>", "input .em file")
|
|
1178
1530
|
.requiredOption("--tests <dir>", "directory to scan recursively for test files citing invariant IDs")
|
|
1531
|
+
.option("--slice <key>", "only this slice (export key), whether ready-to-implement or implemented — no --include-ready needed; " +
|
|
1532
|
+
"--strict then fails on this slice's uncited invariants, or if the slice is not in scope (draft/reviewed/unbound: nothing checked) (MIL-255)")
|
|
1179
1533
|
.option("--strict", "exit non-zero if any invariant ID has zero citations (CI)")
|
|
1180
1534
|
.option("--include-ready", "also count ready-to-implement docs, not just implemented (MIL-207)")
|
|
1181
1535
|
.option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
|
|
@@ -1188,11 +1542,25 @@ program
|
|
|
1188
1542
|
}
|
|
1189
1543
|
const baseDir = dirname(file);
|
|
1190
1544
|
const includeReady = !!opts.includeReady;
|
|
1545
|
+
// MIL-255: resolve --slice before anything else — an unknown key is an error (never a
|
|
1546
|
+
// vacuously passing empty report), and a continuation key (MIL-208) scopes to its
|
|
1547
|
+
// originating slice, which the output names (matching `em validate --slice-ready`).
|
|
1548
|
+
let onlyKey;
|
|
1549
|
+
let continuationOfKey = null;
|
|
1550
|
+
if (opts.slice !== undefined) {
|
|
1551
|
+
const target = resolveCoverageSliceKey(model, refs, opts.slice);
|
|
1552
|
+
if (!target) {
|
|
1553
|
+
console.error(`em coverage --slice: no slice with export key "${opts.slice}" in this model`);
|
|
1554
|
+
process.exit(1);
|
|
1555
|
+
}
|
|
1556
|
+
onlyKey = target.key;
|
|
1557
|
+
continuationOfKey = target.continuationOf;
|
|
1558
|
+
}
|
|
1191
1559
|
// MIL-207: a missing --tests dir is only a defect when something is actually in scope to
|
|
1192
1560
|
// check — cheap to know without touching the test tree (resolveScopedSlices never reads
|
|
1193
1561
|
// --tests). A fresh scaffold (or a doc-only ratification PR, with the MIL-207 default scope)
|
|
1194
1562
|
// has zero implemented docs, so there's nothing yet for a test to cite.
|
|
1195
|
-
const anyInScope = resolveScopedSlices(model, refs, baseDir, includeReady).some((s) => s.inScope);
|
|
1563
|
+
const anyInScope = resolveScopedSlices(model, refs, baseDir, includeReady, onlyKey).some((s) => s.inScope);
|
|
1196
1564
|
if (!existsSync(opts.tests)) {
|
|
1197
1565
|
if (anyInScope) {
|
|
1198
1566
|
console.error(`em coverage: --tests directory not found: ${opts.tests}`);
|
|
@@ -1204,17 +1572,36 @@ program
|
|
|
1204
1572
|
console.error(`em coverage: --tests is not a directory: ${opts.tests}`);
|
|
1205
1573
|
process.exit(1);
|
|
1206
1574
|
}
|
|
1207
|
-
const report = buildCoverageReport(model, refs, baseDir, opts.tests, includeReady);
|
|
1575
|
+
const report = buildCoverageReport(model, refs, baseDir, opts.tests, includeReady, onlyKey);
|
|
1576
|
+
// MIL-255: a scoped slice whose doc is draft/reviewed/unbound/unusable is out of scope — the
|
|
1577
|
+
// same way the unscoped report treats it — but under --strict "nothing was checked" must not
|
|
1578
|
+
// exit 0, or the pre-merge check passes vacuously (the very bug --slice exists to fix). So:
|
|
1579
|
+
// advisory (no --strict) prints the note on stdout and exits 0; --strict prints it on stderr
|
|
1580
|
+
// and exits 1 (also in --json mode, whose document shape is unchanged — consumers read the
|
|
1581
|
+
// slice's `inScope` alongside `ok`). An in-scope doc with zero invariants stays a pass.
|
|
1582
|
+
const unchecked = onlyKey === undefined ? undefined : report.slices.find((sl) => !sl.inScope);
|
|
1583
|
+
const uncheckedNote = unchecked
|
|
1584
|
+
? `slice "${unchecked.key}" is not in scope (${unchecked.status ?? unchecked.docReason ?? "no doc"}) — nothing checked; only ready-to-implement/implemented docs are checked`
|
|
1585
|
+
: null;
|
|
1586
|
+
if (uncheckedNote !== null && (opts.strict || !opts.json)) {
|
|
1587
|
+
(opts.strict ? console.error : console.log)(uncheckedNote);
|
|
1588
|
+
}
|
|
1208
1589
|
if (opts.json) {
|
|
1209
|
-
|
|
1590
|
+
const sliceScope = opts.slice === undefined ? null : { requested: opts.slice, continuationOf: continuationOfKey };
|
|
1591
|
+
process.stdout.write(buildCoverageJson(file, opts.tests, report, includeReady, sliceScope) + "\n");
|
|
1210
1592
|
}
|
|
1211
1593
|
else {
|
|
1594
|
+
if (continuationOfKey !== null) {
|
|
1595
|
+
console.log(`slice "${opts.slice}" is a continuation of "${continuationOfKey}" — checking "${continuationOfKey}"`);
|
|
1596
|
+
}
|
|
1212
1597
|
for (const slice of report.slices) {
|
|
1213
1598
|
if (!slice.inScope)
|
|
1214
1599
|
continue;
|
|
1215
1600
|
console.log(`slice "${slice.key}" (${slice.status}):`);
|
|
1216
1601
|
if (slice.invariants.length === 0) {
|
|
1217
|
-
|
|
1602
|
+
// MIL-255: said as "checked" so an in-scope doc defining zero invariants (a legitimate
|
|
1603
|
+
// pass) is distinguishable from the "not in scope — nothing checked" case above.
|
|
1604
|
+
console.log(` (checked — the doc defines 0 INV-* invariant IDs)`);
|
|
1218
1605
|
continue;
|
|
1219
1606
|
}
|
|
1220
1607
|
for (const inv of slice.invariants) {
|
|
@@ -1233,7 +1620,7 @@ program
|
|
|
1233
1620
|
// Advisory by default (exit 0 even with uncovered IDs) — --strict is the opt-in CI gate,
|
|
1234
1621
|
// same "set exitCode, don't truncate stdout" rationale as em ledger/em diff for the --json
|
|
1235
1622
|
// form, and consistent for the text form too.
|
|
1236
|
-
if (opts.strict && report.uncoveredCount > 0)
|
|
1623
|
+
if (opts.strict && (report.uncoveredCount > 0 || uncheckedNote !== null))
|
|
1237
1624
|
process.exitCode = 1;
|
|
1238
1625
|
});
|
|
1239
1626
|
program
|
|
@@ -1265,11 +1652,11 @@ program
|
|
|
1265
1652
|
const compiled = [];
|
|
1266
1653
|
let anyErrors = false;
|
|
1267
1654
|
for (const file of files) {
|
|
1268
|
-
const { model, refs, diagnostics } = compileFile(file);
|
|
1655
|
+
const { model, refs, diagnostics, source } = compileFile(file);
|
|
1269
1656
|
printDiagnosticsFor(file, diagnostics);
|
|
1270
1657
|
if (hasErrors(diagnostics))
|
|
1271
1658
|
anyErrors = true;
|
|
1272
|
-
compiled.push({ file, model, refs });
|
|
1659
|
+
compiled.push({ file, model, refs, source });
|
|
1273
1660
|
}
|
|
1274
1661
|
if (anyErrors) {
|
|
1275
1662
|
console.error("em status: not reporting status — fix the errors above");
|
|
@@ -1326,7 +1713,13 @@ program
|
|
|
1326
1713
|
const sliceDocFacts = facts.map((f) => ({ key: f.key, status: f.rawStatus, implementedIn: f.implementedIn }));
|
|
1327
1714
|
return resolveConformanceEntry(file, opts.repo, sliceDocFacts);
|
|
1328
1715
|
});
|
|
1329
|
-
|
|
1716
|
+
// MIL-218: one model-version entry per input file.
|
|
1717
|
+
const modelVersion = compiled.map(({ file, model, refs, source }) => resolveModelVersionStatusEntry(file, model, refs, source));
|
|
1718
|
+
// MIL-219: one em-version entry per input file whose state file resolved.
|
|
1719
|
+
const emVersion = compiled
|
|
1720
|
+
.map(({ file }) => resolveEmVersionStatusEntry(file, PKG_VERSION))
|
|
1721
|
+
.filter((e) => e !== null);
|
|
1722
|
+
const report = buildStatusReport(files, sliceFacts, openIssuesCount, invariants, conformance, statusDiagnostics, modelVersion, emVersion);
|
|
1330
1723
|
let output;
|
|
1331
1724
|
if (opts.json)
|
|
1332
1725
|
output = buildStatusJson(report);
|
|
@@ -1371,6 +1764,32 @@ program
|
|
|
1371
1764
|
console.log(formatConformancePart(entry));
|
|
1372
1765
|
}
|
|
1373
1766
|
});
|
|
1767
|
+
program
|
|
1768
|
+
.command("metrics")
|
|
1769
|
+
.description("the pilot metrics named in advance by the register, computed from git history alone " +
|
|
1770
|
+
"(MIL-170): ratification turnaround (reviewed -> ratified -> implemented), conform-cycle " +
|
|
1771
|
+
"cadence + finding counts, and status-vs-reality disagreement over time — plus a fourth, " +
|
|
1772
|
+
"reported as not computable from history (see docs/usage-data.md). `<file>` is an anchor " +
|
|
1773
|
+
".em file, used only to locate slices/, conformance/, and .event-modeling.md relative to " +
|
|
1774
|
+
"it — same convention as em ledger; never parsed or compiled")
|
|
1775
|
+
.argument("<file>", "anchor .em file")
|
|
1776
|
+
.requiredOption("--from <rev>", "baseline revision")
|
|
1777
|
+
.option("--to <rev>", "compare revision (default: HEAD)")
|
|
1778
|
+
.option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
|
|
1779
|
+
.action((file, opts) => {
|
|
1780
|
+
const to = opts.to ?? "HEAD";
|
|
1781
|
+
const computed = computeMetrics(file, opts.from, to);
|
|
1782
|
+
if (!computed.ok) {
|
|
1783
|
+
console.error(computed.message);
|
|
1784
|
+
process.exit(1);
|
|
1785
|
+
}
|
|
1786
|
+
if (opts.json) {
|
|
1787
|
+
process.stdout.write(buildMetricsJson(computed.result) + "\n");
|
|
1788
|
+
}
|
|
1789
|
+
else {
|
|
1790
|
+
console.log(formatMetricsText(computed.result));
|
|
1791
|
+
}
|
|
1792
|
+
});
|
|
1374
1793
|
// `em query` (MIL-168): deterministic graph queries over the compiled model — scoped, token-
|
|
1375
1794
|
// cheap answers instead of a whole-model `em export`. Compiles every input file through the
|
|
1376
1795
|
// LIGHTER query sub-pipeline (query/pipeline.ts: parse -> normalize -> refs -> validate against
|
|
@@ -1721,6 +2140,18 @@ skill
|
|
|
1721
2140
|
console.log(f.message);
|
|
1722
2141
|
console.log(result.ok ? `ok — vendored skill matches em ${PKG_VERSION}` : `${result.findings.length} mismatch(es)`);
|
|
1723
2142
|
}
|
|
2143
|
+
// MIL-219 ruling A: `em skill check` also warns when [path]'s own state file records an
|
|
2144
|
+
// `Em version:` behind the installed em — best-effort (no state file, or an unparseable
|
|
2145
|
+
// one, is silently skipped; that's `em upgrade`'s own hard-incompatibility case to report,
|
|
2146
|
+
// not this command's). Always stderr, independent of --json/exit code — advisory only, and
|
|
2147
|
+
// this command's own exit code stays about skill-bundle drift, never about this.
|
|
2148
|
+
const loaded = loadStateFile(resolve(path));
|
|
2149
|
+
if (loaded.ok) {
|
|
2150
|
+
const parsed = parseState(loaded.text);
|
|
2151
|
+
if (parsed.ok && isOlder(parsed.state.emVersion, PKG_VERSION)) {
|
|
2152
|
+
console.error(`warn: recorded Em version (${parsed.state.emVersion}) is behind installed em (${PKG_VERSION}) — run \`em upgrade\``);
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
1724
2155
|
// Set the code rather than process.exit(): same rationale as em ledger/em diff — stdout to
|
|
1725
2156
|
// a pipe (a --json document) shouldn't risk truncation.
|
|
1726
2157
|
if (!result.ok)
|
|
@@ -1735,7 +2166,7 @@ ci.command("init")
|
|
|
1735
2166
|
"idempotent, --check for CI self-verification (MIL-166, see docs/ci.md)")
|
|
1736
2167
|
.argument("<model>", "anchor .em file the coverage/ledger/slice-index/status steps point at")
|
|
1737
2168
|
.option("--tests <dir>", "test directory the coverage/status steps scan for INV-* citations", "test")
|
|
1738
|
-
.option("-f, --force", "replace an existing workflow file that has no GENERATED markers")
|
|
2169
|
+
.option("-f, --force", "replace an existing workflow file that has no GENERATED markers, or a managed block generated for a different model")
|
|
1739
2170
|
.option("--check", "verify both files match the current preset; exit non-zero on drift without writing (CI)")
|
|
1740
2171
|
.action((model, opts) => {
|
|
1741
2172
|
const unsafe = findUnsafeCiInitArg(model) ?? findUnsafeCiInitArg(opts.tests);
|
|
@@ -1757,10 +2188,17 @@ ci.command("init")
|
|
|
1757
2188
|
planCiFile(conformPath, buildConformWorkflowFile(model, PKG_VERSION), conformManagedBody(model, PKG_VERSION), CONFORM_WORKFLOW_MARKER, !!opts.force),
|
|
1758
2189
|
],
|
|
1759
2190
|
];
|
|
2191
|
+
// MIL-256 (#174): the managed block is single-model (multi-model: MIL-233). Another model's
|
|
2192
|
+
// block is its own condition - never `stale`, never silently replaced.
|
|
2193
|
+
const otherModel = (previous, path) => `${path} was generated for ${previous}, not ${model} - the managed block is single-model until multi-model support lands (MIL-233)`;
|
|
1760
2194
|
if (opts.check) {
|
|
1761
2195
|
let drift = false;
|
|
1762
2196
|
for (const [path, status] of files) {
|
|
1763
|
-
if (status.kind === "
|
|
2197
|
+
if (status.kind === "other-model" || status.kind === "replace-model") {
|
|
2198
|
+
console.log(`different model: ${otherModel(status.previous, path)} (re-run \`em ci init ${model} --force\` to replace it)`);
|
|
2199
|
+
drift = true;
|
|
2200
|
+
}
|
|
2201
|
+
else if (status.kind === "create") {
|
|
1764
2202
|
console.log(`missing: ${path} — run \`em ci init ${model}\` to create it`);
|
|
1765
2203
|
drift = true;
|
|
1766
2204
|
}
|
|
@@ -1781,10 +2219,22 @@ ci.command("init")
|
|
|
1781
2219
|
process.exitCode = 1;
|
|
1782
2220
|
return;
|
|
1783
2221
|
}
|
|
2222
|
+
// Refuse before writing anything, so a refusal on one file never leaves the other updated.
|
|
2223
|
+
const refused = files.filter(([, s]) => s.kind === "other-model");
|
|
2224
|
+
if (refused.length > 0) {
|
|
2225
|
+
for (const [path, status] of refused) {
|
|
2226
|
+
if (status.kind === "other-model")
|
|
2227
|
+
console.error(`em ci init: ${otherModel(status.previous, path)}; re-run with --force to replace it`);
|
|
2228
|
+
}
|
|
2229
|
+
process.exitCode = 1;
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
1784
2232
|
mkdirSync(dirname(ciPath), { recursive: true });
|
|
1785
2233
|
for (const [path, status] of files) {
|
|
1786
2234
|
applyCiFile(path, status);
|
|
1787
|
-
if (status.kind === "
|
|
2235
|
+
if (status.kind === "replace-model")
|
|
2236
|
+
console.log(`replaced ${path} (--force; was generated for ${status.previous})`);
|
|
2237
|
+
else if (status.kind === "create")
|
|
1788
2238
|
console.log(`installed ${path}`);
|
|
1789
2239
|
else if (status.kind === "stale")
|
|
1790
2240
|
console.log(`updated ${path}`);
|
|
@@ -1796,6 +2246,106 @@ ci.command("init")
|
|
|
1796
2246
|
console.log(`${path} already up to date`);
|
|
1797
2247
|
}
|
|
1798
2248
|
});
|
|
2249
|
+
program
|
|
2250
|
+
.command("upgrade")
|
|
2251
|
+
.description("bring a model repo authored under an older em (1.6 forward) up to the installed version: " +
|
|
2252
|
+
"an ordered list of mechanical steps (skill bundle, reaction shape, state-file bullets, " +
|
|
2253
|
+
"generated CI blocks, constitution scaffold) plus a human list of things no command can " +
|
|
2254
|
+
"safely decide by itself (MIL-219, see docs/upgrading.md). Dry-run by default; --apply " +
|
|
2255
|
+
"makes one git commit per applicable step, refusing on a dirty working tree")
|
|
2256
|
+
.argument("<file>", "input .em model file")
|
|
2257
|
+
.option("--apply", "apply every applicable mechanical step, one git commit each, then a final `Em version:` commit")
|
|
2258
|
+
.option("--check", "exit non-zero only on a hard incompatibility (an existing but unparseable state file, an un-migratable .em shape) — writes nothing; what CI runs; the final line says why")
|
|
2259
|
+
.option("--json", "print a JSON document instead of text (dry-run/--check only, never with --apply)")
|
|
2260
|
+
.action((file, opts) => {
|
|
2261
|
+
if (opts.apply && opts.check) {
|
|
2262
|
+
console.error("em upgrade: --apply and --check are mutually exclusive");
|
|
2263
|
+
process.exit(1);
|
|
2264
|
+
}
|
|
2265
|
+
const { model, refs, diagnostics, source } = compileFile(file);
|
|
2266
|
+
printDiagnostics(diagnostics);
|
|
2267
|
+
// Deliberately does NOT refuse on hasErrors(diagnostics), unlike every other command that
|
|
2268
|
+
// compiles a model first: the old pre-1.7.1 two-slice reaction shape `reaction-shape`
|
|
2269
|
+
// migrates is itself a validation ERROR under the current rules (MIL-120 changed what
|
|
2270
|
+
// `em validate` accepts) — refusing here would make that step, and the `predates-1.6` human
|
|
2271
|
+
// item, unreachable for exactly the repos that need them. Every step/human-detector either
|
|
2272
|
+
// reads the raw source directly (reaction-shape/predates-1.6, via planMigration) or a
|
|
2273
|
+
// best-effort compiled model that's still structurally usable even with validation errors
|
|
2274
|
+
// present (compile() never throws for a semantic error, only a parse error — already
|
|
2275
|
+
// handled by compileFile() above).
|
|
2276
|
+
if (hasErrors(diagnostics)) {
|
|
2277
|
+
console.error(`em upgrade: "${file}" has validation errors — proceeding anyway (this is often exactly what needs upgrading)`);
|
|
2278
|
+
}
|
|
2279
|
+
void source; // compileFile's own source read; steps re-read from disk themselves (module header)
|
|
2280
|
+
const baseDir = dirname(file);
|
|
2281
|
+
const repoRootResult = resolveRepoRoot(baseDir);
|
|
2282
|
+
if (!repoRootResult.ok) {
|
|
2283
|
+
console.error(repoRootResult.message);
|
|
2284
|
+
process.exit(1);
|
|
2285
|
+
}
|
|
2286
|
+
const ctx = {
|
|
2287
|
+
modelFile: file,
|
|
2288
|
+
baseDir,
|
|
2289
|
+
repoRoot: repoRootResult.repoRoot,
|
|
2290
|
+
installedVersion: PKG_VERSION,
|
|
2291
|
+
packagedSkillsRoot: packagedSkillsRoot(),
|
|
2292
|
+
model,
|
|
2293
|
+
refs,
|
|
2294
|
+
};
|
|
2295
|
+
if (opts.check) {
|
|
2296
|
+
const { ok, report, reasons } = checkUpgrade(ctx);
|
|
2297
|
+
console.error(`em upgrade --check: from ${report.from.version}${report.from.inferred ? ` (inferred: ${report.from.basis})` : ""} → to ${report.to}`);
|
|
2298
|
+
for (const s of report.steps) {
|
|
2299
|
+
console.error(` [${s.applicable ? "x" : " "}] ${s.id} (since ${s.sinceVersion}): ${s.reason}`);
|
|
2300
|
+
}
|
|
2301
|
+
for (const h of report.human)
|
|
2302
|
+
console.error(` human: ${h.id}: ${h.reason}`);
|
|
2303
|
+
// MIL-257: the final line always says why (previously visible only via --json).
|
|
2304
|
+
console.error(ok ? "ok — no hard incompatibility" : `em upgrade --check: hard incompatibility found: ${reasons.join("; ")}`);
|
|
2305
|
+
if (!ok)
|
|
2306
|
+
process.exitCode = 1;
|
|
2307
|
+
return;
|
|
2308
|
+
}
|
|
2309
|
+
if (opts.apply) {
|
|
2310
|
+
const result = applyUpgrade(ctx);
|
|
2311
|
+
for (const s of result.applied) {
|
|
2312
|
+
if (s.applied)
|
|
2313
|
+
console.log(`applied: ${s.id} — ${s.changedFiles.join(", ")} (${s.commit})`);
|
|
2314
|
+
else
|
|
2315
|
+
console.log(`skipped: ${s.id}`);
|
|
2316
|
+
}
|
|
2317
|
+
if (!result.ok) {
|
|
2318
|
+
console.error(result.message);
|
|
2319
|
+
process.exit(1);
|
|
2320
|
+
}
|
|
2321
|
+
if (result.emVersionCommit)
|
|
2322
|
+
console.log(`recorded: ${result.emVersionCommit}`);
|
|
2323
|
+
else
|
|
2324
|
+
console.log("Em version: already up to date — no commit needed");
|
|
2325
|
+
for (const h of result.report.human)
|
|
2326
|
+
console.log(`human: ${h.id}: ${h.reason}`);
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
2329
|
+
const report = detectUpgrade(ctx);
|
|
2330
|
+
if (opts.json) {
|
|
2331
|
+
console.log(buildUpgradeJson(file, report));
|
|
2332
|
+
return;
|
|
2333
|
+
}
|
|
2334
|
+
console.log(`em upgrade: from ${report.from.version}${report.from.inferred ? ` (inferred: ${report.from.basis})` : ""} → to ${report.to}`);
|
|
2335
|
+
if (report.stateFileError)
|
|
2336
|
+
console.log(`state file: ${report.stateFileError}`);
|
|
2337
|
+
console.log("Steps:");
|
|
2338
|
+
for (const s of report.steps) {
|
|
2339
|
+
console.log(` [${s.applicable ? "x" : " "}] ${s.id} (since ${s.sinceVersion}): ${s.reason}`);
|
|
2340
|
+
}
|
|
2341
|
+
console.log("Human:");
|
|
2342
|
+
if (report.human.length === 0)
|
|
2343
|
+
console.log(" (none)");
|
|
2344
|
+
for (const h of report.human)
|
|
2345
|
+
console.log(` ${h.id}: ${h.reason}`);
|
|
2346
|
+
const applicableCount = report.steps.filter((s) => s.applicable).length;
|
|
2347
|
+
console.log(`${applicableCount} step(s) applicable, ${report.human.length} human item(s) — re-run with --apply to apply mechanical steps`);
|
|
2348
|
+
});
|
|
1799
2349
|
// Exported so dev tooling (e.g. scripts/generate-skill-docs.ts) can introspect the registered
|
|
1800
2350
|
// commands/options without triggering a real CLI run. Guarded the same way Node's CJS
|
|
1801
2351
|
// `require.main === module` used to: only actually parse argv when this file is the process's
|
|
@@ -1838,15 +2388,17 @@ function compileFile(file, opts = {},
|
|
|
1838
2388
|
}
|
|
1839
2389
|
}
|
|
1840
2390
|
/** Lineage-ref resolution (MIL-84), frontmatter-coherence (MIL-85), note-binding mismatches
|
|
1841
|
-
* (MIL-126), doc↔model consistency (MIL-124),
|
|
1842
|
-
* validate`'s fs-aware rules — every other diagnostic in
|
|
1843
|
-
* pure function of the .em source. All
|
|
1844
|
-
*
|
|
1845
|
-
*
|
|
1846
|
-
*
|
|
1847
|
-
*
|
|
1848
|
-
*
|
|
1849
|
-
|
|
2391
|
+
* (MIL-126), doc↔model consistency (MIL-124), orphaned slice docs (MIL-183), and model-version
|
|
2392
|
+
* staleness (MIL-218) are `em validate`'s fs-aware rules — every other diagnostic in
|
|
2393
|
+
* `diagnostics` (from `compileFile`) is a pure function of the .em source. All six read
|
|
2394
|
+
* `slices/*.md`/`model-versions/*.json` alongside the model. Doc↔model consistency,
|
|
2395
|
+
* orphaned-slice-doc, and model-version-stale are deliberately validate-only (unlike
|
|
2396
|
+
* note-binding, which `em render` also folds in) — all three are conform-phase concerns, not
|
|
2397
|
+
* something every render needs to recheck. Shared by `em validate` and `em state log-usage`
|
|
2398
|
+
* (MIL-161) — the latter needs the exact same diagnostic set `em validate --json` would
|
|
2399
|
+
* report, since its whole job is logging which categories fired. `source` is the `.em` file's
|
|
2400
|
+
* own text — only `validateModelVersionStale` reads it (the modelHash comparison). */
|
|
2401
|
+
function computeAllDiagnostics(file, model, refs, diagnostics, source) {
|
|
1850
2402
|
return [
|
|
1851
2403
|
...diagnostics,
|
|
1852
2404
|
...validateLineage(model, refs, dirname(file)),
|
|
@@ -1854,6 +2406,7 @@ function computeAllDiagnostics(file, model, refs, diagnostics) {
|
|
|
1854
2406
|
...validateNoteBindings(model, refs, dirname(file)),
|
|
1855
2407
|
...validateDocModelConsistency(model, refs, dirname(file)),
|
|
1856
2408
|
...validateOrphanedSliceDocs(model, refs, dirname(file)),
|
|
2409
|
+
...validateModelVersionStale(model, refs, dirname(file), source),
|
|
1857
2410
|
];
|
|
1858
2411
|
}
|
|
1859
2412
|
/** Read a file's text, or exit with a clear error — shared by `em diff` (both forms) and
|
|
@@ -1953,16 +2506,22 @@ function printDivergences(model) {
|
|
|
1953
2506
|
console.log(` divergence :${el.line} slice "${slice.name}" ${el.kind} "${el.name}": ${el.divergence}`);
|
|
1954
2507
|
}
|
|
1955
2508
|
}
|
|
1956
|
-
/** Print only elements marked `public`: slice, kind, name, line — for an integration-surface
|
|
2509
|
+
/** Print only elements marked `public`: slice, kind, name, line — for an integration-surface
|
|
2510
|
+
* audit. A `public` view also gets a "(no in-model reader)" note (MIL-215) when no `ui`/
|
|
2511
|
+
* reaction anywhere on its timeline reads it — an audit hint, not a warning: a genuinely
|
|
2512
|
+
* cross-model `public` view's reader legitimately lives outside this file (see
|
|
2513
|
+
* `publicViewsWithoutInModelReader`'s own header for why this stays advisory). */
|
|
1957
2514
|
function printPublicElements(model) {
|
|
1958
2515
|
const pub = model.elements.filter((el) => el.public && (el.kind === "event" || el.kind === "view"));
|
|
1959
2516
|
if (pub.length === 0) {
|
|
1960
2517
|
console.log("no public elements");
|
|
1961
2518
|
return;
|
|
1962
2519
|
}
|
|
2520
|
+
const noReader = publicViewsWithoutInModelReader(model);
|
|
1963
2521
|
for (const el of pub) {
|
|
1964
2522
|
const slice = model.slices[el.sliceIndex];
|
|
1965
|
-
|
|
2523
|
+
const note = el.kind === "view" && noReader.has(el.logicalId) ? " (no in-model reader)" : "";
|
|
2524
|
+
console.log(` public :${el.line} slice "${slice.name}" ${el.kind} "${el.name}"${note}`);
|
|
1966
2525
|
}
|
|
1967
2526
|
}
|
|
1968
2527
|
/** Diagnostics in `all` not already present (by reference identity) in `already` — avoids
|