@milehimikey/em 1.12.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 +10 -2
- package/.claude/skills/event-modeling-discover/SKILL.md +1 -1
- package/.claude/skills/event-modeling-implement/SKILL.md +1 -1
- package/.claude/skills/event-modeling-implement/reference/implement.md +12 -6
- package/.claude/skills/event-modeling-review/SKILL.md +5 -1
- package/.claude/skills/event-modeling-shared/reference/em-dsl.md +40 -2
- package/.claude/skills/event-modeling-shared/reference/operating-principles.md +13 -3
- package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +30 -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.js +17 -0
- 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/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 +17 -0
- package/dist/cli/reratify.js +25 -1
- 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 +122 -0
- package/dist/cli/upgrade.js +476 -0
- package/dist/cli/upgrade.js.map +1 -0
- package/dist/cli.js +524 -36
- package/dist/cli.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 +26 -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 +114 -3
- 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 +1 -1
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,6 +28,7 @@ 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";
|
|
@@ -35,9 +36,11 @@ import { planMigration, verifyMigration } from "./cli/migrateReactionShape.js";
|
|
|
35
36
|
import { buildLedgerJson } from "./emit/ledgerJson.js";
|
|
36
37
|
import { buildCoverageReport, resolveScopedSlices } 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 " +
|
|
@@ -587,7 +656,7 @@ slice
|
|
|
587
656
|
.option("--on <date>", "ratification date, YYYY-MM-DD (default: today, local date)")
|
|
588
657
|
.option("--skip-review", "ratify without a recorded review — prints a loud notice on stderr")
|
|
589
658
|
.action((file, sliceKey, opts) => {
|
|
590
|
-
const { model, refs, diagnostics } = compileFile(file);
|
|
659
|
+
const { model, refs, diagnostics, source } = compileFile(file);
|
|
591
660
|
printDiagnostics(diagnostics);
|
|
592
661
|
// Scoped the same way `em slice mark-implemented`/`em export --slice`/`em validate
|
|
593
662
|
// --slice-ready` are: only an error concerning THIS slice refuses.
|
|
@@ -622,6 +691,7 @@ slice
|
|
|
622
691
|
console.log(result.changed
|
|
623
692
|
? `ratified: ${result.path} (ratifiedBy: ${opts.by}, ratifiedOn: ${ratifiedOn})`
|
|
624
693
|
: `already ratified (no-op): ${result.path}`);
|
|
694
|
+
warnModelVersionDrift(dirname(file), model, refs, source, `ratifying "${sliceKey}"`);
|
|
625
695
|
});
|
|
626
696
|
slice
|
|
627
697
|
.command("reratify")
|
|
@@ -634,7 +704,7 @@ slice
|
|
|
634
704
|
.argument("<file>", "input .em file")
|
|
635
705
|
.argument("<slice-key>", "slice export key (kebab-case)")
|
|
636
706
|
.action((file, sliceKey) => {
|
|
637
|
-
const { model, refs, diagnostics } = compileFile(file);
|
|
707
|
+
const { model, refs, diagnostics, source } = compileFile(file);
|
|
638
708
|
printDiagnostics(diagnostics);
|
|
639
709
|
// Scoped the same way `em slice ratify`/`em slice mark-implemented`/`em export --slice`/
|
|
640
710
|
// `em validate --slice-ready` are: only an error concerning THIS slice refuses.
|
|
@@ -648,7 +718,60 @@ slice
|
|
|
648
718
|
console.error(`em slice reratify: ${result.message}`);
|
|
649
719
|
process.exit(1);
|
|
650
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
|
+
}
|
|
651
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}`);
|
|
652
775
|
});
|
|
653
776
|
program
|
|
654
777
|
.command("changelog")
|
|
@@ -673,23 +796,45 @@ program
|
|
|
673
796
|
}
|
|
674
797
|
});
|
|
675
798
|
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
|
-
|
|
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()) {
|
|
679
811
|
const loaded = loadStateFile(dirOrFile);
|
|
680
812
|
if (!loaded.ok) {
|
|
681
813
|
console.error(loaded.message);
|
|
682
814
|
process.exit(1);
|
|
683
815
|
}
|
|
684
|
-
const today = localIsoDate();
|
|
685
816
|
const result = mutate(loaded.text, today);
|
|
686
817
|
if (!result.ok) {
|
|
687
818
|
console.error(`${cmdLabel}: ${result.message}`);
|
|
688
819
|
process.exit(1);
|
|
689
820
|
}
|
|
690
|
-
|
|
821
|
+
const stamped = setEmVersion(result.text, PKG_VERSION, today);
|
|
822
|
+
writeFileSync(loaded.path, stamped.ok ? stamped.text : result.text);
|
|
691
823
|
console.log(`wrote ${loaded.path}`);
|
|
692
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
|
+
}
|
|
693
838
|
const state = program
|
|
694
839
|
.command("state")
|
|
695
840
|
.description("read/write the state file's mechanical fields deterministically (see docs/cli.md)");
|
|
@@ -725,12 +870,95 @@ state
|
|
|
725
870
|
});
|
|
726
871
|
state
|
|
727
872
|
.command("set-conformance")
|
|
728
|
-
.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`)")
|
|
729
879
|
.argument("<revision>", "target-repo revision just diffed against")
|
|
730
880
|
.argument("[dir]", STATE_DIR_HELP, ".")
|
|
731
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")
|
|
732
883
|
.action((revision, dir, opts) => {
|
|
733
|
-
|
|
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
|
+
}
|
|
734
962
|
});
|
|
735
963
|
state
|
|
736
964
|
.command("set-review")
|
|
@@ -767,8 +995,8 @@ state
|
|
|
767
995
|
process.exit(1);
|
|
768
996
|
}
|
|
769
997
|
const phases = sortUsagePhases(rawPhases);
|
|
770
|
-
const { model, diagnostics, refs } = compileFile(file);
|
|
771
|
-
const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics);
|
|
998
|
+
const { model, diagnostics, refs, source } = compileFile(file);
|
|
999
|
+
const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics, source);
|
|
772
1000
|
const categorySet = new Set(allDiagnostics.map((d) => RULES[d.code].usageCategory));
|
|
773
1001
|
const categories = categorySet.size > 0 ? [...categorySet].sort() : ["none"];
|
|
774
1002
|
const loaded = loadStateFile(dirname(file));
|
|
@@ -785,6 +1013,67 @@ state
|
|
|
785
1013
|
writeFileSync(loaded.path, result.text);
|
|
786
1014
|
console.log(`wrote ${loaded.path} — phases: ${phases.join(", ")} — validate: ${categories.join(", ")}`);
|
|
787
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
|
+
});
|
|
788
1077
|
program
|
|
789
1078
|
.command("usage-report")
|
|
790
1079
|
.description("aggregate every .event-modeling.md's Usage log under [root] into phase/diagnostic-category " +
|
|
@@ -870,12 +1159,17 @@ program
|
|
|
870
1159
|
"at ratification time so a reader following the report's file:line citations knows they " +
|
|
871
1160
|
"describe an ancestor of the current model. Additive-only splice, never a rewrite of the " +
|
|
872
1161
|
"report; idempotent on the same --as-of/--findings/--on stamp; refuses if the report " +
|
|
873
|
-
"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)")
|
|
874
1166
|
.argument("<file>", "input .em file (used only to resolve the report path's base directory)")
|
|
875
1167
|
.argument("<report-path>", "path to the conformance report, relative to the model's directory")
|
|
876
1168
|
.requiredOption("--as-of <rev>", "the revision this ruling was made against — same value passed to `em state set-conformance`")
|
|
877
1169
|
.requiredOption("--findings <spec>", 'which finding number(s) this stamps as ruled, e.g. "1-3" or "1,2,4"')
|
|
878
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)")
|
|
879
1173
|
.action((file, reportPath, opts) => {
|
|
880
1174
|
const today = localIsoDate();
|
|
881
1175
|
const on = opts.on ?? today;
|
|
@@ -883,15 +1177,57 @@ program
|
|
|
883
1177
|
console.error(`em conform-supersede: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
|
|
884
1178
|
process.exit(1);
|
|
885
1179
|
}
|
|
886
|
-
|
|
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);
|
|
887
1190
|
if (!result.ok) {
|
|
888
1191
|
console.error(`em conform-supersede: ${result.message}`);
|
|
889
1192
|
process.exit(1);
|
|
890
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
|
+
}
|
|
891
1202
|
console.log(result.changed
|
|
892
1203
|
? `stamped superseded: ${result.path} (as of ${opts.asOf}, findings ${opts.findings}, on ${on})`
|
|
893
1204
|
: `already stamped (no-op): ${result.path}`);
|
|
894
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
|
+
});
|
|
895
1231
|
program
|
|
896
1232
|
.command("watch")
|
|
897
1233
|
.description("re-render on every save")
|
|
@@ -980,8 +1316,8 @@ program
|
|
|
980
1316
|
.option("--json", "print a JSON document instead of text — works on a model WITH errors, unlike `em export` " +
|
|
981
1317
|
"(MIL-128, see docs/cli.md); exit codes are unchanged")
|
|
982
1318
|
.action((file, opts) => {
|
|
983
|
-
const { model, diagnostics, refs } = compileFile(file);
|
|
984
|
-
const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics);
|
|
1319
|
+
const { model, diagnostics, refs, source } = compileFile(file);
|
|
1320
|
+
const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics, source);
|
|
985
1321
|
if (opts.sliceReady) {
|
|
986
1322
|
// MIL-87: a targeted, single-slice readiness gate, not part of the unconditional
|
|
987
1323
|
// diagnostic set above (see sliceReadyValidate.ts's header for why). Folds in MIL-85's
|
|
@@ -1265,11 +1601,11 @@ program
|
|
|
1265
1601
|
const compiled = [];
|
|
1266
1602
|
let anyErrors = false;
|
|
1267
1603
|
for (const file of files) {
|
|
1268
|
-
const { model, refs, diagnostics } = compileFile(file);
|
|
1604
|
+
const { model, refs, diagnostics, source } = compileFile(file);
|
|
1269
1605
|
printDiagnosticsFor(file, diagnostics);
|
|
1270
1606
|
if (hasErrors(diagnostics))
|
|
1271
1607
|
anyErrors = true;
|
|
1272
|
-
compiled.push({ file, model, refs });
|
|
1608
|
+
compiled.push({ file, model, refs, source });
|
|
1273
1609
|
}
|
|
1274
1610
|
if (anyErrors) {
|
|
1275
1611
|
console.error("em status: not reporting status — fix the errors above");
|
|
@@ -1326,7 +1662,13 @@ program
|
|
|
1326
1662
|
const sliceDocFacts = facts.map((f) => ({ key: f.key, status: f.rawStatus, implementedIn: f.implementedIn }));
|
|
1327
1663
|
return resolveConformanceEntry(file, opts.repo, sliceDocFacts);
|
|
1328
1664
|
});
|
|
1329
|
-
|
|
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);
|
|
1330
1672
|
let output;
|
|
1331
1673
|
if (opts.json)
|
|
1332
1674
|
output = buildStatusJson(report);
|
|
@@ -1371,6 +1713,32 @@ program
|
|
|
1371
1713
|
console.log(formatConformancePart(entry));
|
|
1372
1714
|
}
|
|
1373
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
|
+
});
|
|
1374
1742
|
// `em query` (MIL-168): deterministic graph queries over the compiled model — scoped, token-
|
|
1375
1743
|
// cheap answers instead of a whole-model `em export`. Compiles every input file through the
|
|
1376
1744
|
// LIGHTER query sub-pipeline (query/pipeline.ts: parse -> normalize -> refs -> validate against
|
|
@@ -1721,6 +2089,18 @@ skill
|
|
|
1721
2089
|
console.log(f.message);
|
|
1722
2090
|
console.log(result.ok ? `ok — vendored skill matches em ${PKG_VERSION}` : `${result.findings.length} mismatch(es)`);
|
|
1723
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
|
+
}
|
|
1724
2104
|
// Set the code rather than process.exit(): same rationale as em ledger/em diff — stdout to
|
|
1725
2105
|
// a pipe (a --json document) shouldn't risk truncation.
|
|
1726
2106
|
if (!result.ok)
|
|
@@ -1796,6 +2176,105 @@ ci.command("init")
|
|
|
1796
2176
|
console.log(`${path} already up to date`);
|
|
1797
2177
|
}
|
|
1798
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
|
+
});
|
|
1799
2278
|
// Exported so dev tooling (e.g. scripts/generate-skill-docs.ts) can introspect the registered
|
|
1800
2279
|
// commands/options without triggering a real CLI run. Guarded the same way Node's CJS
|
|
1801
2280
|
// `require.main === module` used to: only actually parse argv when this file is the process's
|
|
@@ -1838,15 +2317,17 @@ function compileFile(file, opts = {},
|
|
|
1838
2317
|
}
|
|
1839
2318
|
}
|
|
1840
2319
|
/** 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
|
-
|
|
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) {
|
|
1850
2331
|
return [
|
|
1851
2332
|
...diagnostics,
|
|
1852
2333
|
...validateLineage(model, refs, dirname(file)),
|
|
@@ -1854,6 +2335,7 @@ function computeAllDiagnostics(file, model, refs, diagnostics) {
|
|
|
1854
2335
|
...validateNoteBindings(model, refs, dirname(file)),
|
|
1855
2336
|
...validateDocModelConsistency(model, refs, dirname(file)),
|
|
1856
2337
|
...validateOrphanedSliceDocs(model, refs, dirname(file)),
|
|
2338
|
+
...validateModelVersionStale(model, refs, dirname(file), source),
|
|
1857
2339
|
];
|
|
1858
2340
|
}
|
|
1859
2341
|
/** Read a file's text, or exit with a clear error — shared by `em diff` (both forms) and
|
|
@@ -1953,16 +2435,22 @@ function printDivergences(model) {
|
|
|
1953
2435
|
console.log(` divergence :${el.line} slice "${slice.name}" ${el.kind} "${el.name}": ${el.divergence}`);
|
|
1954
2436
|
}
|
|
1955
2437
|
}
|
|
1956
|
-
/** 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). */
|
|
1957
2443
|
function printPublicElements(model) {
|
|
1958
2444
|
const pub = model.elements.filter((el) => el.public && (el.kind === "event" || el.kind === "view"));
|
|
1959
2445
|
if (pub.length === 0) {
|
|
1960
2446
|
console.log("no public elements");
|
|
1961
2447
|
return;
|
|
1962
2448
|
}
|
|
2449
|
+
const noReader = publicViewsWithoutInModelReader(model);
|
|
1963
2450
|
for (const el of pub) {
|
|
1964
2451
|
const slice = model.slices[el.sliceIndex];
|
|
1965
|
-
|
|
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}`);
|
|
1966
2454
|
}
|
|
1967
2455
|
}
|
|
1968
2456
|
/** Diagnostics in `all` not already present (by reference identity) in `already` — avoids
|