@milehimikey/em 1.13.0 → 1.14.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/agents/em-critic.md +26 -0
- package/.claude/agents/em-implementer.md +34 -0
- package/.claude/agents/em-reviewer.md +31 -0
- package/.claude/agents/em-validator.md +27 -0
- package/.claude/skills/event-modeling/SKILL.md +52 -12
- package/.claude/skills/event-modeling-conform/SKILL.md +1 -1
- package/.claude/skills/event-modeling-conform/reference/conform.md +1 -1
- package/.claude/skills/event-modeling-design/SKILL.md +140 -18
- package/.claude/skills/event-modeling-discover/SKILL.md +1 -1
- package/.claude/skills/event-modeling-engagement/SKILL.md +192 -0
- package/.claude/skills/event-modeling-engagement/reference/engagement.md +143 -0
- package/.claude/skills/event-modeling-implement/SKILL.md +7 -4
- package/.claude/skills/event-modeling-implement/reference/implement.md +51 -12
- package/.claude/skills/event-modeling-review/SKILL.md +1 -1
- package/.claude/skills/event-modeling-shared/reference/em-dsl.md +274 -188
- package/.claude/skills/event-modeling-shared/reference/operating-principles.md +61 -0
- package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +116 -4
- package/.claude/skills/event-modeling-shared/templates/constitution.md +18 -1
- package/.claude/skills/event-modeling-shared/templates/slice.md +64 -39
- package/.claude/skills/event-modeling-shared/templates/state.md +5 -1
- package/.claude-plugin/marketplace.json +14 -0
- package/README.md +25 -11
- package/dist/catalog/apiFirst.d.ts +28 -0
- package/dist/catalog/apiFirst.js +49 -0
- package/dist/catalog/apiFirst.js.map +1 -0
- package/dist/catalog/docJoin.d.ts +13 -0
- package/dist/catalog/docJoin.js +11 -0
- package/dist/catalog/docJoin.js.map +1 -1
- package/dist/catalog/docModelConsistencyValidate.js +44 -4
- package/dist/catalog/docModelConsistencyValidate.js.map +1 -1
- package/dist/catalog/invariantsValidate.d.ts +4 -0
- package/dist/catalog/invariantsValidate.js +102 -0
- package/dist/catalog/invariantsValidate.js.map +1 -0
- package/dist/catalog/scenarios.d.ts +15 -0
- package/dist/catalog/scenarios.js +97 -0
- package/dist/catalog/scenarios.js.map +1 -0
- package/dist/catalog/sliceDoc.d.ts +8 -0
- package/dist/catalog/sliceDoc.js +9 -0
- package/dist/catalog/sliceDoc.js.map +1 -1
- package/dist/catalog/sliceReadyValidate.d.ts +31 -4
- package/dist/catalog/sliceReadyValidate.js +66 -8
- package/dist/catalog/sliceReadyValidate.js.map +1 -1
- package/dist/catalog/sliceSections.d.ts +55 -0
- package/dist/catalog/sliceSections.js +317 -0
- package/dist/catalog/sliceSections.js.map +1 -0
- package/dist/cli/agentsMd.js +7 -1
- package/dist/cli/agentsMd.js.map +1 -1
- package/dist/cli/api.d.ts +90 -0
- package/dist/cli/api.js +318 -0
- package/dist/cli/api.js.map +1 -0
- package/dist/cli/ciInit.d.ts +88 -3
- package/dist/cli/ciInit.js +507 -50
- package/dist/cli/ciInit.js.map +1 -1
- package/dist/cli/conformScope.d.ts +28 -0
- package/dist/cli/conformScope.js +67 -0
- package/dist/cli/conformScope.js.map +1 -1
- package/dist/cli/coverage.d.ts +25 -2
- package/dist/cli/coverage.js +86 -12
- package/dist/cli/coverage.js.map +1 -1
- package/dist/cli/engagement.d.ts +210 -0
- package/dist/cli/engagement.js +444 -0
- package/dist/cli/engagement.js.map +1 -0
- package/dist/cli/engagementFile.d.ts +86 -0
- package/dist/cli/engagementFile.js +255 -0
- package/dist/cli/engagementFile.js.map +1 -0
- package/dist/cli/metrics.d.ts +10 -0
- package/dist/cli/metrics.js +49 -1
- package/dist/cli/metrics.js.map +1 -1
- package/dist/cli/pluginPin.d.ts +55 -0
- package/dist/cli/pluginPin.js +188 -0
- package/dist/cli/pluginPin.js.map +1 -0
- package/dist/cli/ratify.d.ts +27 -2
- package/dist/cli/ratify.js +112 -13
- package/dist/cli/ratify.js.map +1 -1
- package/dist/cli/reratify.d.ts +11 -4
- package/dist/cli/reratify.js +89 -35
- package/dist/cli/reratify.js.map +1 -1
- package/dist/cli/scopeInputs.d.ts +48 -0
- package/dist/cli/scopeInputs.js +176 -0
- package/dist/cli/scopeInputs.js.map +1 -0
- package/dist/cli/skillCheck.d.ts +15 -1
- package/dist/cli/skillCheck.js +22 -1
- package/dist/cli/skillCheck.js.map +1 -1
- package/dist/cli/skillDirs.d.ts +5 -0
- package/dist/cli/skillDirs.js +13 -1
- package/dist/cli/skillDirs.js.map +1 -1
- package/dist/cli/skillSync.d.ts +4 -0
- package/dist/cli/skillSync.js +23 -0
- package/dist/cli/skillSync.js.map +1 -1
- package/dist/cli/sliceNew.d.ts +11 -7
- package/dist/cli/sliceNew.js +91 -18
- package/dist/cli/sliceNew.js.map +1 -1
- package/dist/cli/sliceSync.d.ts +50 -0
- package/dist/cli/sliceSync.js +170 -0
- package/dist/cli/sliceSync.js.map +1 -0
- package/dist/cli/status.d.ts +24 -1
- package/dist/cli/status.js +20 -1
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/systemInputs.d.ts +53 -10
- package/dist/cli/systemInputs.js +186 -11
- package/dist/cli/systemInputs.js.map +1 -1
- package/dist/cli/upgrade.d.ts +30 -7
- package/dist/cli/upgrade.js +434 -50
- package/dist/cli/upgrade.js.map +1 -1
- package/dist/cli/usageLog.d.ts +4 -2
- package/dist/cli/usageLog.js +4 -2
- package/dist/cli/usageLog.js.map +1 -1
- package/dist/cli.js +631 -100
- package/dist/cli.js.map +1 -1
- package/dist/emit/api.d.ts +29 -0
- package/dist/emit/api.js +220 -0
- package/dist/emit/api.js.map +1 -0
- package/dist/emit/apiCheckJson.d.ts +3 -0
- package/dist/emit/apiCheckJson.js +21 -0
- package/dist/emit/apiCheckJson.js.map +1 -0
- package/dist/emit/codeownersJson.d.ts +3 -0
- package/dist/emit/codeownersJson.js +19 -0
- package/dist/emit/codeownersJson.js.map +1 -0
- package/dist/emit/coverageJson.d.ts +5 -2
- package/dist/emit/coverageJson.js +6 -2
- package/dist/emit/coverageJson.js.map +1 -1
- package/dist/emit/diffJson.d.ts +1 -1
- package/dist/emit/diffJson.js +8 -1
- package/dist/emit/diffJson.js.map +1 -1
- package/dist/emit/engagementJson.d.ts +8 -0
- package/dist/emit/engagementJson.js +46 -0
- package/dist/emit/engagementJson.js.map +1 -0
- package/dist/emit/glossaryJson.d.ts +1 -1
- package/dist/emit/glossaryJson.js +4 -1
- package/dist/emit/glossaryJson.js.map +1 -1
- package/dist/emit/json.d.ts +17 -1
- package/dist/emit/json.js +30 -1
- package/dist/emit/json.js.map +1 -1
- package/dist/emit/metricsJson.d.ts +1 -1
- package/dist/emit/metricsJson.js +4 -1
- package/dist/emit/metricsJson.js.map +1 -1
- package/dist/emit/queryJson.d.ts +1 -1
- package/dist/emit/queryJson.js +7 -1
- package/dist/emit/queryJson.js.map +1 -1
- package/dist/emit/scopeJson.d.ts +12 -0
- package/dist/emit/scopeJson.js +26 -0
- package/dist/emit/scopeJson.js.map +1 -0
- package/dist/emit/skillCheckJson.d.ts +3 -2
- package/dist/emit/skillCheckJson.js +10 -2
- package/dist/emit/skillCheckJson.js.map +1 -1
- package/dist/emit/statusJson.d.ts +1 -1
- package/dist/emit/statusJson.js +15 -1
- package/dist/emit/statusJson.js.map +1 -1
- package/dist/emit/systemJson.d.ts +15 -8
- package/dist/emit/systemJson.js +28 -13
- package/dist/emit/systemJson.js.map +1 -1
- package/dist/emit/upgradeJson.d.ts +1 -1
- package/dist/emit/upgradeJson.js +4 -1
- package/dist/emit/upgradeJson.js.map +1 -1
- package/dist/emit/validateJson.d.ts +3 -3
- package/dist/emit/validateJson.js +10 -4
- package/dist/emit/validateJson.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +186 -37
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/diff.d.ts +4 -1
- package/dist/model/diff.js +44 -8
- package/dist/model/diff.js.map +1 -1
- package/dist/model/glossary.d.ts +2 -0
- package/dist/model/glossary.js +1 -0
- package/dist/model/glossary.js.map +1 -1
- package/dist/model/model.d.ts +55 -2
- package/dist/model/model.js +57 -0
- package/dist/model/model.js.map +1 -1
- package/dist/model/qualifiedRef.d.ts +18 -0
- package/dist/model/qualifiedRef.js +26 -0
- package/dist/model/qualifiedRef.js.map +1 -1
- package/dist/model/queryIndex.d.ts +9 -1
- package/dist/model/queryIndex.js +18 -2
- package/dist/model/queryIndex.js.map +1 -1
- package/dist/model/rules.d.ts +143 -2
- package/dist/model/rules.js +162 -3
- package/dist/model/rules.js.map +1 -1
- package/dist/model/validate.d.ts +40 -0
- package/dist/model/validate.js +147 -1
- package/dist/model/validate.js.map +1 -1
- package/dist/parser/ast.d.ts +31 -3
- package/dist/parser/parser.js +148 -14
- package/dist/parser/parser.js.map +1 -1
- package/dist/query/format.js +1 -1
- package/dist/query/format.js.map +1 -1
- package/dist/query/verbs.d.ts +6 -0
- package/dist/query/verbs.js +6 -2
- package/dist/query/verbs.js.map +1 -1
- package/dist/system/adaptation.d.ts +14 -0
- package/dist/system/adaptation.js +49 -0
- package/dist/system/adaptation.js.map +1 -0
- package/dist/system/codeowners.d.ts +77 -0
- package/dist/system/codeowners.js +242 -0
- package/dist/system/codeowners.js.map +1 -0
- package/dist/system/manifest.d.ts +10 -5
- package/dist/system/manifest.js +54 -26
- package/dist/system/manifest.js.map +1 -1
- package/dist/system/migrateManifest.d.ts +28 -0
- package/dist/system/migrateManifest.js +155 -0
- package/dist/system/migrateManifest.js.map +1 -0
- package/dist/system/scope.d.ts +46 -0
- package/dist/system/scope.js +132 -0
- package/dist/system/scope.js.map +1 -0
- package/dist/system/verify.d.ts +50 -13
- package/dist/system/verify.js +179 -58
- package/dist/system/verify.js.map +1 -1
- package/dist/templates.d.ts +2 -2
- package/dist/templates.js +23 -2
- package/dist/templates.js.map +1 -1
- package/package.json +6 -3
- package/plugin/.claude-plugin/plugin.json +18 -0
- package/plugin/agents/em-critic.md +27 -0
- package/plugin/agents/em-implementer.md +35 -0
- package/plugin/agents/em-reviewer.md +32 -0
- package/plugin/agents/em-validator.md +28 -0
- package/plugin/shared/reference/em-dsl.md +866 -0
- package/plugin/shared/reference/methodology.md +329 -0
- package/plugin/shared/reference/operating-principles.md +222 -0
- package/plugin/shared/reference/slice-doc-schema.md +566 -0
- package/plugin/shared/templates/conformance-report.md +85 -0
- package/plugin/shared/templates/constitution.md +136 -0
- package/plugin/shared/templates/model-readme.md +46 -0
- package/plugin/shared/templates/slice.md +251 -0
- package/plugin/shared/templates/state.md +77 -0
- package/plugin/skills/conform/SKILL.md +93 -0
- package/plugin/skills/conform/reference/conform.md +428 -0
- package/plugin/skills/design/SKILL.md +297 -0
- package/plugin/skills/discover/SKILL.md +90 -0
- package/plugin/skills/discover/reference/extract.md +147 -0
- package/plugin/skills/engagement/SKILL.md +193 -0
- package/plugin/skills/engagement/reference/engagement.md +144 -0
- package/plugin/skills/event-modeling/SKILL.md +132 -0
- package/plugin/skills/implement/SKILL.md +61 -0
- package/plugin/skills/implement/reference/implement.md +439 -0
- package/plugin/skills/review/SKILL.md +74 -0
- package/dist/emit/typespec.d.ts +0 -26
- package/dist/emit/typespec.js +0 -356
- package/dist/emit/typespec.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -17,11 +17,12 @@ import { serializeBuilds, watchFile } from "./render/watch.js";
|
|
|
17
17
|
import { startLiveServer } from "./render/serve.js";
|
|
18
18
|
import { formatDiagnostic, hasErrors, publicViewsWithoutInModelReader } from "./model/validate.js";
|
|
19
19
|
import { buildExport, buildSliceExport, GENERATOR_VERSION } from "./emit/json.js";
|
|
20
|
-
import {
|
|
20
|
+
import { generateContract, runApiCheck, formatApiCheckText } from "./cli/api.js";
|
|
21
|
+
import { buildApiCheckJson } from "./emit/apiCheckJson.js";
|
|
21
22
|
import { buildValidateJson, buildSliceReadyJson, buildValidateListJson, collectMarkers } from "./emit/validateJson.js";
|
|
22
23
|
import { buildDiffJson } from "./emit/diffJson.js";
|
|
23
24
|
import { diffModels, formatModelDiff, hasChanges } from "./model/diff.js";
|
|
24
|
-
import { planDiffArgs, resolveRevision, resolveDocAtRevision } from "./cli/diff-inputs.js";
|
|
25
|
+
import { planDiffArgs, realGit, resolveRevision, resolveDocAtRevision } from "./cli/diff-inputs.js";
|
|
25
26
|
import { readSliceDoc } from "./catalog/readSliceDoc.js";
|
|
26
27
|
import { validateLineage } from "./catalog/lineageValidate.js";
|
|
27
28
|
import { validateFrontmatterCoherence } from "./catalog/frontmatterCoherenceValidate.js";
|
|
@@ -29,23 +30,30 @@ import { validateNoteBindings } from "./catalog/noteBindingValidate.js";
|
|
|
29
30
|
import { validateDocModelConsistency } from "./catalog/docModelConsistencyValidate.js";
|
|
30
31
|
import { validateOrphanedSliceDocs } from "./catalog/orphanedSliceDocValidate.js";
|
|
31
32
|
import { validateModelVersionStale } from "./catalog/modelVersionValidate.js";
|
|
32
|
-
import {
|
|
33
|
+
import { validateInvariants } from "./catalog/invariantsValidate.js";
|
|
34
|
+
import { computeSliceReadiness } from "./catalog/sliceReadyValidate.js";
|
|
35
|
+
import { applyEngagementClose, applyEngagementSet, buildNewEngagement, engagementPlanFor, engagementStatusFor, formatEngagementPlanText, formatEngagementStatusText, } from "./cli/engagement.js";
|
|
36
|
+
import { LEDGER_STATES, engagementPath, loadEngagement, readOpenEngagements } from "./cli/engagementFile.js";
|
|
37
|
+
import { buildEngagementPlanJson, buildEngagementStatusJson } from "./emit/engagementJson.js";
|
|
33
38
|
import { detectSliceDocCollisions } from "./catalog/modelCollisionValidate.js";
|
|
34
39
|
import { checkLedger, readLedgerWaiverTrailers, applyLedgerWaivers } from "./cli/ledgerCheck.js";
|
|
35
40
|
import { planMigration, verifyMigration } from "./cli/migrateReactionShape.js";
|
|
36
41
|
import { buildLedgerJson } from "./emit/ledgerJson.js";
|
|
37
|
-
import { buildCoverageReport, resolveScopedSlices } from "./cli/coverage.js";
|
|
42
|
+
import { buildCoverageReport, resolveScopedSlices, resolveCoverageSliceKey } from "./cli/coverage.js";
|
|
38
43
|
import { buildCoverageJson } from "./emit/coverageJson.js";
|
|
39
44
|
import { resolveSliceStatusFacts, countOpenIssues, resolveConformanceEntry, aggregateInvariantTotals, buildStatusReport, formatStatusText, formatStatusMarkdown, buildStatusBadge, formatConformancePart, findSpecifyRoot, resolveModelVersionStatusEntry, resolveEmVersionStatusEntry, } from "./cli/status.js";
|
|
40
45
|
import { buildStatusJson } from "./emit/statusJson.js";
|
|
41
46
|
import { buildFreshnessJson } from "./emit/freshnessJson.js";
|
|
42
47
|
import { computeMetrics, formatMetricsText } from "./cli/metrics.js";
|
|
48
|
+
import { runSystemScope } from "./cli/scopeInputs.js";
|
|
49
|
+
import { buildScopeJson } from "./emit/scopeJson.js";
|
|
43
50
|
import { buildMetricsJson } from "./emit/metricsJson.js";
|
|
44
|
-
import { planSkillSyncBundle, applySkillSyncBundle } from "./cli/skillSync.js";
|
|
45
|
-
import { checkSkillSyncBundle } from "./cli/skillCheck.js";
|
|
51
|
+
import { planSkillSyncBundle, applySkillSyncBundle, planAgentSync, applySkillSync } from "./cli/skillSync.js";
|
|
52
|
+
import { checkSkillSyncBundle, checkAgentFiles } from "./cli/skillCheck.js";
|
|
46
53
|
import { buildSkillCheckJson } from "./emit/skillCheckJson.js";
|
|
54
|
+
import { checkPlugin, claudePluginsDir, detectPlugin, pluginInstallCommands } from "./cli/pluginPin.js";
|
|
47
55
|
import { readContract } from "./cli/contract.js";
|
|
48
|
-
import { EM_SKILL_DIR_NAMES, EM_SHARED_DIR_NAMES, EM_ALL_SKILL_BUNDLE_DIRS, EM_SKILL_ANCHOR_DIR } from "./cli/skillDirs.js";
|
|
56
|
+
import { EM_SKILL_DIR_NAMES, EM_SHARED_DIR_NAMES, EM_ALL_SKILL_BUNDLE_DIRS, EM_SKILL_ANCHOR_DIR, EM_AGENT_FILES } from "./cli/skillDirs.js";
|
|
49
57
|
import { createServer as createMcpServer } from "./mcp/server.js";
|
|
50
58
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
51
59
|
import { syncAgentsMd } from "./cli/agentsMd.js";
|
|
@@ -56,11 +64,13 @@ import { buildCatalog } from "./catalog/build.js";
|
|
|
56
64
|
import { planCatalogArgs } from "./cli/catalog-inputs.js";
|
|
57
65
|
import { runSliceIndex } from "./cli/sliceIndex.js";
|
|
58
66
|
import { runMarkImplemented } from "./cli/markImplemented.js";
|
|
59
|
-
import { runRatify } from "./cli/ratify.js";
|
|
67
|
+
import { runRatify, parseMeaningFlags, meaningConfirmationLine } from "./cli/ratify.js";
|
|
60
68
|
import { runReview } from "./cli/review.js";
|
|
61
69
|
import { runConformSupersede } from "./cli/conformSupersede.js";
|
|
62
70
|
import { buildConformScope, changedPathsSince, resolveSliceDocFacts, seedAsisModel } from "./cli/conformScope.js";
|
|
63
71
|
import { buildSliceDocContent, buildStubDocContent, isSlicePattern, sliceDocKey, SLICE_PATTERNS } from "./cli/sliceNew.js";
|
|
72
|
+
import { buildSliceSyncJson, formatSliceSync, runSliceSync, sliceSyncFailed } from "./cli/sliceSync.js";
|
|
73
|
+
import { buildSliceRegions } from "./catalog/sliceSections.js";
|
|
64
74
|
import { wireSliceNote } from "./cli/sliceLink.js";
|
|
65
75
|
import { isStubStatus, runStubAll, STUB_STATUSES } from "./cli/sliceStubAll.js";
|
|
66
76
|
import { listModelCommits } from "./cli/changelog-git.js";
|
|
@@ -72,7 +82,7 @@ import { STATE_FILE_NAME, PHASES, isPhase, loadStateFile, parseState, setPhase,
|
|
|
72
82
|
import { findCertifiedVersion, latestModelVersionNumber, modelVersionDrift, runCertifyModelVersion, runModelVersionBump, } from "./cli/modelVersion.js";
|
|
73
83
|
import { STARTER_EM, starterEmFor, scaffoldReadme, scaffoldStateFile, scaffoldConstitution } from "./templates.js";
|
|
74
84
|
import { kebabSlug } from "./util/slug.js";
|
|
75
|
-
import { ciWorkflowPath, conformWorkflowPath, buildCiWorkflowFile, buildConformWorkflowFile, ciManagedBody, conformManagedBody, planCiFile, applyCiFile, findUnsafeCiInitArg, CI_WORKFLOW_MARKER, CONFORM_WORKFLOW_MARKER, } from "./cli/ciInit.js";
|
|
85
|
+
import { ciWorkflowPath, conformWorkflowPath, buildCiWorkflowFile, buildConformWorkflowFile, buildCiWorkflowFileMulti, buildConformWorkflowFileMulti, ciManagedBody, ciManagedBodyMulti, conformManagedBody, conformManagedBodyMulti, ciModelsFromManifest, describeOtherModels, describeSetChange, fileBlockModels, planCiFile, applyCiFile, findUnsafeCiInitArg, CI_WORKFLOW_MARKER, CONFORM_WORKFLOW_MARKER, } from "./cli/ciInit.js";
|
|
76
86
|
import { resolveRepoRoot, detectUpgrade, applyUpgrade, checkUpgrade } from "./cli/upgrade.js";
|
|
77
87
|
import { buildUpgradeJson } from "./emit/upgradeJson.js";
|
|
78
88
|
import { isOlder } from "./util/semver.js";
|
|
@@ -84,7 +94,9 @@ import { buildQuerySystem } from "./query/system.js";
|
|
|
84
94
|
import { queryConsumers, queryProducers, queryDownstream, queryUpstream, querySlices, queryInvariant, queryField, queryPath, } from "./query/verbs.js";
|
|
85
95
|
import { formatConsumers, formatProducers, formatClosure, formatSlices, formatInvariant, formatField, formatPath, } from "./query/format.js";
|
|
86
96
|
import { buildQueryJson } from "./emit/queryJson.js";
|
|
87
|
-
import { loadSystem } from "./cli/systemInputs.js";
|
|
97
|
+
import { gitProducerCommit, loadSystem, statusSystemBlock, SYSTEM_MANIFEST_FILE } from "./cli/systemInputs.js";
|
|
98
|
+
import { runCodeowners } from "./system/codeowners.js";
|
|
99
|
+
import { buildCodeownersJson } from "./emit/codeownersJson.js";
|
|
88
100
|
import { verifySystem } from "./system/verify.js";
|
|
89
101
|
import { buildSystemJson } from "./emit/systemJson.js";
|
|
90
102
|
const program = new Command();
|
|
@@ -95,7 +107,10 @@ const PKG_VERSION = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.me
|
|
|
95
107
|
program
|
|
96
108
|
.name("em")
|
|
97
109
|
.description("Event Modeling CLI — slice-first DSL rendered as a strict Graphviz grid")
|
|
98
|
-
.version(PKG_VERSION)
|
|
110
|
+
.version(PKG_VERSION)
|
|
111
|
+
// Program options (`--version`) only before the subcommand name: `em system codeowners --json`
|
|
112
|
+
// must reach the subcommand (MIL-234), and `em system` has its own `--json`.
|
|
113
|
+
.enablePositionalOptions();
|
|
99
114
|
program
|
|
100
115
|
.command("init")
|
|
101
116
|
.description("scaffold a starter .em model")
|
|
@@ -265,30 +280,67 @@ program
|
|
|
265
280
|
process.stdout.write(exported.text + "\n");
|
|
266
281
|
}
|
|
267
282
|
});
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
.
|
|
274
|
-
|
|
275
|
-
const { model, refs, diagnostics, source } = compileFile(file);
|
|
276
|
-
printDiagnostics(diagnostics);
|
|
277
|
-
if (hasErrors(diagnostics)) {
|
|
283
|
+
/** `em api generate` (and its deprecated `em typespec` alias): compile, refuse on errors,
|
|
284
|
+
* write `<modelDir>/contracts/<modelKey>.tsp` (R6) — or `-o`, or stdout. */
|
|
285
|
+
function runApiGenerate(file, opts) {
|
|
286
|
+
const compiled = compileFile(file);
|
|
287
|
+
const { source } = compiled;
|
|
288
|
+
printDiagnostics(compiled.diagnostics);
|
|
289
|
+
if (hasErrors(compiled.diagnostics)) {
|
|
278
290
|
console.error("not generating: fix the errors above");
|
|
279
291
|
process.exit(1);
|
|
280
292
|
}
|
|
281
|
-
const generated =
|
|
282
|
-
printDiagnostics(newDiagnostics(generated.diagnostics, diagnostics));
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
writeFileSync(opts.out, generated.text + "\n");
|
|
287
|
-
console.log(`wrote ${opts.out}`);
|
|
293
|
+
const generated = generateContract(file, source, compiled, opts.out);
|
|
294
|
+
printDiagnostics(newDiagnostics(generated.diagnostics, compiled.diagnostics));
|
|
295
|
+
if (opts.stdout) {
|
|
296
|
+
process.stdout.write(generated.text);
|
|
297
|
+
return;
|
|
288
298
|
}
|
|
289
|
-
|
|
290
|
-
|
|
299
|
+
mkdirSync(dirname(generated.contractPath), { recursive: true });
|
|
300
|
+
writeFileSync(generated.contractPath, generated.text);
|
|
301
|
+
console.log(`wrote ${generated.contractPath}`);
|
|
302
|
+
}
|
|
303
|
+
const api = program
|
|
304
|
+
.command("api")
|
|
305
|
+
.description("the model-owned TypeSpec contract for its `public` surface (MIL-237, see docs/cli.md)");
|
|
306
|
+
api
|
|
307
|
+
.command("generate")
|
|
308
|
+
.description("write the TypeSpec contract for the model's public commands, events and views to " +
|
|
309
|
+
"<model dir>/contracts/<model key>.tsp")
|
|
310
|
+
.argument("<file>", "input .em file")
|
|
311
|
+
.option("-o, --out <path>", "write to this path instead of <model dir>/contracts/<model key>.tsp")
|
|
312
|
+
.option("--stdout", "print the contract instead of writing it")
|
|
313
|
+
.action((file, opts) => runApiGenerate(file, opts));
|
|
314
|
+
api
|
|
315
|
+
.command("check")
|
|
316
|
+
.description("is the committed contract current? With --base, annotate each public-surface change since " +
|
|
317
|
+
"that revision additive or breaking (annotation only — exits 1 only when stale)")
|
|
318
|
+
.argument("<file>", "input .em file")
|
|
319
|
+
.option("--base <rev>", "git revision to diff the public surface against")
|
|
320
|
+
.option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
|
|
321
|
+
.action((file, opts) => {
|
|
322
|
+
const outcome = runApiCheck(file, opts.base);
|
|
323
|
+
printDiagnostics(outcome.diagnostics);
|
|
324
|
+
if (!outcome.ok) {
|
|
325
|
+
console.error(outcome.message);
|
|
326
|
+
process.exit(1);
|
|
291
327
|
}
|
|
328
|
+
if (opts.json)
|
|
329
|
+
process.stdout.write(buildApiCheckJson(outcome.report) + "\n");
|
|
330
|
+
else
|
|
331
|
+
for (const line of formatApiCheckText(outcome.report))
|
|
332
|
+
console.log(line);
|
|
333
|
+
if (!outcome.report.current)
|
|
334
|
+
process.exitCode = 1;
|
|
335
|
+
});
|
|
336
|
+
program
|
|
337
|
+
.command("typespec")
|
|
338
|
+
.description("DEPRECATED alias of `em api generate --stdout` (use em api generate)")
|
|
339
|
+
.argument("<file>", "input .em file")
|
|
340
|
+
.option("-o, --out <path>", "write to a file instead of stdout")
|
|
341
|
+
.action((file, opts) => {
|
|
342
|
+
console.error("warn: em typespec is deprecated — use em api generate");
|
|
343
|
+
runApiGenerate(file, opts.out ? { out: opts.out } : { stdout: true });
|
|
292
344
|
});
|
|
293
345
|
program
|
|
294
346
|
.command("diff")
|
|
@@ -437,9 +489,11 @@ program
|
|
|
437
489
|
const slice = program.command("slice").description("author and maintain slice docs");
|
|
438
490
|
slice
|
|
439
491
|
.command("new")
|
|
440
|
-
.description("scaffold a fresh slices/<key>.md doc — the 5 frontmatter keys required at `status: draft
|
|
441
|
-
"
|
|
442
|
-
"
|
|
492
|
+
.description("scaffold a fresh slices/<key>.md doc — the 5 frontmatter keys required at `status: draft`, " +
|
|
493
|
+
"the `# Slice:` heading and diagram-image stub, then every template section: the generated " +
|
|
494
|
+
"regions (command/event/view field tables, Invariants) filled from the model with --wire " +
|
|
495
|
+
"(template placeholders otherwise — `em slice sync` fills them), the authored sections " +
|
|
496
|
+
"(Intent, Scenarios, Open Questions, ...) with placeholder bullets (see " +
|
|
443
497
|
".claude/skills/event-modeling-shared/reference/slice-doc-schema.md, templates/slice.md)")
|
|
444
498
|
.argument("<name>", "slice display name (e.g. \"Request Payment\") — kebab-cased for the filename")
|
|
445
499
|
.requiredOption("--pattern <pattern>", `slice pattern: ${SLICE_PATTERNS.join(" | ")}`)
|
|
@@ -466,6 +520,9 @@ slice
|
|
|
466
520
|
// (no matching slice, ambiguous primary element, already wired) leaves nothing written at
|
|
467
521
|
// all rather than a doc with no wiring and a non-zero exit.
|
|
468
522
|
let wired = null;
|
|
523
|
+
// MIL-266: with a model in hand (--wire), the doc's generated regions are filled from it;
|
|
524
|
+
// without one they carry the template's placeholders until `em slice sync` fills them.
|
|
525
|
+
let regions;
|
|
469
526
|
if (opts.wire) {
|
|
470
527
|
const { model, refs, diagnostics, source } = compileFile(opts.wire);
|
|
471
528
|
printDiagnostics(diagnostics);
|
|
@@ -479,11 +536,12 @@ slice
|
|
|
479
536
|
process.exit(1);
|
|
480
537
|
}
|
|
481
538
|
wired = result;
|
|
539
|
+
regions = buildSliceRegions(model, [refs.sliceKeys.indexOf(key)]);
|
|
482
540
|
}
|
|
483
541
|
mkdirSync(dir, { recursive: true });
|
|
484
542
|
const content = opts.stub
|
|
485
543
|
? buildStubDocContent(name, opts.pattern, opts.swimlane)
|
|
486
|
-
: buildSliceDocContent(name, key, opts.pattern, opts.swimlane);
|
|
544
|
+
: buildSliceDocContent(name, key, opts.pattern, opts.swimlane, regions);
|
|
487
545
|
writeFileSync(path, content);
|
|
488
546
|
console.log(`wrote ${path}`);
|
|
489
547
|
if (wired) {
|
|
@@ -495,6 +553,39 @@ slice
|
|
|
495
553
|
console.log(` note "${path}"`);
|
|
496
554
|
}
|
|
497
555
|
});
|
|
556
|
+
slice
|
|
557
|
+
.command("sync")
|
|
558
|
+
.description("regenerate the GENERATED regions (command/event/view field tables, the Invariants list) of " +
|
|
559
|
+
"existing slice docs in place from the model, authored sections untouched (MIL-266); docs " +
|
|
560
|
+
"without regions are skipped with a note. --check never writes and exits 1 when a doc is stale")
|
|
561
|
+
.argument("<model-file>", "the .em model whose bound slice docs to sync")
|
|
562
|
+
.argument("[key]", "only the doc this slice's export key resolves to")
|
|
563
|
+
.option("--check", "report ok / stale / no-regions per doc without writing; exit 1 when any is stale")
|
|
564
|
+
.option("--json", "print the machine-readable report (sliceSyncSchemaVersion 1.0)")
|
|
565
|
+
.action((file, key, opts) => {
|
|
566
|
+
const { model, refs, diagnostics } = compileFile(file);
|
|
567
|
+
printDiagnostics(diagnostics.filter((d) => d.severity === "error"));
|
|
568
|
+
if (hasErrors(diagnostics)) {
|
|
569
|
+
console.error(`em slice sync: not syncing — fix the errors in ${file} above`);
|
|
570
|
+
process.exit(1);
|
|
571
|
+
}
|
|
572
|
+
const check = opts.check === true;
|
|
573
|
+
const outcome = runSliceSync(model, refs, dirname(file), key, check);
|
|
574
|
+
if (!outcome.ok) {
|
|
575
|
+
console.error(`em slice sync: ${outcome.message}`);
|
|
576
|
+
process.exit(1);
|
|
577
|
+
}
|
|
578
|
+
const { out, err } = formatSliceSync(outcome.docs, check);
|
|
579
|
+
for (const line of err)
|
|
580
|
+
console.error(line);
|
|
581
|
+
if (opts.json)
|
|
582
|
+
process.stdout.write(buildSliceSyncJson(file, outcome.docs) + "\n");
|
|
583
|
+
else
|
|
584
|
+
for (const line of out)
|
|
585
|
+
console.log(line);
|
|
586
|
+
if (sliceSyncFailed(outcome.docs, check))
|
|
587
|
+
process.exit(1);
|
|
588
|
+
});
|
|
498
589
|
slice
|
|
499
590
|
.command("stub-all")
|
|
500
591
|
.description("scaffold + wire a near-free stub (`em slice new --stub`'s content, MIL-184) for every " +
|
|
@@ -642,12 +733,20 @@ slice
|
|
|
642
733
|
? `reviewed: ${result.path} (reviewedBy: ${opts.by}, reviewedOn: ${reviewedOn})`
|
|
643
734
|
: `already reviewed (no-op): ${result.path}`);
|
|
644
735
|
});
|
|
736
|
+
/** MIL-238: `, meaningConfirmed: true` / `, contractChange: "<why>"` for a success line, or "". */
|
|
737
|
+
function confirmationSuffix(c) {
|
|
738
|
+
return c ? `, ${meaningConfirmationLine(c)}` : "";
|
|
739
|
+
}
|
|
645
740
|
slice
|
|
646
741
|
.command("ratify")
|
|
647
742
|
.description("flip a slice doc's frontmatter to `status: ready-to-implement` and record `ratifiedBy:`/" +
|
|
648
743
|
"`ratifiedOn:` — the handoff sign-off (MIL-165, docs/process.md#the-slice-lifecycle-gates) " +
|
|
649
|
-
"that makes who ratified, and when, a first-class recorded fact.
|
|
650
|
-
"
|
|
744
|
+
"that makes who ratified, and when, a first-class recorded fact. The review gate is a check " +
|
|
745
|
+
"on the doc's current `status:`, not on whether `reviewedBy:`/`reviewedOn:` are populated: " +
|
|
746
|
+
"a doc at `reviewed` or `ready-to-implement` passes (the latter is where `em slice " +
|
|
747
|
+
"reratify` leaves a doc, so the follow-up ratify needs no --skip-review); any other status " +
|
|
748
|
+
"— a `draft` that never went through `em slice review` — is refused unless --skip-review " +
|
|
749
|
+
"is passed. Idempotent on " +
|
|
651
750
|
"the same --by/--on pair; refuses to overwrite a different one already recorded; never " +
|
|
652
751
|
"touches `version:` or the doc body")
|
|
653
752
|
.argument("<file>", "input .em file")
|
|
@@ -655,7 +754,16 @@ slice
|
|
|
655
754
|
.requiredOption("--by <name>", "the ratifier's name")
|
|
656
755
|
.option("--on <date>", "ratification date, YYYY-MM-DD (default: today, local date)")
|
|
657
756
|
.option("--skip-review", "ratify without a recorded review — prints a loud notice on stderr")
|
|
757
|
+
.option("--meaning-unchanged", "API-first sign-off (MIL-238): this version does not change what the public contract means — " +
|
|
758
|
+
"records `meaningConfirmed: true`; required (or --contract-change) on a slice touching a `public` element")
|
|
759
|
+
.option("--contract-change <why>", "API-first sign-off (MIL-238): a consumer must read this change differently, and why — records " +
|
|
760
|
+
"`contractChange: \"<why>\"`")
|
|
658
761
|
.action((file, sliceKey, opts) => {
|
|
762
|
+
const flags = parseMeaningFlags(opts.meaningUnchanged, opts.contractChange);
|
|
763
|
+
if (!flags.ok) {
|
|
764
|
+
console.error(`em slice ratify: ${flags.message}`);
|
|
765
|
+
process.exit(1);
|
|
766
|
+
}
|
|
659
767
|
const { model, refs, diagnostics, source } = compileFile(file);
|
|
660
768
|
printDiagnostics(diagnostics);
|
|
661
769
|
// Scoped the same way `em slice mark-implemented`/`em export --slice`/`em validate
|
|
@@ -671,7 +779,7 @@ slice
|
|
|
671
779
|
console.error(`em slice ratify: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
|
|
672
780
|
process.exit(1);
|
|
673
781
|
}
|
|
674
|
-
const result = runRatify(model, refs, dirname(file), sliceKey, opts.by, ratifiedOn, opts.skipReview === true);
|
|
782
|
+
const result = runRatify(model, refs, dirname(file), sliceKey, opts.by, ratifiedOn, opts.skipReview === true, flags.confirmation);
|
|
675
783
|
if (!result.ok) {
|
|
676
784
|
console.error(`em slice ratify: ${result.message}`);
|
|
677
785
|
process.exit(1);
|
|
@@ -689,7 +797,7 @@ slice
|
|
|
689
797
|
console.error(`warn: ratifying "${sliceKey}" ahead of upstream slice "${upstream.sliceKey}" (status: ${upstream.status})`);
|
|
690
798
|
}
|
|
691
799
|
console.log(result.changed
|
|
692
|
-
? `ratified: ${result.path} (ratifiedBy: ${opts.by}, ratifiedOn: ${ratifiedOn})`
|
|
800
|
+
? `ratified: ${result.path} (ratifiedBy: ${opts.by}, ratifiedOn: ${ratifiedOn}${confirmationSuffix(flags.confirmation)})`
|
|
693
801
|
: `already ratified (no-op): ${result.path}`);
|
|
694
802
|
warnModelVersionDrift(dirname(file), model, refs, source, `ratifying "${sliceKey}"`);
|
|
695
803
|
});
|
|
@@ -697,13 +805,25 @@ slice
|
|
|
697
805
|
.command("reratify")
|
|
698
806
|
.description("bump `version:` and flip a shipped slice doc's frontmatter back to `status: " +
|
|
699
807
|
"ready-to-implement` — the re-ratification mechanical edit (MIL-161, mirrors `em slice " +
|
|
700
|
-
"mark-implemented`).
|
|
808
|
+
"mark-implemented`). Applies to a doc at `status: implemented`, or (MIL-258) to a " +
|
|
809
|
+
"`ready-to-implement` doc that is ratified but not yet shipped — there it bumps `version:` " +
|
|
810
|
+
"and clears the sign-off but leaves `status:` alone; a `ready-to-implement` doc with no " +
|
|
811
|
+
"`ratifiedBy:` refuses (awaiting `em slice ratify --by`). Clears any stale " +
|
|
701
812
|
"`ratifiedBy:`/`ratifiedOn:`/`reviewedBy:`/`reviewedOn:` (they describe the PRIOR version's " +
|
|
702
813
|
"review and sign-off) so a follow-up `em slice ratify --by` applies cleanly — and needs no " +
|
|
703
814
|
"fresh review; never touches `implementedIn:` or the doc body")
|
|
704
815
|
.argument("<file>", "input .em file")
|
|
705
816
|
.argument("<slice-key>", "slice export key (kebab-case)")
|
|
706
|
-
.
|
|
817
|
+
.option("--meaning-unchanged", "API-first sign-off (MIL-238): the new version does not change what the public contract means — " +
|
|
818
|
+
"records `meaningConfirmed: true`; required (or --contract-change) on a slice touching a `public` element")
|
|
819
|
+
.option("--contract-change <why>", "API-first sign-off (MIL-238): a consumer must read the new version differently, and why — records " +
|
|
820
|
+
"`contractChange: \"<why>\"`")
|
|
821
|
+
.action((file, sliceKey, opts) => {
|
|
822
|
+
const flags = parseMeaningFlags(opts.meaningUnchanged, opts.contractChange);
|
|
823
|
+
if (!flags.ok) {
|
|
824
|
+
console.error(`em slice reratify: ${flags.message}`);
|
|
825
|
+
process.exit(1);
|
|
826
|
+
}
|
|
707
827
|
const { model, refs, diagnostics, source } = compileFile(file);
|
|
708
828
|
printDiagnostics(diagnostics);
|
|
709
829
|
// Scoped the same way `em slice ratify`/`em slice mark-implemented`/`em export --slice`/
|
|
@@ -713,20 +833,28 @@ slice
|
|
|
713
833
|
console.error(`em slice reratify: slice "${sliceKey}" has errors — fix them first`);
|
|
714
834
|
process.exit(1);
|
|
715
835
|
}
|
|
716
|
-
const result = runReratify(model, refs, dirname(file), sliceKey);
|
|
836
|
+
const result = runReratify(model, refs, dirname(file), sliceKey, flags.confirmation);
|
|
717
837
|
if (!result.ok) {
|
|
718
838
|
console.error(`em slice reratify: ${result.message}`);
|
|
719
839
|
process.exit(1);
|
|
720
840
|
}
|
|
721
841
|
// MIL-214: advisory only, never refuses — see reratify.ts's reratifyAdvisory. Both warnings
|
|
722
842
|
// can fire together (a version that was never certified AND still carries unruled findings).
|
|
723
|
-
if (result.advisory
|
|
843
|
+
if (result.advisory?.neverCertified) {
|
|
724
844
|
console.error(`warn: reratifying "${sliceKey}" whose v${result.newVersion - 1} was never certified`);
|
|
725
845
|
}
|
|
726
|
-
if (result.advisory.unruledFindingsCount > 0) {
|
|
846
|
+
if (result.advisory && result.advisory.unruledFindingsCount > 0) {
|
|
727
847
|
console.error(`warn: reratifying "${sliceKey}" has ${result.advisory.unruledFindingsCount} unruled conformance finding(s)`);
|
|
728
848
|
}
|
|
729
|
-
|
|
849
|
+
if (result.kind === "unshipped") {
|
|
850
|
+
// MIL-258: nothing was flipped — say what DID happen, and that the new version is unsigned.
|
|
851
|
+
console.log(`reratified: ${result.path} (version: ${result.newVersion - 1} -> ${result.newVersion}, sign-off cleared, ` +
|
|
852
|
+
`status unchanged: ready-to-implement${confirmationSuffix(flags.confirmation)}) — not ratified until ` +
|
|
853
|
+
`\`em slice ratify --by <name>\` records the new sign-off`);
|
|
854
|
+
}
|
|
855
|
+
else {
|
|
856
|
+
console.log(`reratified: ${result.path} (version: ${result.newVersion}, status: ready-to-implement${confirmationSuffix(flags.confirmation)})`);
|
|
857
|
+
}
|
|
730
858
|
warnModelVersionDrift(dirname(file), model, refs, source, `reratifying "${sliceKey}"`);
|
|
731
859
|
});
|
|
732
860
|
slice
|
|
@@ -1309,10 +1437,12 @@ program
|
|
|
1309
1437
|
.allowExcessArguments(false)
|
|
1310
1438
|
.option("--list-issues", "print only open `issue` diagnostics (slice, element, line, text)")
|
|
1311
1439
|
.option("--list-divergences", "print only accepted-divergence annotations (slice, element, line, text) — never fails the build")
|
|
1312
|
-
.option("--list-public", "print only events and views marked `public` (slice, kind, name, line) — an integration-surface audit, never fails the build")
|
|
1440
|
+
.option("--list-public", "print only commands, events and views marked `public` (slice, kind, name, line) — an integration-surface audit, never fails the build")
|
|
1313
1441
|
.option("--fail-on-issues", "exit non-zero if the model has any open `issue`s (opt-in — issues are warnings and don't block by default)")
|
|
1314
1442
|
.option("--slice-ready <key>", "readiness gate for one slice (export key): status ready-to-implement, doc resolvable via " +
|
|
1315
|
-
"note binding, zero unchecked Open Questions
|
|
1443
|
+
"note binding, zero unchecked Open Questions, a recorded ratifiedBy, and — for a slice " +
|
|
1444
|
+
"touching a `public` element — a current contracts/<model key>.tsp; exits non-zero if not " +
|
|
1445
|
+
"ready (MIL-87, MIL-259, MIL-238)")
|
|
1316
1446
|
.option("--json", "print a JSON document instead of text — works on a model WITH errors, unlike `em export` " +
|
|
1317
1447
|
"(MIL-128, see docs/cli.md); exit codes are unchanged")
|
|
1318
1448
|
.action((file, opts) => {
|
|
@@ -1333,17 +1463,18 @@ program
|
|
|
1333
1463
|
// diagnostics printed — on an unrelated slice's breakage; scoping to this slice alone
|
|
1334
1464
|
// matches the ticket's own scenario (check one slice while the rest of a large model is
|
|
1335
1465
|
// still WIP) and this module's own "single-slice" framing.
|
|
1336
|
-
|
|
1337
|
-
|
|
1466
|
+
// MIL-238: the API-first gate needs the model path as given (the contract path is built
|
|
1467
|
+
// from it, as `em api generate` does), its source, and the compile diagnostics.
|
|
1468
|
+
// R34 (MIL-268): one shared verdict helper for this flag, the MCP `slice_ready` tool, and
|
|
1469
|
+
// `em engagement plan`.
|
|
1470
|
+
const contractInput = { file, source, diagnostics };
|
|
1338
1471
|
const key = opts.sliceReady;
|
|
1339
|
-
const scoped =
|
|
1472
|
+
const { scoped, ready, result } = computeSliceReadiness(model, refs, dirname(file), key, contractInput, allDiagnostics);
|
|
1340
1473
|
printDiagnostics(scoped);
|
|
1341
|
-
const ready = scoped.length === 0;
|
|
1342
1474
|
if (opts.json) {
|
|
1343
|
-
// MIL-128: the
|
|
1475
|
+
// MIL-128: the named gates (see computeSliceReadyGates) plus the same `scoped`
|
|
1344
1476
|
// diagnostics and `ready` verdict driving the exit code below — replaces both the
|
|
1345
1477
|
// scraped warning prose and the two hand-parsed English sentences.
|
|
1346
|
-
const result = computeSliceReadyGates(model, refs, dirname(file), key);
|
|
1347
1478
|
process.stdout.write(buildSliceReadyJson(file, key, result?.gates ?? null, scoped, ready, result?.continuationOf ?? null) + "\n");
|
|
1348
1479
|
}
|
|
1349
1480
|
else {
|
|
@@ -1509,9 +1640,13 @@ program
|
|
|
1509
1640
|
"definition-of-done citation check; advisory by default, --strict for CI; " +
|
|
1510
1641
|
"--include-ready also counts ready-to-implement docs (forward-looking report); a " +
|
|
1511
1642
|
"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"
|
|
1643
|
+
"from the report — its invariants, if any, live in the originating slice's own doc; " +
|
|
1644
|
+
"--slice <key> scopes the report to one slice whatever its status (MIL-255), the " +
|
|
1645
|
+
"pre-merge definition-of-done check")
|
|
1513
1646
|
.argument("<file>", "input .em file")
|
|
1514
1647
|
.requiredOption("--tests <dir>", "directory to scan recursively for test files citing invariant IDs")
|
|
1648
|
+
.option("--slice <key>", "only this slice (export key), whether ready-to-implement or implemented — no --include-ready needed; " +
|
|
1649
|
+
"--strict then fails on this slice's uncited invariants, or if the slice is not in scope (draft/reviewed/unbound: nothing checked) (MIL-255)")
|
|
1515
1650
|
.option("--strict", "exit non-zero if any invariant ID has zero citations (CI)")
|
|
1516
1651
|
.option("--include-ready", "also count ready-to-implement docs, not just implemented (MIL-207)")
|
|
1517
1652
|
.option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
|
|
@@ -1524,11 +1659,25 @@ program
|
|
|
1524
1659
|
}
|
|
1525
1660
|
const baseDir = dirname(file);
|
|
1526
1661
|
const includeReady = !!opts.includeReady;
|
|
1662
|
+
// MIL-255: resolve --slice before anything else — an unknown key is an error (never a
|
|
1663
|
+
// vacuously passing empty report), and a continuation key (MIL-208) scopes to its
|
|
1664
|
+
// originating slice, which the output names (matching `em validate --slice-ready`).
|
|
1665
|
+
let onlyKey;
|
|
1666
|
+
let continuationOfKey = null;
|
|
1667
|
+
if (opts.slice !== undefined) {
|
|
1668
|
+
const target = resolveCoverageSliceKey(model, refs, opts.slice);
|
|
1669
|
+
if (!target) {
|
|
1670
|
+
console.error(`em coverage --slice: no slice with export key "${opts.slice}" in this model`);
|
|
1671
|
+
process.exit(1);
|
|
1672
|
+
}
|
|
1673
|
+
onlyKey = target.key;
|
|
1674
|
+
continuationOfKey = target.continuationOf;
|
|
1675
|
+
}
|
|
1527
1676
|
// MIL-207: a missing --tests dir is only a defect when something is actually in scope to
|
|
1528
1677
|
// check — cheap to know without touching the test tree (resolveScopedSlices never reads
|
|
1529
1678
|
// --tests). A fresh scaffold (or a doc-only ratification PR, with the MIL-207 default scope)
|
|
1530
1679
|
// 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);
|
|
1680
|
+
const anyInScope = resolveScopedSlices(model, refs, baseDir, includeReady, onlyKey).some((s) => s.inScope);
|
|
1532
1681
|
if (!existsSync(opts.tests)) {
|
|
1533
1682
|
if (anyInScope) {
|
|
1534
1683
|
console.error(`em coverage: --tests directory not found: ${opts.tests}`);
|
|
@@ -1540,17 +1689,36 @@ program
|
|
|
1540
1689
|
console.error(`em coverage: --tests is not a directory: ${opts.tests}`);
|
|
1541
1690
|
process.exit(1);
|
|
1542
1691
|
}
|
|
1543
|
-
const report = buildCoverageReport(model, refs, baseDir, opts.tests, includeReady);
|
|
1692
|
+
const report = buildCoverageReport(model, refs, baseDir, opts.tests, includeReady, onlyKey);
|
|
1693
|
+
// MIL-255: a scoped slice whose doc is draft/reviewed/unbound/unusable is out of scope — the
|
|
1694
|
+
// same way the unscoped report treats it — but under --strict "nothing was checked" must not
|
|
1695
|
+
// exit 0, or the pre-merge check passes vacuously (the very bug --slice exists to fix). So:
|
|
1696
|
+
// advisory (no --strict) prints the note on stdout and exits 0; --strict prints it on stderr
|
|
1697
|
+
// and exits 1 (also in --json mode, whose document shape is unchanged — consumers read the
|
|
1698
|
+
// slice's `inScope` alongside `ok`). An in-scope doc with zero invariants stays a pass.
|
|
1699
|
+
const unchecked = onlyKey === undefined ? undefined : report.slices.find((sl) => !sl.inScope);
|
|
1700
|
+
const uncheckedNote = unchecked
|
|
1701
|
+
? `slice "${unchecked.key}" is not in scope (${unchecked.status ?? unchecked.docReason ?? "no doc"}) — nothing checked; only ready-to-implement/implemented docs are checked`
|
|
1702
|
+
: null;
|
|
1703
|
+
if (uncheckedNote !== null && (opts.strict || !opts.json)) {
|
|
1704
|
+
(opts.strict ? console.error : console.log)(uncheckedNote);
|
|
1705
|
+
}
|
|
1544
1706
|
if (opts.json) {
|
|
1545
|
-
|
|
1707
|
+
const sliceScope = opts.slice === undefined ? null : { requested: opts.slice, continuationOf: continuationOfKey };
|
|
1708
|
+
process.stdout.write(buildCoverageJson(file, opts.tests, report, includeReady, sliceScope) + "\n");
|
|
1546
1709
|
}
|
|
1547
1710
|
else {
|
|
1711
|
+
if (continuationOfKey !== null) {
|
|
1712
|
+
console.log(`slice "${opts.slice}" is a continuation of "${continuationOfKey}" — checking "${continuationOfKey}"`);
|
|
1713
|
+
}
|
|
1548
1714
|
for (const slice of report.slices) {
|
|
1549
1715
|
if (!slice.inScope)
|
|
1550
1716
|
continue;
|
|
1551
1717
|
console.log(`slice "${slice.key}" (${slice.status}):`);
|
|
1552
1718
|
if (slice.invariants.length === 0) {
|
|
1553
|
-
|
|
1719
|
+
// MIL-255: said as "checked" so an in-scope doc defining zero invariants (a legitimate
|
|
1720
|
+
// pass) is distinguishable from the "not in scope — nothing checked" case above.
|
|
1721
|
+
console.log(` (checked — the doc defines 0 INV-* invariant IDs)`);
|
|
1554
1722
|
continue;
|
|
1555
1723
|
}
|
|
1556
1724
|
for (const inv of slice.invariants) {
|
|
@@ -1569,7 +1737,7 @@ program
|
|
|
1569
1737
|
// Advisory by default (exit 0 even with uncovered IDs) — --strict is the opt-in CI gate,
|
|
1570
1738
|
// same "set exitCode, don't truncate stdout" rationale as em ledger/em diff for the --json
|
|
1571
1739
|
// form, and consistent for the text form too.
|
|
1572
|
-
if (opts.strict && report.uncoveredCount > 0)
|
|
1740
|
+
if (opts.strict && (report.uncoveredCount > 0 || uncheckedNote !== null))
|
|
1573
1741
|
process.exitCode = 1;
|
|
1574
1742
|
});
|
|
1575
1743
|
program
|
|
@@ -1668,7 +1836,9 @@ program
|
|
|
1668
1836
|
const emVersion = compiled
|
|
1669
1837
|
.map(({ file }) => resolveEmVersionStatusEntry(file, PKG_VERSION))
|
|
1670
1838
|
.filter((e) => e !== null);
|
|
1671
|
-
|
|
1839
|
+
// MIL-239 (R11): the nearest system.yaml above the first input file, consumer-adaptation counted.
|
|
1840
|
+
const system = statusSystemBlock(files[0], realGit, (m) => console.error(`warn: em status: ${m}`));
|
|
1841
|
+
const report = buildStatusReport(files, sliceFacts, openIssuesCount, invariants, conformance, statusDiagnostics, modelVersion, emVersion, system, readOpenEngagements(files));
|
|
1672
1842
|
let output;
|
|
1673
1843
|
if (opts.json)
|
|
1674
1844
|
output = buildStatusJson(report);
|
|
@@ -1900,37 +2070,180 @@ query
|
|
|
1900
2070
|
.action((files, opts) => {
|
|
1901
2071
|
runQueryVerb(files, "path", { from: opts.from, to: opts.to }, opts.json, (s) => queryPath(s, opts.from, opts.to), formatPath);
|
|
1902
2072
|
});
|
|
1903
|
-
program
|
|
2073
|
+
const engagement = program
|
|
2074
|
+
.command("engagement")
|
|
2075
|
+
.description("a named chunk of the model built as one stack of slice PRs: its levelled build plan and its " +
|
|
2076
|
+
"in-flight Ledger (MIL-268, see docs/cli.md and docs/engagement-schema.md)");
|
|
2077
|
+
engagement
|
|
2078
|
+
.command("new")
|
|
2079
|
+
.description("write <model dir>/engagements/<slug>.md — frontmatter (slices, parallel ceiling) plus the " +
|
|
2080
|
+
"generated Ledger table; selection by --slices, --context, or --downstream-of (exactly one)")
|
|
2081
|
+
.argument("<file>", "input .em file")
|
|
2082
|
+
.argument("<slug>", "engagement slug (kebab-case; the file name)")
|
|
2083
|
+
.option("--slices <keys>", "comma-separated slice export keys")
|
|
2084
|
+
.option("--context <name>", "every slice with an event in this context")
|
|
2085
|
+
.option("--downstream-of <ref>", "a slice key, element ref, or display name: it and everything downstream of it (loops-to excluded)")
|
|
2086
|
+
.option("--parallel <n>", "how many slices of one level may be in flight at once", "3")
|
|
2087
|
+
.option("--by <name>", "who created the engagement (createdBy)")
|
|
2088
|
+
.option("--force", "overwrite an existing engagement file with this slug")
|
|
2089
|
+
.action((file, slug, opts) => {
|
|
2090
|
+
const fail = (m) => {
|
|
2091
|
+
console.error(`em engagement new: ${m}`);
|
|
2092
|
+
process.exit(1);
|
|
2093
|
+
};
|
|
2094
|
+
const given = [opts.slices, opts.context, opts.downstreamOf].filter((v) => v !== undefined).length;
|
|
2095
|
+
if (given !== 1)
|
|
2096
|
+
fail("pass exactly one of --slices, --context, --downstream-of");
|
|
2097
|
+
const selector = opts.slices !== undefined
|
|
2098
|
+
? { kind: "slices", keys: opts.slices.split(",").map((k) => k.trim()).filter((k) => k !== "") }
|
|
2099
|
+
: opts.context !== undefined
|
|
2100
|
+
? { kind: "context", context: opts.context }
|
|
2101
|
+
: { kind: "downstream-of", ref: opts.downstreamOf };
|
|
2102
|
+
const parallel = /^\d+$/.test(opts.parallel) ? Number(opts.parallel) : NaN;
|
|
2103
|
+
const { model, refs } = compileFile(file);
|
|
2104
|
+
const path = engagementPath(file, slug);
|
|
2105
|
+
const result = buildNewEngagement({ file, model, refs, baseDir: dirname(file) }, { slug, selector, parallel, createdBy: opts.by ?? null, created: localIsoDate() });
|
|
2106
|
+
if (!result.ok)
|
|
2107
|
+
return fail(result.message);
|
|
2108
|
+
if (existsSync(path) && !opts.force)
|
|
2109
|
+
fail(`engagement "${slug}" already exists (${path}) — pass --force to overwrite`);
|
|
2110
|
+
if (result.warning)
|
|
2111
|
+
console.error(result.warning);
|
|
2112
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2113
|
+
writeFileSync(path, result.text);
|
|
2114
|
+
console.log(`wrote ${path} (${result.units.length} slice${result.units.length === 1 ? "" : "s"}: ${result.units.join(", ")})`);
|
|
2115
|
+
});
|
|
2116
|
+
/** Compile `file` with the full `em validate` diagnostic set — what `engagement plan` needs for
|
|
2117
|
+
* each slice's `--slice-ready` verdict (R34). */
|
|
2118
|
+
function engagementModelInput(file) {
|
|
2119
|
+
const { model, refs, diagnostics, source } = compileFile(file);
|
|
2120
|
+
const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics, source);
|
|
2121
|
+
return { file, model, refs, baseDir: dirname(file), contract: { file, source, diagnostics }, allDiagnostics };
|
|
2122
|
+
}
|
|
2123
|
+
engagement
|
|
2124
|
+
.command("plan")
|
|
2125
|
+
.description("the engagement's build plan: dependency levels over model.edges (loops-to excluded), and per " +
|
|
2126
|
+
"slice its readiness, branch impl/<key>, base (main or impl/<upstream>), and any hold")
|
|
2127
|
+
.argument("<file>", "input .em file")
|
|
2128
|
+
.argument("<slug>", "engagement slug")
|
|
2129
|
+
.option("--json", "print a JSON document instead of text (same document as the MCP engagement_plan tool)")
|
|
2130
|
+
.action((file, slug, opts) => {
|
|
2131
|
+
const result = engagementPlanFor(engagementModelInput(file), slug);
|
|
2132
|
+
if (!result.ok) {
|
|
2133
|
+
console.error(`em engagement plan: ${result.message}`);
|
|
2134
|
+
process.exit(1);
|
|
2135
|
+
}
|
|
2136
|
+
process.stdout.write((opts.json ? buildEngagementPlanJson(result.plan) : formatEngagementPlanText(result.plan)) + "\n");
|
|
2137
|
+
});
|
|
2138
|
+
engagement
|
|
2139
|
+
.command("set")
|
|
2140
|
+
.description("the only write path to the Ledger: set one slice's state (and optionally branch/base/pr); " +
|
|
2141
|
+
"idempotent; merged is terminal; --state held records a human hold")
|
|
2142
|
+
.argument("<file>", "input .em file")
|
|
2143
|
+
.argument("<slug>", "engagement slug")
|
|
2144
|
+
.argument("<key>", "slice export key")
|
|
2145
|
+
.requiredOption("--state <state>", `one of: ${LEDGER_STATES.join(" | ")}`)
|
|
2146
|
+
.option("--branch <name>", "the slice's branch")
|
|
2147
|
+
.option("--base <name>", "the branch the slice's PR targets")
|
|
2148
|
+
.option("--pr <url>", "the slice's PR URL")
|
|
2149
|
+
.action((file, slug, key, opts) => {
|
|
2150
|
+
const fail = (m) => {
|
|
2151
|
+
console.error(`em engagement set: ${m}`);
|
|
2152
|
+
process.exit(1);
|
|
2153
|
+
};
|
|
2154
|
+
const loaded = loadEngagement(file, slug);
|
|
2155
|
+
if (!loaded.ok)
|
|
2156
|
+
return fail(loaded.message);
|
|
2157
|
+
const { model, refs } = compileFile(file);
|
|
2158
|
+
const result = applyEngagementSet({ file, model, refs, baseDir: dirname(file) }, slug, loaded.text, loaded.file, { key, ...opts });
|
|
2159
|
+
if (!result.ok)
|
|
2160
|
+
return fail(result.message);
|
|
2161
|
+
if (!result.changed) {
|
|
2162
|
+
console.log(`no change: ${key} is already ${result.entry.state} (${loaded.path})`);
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
writeFileSync(loaded.path, result.text);
|
|
2166
|
+
console.log(`set ${key}: ${result.entry.state} (${loaded.path})`);
|
|
2167
|
+
});
|
|
2168
|
+
engagement
|
|
2169
|
+
.command("status")
|
|
2170
|
+
.description("the Ledger joined with each slice doc's current status, plus whether the engagement is closable")
|
|
2171
|
+
.argument("<file>", "input .em file")
|
|
2172
|
+
.argument("<slug>", "engagement slug")
|
|
2173
|
+
.option("--json", "print a JSON document instead of text (same document as the MCP engagement_status tool)")
|
|
2174
|
+
.action((file, slug, opts) => {
|
|
2175
|
+
const { model, refs } = compileFile(file);
|
|
2176
|
+
const result = engagementStatusFor({ file, model, refs, baseDir: dirname(file) }, slug);
|
|
2177
|
+
if (!result.ok) {
|
|
2178
|
+
console.error(`em engagement status: ${result.message}`);
|
|
2179
|
+
process.exit(1);
|
|
2180
|
+
}
|
|
2181
|
+
process.stdout.write((opts.json ? buildEngagementStatusJson(result.status) : formatEngagementStatusText(result.status)) + "\n");
|
|
2182
|
+
});
|
|
2183
|
+
engagement
|
|
2184
|
+
.command("close")
|
|
2185
|
+
.description("set status: closed — refuses unless every slice is merged (recorded or inferred) or gap")
|
|
2186
|
+
.argument("<file>", "input .em file")
|
|
2187
|
+
.argument("<slug>", "engagement slug")
|
|
2188
|
+
.action((file, slug) => {
|
|
2189
|
+
const fail = (m) => {
|
|
2190
|
+
console.error(`em engagement close: ${m}`);
|
|
2191
|
+
process.exit(1);
|
|
2192
|
+
};
|
|
2193
|
+
const { model, refs } = compileFile(file);
|
|
2194
|
+
const status = engagementStatusFor({ file, model, refs, baseDir: dirname(file) }, slug);
|
|
2195
|
+
if (!status.ok)
|
|
2196
|
+
return fail(status.message);
|
|
2197
|
+
const loaded = loadEngagement(file, slug);
|
|
2198
|
+
if (!loaded.ok)
|
|
2199
|
+
return fail(loaded.message);
|
|
2200
|
+
const result = applyEngagementClose(slug, loaded.text, status.status);
|
|
2201
|
+
if (!result.ok)
|
|
2202
|
+
return fail(result.message);
|
|
2203
|
+
if (!result.changed) {
|
|
2204
|
+
console.log(`no change: engagement "${slug}" is already closed (${loaded.path})`);
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
writeFileSync(loaded.path, result.text);
|
|
2208
|
+
console.log(`closed engagement "${slug}" (${loaded.path})`);
|
|
2209
|
+
});
|
|
2210
|
+
const systemCommand = program
|
|
1904
2211
|
.command("system")
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
.
|
|
2212
|
+
// `--json` exists on both `em system` and `em system codeowners`: parent options are only
|
|
2213
|
+
// recognized before the subcommand name, so the subcommand owns the one that follows it.
|
|
2214
|
+
.enablePositionalOptions()
|
|
2215
|
+
.description("verify a system — every model's `consumes` refs resolved against the other models' `public` " +
|
|
2216
|
+
"events/views, the cross-model half of \"both ends of a flow\" (MIL-194/MIL-235, see docs/cli.md). " +
|
|
2217
|
+
"Reads system.yaml when given (or found in the directory), else discovers every *.em in the repo")
|
|
2218
|
+
.argument("[target]", "a system.yaml path, or a directory (its system.yaml if present, else discovery); default: the working directory")
|
|
1908
2219
|
// Same reasoning as `em validate`'s allowExcessArguments(false) (MIL-123): a second positional
|
|
1909
|
-
// would be silently dropped, and "one
|
|
2220
|
+
// would be silently dropped, and "one system per run" is the whole contract here.
|
|
1910
2221
|
.allowExcessArguments(false)
|
|
1911
2222
|
.option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
|
|
1912
|
-
.action((
|
|
1913
|
-
const
|
|
2223
|
+
.action((target, opts) => {
|
|
2224
|
+
const shown = target ?? ".";
|
|
2225
|
+
const loaded = loadSystem(shown);
|
|
1914
2226
|
if (!loaded.ok) {
|
|
1915
2227
|
// Same refusal posture as `em export`/`em status`: a manifest that can't be parsed, or a
|
|
1916
2228
|
// source that can't be read/compiled, is not a system to verify — no document, exit 1.
|
|
1917
2229
|
printSystemDiagnostics(loaded.diagnostics);
|
|
1918
|
-
console.error(`em system: not verifying — ${
|
|
2230
|
+
console.error(`em system: not verifying — ${shown} could not be loaded; fix the above first`);
|
|
1919
2231
|
process.exit(1);
|
|
1920
2232
|
}
|
|
1921
|
-
const report = verifySystem(loaded.manifest, loaded.models,
|
|
2233
|
+
const report = verifySystem(loaded.manifest, loaded.models, loaded.manifestPath, loaded.diagnostics, { producerCommit: gitProducerCommit() });
|
|
1922
2234
|
if (opts.json) {
|
|
1923
2235
|
// `em validate --json`'s convention: diagnostics still go to stderr in the human format,
|
|
1924
2236
|
// the document (which carries the same diagnostics) to stdout; exit code unchanged.
|
|
1925
2237
|
printSystemDiagnostics(report.diagnostics);
|
|
1926
|
-
process.stdout.write(buildSystemJson(
|
|
2238
|
+
process.stdout.write(buildSystemJson(loaded, report) + "\n");
|
|
1927
2239
|
}
|
|
1928
2240
|
else {
|
|
1929
2241
|
const verified = report.seams.filter((s) => s.status === "verified").length;
|
|
1930
2242
|
const failing = report.seams.length - verified;
|
|
1931
2243
|
const label = report.name === null ? "system" : `system "${report.name}"`;
|
|
2244
|
+
const via = loaded.discovery ? ` (discovered under ${loaded.discovery.root} — no ${SYSTEM_MANIFEST_FILE})` : "";
|
|
1932
2245
|
console.log(`${label}: ${report.models.length} model${report.models.length === 1 ? "" : "s"}, ` +
|
|
1933
|
-
`${report.seams.length} seam${report.seams.length === 1 ? "" : "s"} (${verified} verified, ${failing} failing)`);
|
|
2246
|
+
`${report.seams.length} seam${report.seams.length === 1 ? "" : "s"} (${verified} verified, ${failing} failing)${via}`);
|
|
1934
2247
|
printSystemDiagnostics(report.diagnostics);
|
|
1935
2248
|
if (report.diagnostics.length === 0)
|
|
1936
2249
|
console.log("ok — no issues");
|
|
@@ -1940,6 +2253,96 @@ program
|
|
|
1940
2253
|
if (hasErrors(report.diagnostics))
|
|
1941
2254
|
process.exitCode = 1;
|
|
1942
2255
|
});
|
|
2256
|
+
systemCommand
|
|
2257
|
+
.command("codeowners")
|
|
2258
|
+
.description("generate (or --check) the managed CODEOWNERS block that routes review: each model's team on its " +
|
|
2259
|
+
"design directory, and every team that `consumes` a model's public surface on that model's " +
|
|
2260
|
+
"contract file (MIL-234, see docs/ci.md and docs/cli.md)")
|
|
2261
|
+
.argument("[target]", "a system.yaml path, or a directory (its system.yaml if present, else discovery); default: the working directory")
|
|
2262
|
+
.allowExcessArguments(false)
|
|
2263
|
+
.option("-o, --output <path>", "the CODEOWNERS file to splice into (default: the first of CODEOWNERS, .github/CODEOWNERS, docs/CODEOWNERS that exists, else CODEOWNERS at the repo root)")
|
|
2264
|
+
.option("--check", "verify the committed file already carries the generated block; never writes, exit 1 on drift (CI)")
|
|
2265
|
+
.option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
|
|
2266
|
+
.action((target, opts) => {
|
|
2267
|
+
const shown = target ?? ".";
|
|
2268
|
+
const result = runCodeowners(shown, { output: opts.output, write: !opts.check });
|
|
2269
|
+
if (!result.ok) {
|
|
2270
|
+
console.error(`em system codeowners: ${result.message}`);
|
|
2271
|
+
process.exitCode = 1;
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
for (const note of result.plan.notes)
|
|
2275
|
+
console.error(`note: ${note}`);
|
|
2276
|
+
if (opts.json) {
|
|
2277
|
+
process.stdout.write(buildCodeownersJson(result.file, result.plan.entries, result.status) + "\n");
|
|
2278
|
+
}
|
|
2279
|
+
else if (opts.check) {
|
|
2280
|
+
const hint = `run \`em system codeowners${target ? ` ${target}` : ""}${opts.output ? ` -o ${opts.output}` : ""}\` and commit the result`;
|
|
2281
|
+
if (result.status === "ok")
|
|
2282
|
+
console.log(`ok: ${result.file} carries the generated block`);
|
|
2283
|
+
else if (result.status === "missing")
|
|
2284
|
+
console.log(`missing: ${result.file} does not exist — ${hint}`);
|
|
2285
|
+
else if (result.status === "no-markers")
|
|
2286
|
+
console.log(`no-markers: ${result.file} has no GENERATED:em-codeowners block — ${hint}`);
|
|
2287
|
+
else
|
|
2288
|
+
console.log(`stale: ${result.file} differs from the generated block — ${hint}`);
|
|
2289
|
+
}
|
|
2290
|
+
else {
|
|
2291
|
+
const verb = result.status === "created" ? "installed" : result.status === "updated" ? "updated" : "already up to date:";
|
|
2292
|
+
console.log(`${verb} ${result.file} (${result.plan.entries.length} entr${result.plan.entries.length === 1 ? "y" : "ies"})`);
|
|
2293
|
+
}
|
|
2294
|
+
// --check never writes and fails on anything but ok (same vocabulary as `em ci init --check`).
|
|
2295
|
+
if (opts.check && result.status !== "ok")
|
|
2296
|
+
process.exitCode = 1;
|
|
2297
|
+
});
|
|
2298
|
+
const scopeCommand = systemCommand
|
|
2299
|
+
.command("scope")
|
|
2300
|
+
.description("check a change set against the seams (MIL-240): fail only when it alters a producer's public surface " +
|
|
2301
|
+
"(or contract file) AND a consuming model's design dir together (`seam-crossing`); warn on other " +
|
|
2302
|
+
"multi-model change sets. Files changed only by `Em-Upgrade:` commits are exempt. There is no " +
|
|
2303
|
+
"override - review on the contract file is the gate (see docs/cli.md, docs/ci.md)")
|
|
2304
|
+
.argument("[target]", "a system.yaml path, or a directory (its system.yaml if present, else discovery); default: the working directory")
|
|
2305
|
+
.allowExcessArguments(false)
|
|
2306
|
+
.option("--base <rev>", "the change set committed since <rev> (merge-base form, <rev>...HEAD); CI passes the PR base")
|
|
2307
|
+
.option("--staged", "also (or only) check what is staged right now; never exempted")
|
|
2308
|
+
.option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
|
|
2309
|
+
.action((target, opts) => {
|
|
2310
|
+
const shown = target ?? ".";
|
|
2311
|
+
const run = runSystemScope(shown, { base: opts.base, staged: opts.staged });
|
|
2312
|
+
if (!run.ok) {
|
|
2313
|
+
printSystemDiagnostics(run.diagnostics);
|
|
2314
|
+
console.error(run.message);
|
|
2315
|
+
process.exit(1);
|
|
2316
|
+
}
|
|
2317
|
+
const { facts, report } = run;
|
|
2318
|
+
// `--json` is also an option of the parent `system` command, which swallows it before this
|
|
2319
|
+
// subcommand sees it: read the merged options.
|
|
2320
|
+
const json = scopeCommand.optsWithGlobals().json === true;
|
|
2321
|
+
const printFindings = () => {
|
|
2322
|
+
for (const d of report.diagnostics) {
|
|
2323
|
+
const line = `${d.file}: ${d.severity} [${d.code}] ${d.message}`;
|
|
2324
|
+
if (d.severity === "error")
|
|
2325
|
+
console.error(line);
|
|
2326
|
+
else
|
|
2327
|
+
console.warn(line);
|
|
2328
|
+
}
|
|
2329
|
+
};
|
|
2330
|
+
if (json) {
|
|
2331
|
+
printFindings();
|
|
2332
|
+
process.stdout.write(buildScopeJson(facts, report) + "\n");
|
|
2333
|
+
}
|
|
2334
|
+
else {
|
|
2335
|
+
const range = [facts.base !== null ? `since ${facts.base}` : null, facts.staged ? "staged" : null].filter(Boolean).join(" + ");
|
|
2336
|
+
console.log(`scope ${range}: ${facts.changedPaths.length} changed path${facts.changedPaths.length === 1 ? "" : "s"}, ` +
|
|
2337
|
+
`${report.touched.length} model${report.touched.length === 1 ? "" : "s"} touched` +
|
|
2338
|
+
(facts.exemptPaths.length > 0 ? `, ${facts.exemptPaths.length} exempt (Em-Upgrade)` : ""));
|
|
2339
|
+
printFindings();
|
|
2340
|
+
if (report.diagnostics.length === 0)
|
|
2341
|
+
console.log("ok - no issues");
|
|
2342
|
+
}
|
|
2343
|
+
if (report.crossings > 0)
|
|
2344
|
+
process.exitCode = 1;
|
|
2345
|
+
});
|
|
1943
2346
|
// Shared by install/sync/check: the `.claude/skills/` root bundled with whatever em package is
|
|
1944
2347
|
// actually running (works whether em was installed from npm or run from a checkout, and
|
|
1945
2348
|
// regardless of a symlinked global install — see pkgDir's own resolution above for
|
|
@@ -1954,6 +2357,14 @@ function packagedSkillsRoot() {
|
|
|
1954
2357
|
function vendoredSkillsRoot(repoRoot) {
|
|
1955
2358
|
return join(resolve(repoRoot), ".claude", "skills");
|
|
1956
2359
|
}
|
|
2360
|
+
// MIL-269 (R32): the four em sub-agent definitions live in `.claude/agents/`, a directory shared
|
|
2361
|
+
// with the consumer's own agents - install/sync/check only ever handle the fixed EM_AGENT_FILES.
|
|
2362
|
+
function packagedAgentsRoot() {
|
|
2363
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..", ".claude", "agents");
|
|
2364
|
+
}
|
|
2365
|
+
function vendoredAgentsRoot(repoRoot) {
|
|
2366
|
+
return join(resolve(repoRoot), ".claude", "agents");
|
|
2367
|
+
}
|
|
1957
2368
|
program
|
|
1958
2369
|
.command("contract")
|
|
1959
2370
|
.description("print the packaged implementation contract (reference/implement.md) to stdout — the " +
|
|
@@ -1985,9 +2396,27 @@ function agentsMdMessage(result) {
|
|
|
1985
2396
|
return `updated the agent-contract section in ${result.path}`;
|
|
1986
2397
|
return `${result.path} already has an up-to-date agent-contract section`;
|
|
1987
2398
|
}
|
|
2399
|
+
/** MIL-231: `em skill install`/`sync` keep working but the vendored bundle is deprecated in favour of
|
|
2400
|
+
* the em plugin. One stderr line naming the two-command install; the exit code is untouched. */
|
|
2401
|
+
function warnVendoredDeprecated() {
|
|
2402
|
+
console.error(`warn: the vendored skill bundle is deprecated since em 1.14 — install the em plugin instead: ${pluginInstallCommands(PKG_VERSION).join(" && ")}`);
|
|
2403
|
+
}
|
|
1988
2404
|
const skill = program
|
|
1989
2405
|
.command("skill")
|
|
1990
2406
|
.description("manage Claude Code skills bundled with em");
|
|
2407
|
+
// MIL-269: reconcile the four `.claude/agents/em-*.md` files (create the directory if absent);
|
|
2408
|
+
// byte-exact restore, never touches any other file there.
|
|
2409
|
+
function installAgentFiles(repoRoot) {
|
|
2410
|
+
const packaged = packagedAgentsRoot();
|
|
2411
|
+
const vendored = vendoredAgentsRoot(repoRoot);
|
|
2412
|
+
const plan = planAgentSync(packaged, vendored, EM_AGENT_FILES);
|
|
2413
|
+
if (plan.changes.length === 0)
|
|
2414
|
+
return;
|
|
2415
|
+
applySkillSync(plan, packaged, vendored);
|
|
2416
|
+
for (const c of plan.changes)
|
|
2417
|
+
console.log(`${c.kind}: agents/${c.relPath}`);
|
|
2418
|
+
console.log(`installed em sub-agent definitions → ${vendored} (${plan.changes.length} file(s))`);
|
|
2419
|
+
}
|
|
1991
2420
|
skill
|
|
1992
2421
|
.command("install")
|
|
1993
2422
|
.description("copy the event-modeling skill bundle into .claude/skills/ (event-modeling, event-modeling-" +
|
|
@@ -2000,6 +2429,7 @@ skill
|
|
|
2000
2429
|
"unrelated sibling skill (MIL-180)")
|
|
2001
2430
|
.option("--no-agents-md", "skip writing/updating the AGENTS.md agent-contract section (on by default, MIL-129)")
|
|
2002
2431
|
.action(async (opts) => {
|
|
2432
|
+
warnVendoredDeprecated();
|
|
2003
2433
|
const srcRoot = packagedSkillsRoot();
|
|
2004
2434
|
const destRoot = vendoredSkillsRoot(process.cwd());
|
|
2005
2435
|
const anchor = join(destRoot, EM_SKILL_ANCHOR_DIR);
|
|
@@ -2031,6 +2461,7 @@ skill
|
|
|
2031
2461
|
`(${removedCount} removed), ${totalUnchanged} unchanged`);
|
|
2032
2462
|
}
|
|
2033
2463
|
console.log("in Claude Code, run /event-modeling to start a guided session");
|
|
2464
|
+
installAgentFiles(process.cwd());
|
|
2034
2465
|
}
|
|
2035
2466
|
else {
|
|
2036
2467
|
await mkdir(destRoot, { recursive: true });
|
|
@@ -2039,6 +2470,7 @@ skill
|
|
|
2039
2470
|
}
|
|
2040
2471
|
console.log(`installed event-modeling skill bundle → ${destRoot}`);
|
|
2041
2472
|
console.log("in Claude Code, run /event-modeling to start a guided session");
|
|
2473
|
+
installAgentFiles(process.cwd());
|
|
2042
2474
|
}
|
|
2043
2475
|
if (opts.agentsMd !== false) {
|
|
2044
2476
|
console.log(agentsMdMessage(syncAgentsMd(process.cwd())));
|
|
@@ -2051,20 +2483,29 @@ skill
|
|
|
2051
2483
|
.argument("[path]", "consumer repo root", ".")
|
|
2052
2484
|
.option("--no-agents-md", "skip writing/updating the AGENTS.md agent-contract section (on by default, MIL-129)")
|
|
2053
2485
|
.action(async (path, opts) => {
|
|
2486
|
+
warnVendoredDeprecated();
|
|
2054
2487
|
const packagedRoot = packagedSkillsRoot();
|
|
2055
2488
|
const vendoredRoot = vendoredSkillsRoot(path);
|
|
2056
2489
|
const bundlePlan = planSkillSyncBundle(packagedRoot, vendoredRoot, EM_ALL_SKILL_BUNDLE_DIRS);
|
|
2057
|
-
const
|
|
2058
|
-
|
|
2490
|
+
const skillChanges = bundlePlan.reduce((n, { plan }) => n + plan.changes.length, 0);
|
|
2491
|
+
// MIL-269 (R32): the fixed agent-file list, never the shared .claude/agents/ directory as a whole.
|
|
2492
|
+
const packagedAgents = packagedAgentsRoot();
|
|
2493
|
+
const vendoredAgents = vendoredAgentsRoot(path);
|
|
2494
|
+
const agentPlan = planAgentSync(packagedAgents, vendoredAgents, EM_AGENT_FILES);
|
|
2495
|
+
const totalChanges = skillChanges + agentPlan.changes.length;
|
|
2496
|
+
const totalUnchanged = bundlePlan.reduce((n, { plan }) => n + plan.unchangedCount, 0) + agentPlan.unchangedCount;
|
|
2059
2497
|
if (totalChanges === 0) {
|
|
2060
2498
|
console.log(`up to date — ${vendoredRoot} already matches the installed skill bundle (${totalUnchanged} file(s))`);
|
|
2061
2499
|
}
|
|
2062
2500
|
else {
|
|
2063
2501
|
applySkillSyncBundle(bundlePlan, packagedRoot, vendoredRoot);
|
|
2502
|
+
applySkillSync(agentPlan, packagedAgents, vendoredAgents);
|
|
2064
2503
|
for (const { dirName, plan } of bundlePlan) {
|
|
2065
2504
|
for (const c of plan.changes)
|
|
2066
2505
|
console.log(`${c.kind}: ${dirName}/${c.relPath}`);
|
|
2067
2506
|
}
|
|
2507
|
+
for (const c of agentPlan.changes)
|
|
2508
|
+
console.log(`${c.kind}: agents/${c.relPath}`);
|
|
2068
2509
|
console.log(`synced ${vendoredRoot} — ${totalChanges} file(s) changed, ${totalUnchanged} unchanged`);
|
|
2069
2510
|
}
|
|
2070
2511
|
if (opts.agentsMd !== false) {
|
|
@@ -2073,21 +2514,55 @@ skill
|
|
|
2073
2514
|
});
|
|
2074
2515
|
skill
|
|
2075
2516
|
.command("check")
|
|
2076
|
-
.description("check
|
|
2077
|
-
"
|
|
2517
|
+
.description("check [path]'s em skills for drift against the installed em package: the em Claude Code plugin's " +
|
|
2518
|
+
"pin (settings + this machine's marketplace registry) when the repo declares it, and the vendored " +
|
|
2519
|
+
".claude/skills/ bundle when present; exits non-zero on any mismatch (CI-ready, MIL-93, MIL-231)")
|
|
2078
2520
|
.argument("[path]", "consumer repo root", ".")
|
|
2079
2521
|
.option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
|
|
2522
|
+
.option("--ci", "also fail (exit 1) when the declared plugin is not registered on this machine (implied by CI=true in the environment); " +
|
|
2523
|
+
"without it that finding prints the install commands on stderr and exits 0")
|
|
2080
2524
|
.action((path, opts) => {
|
|
2081
2525
|
const packagedRoot = packagedSkillsRoot();
|
|
2082
2526
|
const vendoredRoot = vendoredSkillsRoot(path);
|
|
2083
|
-
const
|
|
2527
|
+
const ciMode = !!opts.ci || process.env.CI === "true";
|
|
2528
|
+
// MIL-231: the plugin signal (a pinned marketplace in .claude/settings*.json) sits beside the
|
|
2529
|
+
// vendored signal (the anchor directory). Plugin only -> only the plugin is checked; vendored
|
|
2530
|
+
// only -> exactly the pre-1.14 behavior; both -> both; neither -> skill-check-not-installed.
|
|
2531
|
+
const pluginCheck = checkPlugin(resolve(path), PKG_VERSION, claudePluginsDir());
|
|
2532
|
+
const vendoredPresent = existsSync(join(vendoredRoot, EM_SKILL_ANCHOR_DIR));
|
|
2533
|
+
const vendored = pluginCheck && !vendoredPresent
|
|
2534
|
+
? { findings: [], ok: true }
|
|
2535
|
+
: checkSkillSyncBundle(packagedRoot, vendoredRoot, PKG_VERSION, EM_SKILL_DIR_NAMES, EM_SHARED_DIR_NAMES);
|
|
2536
|
+
// MIL-269 (R32): the four agent files, only when the vendored bundle is present (a plugin-only
|
|
2537
|
+
// repo ships them via the plugin); checked by fixed name, never the shared directory.
|
|
2538
|
+
const agents = vendoredPresent
|
|
2539
|
+
? checkAgentFiles(packagedAgentsRoot(), vendoredAgentsRoot(path), EM_AGENT_FILES)
|
|
2540
|
+
: { findings: [], ok: true };
|
|
2541
|
+
const findings = [...vendored.findings, ...agents.findings, ...(pluginCheck?.findings ?? [])];
|
|
2542
|
+
const failing = findings.filter((f) => f.code !== "plugin-not-installed-locally" || ciMode);
|
|
2543
|
+
const ok = failing.length === 0;
|
|
2544
|
+
const result = { findings, ok };
|
|
2084
2545
|
if (opts.json) {
|
|
2085
|
-
process.stdout.write(buildSkillCheckJson(result, vendoredRoot, PKG_VERSION) + "\n");
|
|
2546
|
+
process.stdout.write(buildSkillCheckJson(result, vendoredRoot, PKG_VERSION, pluginCheck?.plugin ?? null) + "\n");
|
|
2086
2547
|
}
|
|
2087
2548
|
else {
|
|
2088
|
-
for (const f of
|
|
2089
|
-
|
|
2090
|
-
|
|
2549
|
+
for (const f of findings) {
|
|
2550
|
+
if (f.code === "plugin-not-installed-locally" && !ciMode)
|
|
2551
|
+
console.error(f.message);
|
|
2552
|
+
else
|
|
2553
|
+
console.log(f.message);
|
|
2554
|
+
}
|
|
2555
|
+
if (!ok)
|
|
2556
|
+
console.log(`${failing.length} mismatch(es)`);
|
|
2557
|
+
else {
|
|
2558
|
+
if (!pluginCheck || vendoredPresent)
|
|
2559
|
+
console.log(`ok — vendored skill matches em ${PKG_VERSION}`);
|
|
2560
|
+
if (pluginCheck) {
|
|
2561
|
+
const unregistered = findings.some((f) => f.code === "plugin-not-installed-locally");
|
|
2562
|
+
console.log(`ok — em plugin ${pluginCheck.plugin.name} (${pluginCheck.plugin.ref}) matches em ${PKG_VERSION}` +
|
|
2563
|
+
(unregistered ? " (not registered on this machine - see the install commands on stderr)" : ""));
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2091
2566
|
}
|
|
2092
2567
|
// MIL-219 ruling A: `em skill check` also warns when [path]'s own state file records an
|
|
2093
2568
|
// `Em version:` behind the installed em — best-effort (no state file, or an unparseable
|
|
@@ -2103,7 +2578,7 @@ skill
|
|
|
2103
2578
|
}
|
|
2104
2579
|
// Set the code rather than process.exit(): same rationale as em ledger/em diff — stdout to
|
|
2105
2580
|
// a pipe (a --json document) shouldn't risk truncation.
|
|
2106
|
-
if (!
|
|
2581
|
+
if (!ok)
|
|
2107
2582
|
process.exitCode = 1;
|
|
2108
2583
|
});
|
|
2109
2584
|
const ci = program.command("ci").description("scaffold/check the CI enforcement preset (MIL-166, see docs/ci.md)");
|
|
@@ -2112,10 +2587,12 @@ ci.command("init")
|
|
|
2112
2587
|
"coverage --strict, em ledger, em skill check, em glossary --fail-on-conflicts, plus a " +
|
|
2113
2588
|
"push-triggered status-badge rebuild) and em-conform.yml (scheduled, advisory-only " +
|
|
2114
2589
|
"conformance cadence) — same install discipline as `em skill install`: marker-delimited, " +
|
|
2115
|
-
"idempotent, --check for CI self-verification (MIL-166, see docs/ci.md)"
|
|
2116
|
-
.
|
|
2590
|
+
"idempotent, --check for CI self-verification (MIL-166, see docs/ci.md). Give it a " +
|
|
2591
|
+
"system.yaml (or its directory) instead of a model to cover every model in the system: one " +
|
|
2592
|
+
"workflow, per-model jobs, a conform matrix (MIL-233)")
|
|
2593
|
+
.argument("<model>", "anchor .em file the coverage/ledger/slice-index/status steps point at, or a system.yaml (or a directory holding one) to cover every model in it")
|
|
2117
2594
|
.option("--tests <dir>", "test directory the coverage/status steps scan for INV-* citations", "test")
|
|
2118
|
-
.option("-f, --force", "replace an existing workflow file that has no GENERATED markers")
|
|
2595
|
+
.option("-f, --force", "replace an existing workflow file that has no GENERATED markers, or a managed block generated for a different set of models")
|
|
2119
2596
|
.option("--check", "verify both files match the current preset; exit non-zero on drift without writing (CI)")
|
|
2120
2597
|
.action((model, opts) => {
|
|
2121
2598
|
const unsafe = findUnsafeCiInitArg(model) ?? findUnsafeCiInitArg(opts.tests);
|
|
@@ -2127,20 +2604,55 @@ ci.command("init")
|
|
|
2127
2604
|
const repoRoot = process.cwd();
|
|
2128
2605
|
const ciPath = ciWorkflowPath(repoRoot);
|
|
2129
2606
|
const conformPath = conformWorkflowPath(repoRoot);
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2607
|
+
// MIL-231: a repo that declares the em plugin gets a conform workflow that installs the plugin
|
|
2608
|
+
// and runs /em:conform; any other repo keeps the vendored install lines.
|
|
2609
|
+
const usePlugin = detectPlugin(repoRoot) !== null;
|
|
2610
|
+
// MIL-233: a system manifest (a .yaml/.yml file, or a directory) covers every model in it;
|
|
2611
|
+
// anything else is the single-model form, unchanged.
|
|
2612
|
+
const isDir = existsSync(model) && statSync(model).isDirectory();
|
|
2613
|
+
const manifestForm = isDir || /\.ya?ml$/i.test(model);
|
|
2614
|
+
let files;
|
|
2615
|
+
if (manifestForm) {
|
|
2616
|
+
const loaded = ciModelsFromManifest(model, repoRoot);
|
|
2617
|
+
if (!loaded.ok) {
|
|
2618
|
+
console.error(`em ci init: ${loaded.message}`);
|
|
2619
|
+
process.exit(1);
|
|
2620
|
+
}
|
|
2621
|
+
files = [
|
|
2622
|
+
[
|
|
2623
|
+
ciPath,
|
|
2624
|
+
planCiFile(ciPath, buildCiWorkflowFileMulti(model, loaded.models, opts.tests, PKG_VERSION), ciManagedBodyMulti(loaded.models, opts.tests, PKG_VERSION), CI_WORKFLOW_MARKER, !!opts.force, true),
|
|
2625
|
+
],
|
|
2626
|
+
[
|
|
2627
|
+
conformPath,
|
|
2628
|
+
planCiFile(conformPath, buildConformWorkflowFileMulti(model, loaded.models, PKG_VERSION, usePlugin), conformManagedBodyMulti(loaded.models, PKG_VERSION, usePlugin), CONFORM_WORKFLOW_MARKER, !!opts.force, true),
|
|
2629
|
+
],
|
|
2630
|
+
];
|
|
2631
|
+
}
|
|
2632
|
+
else {
|
|
2633
|
+
files = [
|
|
2634
|
+
[
|
|
2635
|
+
ciPath,
|
|
2636
|
+
planCiFile(ciPath, buildCiWorkflowFile(model, opts.tests, PKG_VERSION), ciManagedBody(model, opts.tests, PKG_VERSION), CI_WORKFLOW_MARKER, !!opts.force),
|
|
2637
|
+
],
|
|
2638
|
+
[
|
|
2639
|
+
conformPath,
|
|
2640
|
+
planCiFile(conformPath, buildConformWorkflowFile(model, PKG_VERSION, usePlugin), conformManagedBody(model, PKG_VERSION, usePlugin), CONFORM_WORKFLOW_MARKER, !!opts.force),
|
|
2641
|
+
],
|
|
2642
|
+
];
|
|
2643
|
+
}
|
|
2644
|
+
// MIL-256 (#174) / MIL-233: a managed block is identified by the SET of models it names. A
|
|
2645
|
+
// disjoint set - or a single-model argument against a multi-model block - is its own
|
|
2646
|
+
// condition: never `stale`, never silently replaced.
|
|
2647
|
+
const otherModels = (status, path) => describeOtherModels(path, status);
|
|
2140
2648
|
if (opts.check) {
|
|
2141
2649
|
let drift = false;
|
|
2142
2650
|
for (const [path, status] of files) {
|
|
2143
|
-
if (status.kind === "
|
|
2651
|
+
if (status.kind === "other-models" || status.kind === "replace-models") {
|
|
2652
|
+
console.log(`different models: ${otherModels(status, path)}`);
|
|
2653
|
+
drift = true;
|
|
2654
|
+
}
|
|
2655
|
+
else if (status.kind === "create") {
|
|
2144
2656
|
console.log(`missing: ${path} — run \`em ci init ${model}\` to create it`);
|
|
2145
2657
|
drift = true;
|
|
2146
2658
|
}
|
|
@@ -2149,7 +2661,11 @@ ci.command("init")
|
|
|
2149
2661
|
drift = true;
|
|
2150
2662
|
}
|
|
2151
2663
|
else if (status.kind === "stale") {
|
|
2152
|
-
|
|
2664
|
+
const marker = path === ciPath ? CI_WORKFLOW_MARKER : CONFORM_WORKFLOW_MARKER;
|
|
2665
|
+
const change = manifestForm
|
|
2666
|
+
? describeSetChange(fileBlockModels(status.current, marker), fileBlockModels(status.content, marker))
|
|
2667
|
+
: null;
|
|
2668
|
+
console.log(`stale: ${path} — run \`em ci init ${model}\` and commit the result${change ? ` (${change})` : ""}`);
|
|
2153
2669
|
drift = true;
|
|
2154
2670
|
}
|
|
2155
2671
|
else {
|
|
@@ -2161,10 +2677,22 @@ ci.command("init")
|
|
|
2161
2677
|
process.exitCode = 1;
|
|
2162
2678
|
return;
|
|
2163
2679
|
}
|
|
2680
|
+
// Refuse before writing anything, so a refusal on one file never leaves the other updated.
|
|
2681
|
+
const refused = files.filter(([, s]) => s.kind === "other-models");
|
|
2682
|
+
if (refused.length > 0) {
|
|
2683
|
+
for (const [path, status] of refused) {
|
|
2684
|
+
if (status.kind === "other-models")
|
|
2685
|
+
console.error(`em ci init: ${otherModels(status, path)}`);
|
|
2686
|
+
}
|
|
2687
|
+
process.exitCode = 1;
|
|
2688
|
+
return;
|
|
2689
|
+
}
|
|
2164
2690
|
mkdirSync(dirname(ciPath), { recursive: true });
|
|
2165
2691
|
for (const [path, status] of files) {
|
|
2166
2692
|
applyCiFile(path, status);
|
|
2167
|
-
if (status.kind === "
|
|
2693
|
+
if (status.kind === "replace-models")
|
|
2694
|
+
console.log(`replaced ${path} (--force; was generated for ${status.previous.join(", ")})`);
|
|
2695
|
+
else if (status.kind === "create")
|
|
2168
2696
|
console.log(`installed ${path}`);
|
|
2169
2697
|
else if (status.kind === "stale")
|
|
2170
2698
|
console.log(`updated ${path}`);
|
|
@@ -2185,7 +2713,7 @@ program
|
|
|
2185
2713
|
"makes one git commit per applicable step, refusing on a dirty working tree")
|
|
2186
2714
|
.argument("<file>", "input .em model file")
|
|
2187
2715
|
.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")
|
|
2716
|
+
.option("--check", "exit non-zero only on a hard incompatibility (an existing but unparseable state file, an un-migratable .em shape) — writes nothing; what CI runs; the final line says why")
|
|
2189
2717
|
.option("--json", "print a JSON document instead of text (dry-run/--check only, never with --apply)")
|
|
2190
2718
|
.action((file, opts) => {
|
|
2191
2719
|
if (opts.apply && opts.check) {
|
|
@@ -2219,18 +2747,20 @@ program
|
|
|
2219
2747
|
repoRoot: repoRootResult.repoRoot,
|
|
2220
2748
|
installedVersion: PKG_VERSION,
|
|
2221
2749
|
packagedSkillsRoot: packagedSkillsRoot(),
|
|
2750
|
+
packagedAgentsRoot: packagedAgentsRoot(),
|
|
2222
2751
|
model,
|
|
2223
2752
|
refs,
|
|
2224
2753
|
};
|
|
2225
2754
|
if (opts.check) {
|
|
2226
|
-
const { ok, report } = checkUpgrade(ctx);
|
|
2755
|
+
const { ok, report, reasons } = checkUpgrade(ctx);
|
|
2227
2756
|
console.error(`em upgrade --check: from ${report.from.version}${report.from.inferred ? ` (inferred: ${report.from.basis})` : ""} → to ${report.to}`);
|
|
2228
2757
|
for (const s of report.steps) {
|
|
2229
2758
|
console.error(` [${s.applicable ? "x" : " "}] ${s.id} (since ${s.sinceVersion}): ${s.reason}`);
|
|
2230
2759
|
}
|
|
2231
2760
|
for (const h of report.human)
|
|
2232
2761
|
console.error(` human: ${h.id}: ${h.reason}`);
|
|
2233
|
-
|
|
2762
|
+
// MIL-257: the final line always says why (previously visible only via --json).
|
|
2763
|
+
console.error(ok ? "ok — no hard incompatibility" : `em upgrade --check: hard incompatibility found: ${reasons.join("; ")}`);
|
|
2234
2764
|
if (!ok)
|
|
2235
2765
|
process.exitCode = 1;
|
|
2236
2766
|
return;
|
|
@@ -2336,6 +2866,7 @@ function computeAllDiagnostics(file, model, refs, diagnostics, source) {
|
|
|
2336
2866
|
...validateDocModelConsistency(model, refs, dirname(file)),
|
|
2337
2867
|
...validateOrphanedSliceDocs(model, refs, dirname(file)),
|
|
2338
2868
|
...validateModelVersionStale(model, refs, dirname(file), source),
|
|
2869
|
+
...validateInvariants(model, refs, dirname(file)),
|
|
2339
2870
|
];
|
|
2340
2871
|
}
|
|
2341
2872
|
/** Read a file's text, or exit with a clear error — shared by `em diff` (both forms) and
|
|
@@ -2441,7 +2972,7 @@ function printDivergences(model) {
|
|
|
2441
2972
|
* cross-model `public` view's reader legitimately lives outside this file (see
|
|
2442
2973
|
* `publicViewsWithoutInModelReader`'s own header for why this stays advisory). */
|
|
2443
2974
|
function printPublicElements(model) {
|
|
2444
|
-
const pub = model.elements.filter((el) => el.public && (el.kind === "event" || el.kind === "view"));
|
|
2975
|
+
const pub = model.elements.filter((el) => el.public && (el.kind === "command" || el.kind === "event" || el.kind === "view"));
|
|
2445
2976
|
if (pub.length === 0) {
|
|
2446
2977
|
console.log("no public elements");
|
|
2447
2978
|
return;
|