@milehimikey/em 1.11.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/event-modeling/SKILL.md +4 -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 +22 -6
- package/.claude/skills/event-modeling-discover/SKILL.md +1 -1
- package/.claude/skills/event-modeling-implement/SKILL.md +12 -6
- package/.claude/skills/event-modeling-implement/reference/implement.md +165 -18
- package/.claude/skills/event-modeling-review/SKILL.md +5 -1
- package/.claude/skills/event-modeling-shared/reference/em-dsl.md +57 -8
- package/.claude/skills/event-modeling-shared/reference/operating-principles.md +13 -3
- package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +69 -0
- package/.claude/skills/event-modeling-shared/templates/conformance-report.md +8 -1
- package/.claude/skills/event-modeling-shared/templates/constitution.md +8 -1
- package/.claude/skills/event-modeling-shared/templates/state.md +3 -0
- package/README.md +1 -0
- package/dist/catalog/build.js +42 -1
- package/dist/catalog/build.js.map +1 -1
- package/dist/catalog/coverage.d.ts +15 -0
- package/dist/catalog/coverage.js +34 -0
- package/dist/catalog/coverage.js.map +1 -0
- package/dist/catalog/docJoin.d.ts +20 -2
- package/dist/catalog/docJoin.js +32 -4
- package/dist/catalog/docJoin.js.map +1 -1
- package/dist/catalog/docModelConsistencyValidate.js +1 -1
- package/dist/catalog/docModelConsistencyValidate.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/noteBindingValidate.js +18 -0
- package/dist/catalog/noteBindingValidate.js.map +1 -1
- package/dist/catalog/orphanedSliceDocValidate.d.ts +8 -1
- package/dist/catalog/orphanedSliceDocValidate.js +33 -6
- package/dist/catalog/orphanedSliceDocValidate.js.map +1 -1
- package/dist/catalog/pages.d.ts +16 -1
- package/dist/catalog/pages.js +16 -4
- package/dist/catalog/pages.js.map +1 -1
- 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/catalog/sliceReadyValidate.d.ts +12 -1
- package/dist/catalog/sliceReadyValidate.js +4 -4
- package/dist/catalog/sliceReadyValidate.js.map +1 -1
- package/dist/cli/agentsMd.js +9 -3
- package/dist/cli/agentsMd.js.map +1 -1
- package/dist/cli/ciInit.js +20 -0
- package/dist/cli/ciInit.js.map +1 -1
- package/dist/cli/conformScope.d.ts +1 -0
- package/dist/cli/conformScope.js +13 -4
- 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 +34 -9
- package/dist/cli/coverage.js +61 -24
- 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/markImplemented.js +12 -1
- package/dist/cli/markImplemented.js.map +1 -1
- 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/ratify.js +17 -1
- package/dist/cli/ratify.js.map +1 -1
- package/dist/cli/reratify.d.ts +17 -0
- package/dist/cli/reratify.js +37 -2
- package/dist/cli/reratify.js.map +1 -1
- package/dist/cli/review.js +12 -1
- package/dist/cli/review.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 +24 -7
- package/dist/cli/sliceIndex.js.map +1 -1
- package/dist/cli/sliceLink.js +16 -0
- package/dist/cli/sliceLink.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 +90 -1
- package/dist/cli/status.js +151 -16
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/upgrade.d.ts +122 -0
- package/dist/cli/upgrade.js +476 -0
- package/dist/cli/upgrade.js.map +1 -0
- package/dist/cli.js +586 -66
- package/dist/cli.js.map +1 -1
- package/dist/emit/coverageJson.d.ts +2 -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 +34 -1
- package/dist/emit/json.js +84 -2
- 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 +22 -1
- package/dist/emit/statusJson.js.map +1 -1
- package/dist/emit/upgradeJson.d.ts +5 -0
- package/dist/emit/upgradeJson.js +26 -0
- package/dist/emit/upgradeJson.js.map +1 -0
- package/dist/emit/validateJson.d.ts +10 -5
- package/dist/emit/validateJson.js +22 -8
- package/dist/emit/validateJson.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +151 -22
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/continuation.d.ts +23 -0
- package/dist/model/continuation.js +49 -0
- package/dist/model/continuation.js.map +1 -0
- package/dist/model/queryIndex.js +1 -1
- package/dist/model/queryIndex.js.map +1 -1
- package/dist/model/rules.d.ts +13 -0
- package/dist/model/rules.js +18 -0
- package/dist/model/rules.js.map +1 -1
- package/dist/model/sliceDocIndex.d.ts +12 -5
- package/dist/model/sliceDocIndex.js +18 -7
- package/dist/model/sliceDocIndex.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/render/sliceStatus.d.ts +3 -3
- package/dist/render/sliceStatus.js +27 -29
- package/dist/render/sliceStatus.js.map +1 -1
- package/dist/templates.d.ts +3 -3
- package/dist/templates.js +15 -2
- package/dist/templates.js.map +1 -1
- package/dist/util/localDate.d.ts +2 -0
- package/dist/util/localDate.js +17 -0
- package/dist/util/localDate.js.map +1 -0
- 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 +1 -1
package/dist/cli.js
CHANGED
|
@@ -7,14 +7,16 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { Command } from "commander";
|
|
8
8
|
import { compile } from "./pipeline.js";
|
|
9
9
|
import { isMainModule } from "./util/isMainModule.js";
|
|
10
|
+
import { localIsoDate } from "./util/localDate.js";
|
|
11
|
+
import { continuationOf } from "./model/continuation.js";
|
|
10
12
|
import { ParseError } from "./parser/parser.js";
|
|
11
13
|
import { renderDot, layoutDot, composeSvg, writeRendered, formatFromPath } from "./render/render.js";
|
|
12
14
|
import { buildSliceDiagram } from "./render/sliceDiagram.js";
|
|
13
15
|
import { resolveSliceArg, defaultSliceOut } from "./cli/render-inputs.js";
|
|
14
16
|
import { serializeBuilds, watchFile } from "./render/watch.js";
|
|
15
17
|
import { startLiveServer } from "./render/serve.js";
|
|
16
|
-
import { formatDiagnostic, hasErrors } from "./model/validate.js";
|
|
17
|
-
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";
|
|
18
20
|
import { buildTypeSpec } from "./emit/typespec.js";
|
|
19
21
|
import { buildValidateJson, buildSliceReadyJson, buildValidateListJson, collectMarkers } from "./emit/validateJson.js";
|
|
20
22
|
import { buildDiffJson } from "./emit/diffJson.js";
|
|
@@ -26,16 +28,19 @@ import { validateFrontmatterCoherence } from "./catalog/frontmatterCoherenceVali
|
|
|
26
28
|
import { validateNoteBindings } from "./catalog/noteBindingValidate.js";
|
|
27
29
|
import { validateDocModelConsistency } from "./catalog/docModelConsistencyValidate.js";
|
|
28
30
|
import { validateOrphanedSliceDocs } from "./catalog/orphanedSliceDocValidate.js";
|
|
31
|
+
import { validateModelVersionStale } from "./catalog/modelVersionValidate.js";
|
|
29
32
|
import { validateSliceReady, computeSliceReadyGates } from "./catalog/sliceReadyValidate.js";
|
|
30
33
|
import { detectSliceDocCollisions } from "./catalog/modelCollisionValidate.js";
|
|
31
34
|
import { checkLedger, readLedgerWaiverTrailers, applyLedgerWaivers } from "./cli/ledgerCheck.js";
|
|
32
35
|
import { planMigration, verifyMigration } from "./cli/migrateReactionShape.js";
|
|
33
36
|
import { buildLedgerJson } from "./emit/ledgerJson.js";
|
|
34
|
-
import { buildCoverageReport } from "./cli/coverage.js";
|
|
37
|
+
import { buildCoverageReport, resolveScopedSlices } from "./cli/coverage.js";
|
|
35
38
|
import { buildCoverageJson } from "./emit/coverageJson.js";
|
|
36
|
-
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";
|
|
37
40
|
import { buildStatusJson } from "./emit/statusJson.js";
|
|
38
41
|
import { buildFreshnessJson } from "./emit/freshnessJson.js";
|
|
42
|
+
import { computeMetrics, formatMetricsText } from "./cli/metrics.js";
|
|
43
|
+
import { buildMetricsJson } from "./emit/metricsJson.js";
|
|
39
44
|
import { planSkillSyncBundle, applySkillSyncBundle } from "./cli/skillSync.js";
|
|
40
45
|
import { checkSkillSyncBundle } from "./cli/skillCheck.js";
|
|
41
46
|
import { buildSkillCheckJson } from "./emit/skillCheckJson.js";
|
|
@@ -55,15 +60,22 @@ import { runRatify } from "./cli/ratify.js";
|
|
|
55
60
|
import { runReview } from "./cli/review.js";
|
|
56
61
|
import { runConformSupersede } from "./cli/conformSupersede.js";
|
|
57
62
|
import { buildConformScope, changedPathsSince, resolveSliceDocFacts, seedAsisModel } from "./cli/conformScope.js";
|
|
58
|
-
import { buildSliceDocContent, isSlicePattern, sliceDocKey, SLICE_PATTERNS } from "./cli/sliceNew.js";
|
|
63
|
+
import { buildSliceDocContent, buildStubDocContent, isSlicePattern, sliceDocKey, SLICE_PATTERNS } from "./cli/sliceNew.js";
|
|
59
64
|
import { wireSliceNote } from "./cli/sliceLink.js";
|
|
65
|
+
import { isStubStatus, runStubAll, STUB_STATUSES } from "./cli/sliceStubAll.js";
|
|
60
66
|
import { listModelCommits } from "./cli/changelog-git.js";
|
|
61
67
|
import { buildChangelogDoc } from "./cli/changelogBuild.js";
|
|
62
68
|
import { runReratify } from "./cli/reratify.js";
|
|
63
|
-
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";
|
|
64
73
|
import { STARTER_EM, starterEmFor, scaffoldReadme, scaffoldStateFile, scaffoldConstitution } from "./templates.js";
|
|
65
74
|
import { kebabSlug } from "./util/slug.js";
|
|
66
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";
|
|
67
79
|
import { RULES } from "./model/rules.js";
|
|
68
80
|
import { USAGE_PHASES, isUsagePhase, sortUsagePhases, appendUsageLogEntry, findStateFiles, aggregateUsageReport, formatUsageReportText, } from "./cli/usageLog.js";
|
|
69
81
|
import { buildUsageReportJson } from "./emit/usageReportJson.js";
|
|
@@ -121,10 +133,10 @@ program
|
|
|
121
133
|
process.exit(1);
|
|
122
134
|
}
|
|
123
135
|
await mkdir(dirPath, { recursive: true });
|
|
124
|
-
const today =
|
|
136
|
+
const today = localIsoDate();
|
|
125
137
|
writeFileSync(join(dirPath, `${slugName}.em`), starterEmFor(name));
|
|
126
138
|
writeFileSync(join(dirPath, "README.md"), scaffoldReadme(name, slugName));
|
|
127
|
-
writeFileSync(join(dirPath, STATE_FILE_NAME), scaffoldStateFile(name, slugName, today));
|
|
139
|
+
writeFileSync(join(dirPath, STATE_FILE_NAME), scaffoldStateFile(name, slugName, today, PKG_VERSION));
|
|
128
140
|
// MIL-202: the implementation constitution. One document per project, never two — in a
|
|
129
141
|
// spec-kit project `.specify/memory/constitution.md` already IS that slot, so em defers to it
|
|
130
142
|
// and writes nothing rather than seeding a second, competing copy beside the model. Same
|
|
@@ -435,6 +447,9 @@ slice
|
|
|
435
447
|
.option("-f, --force", "overwrite the file if it already exists")
|
|
436
448
|
.option("--wire <model-file>", "also insert the `note \"slices/<key>.md\"` line onto the slice's primary element in this " +
|
|
437
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")
|
|
438
453
|
.action((name, opts) => {
|
|
439
454
|
if (!isSlicePattern(opts.pattern)) {
|
|
440
455
|
console.error(`em slice new: invalid --pattern "${opts.pattern}" — expected one of: ${SLICE_PATTERNS.join(", ")}`);
|
|
@@ -466,7 +481,10 @@ slice
|
|
|
466
481
|
wired = result;
|
|
467
482
|
}
|
|
468
483
|
mkdirSync(dir, { recursive: true });
|
|
469
|
-
|
|
484
|
+
const content = opts.stub
|
|
485
|
+
? buildStubDocContent(name, opts.pattern, opts.swimlane)
|
|
486
|
+
: buildSliceDocContent(name, key, opts.pattern, opts.swimlane);
|
|
487
|
+
writeFileSync(path, content);
|
|
470
488
|
console.log(`wrote ${path}`);
|
|
471
489
|
if (wired) {
|
|
472
490
|
writeFileSync(opts.wire, wired.content);
|
|
@@ -477,6 +495,59 @@ slice
|
|
|
477
495
|
console.log(` note "${path}"`);
|
|
478
496
|
}
|
|
479
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
|
+
});
|
|
480
551
|
slice
|
|
481
552
|
.command("index")
|
|
482
553
|
.description("rewrite the model's sibling README.md's GENERATED Slices table from `em export`'s slice " +
|
|
@@ -545,7 +616,7 @@ slice
|
|
|
545
616
|
.argument("<file>", "input .em file")
|
|
546
617
|
.argument("<slice-key>", "slice export key (kebab-case)")
|
|
547
618
|
.requiredOption("--by <name>", "the reviewer's (or facilitator's) name")
|
|
548
|
-
.option("--on <date>", "review date, YYYY-MM-DD (default: today)")
|
|
619
|
+
.option("--on <date>", "review date, YYYY-MM-DD (default: today, local date)")
|
|
549
620
|
.action((file, sliceKey, opts) => {
|
|
550
621
|
const { model, refs, diagnostics } = compileFile(file);
|
|
551
622
|
printDiagnostics(diagnostics);
|
|
@@ -556,7 +627,7 @@ slice
|
|
|
556
627
|
console.error(`em slice review: slice "${sliceKey}" has errors — fix them first`);
|
|
557
628
|
process.exit(1);
|
|
558
629
|
}
|
|
559
|
-
const today =
|
|
630
|
+
const today = localIsoDate();
|
|
560
631
|
const reviewedOn = opts.on ?? today;
|
|
561
632
|
if (opts.on !== undefined && !isValidDateString(opts.on)) {
|
|
562
633
|
console.error(`em slice review: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
|
|
@@ -582,10 +653,10 @@ slice
|
|
|
582
653
|
.argument("<file>", "input .em file")
|
|
583
654
|
.argument("<slice-key>", "slice export key (kebab-case)")
|
|
584
655
|
.requiredOption("--by <name>", "the ratifier's name")
|
|
585
|
-
.option("--on <date>", "ratification date, YYYY-MM-DD (default: today)")
|
|
656
|
+
.option("--on <date>", "ratification date, YYYY-MM-DD (default: today, local date)")
|
|
586
657
|
.option("--skip-review", "ratify without a recorded review — prints a loud notice on stderr")
|
|
587
658
|
.action((file, sliceKey, opts) => {
|
|
588
|
-
const { model, refs, diagnostics } = compileFile(file);
|
|
659
|
+
const { model, refs, diagnostics, source } = compileFile(file);
|
|
589
660
|
printDiagnostics(diagnostics);
|
|
590
661
|
// Scoped the same way `em slice mark-implemented`/`em export --slice`/`em validate
|
|
591
662
|
// --slice-ready` are: only an error concerning THIS slice refuses.
|
|
@@ -594,7 +665,7 @@ slice
|
|
|
594
665
|
console.error(`em slice ratify: slice "${sliceKey}" has errors — fix them first`);
|
|
595
666
|
process.exit(1);
|
|
596
667
|
}
|
|
597
|
-
const today =
|
|
668
|
+
const today = localIsoDate();
|
|
598
669
|
const ratifiedOn = opts.on ?? today;
|
|
599
670
|
if (opts.on !== undefined && !isValidDateString(opts.on)) {
|
|
600
671
|
console.error(`em slice ratify: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
|
|
@@ -620,6 +691,7 @@ slice
|
|
|
620
691
|
console.log(result.changed
|
|
621
692
|
? `ratified: ${result.path} (ratifiedBy: ${opts.by}, ratifiedOn: ${ratifiedOn})`
|
|
622
693
|
: `already ratified (no-op): ${result.path}`);
|
|
694
|
+
warnModelVersionDrift(dirname(file), model, refs, source, `ratifying "${sliceKey}"`);
|
|
623
695
|
});
|
|
624
696
|
slice
|
|
625
697
|
.command("reratify")
|
|
@@ -632,7 +704,7 @@ slice
|
|
|
632
704
|
.argument("<file>", "input .em file")
|
|
633
705
|
.argument("<slice-key>", "slice export key (kebab-case)")
|
|
634
706
|
.action((file, sliceKey) => {
|
|
635
|
-
const { model, refs, diagnostics } = compileFile(file);
|
|
707
|
+
const { model, refs, diagnostics, source } = compileFile(file);
|
|
636
708
|
printDiagnostics(diagnostics);
|
|
637
709
|
// Scoped the same way `em slice ratify`/`em slice mark-implemented`/`em export --slice`/
|
|
638
710
|
// `em validate --slice-ready` are: only an error concerning THIS slice refuses.
|
|
@@ -646,7 +718,60 @@ slice
|
|
|
646
718
|
console.error(`em slice reratify: ${result.message}`);
|
|
647
719
|
process.exit(1);
|
|
648
720
|
}
|
|
721
|
+
// MIL-214: advisory only, never refuses — see reratify.ts's reratifyAdvisory. Both warnings
|
|
722
|
+
// can fire together (a version that was never certified AND still carries unruled findings).
|
|
723
|
+
if (result.advisory.neverCertified) {
|
|
724
|
+
console.error(`warn: reratifying "${sliceKey}" whose v${result.newVersion - 1} was never certified`);
|
|
725
|
+
}
|
|
726
|
+
if (result.advisory.unruledFindingsCount > 0) {
|
|
727
|
+
console.error(`warn: reratifying "${sliceKey}" has ${result.advisory.unruledFindingsCount} unruled conformance finding(s)`);
|
|
728
|
+
}
|
|
649
729
|
console.log(`reratified: ${result.path} (version: ${result.newVersion}, status: ready-to-implement)`);
|
|
730
|
+
warnModelVersionDrift(dirname(file), model, refs, source, `reratifying "${sliceKey}"`);
|
|
731
|
+
});
|
|
732
|
+
slice
|
|
733
|
+
.command("conform")
|
|
734
|
+
.description("record per-slice-per-version conformance certification: sets `conformedVersion:`/" +
|
|
735
|
+
"`conformedAt:`/`conformedOn:` on a slice doc (MIL-214) — the fact `driftSignal: in-sync` " +
|
|
736
|
+
"now depends on. Legal only for `status: implemented` with an `implementedIn:` link. " +
|
|
737
|
+
"Idempotent on the same (version, --at) pair; a different --at simply overwrites (a " +
|
|
738
|
+
"later re-certification is normal — there's no way for em to tell 'later' from 'earlier' " +
|
|
739
|
+
"for an arbitrary revision string, so there's no --force to reach for). Refuses when the " +
|
|
740
|
+
"newest conformance/*-findings.json matching --at still has an unruled finding in scope " +
|
|
741
|
+
"for this slice; --skip-findings-check overrides with a loud notice.")
|
|
742
|
+
.argument("<file>", "input .em file")
|
|
743
|
+
.argument("<slice-key>", "slice export key (kebab-case)")
|
|
744
|
+
.requiredOption("--at <rev>", "the target-repo revision this certification sweep diffed against")
|
|
745
|
+
.option("--on <date>", "certification date, YYYY-MM-DD (default: today, local date)")
|
|
746
|
+
.option("--skip-findings-check", "certify even with unruled findings in scope — prints a loud notice on stderr")
|
|
747
|
+
.action((file, sliceKey, opts) => {
|
|
748
|
+
const { model, refs, diagnostics } = compileFile(file);
|
|
749
|
+
printDiagnostics(diagnostics);
|
|
750
|
+
// Scoped the same way `em slice ratify`/`em slice reratify`/`em slice mark-implemented` are:
|
|
751
|
+
// only an error concerning THIS slice refuses.
|
|
752
|
+
const scopedErrors = diagnostics.filter((d) => d.severity === "error" && d.refs?.some((r) => r === sliceKey || r.startsWith(`${sliceKey}/`)));
|
|
753
|
+
if (scopedErrors.length > 0) {
|
|
754
|
+
console.error(`em slice conform: slice "${sliceKey}" has errors — fix them first`);
|
|
755
|
+
process.exit(1);
|
|
756
|
+
}
|
|
757
|
+
const today = localIsoDate();
|
|
758
|
+
const on = opts.on ?? today;
|
|
759
|
+
if (opts.on !== undefined && !isValidDateString(opts.on)) {
|
|
760
|
+
console.error(`em slice conform: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
|
|
761
|
+
process.exit(1);
|
|
762
|
+
}
|
|
763
|
+
const result = runSliceConform(model, refs, dirname(file), sliceKey, opts.at, on, opts.skipFindingsCheck === true);
|
|
764
|
+
if (!result.ok) {
|
|
765
|
+
console.error(`em slice conform: ${result.message}`);
|
|
766
|
+
process.exit(1);
|
|
767
|
+
}
|
|
768
|
+
if (result.skippedFindingsCheck !== null) {
|
|
769
|
+
console.error(`notice: --skip-findings-check — certifying "${sliceKey}" with ${result.skippedFindingsCheck.length} ` +
|
|
770
|
+
`unruled conformance finding(s) still in scope (id ${result.skippedFindingsCheck.join(", ")})`);
|
|
771
|
+
}
|
|
772
|
+
console.log(result.changed
|
|
773
|
+
? `certified: ${result.path} (conformedVersion: ${result.version}, conformedAt: ${opts.at}, conformedOn: ${on})`
|
|
774
|
+
: `already certified (no-op): ${result.path}`);
|
|
650
775
|
});
|
|
651
776
|
program
|
|
652
777
|
.command("changelog")
|
|
@@ -671,23 +796,45 @@ program
|
|
|
671
796
|
}
|
|
672
797
|
});
|
|
673
798
|
const STATE_DIR_HELP = "model directory containing .event-modeling.md, or a direct path to that file (default: current directory)";
|
|
674
|
-
/** Shared by every `em state` writer: load, apply the pure patch,
|
|
675
|
-
*
|
|
676
|
-
|
|
799
|
+
/** Shared by every `em state` writer: load, apply the pure patch, stamp `Em version:` to the
|
|
800
|
+
* running em (MIL-219 ruling A — every state-file writer refreshes it, since each is already
|
|
801
|
+
* rewriting the file), write, report — the only difference between set-phase/set-conformance/
|
|
802
|
+
* set-review/set-model-version is which `mutate` closure they pass. `today` defaults to
|
|
803
|
+
* `localIsoDate()`; a caller that also needs to stamp a SECOND artifact (MIL-218: `em state
|
|
804
|
+
* set-conformance`'s model-version certify step) with the exact same date passes it in
|
|
805
|
+
* explicitly, computed once, rather than each call independently reading the clock and risking
|
|
806
|
+
* a midnight-rollover mismatch between the two. The `Em version:` stamp is best-effort — a
|
|
807
|
+
* failure there (should never happen: `Model file:` is one of the six bullets a successful
|
|
808
|
+
* `loadStateFile`+`mutate` already proves is present) falls back to `mutate`'s own output rather
|
|
809
|
+
* than losing the write the caller actually asked for. */
|
|
810
|
+
function writeStateUpdate(dirOrFile, cmdLabel, mutate, today = localIsoDate()) {
|
|
677
811
|
const loaded = loadStateFile(dirOrFile);
|
|
678
812
|
if (!loaded.ok) {
|
|
679
813
|
console.error(loaded.message);
|
|
680
814
|
process.exit(1);
|
|
681
815
|
}
|
|
682
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
683
816
|
const result = mutate(loaded.text, today);
|
|
684
817
|
if (!result.ok) {
|
|
685
818
|
console.error(`${cmdLabel}: ${result.message}`);
|
|
686
819
|
process.exit(1);
|
|
687
820
|
}
|
|
688
|
-
|
|
821
|
+
const stamped = setEmVersion(result.text, PKG_VERSION, today);
|
|
822
|
+
writeFileSync(loaded.path, stamped.ok ? stamped.text : result.text);
|
|
689
823
|
console.log(`wrote ${loaded.path}`);
|
|
690
824
|
}
|
|
825
|
+
/** MIL-218 ruling C: the one-line advisory `em slice ratify`/`reratify` print when the write
|
|
826
|
+
* they just made pushed the slice-version vector (or, in principle, the `.em` content hash —
|
|
827
|
+
* never true for these two commands, which only ever edit a slice doc's frontmatter, but
|
|
828
|
+
* checked anyway so this helper stays the one true predicate) past the last bumped design
|
|
829
|
+
* version. Silent when no manifest exists at all (`drift.current === null`) — a repo that
|
|
830
|
+
* never bumped a design version has nothing to be stale against, same silence
|
|
831
|
+
* `model-version-stale` (rules.ts) holds. Never refuses; advisory only. */
|
|
832
|
+
function warnModelVersionDrift(baseDir, model, refs, source, cmdLabel) {
|
|
833
|
+
const drift = modelVersionDrift(baseDir, model, refs, source);
|
|
834
|
+
if (drift.current === null || (!drift.hashChanged && drift.slicesChanged.length === 0))
|
|
835
|
+
return;
|
|
836
|
+
console.error(`warn: ${cmdLabel} moved the model past v${drift.current} — run \`em model version bump\` to record it`);
|
|
837
|
+
}
|
|
691
838
|
const state = program
|
|
692
839
|
.command("state")
|
|
693
840
|
.description("read/write the state file's mechanical fields deterministically (see docs/cli.md)");
|
|
@@ -723,12 +870,95 @@ state
|
|
|
723
870
|
});
|
|
724
871
|
state
|
|
725
872
|
.command("set-conformance")
|
|
726
|
-
.description("rewrite Last conformance: (and Last updated:) in the exact format reference/conform.md
|
|
873
|
+
.description("rewrite Last conformance: (and Last updated:) in the exact format reference/conform.md " +
|
|
874
|
+
"parses. Refuses (MIL-214) while any `implemented` slice has an unruled conformance " +
|
|
875
|
+
"finding in the findings JSON beside --report (a `slice: null` finding blocks every " +
|
|
876
|
+
"slice); --partial records the marker with a ` (partial)` suffix instead, with a loud " +
|
|
877
|
+
"notice. Without a findings JSON beside --report: warns once and records the marker as " +
|
|
878
|
+
"given (migration path for a report predating `em conform-findings`)")
|
|
727
879
|
.argument("<revision>", "target-repo revision just diffed against")
|
|
728
880
|
.argument("[dir]", STATE_DIR_HELP, ".")
|
|
729
881
|
.requiredOption("--report <path>", "path to the conformance report just written")
|
|
882
|
+
.option("--partial", "record the marker as PARTIAL even though some in-scope findings are still unruled")
|
|
730
883
|
.action((revision, dir, opts) => {
|
|
731
|
-
|
|
884
|
+
const loaded = loadStateFile(dir);
|
|
885
|
+
if (!loaded.ok) {
|
|
886
|
+
console.error(loaded.message);
|
|
887
|
+
process.exit(1);
|
|
888
|
+
}
|
|
889
|
+
const parsed = parseState(loaded.text);
|
|
890
|
+
if (!parsed.ok) {
|
|
891
|
+
console.error(`em state set-conformance: ${parsed.message}`);
|
|
892
|
+
process.exit(1);
|
|
893
|
+
}
|
|
894
|
+
const modelDir = dirname(loaded.path);
|
|
895
|
+
const modelFile = join(modelDir, parsed.state.modelPath);
|
|
896
|
+
// The "in scope" set for the findings gate below — every `implemented` slice, per the
|
|
897
|
+
// ruling. Best-effort: a model that doesn't exist or doesn't compile cleanly can't tell us
|
|
898
|
+
// its implemented slices, so the gate is skipped (not refused) in that case rather than
|
|
899
|
+
// blocking a state-file operation on an unrelated model problem — `em validate` is where
|
|
900
|
+
// that problem gets its own diagnostic. Also the compiled model MIL-218's certify step
|
|
901
|
+
// needs below (kept, not just the derived `inScope` set, since certify needs the model/refs
|
|
902
|
+
// themselves — never re-compiled a second time).
|
|
903
|
+
let inScope = new Set();
|
|
904
|
+
let compiledForCertify = null;
|
|
905
|
+
if (existsSync(modelFile)) {
|
|
906
|
+
const compiled = compileFile(modelFile);
|
|
907
|
+
if (!hasErrors(compiled.diagnostics)) {
|
|
908
|
+
const { facts } = resolveSliceDocFacts(compiled.model, compiled.refs, modelDir);
|
|
909
|
+
inScope = new Set(facts.filter((f) => f.status === "implemented").map((f) => f.key));
|
|
910
|
+
compiledForCertify = { model: compiled.model, refs: compiled.refs, source: compiled.source };
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
let unruled = [];
|
|
914
|
+
const lookup = lookupFindingsBesideReport(modelDir, opts.report);
|
|
915
|
+
if (lookup.kind === "invalid") {
|
|
916
|
+
console.error(`em state set-conformance: ${lookup.message}`);
|
|
917
|
+
process.exit(1);
|
|
918
|
+
}
|
|
919
|
+
else if (lookup.kind === "absent") {
|
|
920
|
+
console.error(`warn: no findings JSON found beside ${opts.report} — recording the marker without verifying every ` +
|
|
921
|
+
"finding is ruled (migration path for a report written before `em conform-findings`)");
|
|
922
|
+
}
|
|
923
|
+
else {
|
|
924
|
+
unruled = unruledFindingsInScope(lookup.doc.findings, inScope);
|
|
925
|
+
}
|
|
926
|
+
const partialFlag = opts.partial === true;
|
|
927
|
+
if (unruled.length > 0 && !partialFlag) {
|
|
928
|
+
console.error(`em state set-conformance: ${unruled.length} unruled conformance finding(s) among implemented slices ` +
|
|
929
|
+
`(id ${unruled.map((f) => f.id).join(", ")}) — rule on them (\`em conform-supersede --locus --by\`) or pass --partial`);
|
|
930
|
+
process.exit(1);
|
|
931
|
+
}
|
|
932
|
+
const recordPartial = partialFlag && unruled.length > 0;
|
|
933
|
+
if (recordPartial) {
|
|
934
|
+
console.error(`notice: conformance marker recorded as PARTIAL — ${unruled.length} finding(s) unruled`);
|
|
935
|
+
}
|
|
936
|
+
const today = localIsoDate();
|
|
937
|
+
// MIL-218 ruling D: a FULL (non-`--partial`) marker also certifies the current design
|
|
938
|
+
// version's model-versions/*.json manifest. Checked (and, on success, WRITTEN) before the
|
|
939
|
+
// `Last conformance:` bullet below — a refusal here (no manifest yet, or the vector has
|
|
940
|
+
// drifted since it was bumped) must leave the state file untouched, same "refuse before any
|
|
941
|
+
// write" discipline every other em command holds; never leaves `Last conformance:` updated
|
|
942
|
+
// with no matching `Certified:` to show for it. Best-effort, same posture as the `inScope`
|
|
943
|
+
// computation above, for exactly one case: a model that doesn't compile cleanly can't be
|
|
944
|
+
// certified against, so certification is silently SKIPPED (not a `set-conformance` failure)
|
|
945
|
+
// rather than blocking the conformance marker on an unrelated model problem — `em validate`
|
|
946
|
+
// is where that problem gets its own diagnostic.
|
|
947
|
+
let certifiedVersion = null;
|
|
948
|
+
if (!recordPartial && compiledForCertify) {
|
|
949
|
+
const findingsPath = lookup.kind === "found" ? lookup.path : null;
|
|
950
|
+
const certifyResult = runCertifyModelVersion(modelDir, compiledForCertify.model, compiledForCertify.refs, compiledForCertify.source, revision, today, opts.report, findingsPath);
|
|
951
|
+
if (!certifyResult.ok) {
|
|
952
|
+
console.error(`em state set-conformance: ${certifyResult.message}`);
|
|
953
|
+
process.exit(1);
|
|
954
|
+
}
|
|
955
|
+
certifiedVersion = certifyResult.version;
|
|
956
|
+
console.log(`certified: ${certifyResult.path} (v${certifyResult.version} @ ${revision})`);
|
|
957
|
+
}
|
|
958
|
+
writeStateUpdate(dir, "em state set-conformance", (text, t) => setConformance(text, revision, opts.report, t, recordPartial), today);
|
|
959
|
+
if (certifiedVersion !== null) {
|
|
960
|
+
writeStateUpdate(dir, "em state set-conformance", (text, t) => setCertified(text, certifiedVersion, revision, t, t), today);
|
|
961
|
+
}
|
|
732
962
|
});
|
|
733
963
|
state
|
|
734
964
|
.command("set-review")
|
|
@@ -765,8 +995,8 @@ state
|
|
|
765
995
|
process.exit(1);
|
|
766
996
|
}
|
|
767
997
|
const phases = sortUsagePhases(rawPhases);
|
|
768
|
-
const { model, diagnostics, refs } = compileFile(file);
|
|
769
|
-
const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics);
|
|
998
|
+
const { model, diagnostics, refs, source } = compileFile(file);
|
|
999
|
+
const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics, source);
|
|
770
1000
|
const categorySet = new Set(allDiagnostics.map((d) => RULES[d.code].usageCategory));
|
|
771
1001
|
const categories = categorySet.size > 0 ? [...categorySet].sort() : ["none"];
|
|
772
1002
|
const loaded = loadStateFile(dirname(file));
|
|
@@ -774,7 +1004,7 @@ state
|
|
|
774
1004
|
console.error(loaded.message);
|
|
775
1005
|
process.exit(1);
|
|
776
1006
|
}
|
|
777
|
-
const today =
|
|
1007
|
+
const today = localIsoDate();
|
|
778
1008
|
const result = appendUsageLogEntry(loaded.text, today, phases, categories);
|
|
779
1009
|
if (!result.ok) {
|
|
780
1010
|
console.error(`em state log-usage: ${result.message}`);
|
|
@@ -783,6 +1013,67 @@ state
|
|
|
783
1013
|
writeFileSync(loaded.path, result.text);
|
|
784
1014
|
console.log(`wrote ${loaded.path} — phases: ${phases.join(", ")} — validate: ${categories.join(", ")}`);
|
|
785
1015
|
});
|
|
1016
|
+
const model = program.command("model").description("model-level version: design version + certified version (see docs/model-versions.md)");
|
|
1017
|
+
const modelVersionCmd = model.command("version").description("bump/inspect the model's design version (MIL-218)");
|
|
1018
|
+
modelVersionCmd
|
|
1019
|
+
.command("bump")
|
|
1020
|
+
.description("bump the model's design version: write model-versions/v<N+1>.json (sibling of slices/, " +
|
|
1021
|
+
"conformance/) and rewrite the state file's `Model version:` bullet (MIL-218). Refuses " +
|
|
1022
|
+
"when the state file is missing (run `em scaffold` first), when --by is empty, and when " +
|
|
1023
|
+
"nothing has changed since the current version (same model content hash AND same " +
|
|
1024
|
+
"slice-version vector) unless --force — a no-op bump is the one case --force is right for.")
|
|
1025
|
+
.argument("<file>", "input .em file")
|
|
1026
|
+
.requiredOption("--by <name>", "the bumper's name")
|
|
1027
|
+
.option("--on <date>", "bump date, YYYY-MM-DD (default: today, local date)")
|
|
1028
|
+
.option("--force", "bump even though nothing has changed since the current version")
|
|
1029
|
+
.action((file, opts) => {
|
|
1030
|
+
const baseDir = dirname(file);
|
|
1031
|
+
const loaded = loadStateFile(baseDir);
|
|
1032
|
+
if (!loaded.ok) {
|
|
1033
|
+
console.error(`em model version bump: no state file — run \`em scaffold\` first (${loaded.message})`);
|
|
1034
|
+
process.exit(1);
|
|
1035
|
+
}
|
|
1036
|
+
if (opts.on !== undefined && !isValidDateString(opts.on)) {
|
|
1037
|
+
console.error(`em model version bump: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
|
|
1038
|
+
process.exit(1);
|
|
1039
|
+
}
|
|
1040
|
+
const today = localIsoDate();
|
|
1041
|
+
const on = opts.on ?? today;
|
|
1042
|
+
const { model: compiledModel, refs, diagnostics, source } = compileFile(file);
|
|
1043
|
+
printDiagnostics(diagnostics);
|
|
1044
|
+
if (hasErrors(diagnostics)) {
|
|
1045
|
+
console.error(`em model version bump: "${file}" has errors — fix them first`);
|
|
1046
|
+
process.exit(1);
|
|
1047
|
+
}
|
|
1048
|
+
const result = runModelVersionBump(baseDir, basename(file), compiledModel, refs, source, opts.by, on, GENERATOR_VERSION, opts.force === true);
|
|
1049
|
+
if (!result.ok) {
|
|
1050
|
+
console.error(`em model version bump: ${result.message}`);
|
|
1051
|
+
process.exit(1);
|
|
1052
|
+
}
|
|
1053
|
+
writeStateUpdate(baseDir, "em model version bump", (text, t) => setModelVersion(text, result.version, t), today);
|
|
1054
|
+
console.log(`bumped: ${result.path} (v${result.version})`);
|
|
1055
|
+
});
|
|
1056
|
+
modelVersionCmd
|
|
1057
|
+
.command("show")
|
|
1058
|
+
.description("print the model's current design version and the most recently certified version, if any " +
|
|
1059
|
+
"(model-versions/*.json — MIL-218)")
|
|
1060
|
+
.argument("<file>", "input .em file")
|
|
1061
|
+
.option("--json", "print a JSON document instead of text")
|
|
1062
|
+
.action((file, opts) => {
|
|
1063
|
+
const baseDir = dirname(file);
|
|
1064
|
+
const design = latestModelVersionNumber(baseDir);
|
|
1065
|
+
const certifiedFound = findCertifiedVersion(baseDir);
|
|
1066
|
+
const certified = certifiedFound
|
|
1067
|
+
? { version: certifiedFound.version, at: certifiedFound.certified.at, on: certifiedFound.certified.on }
|
|
1068
|
+
: null;
|
|
1069
|
+
if (opts.json) {
|
|
1070
|
+
console.log(JSON.stringify({ file, design, certified }, null, 2));
|
|
1071
|
+
}
|
|
1072
|
+
else {
|
|
1073
|
+
console.log(`design version: ${design ?? "none"}`);
|
|
1074
|
+
console.log(`certified: ${certified ? `v${certified.version} @ ${certified.at} (${certified.on})` : "never"}`);
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
786
1077
|
program
|
|
787
1078
|
.command("usage-report")
|
|
788
1079
|
.description("aggregate every .event-modeling.md's Usage log under [root] into phase/diagnostic-category " +
|
|
@@ -868,28 +1159,75 @@ program
|
|
|
868
1159
|
"at ratification time so a reader following the report's file:line citations knows they " +
|
|
869
1160
|
"describe an ancestor of the current model. Additive-only splice, never a rewrite of the " +
|
|
870
1161
|
"report; idempotent on the same --as-of/--findings/--on stamp; refuses if the report " +
|
|
871
|
-
"doesn't exist"
|
|
1162
|
+
"doesn't exist. --locus/--by (MIL-214, both required together) additionally record the " +
|
|
1163
|
+
"ruling itself — locus/resolvedBy/resolvedOn — on the findings named by --findings in the " +
|
|
1164
|
+
"sibling conformance/<date>-findings.json, when one exists (warns once and stamps the " +
|
|
1165
|
+
"banner only when it doesn't — the migration path for a report predating em conform-findings)")
|
|
872
1166
|
.argument("<file>", "input .em file (used only to resolve the report path's base directory)")
|
|
873
1167
|
.argument("<report-path>", "path to the conformance report, relative to the model's directory")
|
|
874
1168
|
.requiredOption("--as-of <rev>", "the revision this ruling was made against — same value passed to `em state set-conformance`")
|
|
875
1169
|
.requiredOption("--findings <spec>", 'which finding number(s) this stamps as ruled, e.g. "1-3" or "1,2,4"')
|
|
876
|
-
.option("--on <date>", "ruling date, YYYY-MM-DD (default: today)")
|
|
1170
|
+
.option("--on <date>", "ruling date, YYYY-MM-DD (default: today, local date)")
|
|
1171
|
+
.option("--locus <locus>", `who/what the named finding(s) say is wrong: ${FINDING_LOCI.join(" | ")} (requires --by)`)
|
|
1172
|
+
.option("--by <name>", "who ruled on the named finding(s) (requires --locus)")
|
|
877
1173
|
.action((file, reportPath, opts) => {
|
|
878
|
-
const today =
|
|
1174
|
+
const today = localIsoDate();
|
|
879
1175
|
const on = opts.on ?? today;
|
|
880
1176
|
if (opts.on !== undefined && !isValidDateString(opts.on)) {
|
|
881
1177
|
console.error(`em conform-supersede: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
|
|
882
1178
|
process.exit(1);
|
|
883
1179
|
}
|
|
884
|
-
|
|
1180
|
+
if ((opts.locus !== undefined) !== (opts.by !== undefined)) {
|
|
1181
|
+
console.error("em conform-supersede: --locus and --by must be given together");
|
|
1182
|
+
process.exit(1);
|
|
1183
|
+
}
|
|
1184
|
+
if (opts.locus !== undefined && !FINDING_LOCI.includes(opts.locus)) {
|
|
1185
|
+
console.error(`em conform-supersede: invalid --locus "${opts.locus}" — expected one of: ${FINDING_LOCI.join(", ")}`);
|
|
1186
|
+
process.exit(1);
|
|
1187
|
+
}
|
|
1188
|
+
const ruling = opts.locus !== undefined && opts.by !== undefined ? { locus: opts.locus, by: opts.by } : undefined;
|
|
1189
|
+
const result = runConformSupersede(dirname(file), reportPath, opts.asOf, opts.findings, on, ruling);
|
|
885
1190
|
if (!result.ok) {
|
|
886
1191
|
console.error(`em conform-supersede: ${result.message}`);
|
|
887
1192
|
process.exit(1);
|
|
888
1193
|
}
|
|
1194
|
+
if (result.findingsRuling.kind === "no-findings-file") {
|
|
1195
|
+
console.error(`warn: ${result.findingsRuling.warning}`);
|
|
1196
|
+
}
|
|
1197
|
+
else if (result.findingsRuling.kind === "applied") {
|
|
1198
|
+
console.log(result.findingsRuling.changed
|
|
1199
|
+
? `ruled: ${result.findingsRuling.path} (finding(s) ${result.findingsRuling.ids.join(", ")} -> locus: ${opts.locus}, by: ${opts.by})`
|
|
1200
|
+
: `already ruled (no-op): ${result.findingsRuling.path}`);
|
|
1201
|
+
}
|
|
889
1202
|
console.log(result.changed
|
|
890
1203
|
? `stamped superseded: ${result.path} (as of ${opts.asOf}, findings ${opts.findings}, on ${on})`
|
|
891
1204
|
: `already stamped (no-op): ${result.path}`);
|
|
892
1205
|
});
|
|
1206
|
+
const conformFindings = program.command("conform-findings").description("read-only checks for the conformance findings record (MIL-214)");
|
|
1207
|
+
conformFindings
|
|
1208
|
+
.command("check")
|
|
1209
|
+
.description("shape-validate a conformance/<date>-findings.json file (findingsSchemaVersion, required " +
|
|
1210
|
+
"fields, enum values, ids sorted+unique, a ruled finding carries resolvedBy+resolvedOn) — " +
|
|
1211
|
+
"exit 1 on shape errors. The conform skill writes this file directly alongside its report; " +
|
|
1212
|
+
"this is how a headless run verifies what it just wrote.")
|
|
1213
|
+
.argument("<path>", "path to a conformance/<date>-findings.json file")
|
|
1214
|
+
.option("--json", "print a JSON document instead of the text report")
|
|
1215
|
+
.action((path, opts) => {
|
|
1216
|
+
const result = checkFindingsFile(path);
|
|
1217
|
+
if (opts.json) {
|
|
1218
|
+
console.log(buildCheckFindingsJson(result));
|
|
1219
|
+
}
|
|
1220
|
+
else if (result.ok) {
|
|
1221
|
+
console.log(`ok — ${result.path}: ${result.findingsCount} finding(s), shape valid`);
|
|
1222
|
+
}
|
|
1223
|
+
else {
|
|
1224
|
+
console.error(`em conform-findings check: ${result.path} — shape errors:`);
|
|
1225
|
+
for (const e of result.errors)
|
|
1226
|
+
console.error(` - ${e}`);
|
|
1227
|
+
}
|
|
1228
|
+
if (!result.ok)
|
|
1229
|
+
process.exit(1);
|
|
1230
|
+
});
|
|
893
1231
|
program
|
|
894
1232
|
.command("watch")
|
|
895
1233
|
.description("re-render on every save")
|
|
@@ -978,8 +1316,8 @@ program
|
|
|
978
1316
|
.option("--json", "print a JSON document instead of text — works on a model WITH errors, unlike `em export` " +
|
|
979
1317
|
"(MIL-128, see docs/cli.md); exit codes are unchanged")
|
|
980
1318
|
.action((file, opts) => {
|
|
981
|
-
const { model, diagnostics, refs } = compileFile(file);
|
|
982
|
-
const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics);
|
|
1319
|
+
const { model, diagnostics, refs, source } = compileFile(file);
|
|
1320
|
+
const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics, source);
|
|
983
1321
|
if (opts.sliceReady) {
|
|
984
1322
|
// MIL-87: a targeted, single-slice readiness gate, not part of the unconditional
|
|
985
1323
|
// diagnostic set above (see sliceReadyValidate.ts's header for why). Folds in MIL-85's
|
|
@@ -1005,11 +1343,18 @@ program
|
|
|
1005
1343
|
// MIL-128: the 4 named gates (see computeSliceReadyGates) plus the same `scoped`
|
|
1006
1344
|
// diagnostics and `ready` verdict driving the exit code below — replaces both the
|
|
1007
1345
|
// scraped warning prose and the two hand-parsed English sentences.
|
|
1008
|
-
const
|
|
1009
|
-
process.stdout.write(buildSliceReadyJson(file, key, gates, scoped, ready) + "\n");
|
|
1346
|
+
const result = computeSliceReadyGates(model, refs, dirname(file), key);
|
|
1347
|
+
process.stdout.write(buildSliceReadyJson(file, key, result?.gates ?? null, scoped, ready, result?.continuationOf ?? null) + "\n");
|
|
1010
1348
|
}
|
|
1011
1349
|
else {
|
|
1012
1350
|
console.log(ready ? `slice "${key}" is ready-to-implement` : `slice "${key}" is NOT ready-to-implement`);
|
|
1351
|
+
// MIL-208: `--slice-ready` on a continuation key transparently reports the
|
|
1352
|
+
// originating slice's own verdict (the doc join resolves straight through) — name
|
|
1353
|
+
// that slice so the text mode doesn't look like this key has a doc of its own.
|
|
1354
|
+
const sliceIdx = refs.sliceKeys.indexOf(key);
|
|
1355
|
+
const continuation = sliceIdx === -1 ? null : continuationOf(model, refs, sliceIdx);
|
|
1356
|
+
if (continuation)
|
|
1357
|
+
console.log(` (continuation of "${continuation.sliceKey}")`);
|
|
1013
1358
|
}
|
|
1014
1359
|
if (!ready)
|
|
1015
1360
|
process.exit(1);
|
|
@@ -1159,12 +1504,16 @@ program
|
|
|
1159
1504
|
});
|
|
1160
1505
|
program
|
|
1161
1506
|
.command("coverage")
|
|
1162
|
-
.description("check that every INV-* invariant ID cited in
|
|
1163
|
-
"
|
|
1164
|
-
"definition-of-done citation check; advisory by default, --strict for CI"
|
|
1507
|
+
.description("check that every INV-* invariant ID cited in an implemented slice doc is cited by a test " +
|
|
1508
|
+
"under --tests <dir> (MIL-130/MIL-207) — mechanizes reference/implement.md's " +
|
|
1509
|
+
"definition-of-done citation check; advisory by default, --strict for CI; " +
|
|
1510
|
+
"--include-ready also counts ready-to-implement docs (forward-looking report); a " +
|
|
1511
|
+
"continuation slice (an `again` view instance with no doc of its own, MIL-208) is excluded " +
|
|
1512
|
+
"from the report — its invariants, if any, live in the originating slice's own doc")
|
|
1165
1513
|
.argument("<file>", "input .em file")
|
|
1166
1514
|
.requiredOption("--tests <dir>", "directory to scan recursively for test files citing invariant IDs")
|
|
1167
1515
|
.option("--strict", "exit non-zero if any invariant ID has zero citations (CI)")
|
|
1516
|
+
.option("--include-ready", "also count ready-to-implement docs, not just implemented (MIL-207)")
|
|
1168
1517
|
.option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
|
|
1169
1518
|
.action((file, opts) => {
|
|
1170
1519
|
const { model, diagnostics, refs } = compileFile(file);
|
|
@@ -1173,17 +1522,27 @@ program
|
|
|
1173
1522
|
console.error("em coverage: model has errors — fix them first");
|
|
1174
1523
|
process.exit(1);
|
|
1175
1524
|
}
|
|
1525
|
+
const baseDir = dirname(file);
|
|
1526
|
+
const includeReady = !!opts.includeReady;
|
|
1527
|
+
// MIL-207: a missing --tests dir is only a defect when something is actually in scope to
|
|
1528
|
+
// check — cheap to know without touching the test tree (resolveScopedSlices never reads
|
|
1529
|
+
// --tests). A fresh scaffold (or a doc-only ratification PR, with the MIL-207 default scope)
|
|
1530
|
+
// has zero implemented docs, so there's nothing yet for a test to cite.
|
|
1531
|
+
const anyInScope = resolveScopedSlices(model, refs, baseDir, includeReady).some((s) => s.inScope);
|
|
1176
1532
|
if (!existsSync(opts.tests)) {
|
|
1177
|
-
|
|
1178
|
-
|
|
1533
|
+
if (anyInScope) {
|
|
1534
|
+
console.error(`em coverage: --tests directory not found: ${opts.tests}`);
|
|
1535
|
+
process.exit(1);
|
|
1536
|
+
}
|
|
1537
|
+
console.error(`warn: em coverage: --tests directory not found: ${opts.tests} — nothing in scope yet`);
|
|
1179
1538
|
}
|
|
1180
|
-
if (!statSync(opts.tests).isDirectory()) {
|
|
1539
|
+
else if (!statSync(opts.tests).isDirectory()) {
|
|
1181
1540
|
console.error(`em coverage: --tests is not a directory: ${opts.tests}`);
|
|
1182
1541
|
process.exit(1);
|
|
1183
1542
|
}
|
|
1184
|
-
const report = buildCoverageReport(model, refs,
|
|
1543
|
+
const report = buildCoverageReport(model, refs, baseDir, opts.tests, includeReady);
|
|
1185
1544
|
if (opts.json) {
|
|
1186
|
-
process.stdout.write(buildCoverageJson(file, opts.tests, report) + "\n");
|
|
1545
|
+
process.stdout.write(buildCoverageJson(file, opts.tests, report, includeReady) + "\n");
|
|
1187
1546
|
}
|
|
1188
1547
|
else {
|
|
1189
1548
|
for (const slice of report.slices) {
|
|
@@ -1232,29 +1591,36 @@ program
|
|
|
1232
1591
|
console.error("em status: --json, --md, and --badge are mutually exclusive");
|
|
1233
1592
|
process.exit(1);
|
|
1234
1593
|
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
console.error(`em status: --tests is not a directory: ${opts.tests}`);
|
|
1242
|
-
process.exit(1);
|
|
1243
|
-
}
|
|
1594
|
+
// The "directory not found" half of the --tests check is deferred until after compiling
|
|
1595
|
+
// (below) — MIL-207: whether that's an error depends on whether anything is actually in
|
|
1596
|
+
// scope across the given models, which needs the compiled model+refs to answer.
|
|
1597
|
+
if (opts.tests && existsSync(opts.tests) && !statSync(opts.tests).isDirectory()) {
|
|
1598
|
+
console.error(`em status: --tests is not a directory: ${opts.tests}`);
|
|
1599
|
+
process.exit(1);
|
|
1244
1600
|
}
|
|
1245
1601
|
const compiled = [];
|
|
1246
1602
|
let anyErrors = false;
|
|
1247
1603
|
for (const file of files) {
|
|
1248
|
-
const { model, refs, diagnostics } = compileFile(file);
|
|
1604
|
+
const { model, refs, diagnostics, source } = compileFile(file);
|
|
1249
1605
|
printDiagnosticsFor(file, diagnostics);
|
|
1250
1606
|
if (hasErrors(diagnostics))
|
|
1251
1607
|
anyErrors = true;
|
|
1252
|
-
compiled.push({ file, model, refs });
|
|
1608
|
+
compiled.push({ file, model, refs, source });
|
|
1253
1609
|
}
|
|
1254
1610
|
if (anyErrors) {
|
|
1255
1611
|
console.error("em status: not reporting status — fix the errors above");
|
|
1256
1612
|
process.exit(1);
|
|
1257
1613
|
}
|
|
1614
|
+
if (opts.tests && !existsSync(opts.tests)) {
|
|
1615
|
+
// MIL-207: same leniency as `em coverage` — a missing --tests dir is only a defect when
|
|
1616
|
+
// at least one model has something in scope (`implemented`) to check.
|
|
1617
|
+
const anyInScope = compiled.some(({ model, refs, file }) => resolveScopedSlices(model, refs, dirname(file), false).some((s) => s.inScope));
|
|
1618
|
+
if (anyInScope) {
|
|
1619
|
+
console.error(`em status: --tests directory not found: ${opts.tests}`);
|
|
1620
|
+
process.exit(1);
|
|
1621
|
+
}
|
|
1622
|
+
console.error(`warn: em status: --tests directory not found: ${opts.tests} — nothing in scope yet`);
|
|
1623
|
+
}
|
|
1258
1624
|
const sliceFacts = [];
|
|
1259
1625
|
const factsByFile = new Map();
|
|
1260
1626
|
const statusDiagnostics = [];
|
|
@@ -1296,7 +1662,13 @@ program
|
|
|
1296
1662
|
const sliceDocFacts = facts.map((f) => ({ key: f.key, status: f.rawStatus, implementedIn: f.implementedIn }));
|
|
1297
1663
|
return resolveConformanceEntry(file, opts.repo, sliceDocFacts);
|
|
1298
1664
|
});
|
|
1299
|
-
|
|
1665
|
+
// MIL-218: one model-version entry per input file.
|
|
1666
|
+
const modelVersion = compiled.map(({ file, model, refs, source }) => resolveModelVersionStatusEntry(file, model, refs, source));
|
|
1667
|
+
// MIL-219: one em-version entry per input file whose state file resolved.
|
|
1668
|
+
const emVersion = compiled
|
|
1669
|
+
.map(({ file }) => resolveEmVersionStatusEntry(file, PKG_VERSION))
|
|
1670
|
+
.filter((e) => e !== null);
|
|
1671
|
+
const report = buildStatusReport(files, sliceFacts, openIssuesCount, invariants, conformance, statusDiagnostics, modelVersion, emVersion);
|
|
1300
1672
|
let output;
|
|
1301
1673
|
if (opts.json)
|
|
1302
1674
|
output = buildStatusJson(report);
|
|
@@ -1341,6 +1713,32 @@ program
|
|
|
1341
1713
|
console.log(formatConformancePart(entry));
|
|
1342
1714
|
}
|
|
1343
1715
|
});
|
|
1716
|
+
program
|
|
1717
|
+
.command("metrics")
|
|
1718
|
+
.description("the pilot metrics named in advance by the register, computed from git history alone " +
|
|
1719
|
+
"(MIL-170): ratification turnaround (reviewed -> ratified -> implemented), conform-cycle " +
|
|
1720
|
+
"cadence + finding counts, and status-vs-reality disagreement over time — plus a fourth, " +
|
|
1721
|
+
"reported as not computable from history (see docs/usage-data.md). `<file>` is an anchor " +
|
|
1722
|
+
".em file, used only to locate slices/, conformance/, and .event-modeling.md relative to " +
|
|
1723
|
+
"it — same convention as em ledger; never parsed or compiled")
|
|
1724
|
+
.argument("<file>", "anchor .em file")
|
|
1725
|
+
.requiredOption("--from <rev>", "baseline revision")
|
|
1726
|
+
.option("--to <rev>", "compare revision (default: HEAD)")
|
|
1727
|
+
.option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
|
|
1728
|
+
.action((file, opts) => {
|
|
1729
|
+
const to = opts.to ?? "HEAD";
|
|
1730
|
+
const computed = computeMetrics(file, opts.from, to);
|
|
1731
|
+
if (!computed.ok) {
|
|
1732
|
+
console.error(computed.message);
|
|
1733
|
+
process.exit(1);
|
|
1734
|
+
}
|
|
1735
|
+
if (opts.json) {
|
|
1736
|
+
process.stdout.write(buildMetricsJson(computed.result) + "\n");
|
|
1737
|
+
}
|
|
1738
|
+
else {
|
|
1739
|
+
console.log(formatMetricsText(computed.result));
|
|
1740
|
+
}
|
|
1741
|
+
});
|
|
1344
1742
|
// `em query` (MIL-168): deterministic graph queries over the compiled model — scoped, token-
|
|
1345
1743
|
// cheap answers instead of a whole-model `em export`. Compiles every input file through the
|
|
1346
1744
|
// LIGHTER query sub-pipeline (query/pipeline.ts: parse -> normalize -> refs -> validate against
|
|
@@ -1567,8 +1965,10 @@ program
|
|
|
1567
1965
|
program
|
|
1568
1966
|
.command("mcp")
|
|
1569
1967
|
.description("start an MCP (Model Context Protocol) server over stdio (MIL-21) — a structured, agent-" +
|
|
1570
|
-
"facing alternative to shelling out to `em`;
|
|
1571
|
-
"
|
|
1968
|
+
"facing alternative to shelling out to `em`; every tool mirrors a CLI `--json` surface " +
|
|
1969
|
+
"byte-for-byte. docs/mcp.md isn't vendored into the skill bundle — for the handshake and " +
|
|
1970
|
+
"the full, current tool table (the list changes as commands gain MCP parity, so it's not " +
|
|
1971
|
+
"repeated here — MIL-187) see https://github.com/milehimikey/em/blob/main/docs/mcp.md. " +
|
|
1572
1972
|
"Equivalent to running the `em-mcp` bin directly")
|
|
1573
1973
|
.action(async () => {
|
|
1574
1974
|
// A 3-line wrapper only, for discoverability — the server itself (src/mcp/) never imports
|
|
@@ -1689,6 +2089,18 @@ skill
|
|
|
1689
2089
|
console.log(f.message);
|
|
1690
2090
|
console.log(result.ok ? `ok — vendored skill matches em ${PKG_VERSION}` : `${result.findings.length} mismatch(es)`);
|
|
1691
2091
|
}
|
|
2092
|
+
// MIL-219 ruling A: `em skill check` also warns when [path]'s own state file records an
|
|
2093
|
+
// `Em version:` behind the installed em — best-effort (no state file, or an unparseable
|
|
2094
|
+
// one, is silently skipped; that's `em upgrade`'s own hard-incompatibility case to report,
|
|
2095
|
+
// not this command's). Always stderr, independent of --json/exit code — advisory only, and
|
|
2096
|
+
// this command's own exit code stays about skill-bundle drift, never about this.
|
|
2097
|
+
const loaded = loadStateFile(resolve(path));
|
|
2098
|
+
if (loaded.ok) {
|
|
2099
|
+
const parsed = parseState(loaded.text);
|
|
2100
|
+
if (parsed.ok && isOlder(parsed.state.emVersion, PKG_VERSION)) {
|
|
2101
|
+
console.error(`warn: recorded Em version (${parsed.state.emVersion}) is behind installed em (${PKG_VERSION}) — run \`em upgrade\``);
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
1692
2104
|
// Set the code rather than process.exit(): same rationale as em ledger/em diff — stdout to
|
|
1693
2105
|
// a pipe (a --json document) shouldn't risk truncation.
|
|
1694
2106
|
if (!result.ok)
|
|
@@ -1764,6 +2176,105 @@ ci.command("init")
|
|
|
1764
2176
|
console.log(`${path} already up to date`);
|
|
1765
2177
|
}
|
|
1766
2178
|
});
|
|
2179
|
+
program
|
|
2180
|
+
.command("upgrade")
|
|
2181
|
+
.description("bring a model repo authored under an older em (1.6 forward) up to the installed version: " +
|
|
2182
|
+
"an ordered list of mechanical steps (skill bundle, reaction shape, state-file bullets, " +
|
|
2183
|
+
"generated CI blocks, constitution scaffold) plus a human list of things no command can " +
|
|
2184
|
+
"safely decide by itself (MIL-219, see docs/upgrading.md). Dry-run by default; --apply " +
|
|
2185
|
+
"makes one git commit per applicable step, refusing on a dirty working tree")
|
|
2186
|
+
.argument("<file>", "input .em model file")
|
|
2187
|
+
.option("--apply", "apply every applicable mechanical step, one git commit each, then a final `Em version:` commit")
|
|
2188
|
+
.option("--check", "exit non-zero only on a hard incompatibility (unparseable state file, an un-migratable .em shape) — writes nothing; what CI runs")
|
|
2189
|
+
.option("--json", "print a JSON document instead of text (dry-run/--check only, never with --apply)")
|
|
2190
|
+
.action((file, opts) => {
|
|
2191
|
+
if (opts.apply && opts.check) {
|
|
2192
|
+
console.error("em upgrade: --apply and --check are mutually exclusive");
|
|
2193
|
+
process.exit(1);
|
|
2194
|
+
}
|
|
2195
|
+
const { model, refs, diagnostics, source } = compileFile(file);
|
|
2196
|
+
printDiagnostics(diagnostics);
|
|
2197
|
+
// Deliberately does NOT refuse on hasErrors(diagnostics), unlike every other command that
|
|
2198
|
+
// compiles a model first: the old pre-1.7.1 two-slice reaction shape `reaction-shape`
|
|
2199
|
+
// migrates is itself a validation ERROR under the current rules (MIL-120 changed what
|
|
2200
|
+
// `em validate` accepts) — refusing here would make that step, and the `predates-1.6` human
|
|
2201
|
+
// item, unreachable for exactly the repos that need them. Every step/human-detector either
|
|
2202
|
+
// reads the raw source directly (reaction-shape/predates-1.6, via planMigration) or a
|
|
2203
|
+
// best-effort compiled model that's still structurally usable even with validation errors
|
|
2204
|
+
// present (compile() never throws for a semantic error, only a parse error — already
|
|
2205
|
+
// handled by compileFile() above).
|
|
2206
|
+
if (hasErrors(diagnostics)) {
|
|
2207
|
+
console.error(`em upgrade: "${file}" has validation errors — proceeding anyway (this is often exactly what needs upgrading)`);
|
|
2208
|
+
}
|
|
2209
|
+
void source; // compileFile's own source read; steps re-read from disk themselves (module header)
|
|
2210
|
+
const baseDir = dirname(file);
|
|
2211
|
+
const repoRootResult = resolveRepoRoot(baseDir);
|
|
2212
|
+
if (!repoRootResult.ok) {
|
|
2213
|
+
console.error(repoRootResult.message);
|
|
2214
|
+
process.exit(1);
|
|
2215
|
+
}
|
|
2216
|
+
const ctx = {
|
|
2217
|
+
modelFile: file,
|
|
2218
|
+
baseDir,
|
|
2219
|
+
repoRoot: repoRootResult.repoRoot,
|
|
2220
|
+
installedVersion: PKG_VERSION,
|
|
2221
|
+
packagedSkillsRoot: packagedSkillsRoot(),
|
|
2222
|
+
model,
|
|
2223
|
+
refs,
|
|
2224
|
+
};
|
|
2225
|
+
if (opts.check) {
|
|
2226
|
+
const { ok, report } = checkUpgrade(ctx);
|
|
2227
|
+
console.error(`em upgrade --check: from ${report.from.version}${report.from.inferred ? ` (inferred: ${report.from.basis})` : ""} → to ${report.to}`);
|
|
2228
|
+
for (const s of report.steps) {
|
|
2229
|
+
console.error(` [${s.applicable ? "x" : " "}] ${s.id} (since ${s.sinceVersion}): ${s.reason}`);
|
|
2230
|
+
}
|
|
2231
|
+
for (const h of report.human)
|
|
2232
|
+
console.error(` human: ${h.id}: ${h.reason}`);
|
|
2233
|
+
console.error(ok ? "ok — no hard incompatibility" : "em upgrade --check: hard incompatibility found");
|
|
2234
|
+
if (!ok)
|
|
2235
|
+
process.exitCode = 1;
|
|
2236
|
+
return;
|
|
2237
|
+
}
|
|
2238
|
+
if (opts.apply) {
|
|
2239
|
+
const result = applyUpgrade(ctx);
|
|
2240
|
+
for (const s of result.applied) {
|
|
2241
|
+
if (s.applied)
|
|
2242
|
+
console.log(`applied: ${s.id} — ${s.changedFiles.join(", ")} (${s.commit})`);
|
|
2243
|
+
else
|
|
2244
|
+
console.log(`skipped: ${s.id}`);
|
|
2245
|
+
}
|
|
2246
|
+
if (!result.ok) {
|
|
2247
|
+
console.error(result.message);
|
|
2248
|
+
process.exit(1);
|
|
2249
|
+
}
|
|
2250
|
+
if (result.emVersionCommit)
|
|
2251
|
+
console.log(`recorded: ${result.emVersionCommit}`);
|
|
2252
|
+
else
|
|
2253
|
+
console.log("Em version: already up to date — no commit needed");
|
|
2254
|
+
for (const h of result.report.human)
|
|
2255
|
+
console.log(`human: ${h.id}: ${h.reason}`);
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
const report = detectUpgrade(ctx);
|
|
2259
|
+
if (opts.json) {
|
|
2260
|
+
console.log(buildUpgradeJson(file, report));
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
console.log(`em upgrade: from ${report.from.version}${report.from.inferred ? ` (inferred: ${report.from.basis})` : ""} → to ${report.to}`);
|
|
2264
|
+
if (report.stateFileError)
|
|
2265
|
+
console.log(`state file: ${report.stateFileError}`);
|
|
2266
|
+
console.log("Steps:");
|
|
2267
|
+
for (const s of report.steps) {
|
|
2268
|
+
console.log(` [${s.applicable ? "x" : " "}] ${s.id} (since ${s.sinceVersion}): ${s.reason}`);
|
|
2269
|
+
}
|
|
2270
|
+
console.log("Human:");
|
|
2271
|
+
if (report.human.length === 0)
|
|
2272
|
+
console.log(" (none)");
|
|
2273
|
+
for (const h of report.human)
|
|
2274
|
+
console.log(` ${h.id}: ${h.reason}`);
|
|
2275
|
+
const applicableCount = report.steps.filter((s) => s.applicable).length;
|
|
2276
|
+
console.log(`${applicableCount} step(s) applicable, ${report.human.length} human item(s) — re-run with --apply to apply mechanical steps`);
|
|
2277
|
+
});
|
|
1767
2278
|
// Exported so dev tooling (e.g. scripts/generate-skill-docs.ts) can introspect the registered
|
|
1768
2279
|
// commands/options without triggering a real CLI run. Guarded the same way Node's CJS
|
|
1769
2280
|
// `require.main === module` used to: only actually parse argv when this file is the process's
|
|
@@ -1806,15 +2317,17 @@ function compileFile(file, opts = {},
|
|
|
1806
2317
|
}
|
|
1807
2318
|
}
|
|
1808
2319
|
/** Lineage-ref resolution (MIL-84), frontmatter-coherence (MIL-85), note-binding mismatches
|
|
1809
|
-
* (MIL-126), doc↔model consistency (MIL-124),
|
|
1810
|
-
* validate`'s fs-aware rules — every other diagnostic in
|
|
1811
|
-
* pure function of the .em source. All
|
|
1812
|
-
*
|
|
1813
|
-
*
|
|
1814
|
-
*
|
|
1815
|
-
*
|
|
1816
|
-
*
|
|
1817
|
-
|
|
2320
|
+
* (MIL-126), doc↔model consistency (MIL-124), orphaned slice docs (MIL-183), and model-version
|
|
2321
|
+
* staleness (MIL-218) are `em validate`'s fs-aware rules — every other diagnostic in
|
|
2322
|
+
* `diagnostics` (from `compileFile`) is a pure function of the .em source. All six read
|
|
2323
|
+
* `slices/*.md`/`model-versions/*.json` alongside the model. Doc↔model consistency,
|
|
2324
|
+
* orphaned-slice-doc, and model-version-stale are deliberately validate-only (unlike
|
|
2325
|
+
* note-binding, which `em render` also folds in) — all three are conform-phase concerns, not
|
|
2326
|
+
* something every render needs to recheck. Shared by `em validate` and `em state log-usage`
|
|
2327
|
+
* (MIL-161) — the latter needs the exact same diagnostic set `em validate --json` would
|
|
2328
|
+
* report, since its whole job is logging which categories fired. `source` is the `.em` file's
|
|
2329
|
+
* own text — only `validateModelVersionStale` reads it (the modelHash comparison). */
|
|
2330
|
+
function computeAllDiagnostics(file, model, refs, diagnostics, source) {
|
|
1818
2331
|
return [
|
|
1819
2332
|
...diagnostics,
|
|
1820
2333
|
...validateLineage(model, refs, dirname(file)),
|
|
@@ -1822,6 +2335,7 @@ function computeAllDiagnostics(file, model, refs, diagnostics) {
|
|
|
1822
2335
|
...validateNoteBindings(model, refs, dirname(file)),
|
|
1823
2336
|
...validateDocModelConsistency(model, refs, dirname(file)),
|
|
1824
2337
|
...validateOrphanedSliceDocs(model, refs, dirname(file)),
|
|
2338
|
+
...validateModelVersionStale(model, refs, dirname(file), source),
|
|
1825
2339
|
];
|
|
1826
2340
|
}
|
|
1827
2341
|
/** Read a file's text, or exit with a clear error — shared by `em diff` (both forms) and
|
|
@@ -1921,16 +2435,22 @@ function printDivergences(model) {
|
|
|
1921
2435
|
console.log(` divergence :${el.line} slice "${slice.name}" ${el.kind} "${el.name}": ${el.divergence}`);
|
|
1922
2436
|
}
|
|
1923
2437
|
}
|
|
1924
|
-
/** Print only elements marked `public`: slice, kind, name, line — for an integration-surface
|
|
2438
|
+
/** Print only elements marked `public`: slice, kind, name, line — for an integration-surface
|
|
2439
|
+
* audit. A `public` view also gets a "(no in-model reader)" note (MIL-215) when no `ui`/
|
|
2440
|
+
* reaction anywhere on its timeline reads it — an audit hint, not a warning: a genuinely
|
|
2441
|
+
* cross-model `public` view's reader legitimately lives outside this file (see
|
|
2442
|
+
* `publicViewsWithoutInModelReader`'s own header for why this stays advisory). */
|
|
1925
2443
|
function printPublicElements(model) {
|
|
1926
2444
|
const pub = model.elements.filter((el) => el.public && (el.kind === "event" || el.kind === "view"));
|
|
1927
2445
|
if (pub.length === 0) {
|
|
1928
2446
|
console.log("no public elements");
|
|
1929
2447
|
return;
|
|
1930
2448
|
}
|
|
2449
|
+
const noReader = publicViewsWithoutInModelReader(model);
|
|
1931
2450
|
for (const el of pub) {
|
|
1932
2451
|
const slice = model.slices[el.sliceIndex];
|
|
1933
|
-
|
|
2452
|
+
const note = el.kind === "view" && noReader.has(el.logicalId) ? " (no in-model reader)" : "";
|
|
2453
|
+
console.log(` public :${el.line} slice "${slice.name}" ${el.kind} "${el.name}"${note}`);
|
|
1934
2454
|
}
|
|
1935
2455
|
}
|
|
1936
2456
|
/** Diagnostics in `all` not already present (by reference identity) in `already` — avoids
|