@milehimikey/em 1.16.1 → 1.18.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 +1 -1
- package/.claude/skills/event-modeling-conform/SKILL.md +3 -2
- package/.claude/skills/event-modeling-conform/reference/conform.md +7 -0
- package/.claude/skills/event-modeling-design/SKILL.md +46 -5
- package/.claude/skills/event-modeling-discover/SKILL.md +43 -8
- package/.claude/skills/event-modeling-discover/reference/extract.md +6 -1
- package/.claude/skills/event-modeling-discover/reference/prd-rubric.md +102 -0
- package/.claude/skills/event-modeling-discover/reference/prd-spec-path.md +171 -0
- package/.claude/skills/event-modeling-engagement/SKILL.md +28 -6
- package/.claude/skills/event-modeling-engagement/reference/engagement.md +5 -4
- package/.claude/skills/event-modeling-implement/SKILL.md +1 -1
- package/.claude/skills/event-modeling-implement/reference/implement.md +18 -8
- package/.claude/skills/event-modeling-review/SKILL.md +1 -1
- package/.claude/skills/event-modeling-shared/reference/em-dsl.md +107 -10
- package/.claude/skills/event-modeling-shared/reference/methodology.md +268 -0
- package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +122 -15
- package/.claude/skills/event-modeling-shared/templates/slice.md +11 -3
- package/.claude-plugin/marketplace.json +2 -2
- package/dist/catalog/docJoin.d.ts +13 -5
- package/dist/catalog/docJoin.js +6 -1
- package/dist/catalog/docJoin.js.map +1 -1
- package/dist/catalog/docModelConsistencyValidate.js +160 -0
- package/dist/catalog/docModelConsistencyValidate.js.map +1 -1
- package/dist/catalog/invariantsValidate.d.ts +2 -0
- package/dist/catalog/invariantsValidate.js +67 -1
- package/dist/catalog/invariantsValidate.js.map +1 -1
- package/dist/catalog/scenarioQualityValidate.d.ts +4 -0
- package/dist/catalog/scenarioQualityValidate.js +148 -0
- package/dist/catalog/scenarioQualityValidate.js.map +1 -0
- package/dist/catalog/scenarios.d.ts +5 -0
- package/dist/catalog/scenarios.js +80 -0
- package/dist/catalog/scenarios.js.map +1 -1
- package/dist/catalog/signedBodyValidate.d.ts +7 -0
- package/dist/catalog/signedBodyValidate.js +29 -0
- package/dist/catalog/signedBodyValidate.js.map +1 -0
- package/dist/catalog/sliceDoc.d.ts +53 -7
- package/dist/catalog/sliceDoc.js +89 -10
- package/dist/catalog/sliceDoc.js.map +1 -1
- package/dist/catalog/sliceReadyValidate.d.ts +23 -2
- package/dist/catalog/sliceReadyValidate.js +54 -22
- package/dist/catalog/sliceReadyValidate.js.map +1 -1
- package/dist/catalog/sliceSections.d.ts +3 -2
- package/dist/catalog/sliceSections.js +99 -9
- package/dist/catalog/sliceSections.js.map +1 -1
- package/dist/cli/ciInit.d.ts +4 -11
- package/dist/cli/ciInit.js +24 -6
- package/dist/cli/ciInit.js.map +1 -1
- package/dist/cli/coverage.d.ts +9 -0
- package/dist/cli/coverage.js +18 -0
- package/dist/cli/coverage.js.map +1 -1
- package/dist/cli/defer.d.ts +60 -17
- package/dist/cli/defer.js +187 -86
- package/dist/cli/defer.js.map +1 -1
- package/dist/cli/engagement.d.ts +46 -0
- package/dist/cli/engagement.js +156 -1
- package/dist/cli/engagement.js.map +1 -1
- package/dist/cli/engagementFoundation.d.ts +47 -0
- package/dist/cli/engagementFoundation.js +164 -0
- package/dist/cli/engagementFoundation.js.map +1 -0
- package/dist/cli/frontmatterSurgery.d.ts +5 -0
- package/dist/cli/frontmatterSurgery.js +15 -0
- package/dist/cli/frontmatterSurgery.js.map +1 -1
- package/dist/cli/ledgerCheck.d.ts +5 -1
- package/dist/cli/ledgerCheck.js +50 -20
- package/dist/cli/ledgerCheck.js.map +1 -1
- package/dist/cli/markImplemented.d.ts +14 -10
- package/dist/cli/markImplemented.js +49 -1
- package/dist/cli/markImplemented.js.map +1 -1
- package/dist/cli/prdTriage.d.ts +109 -0
- package/dist/cli/prdTriage.js +378 -0
- package/dist/cli/prdTriage.js.map +1 -0
- package/dist/cli/ratify.d.ts +14 -2
- package/dist/cli/ratify.js +34 -6
- package/dist/cli/ratify.js.map +1 -1
- package/dist/cli/refsCheck.d.ts +78 -0
- package/dist/cli/refsCheck.js +153 -0
- package/dist/cli/refsCheck.js.map +1 -0
- package/dist/cli/render-inputs.d.ts +2 -0
- package/dist/cli/render-inputs.js +4 -0
- package/dist/cli/render-inputs.js.map +1 -1
- package/dist/cli/revise.d.ts +12 -3
- package/dist/cli/revise.js +43 -10
- package/dist/cli/revise.js.map +1 -1
- package/dist/cli/shape.d.ts +27 -0
- package/dist/cli/shape.js +77 -0
- package/dist/cli/shape.js.map +1 -0
- package/dist/cli/sliceConform.js +9 -0
- package/dist/cli/sliceConform.js.map +1 -1
- package/dist/cli/sliceIndex.d.ts +6 -1
- package/dist/cli/sliceIndex.js +28 -13
- package/dist/cli/sliceIndex.js.map +1 -1
- package/dist/cli/sliceNew.d.ts +4 -1
- package/dist/cli/sliceNew.js +26 -9
- package/dist/cli/sliceNew.js.map +1 -1
- package/dist/cli/status.d.ts +26 -5
- package/dist/cli/status.js +28 -6
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/storylineSuggest.d.ts +26 -0
- package/dist/cli/storylineSuggest.js +252 -0
- package/dist/cli/storylineSuggest.js.map +1 -0
- package/dist/cli/upgrade.d.ts +15 -3
- package/dist/cli/upgrade.js +537 -8
- package/dist/cli/upgrade.js.map +1 -1
- package/dist/cli/upgradeReshape.d.ts +60 -0
- package/dist/cli/upgradeReshape.js +425 -0
- package/dist/cli/upgradeReshape.js.map +1 -0
- package/dist/cli.js +430 -57
- package/dist/cli.js.map +1 -1
- package/dist/emit/dot.d.ts +2 -0
- package/dist/emit/dot.js +44 -8
- package/dist/emit/dot.js.map +1 -1
- package/dist/emit/engagementJson.d.ts +4 -1
- package/dist/emit/engagementJson.js +27 -1
- package/dist/emit/engagementJson.js.map +1 -1
- package/dist/emit/json.d.ts +21 -1
- package/dist/emit/json.js +29 -2
- package/dist/emit/json.js.map +1 -1
- package/dist/emit/ledgerJson.d.ts +1 -1
- package/dist/emit/ledgerJson.js +6 -1
- package/dist/emit/ledgerJson.js.map +1 -1
- package/dist/emit/queryJson.d.ts +1 -1
- package/dist/emit/queryJson.js +3 -1
- package/dist/emit/queryJson.js.map +1 -1
- package/dist/emit/refsJson.d.ts +4 -0
- package/dist/emit/refsJson.js +19 -0
- package/dist/emit/refsJson.js.map +1 -0
- package/dist/emit/statusJson.d.ts +1 -1
- package/dist/emit/statusJson.js +6 -1
- package/dist/emit/statusJson.js.map +1 -1
- package/dist/emit/theme.d.ts +1 -0
- package/dist/emit/theme.js +13 -0
- package/dist/emit/theme.js.map +1 -1
- package/dist/emit/validateJson.d.ts +5 -2
- package/dist/emit/validateJson.js +41 -2
- package/dist/emit/validateJson.js.map +1 -1
- package/dist/layout/grid.d.ts +5 -1
- package/dist/layout/grid.js +93 -2
- package/dist/layout/grid.js.map +1 -1
- package/dist/mcp/server.js +79 -14
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/edges.d.ts +7 -0
- package/dist/model/edges.js +33 -16
- package/dist/model/edges.js.map +1 -1
- package/dist/model/model.d.ts +33 -0
- package/dist/model/model.js +87 -3
- package/dist/model/model.js.map +1 -1
- package/dist/model/queryIndex.js +2 -2
- package/dist/model/queryIndex.js.map +1 -1
- package/dist/model/refs.js +8 -5
- package/dist/model/refs.js.map +1 -1
- package/dist/model/rules.d.ts +167 -1
- package/dist/model/rules.js +164 -3
- package/dist/model/rules.js.map +1 -1
- package/dist/model/shapeLint.d.ts +12 -0
- package/dist/model/shapeLint.js +170 -0
- package/dist/model/shapeLint.js.map +1 -0
- package/dist/model/storylines.d.ts +51 -0
- package/dist/model/storylines.js +188 -0
- package/dist/model/storylines.js.map +1 -0
- package/dist/model/validate.d.ts +6 -1
- package/dist/model/validate.js +317 -14
- package/dist/model/validate.js.map +1 -1
- package/dist/parser/againSugar.d.ts +30 -0
- package/dist/parser/againSugar.js +105 -0
- package/dist/parser/againSugar.js.map +1 -0
- package/dist/parser/ast.d.ts +33 -0
- package/dist/parser/lexer.d.ts +9 -1
- package/dist/parser/lexer.js +17 -2
- package/dist/parser/lexer.js.map +1 -1
- package/dist/parser/parser.js +284 -4
- package/dist/parser/parser.js.map +1 -1
- package/dist/query/format.js +1 -0
- package/dist/query/format.js.map +1 -1
- package/dist/query/verbs.d.ts +4 -0
- package/dist/query/verbs.js +30 -3
- package/dist/query/verbs.js.map +1 -1
- package/dist/render/drawEdges.js +100 -4
- package/dist/render/drawEdges.js.map +1 -1
- package/dist/render/render.js +20 -2
- package/dist/render/render.js.map +1 -1
- package/dist/render/sliceDiagram.js +5 -5
- package/dist/render/sliceDiagram.js.map +1 -1
- package/dist/render/storylineBands.d.ts +11 -0
- package/dist/render/storylineBands.js +80 -0
- package/dist/render/storylineBands.js.map +1 -0
- package/dist/render/storylineDiagram.d.ts +14 -0
- package/dist/render/storylineDiagram.js +142 -0
- package/dist/render/storylineDiagram.js.map +1 -0
- package/dist/roster/check.d.ts +32 -0
- package/dist/roster/check.js +92 -0
- package/dist/roster/check.js.map +1 -0
- package/dist/roster/context.d.ts +3 -0
- package/dist/roster/context.js +23 -0
- package/dist/roster/context.js.map +1 -0
- package/dist/roster/init.d.ts +15 -0
- package/dist/roster/init.js +52 -0
- package/dist/roster/init.js.map +1 -0
- package/dist/roster/roster.d.ts +79 -0
- package/dist/roster/roster.js +298 -0
- package/dist/roster/roster.js.map +1 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/shared/reference/em-dsl.md +107 -10
- package/plugin/shared/reference/methodology.md +268 -0
- package/plugin/shared/reference/slice-doc-schema.md +122 -15
- package/plugin/shared/templates/slice.md +11 -3
- package/plugin/skills/conform/SKILL.md +3 -2
- package/plugin/skills/conform/reference/conform.md +7 -0
- package/plugin/skills/design/SKILL.md +46 -5
- package/plugin/skills/discover/SKILL.md +43 -8
- package/plugin/skills/discover/reference/extract.md +6 -1
- package/plugin/skills/discover/reference/prd-rubric.md +103 -0
- package/plugin/skills/discover/reference/prd-spec-path.md +172 -0
- package/plugin/skills/engagement/SKILL.md +28 -6
- package/plugin/skills/engagement/reference/engagement.md +5 -4
- package/plugin/skills/event-modeling/SKILL.md +1 -1
- package/plugin/skills/implement/SKILL.md +1 -1
- package/plugin/skills/implement/reference/implement.md +18 -8
- package/plugin/skills/review/SKILL.md +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from "node:fs";
|
|
4
4
|
import { cp, mkdir } from "node:fs/promises";
|
|
5
|
-
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
5
|
+
import { basename, dirname, extname, join, relative, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { Command } from "commander";
|
|
8
8
|
import { compile } from "./pipeline.js";
|
|
@@ -12,7 +12,8 @@ import { continuationOf } from "./model/continuation.js";
|
|
|
12
12
|
import { ParseError } from "./parser/parser.js";
|
|
13
13
|
import { renderDot, layoutDot, composeSvg, writeRendered, formatFromPath } from "./render/render.js";
|
|
14
14
|
import { buildSliceDiagram } from "./render/sliceDiagram.js";
|
|
15
|
-
import { resolveSliceArg, defaultSliceOut } from "./cli/render-inputs.js";
|
|
15
|
+
import { resolveSliceArg, defaultSliceOut, defaultStorylineOut } from "./cli/render-inputs.js";
|
|
16
|
+
import { buildStorylineDiagram, resolveStorylineArg } from "./render/storylineDiagram.js";
|
|
16
17
|
import { resolveSliceKey } from "./model/resolveSlice.js";
|
|
17
18
|
import { serializeBuilds, watchFile } from "./render/watch.js";
|
|
18
19
|
import { startLiveServer } from "./render/serve.js";
|
|
@@ -20,26 +21,36 @@ import { formatDiagnostic, hasErrors, publicViewsWithoutInModelReader } from "./
|
|
|
20
21
|
import { buildExport, buildSliceExport, GENERATOR_VERSION } from "./emit/json.js";
|
|
21
22
|
import { generateContract, runApiCheck, formatApiCheckText } from "./cli/api.js";
|
|
22
23
|
import { buildApiCheckJson } from "./emit/apiCheckJson.js";
|
|
23
|
-
import { buildValidateJson, buildSliceReadyJson, buildValidateListJson, collectMarkers } from "./emit/validateJson.js";
|
|
24
|
+
import { buildValidateJson, buildShapeJson, buildSliceReadyJson, buildValidateListJson, collectMarkers } from "./emit/validateJson.js";
|
|
24
25
|
import { buildDiffJson } from "./emit/diffJson.js";
|
|
25
26
|
import { diffModels, formatModelDiff, hasChanges } from "./model/diff.js";
|
|
26
27
|
import { planDiffArgs, realGit, resolveRevision, resolveDocAtRevision } from "./cli/diff-inputs.js";
|
|
27
28
|
import { readSliceDoc } from "./catalog/readSliceDoc.js";
|
|
28
29
|
import { validateLineage } from "./catalog/lineageValidate.js";
|
|
29
30
|
import { validateFrontmatterCoherence } from "./catalog/frontmatterCoherenceValidate.js";
|
|
31
|
+
import { validateSignedBodies } from "./catalog/signedBodyValidate.js";
|
|
30
32
|
import { validateNoteBindings } from "./catalog/noteBindingValidate.js";
|
|
31
33
|
import { validateDocModelConsistency } from "./catalog/docModelConsistencyValidate.js";
|
|
32
34
|
import { validateOrphanedSliceDocs } from "./catalog/orphanedSliceDocValidate.js";
|
|
33
35
|
import { validateModelVersionStale } from "./catalog/modelVersionValidate.js";
|
|
34
36
|
import { validateInvariants } from "./catalog/invariantsValidate.js";
|
|
35
37
|
import { computeSliceReadiness } from "./catalog/sliceReadyValidate.js";
|
|
36
|
-
import {
|
|
38
|
+
import { checkRosterForWrite, findRosterAbove, loadRoster, ROSTER_FILE } from "./roster/roster.js";
|
|
39
|
+
import { observeSigners, renderRoster } from "./roster/init.js";
|
|
40
|
+
import { rosterContextFor } from "./roster/context.js";
|
|
41
|
+
import { computeModelKey } from "./model/qualifiedRef.js";
|
|
42
|
+
import { checkRoster, formatRosterCheck, buildRosterCheckJson, grandfatheredNotice } from "./roster/check.js";
|
|
43
|
+
import { applyEngagementClose, applyEngagementSet, buildNewEngagement, engagementMembersFor, engagementPlanFor, engagementStatusFor, formatEngagementPlanText, formatEngagementStatusText, } from "./cli/engagement.js";
|
|
37
44
|
import { LEDGER_STATES, engagementPath, loadEngagement, readOpenEngagements } from "./cli/engagementFile.js";
|
|
38
|
-
import { buildEngagementPlanJson, buildEngagementStatusJson } from "./emit/engagementJson.js";
|
|
45
|
+
import { buildEngagementFoundationJson, buildEngagementPlanJson, buildEngagementStatusJson } from "./emit/engagementJson.js";
|
|
46
|
+
import { checkFoundation, formatFoundationText } from "./cli/engagementFoundation.js";
|
|
39
47
|
import { detectSliceDocCollisions } from "./catalog/modelCollisionValidate.js";
|
|
40
48
|
import { checkLedger, readLedgerWaiverTrailers, applyLedgerWaivers } from "./cli/ledgerCheck.js";
|
|
41
49
|
import { planMigration, verifyMigration } from "./cli/migrateReactionShape.js";
|
|
42
50
|
import { buildLedgerJson } from "./emit/ledgerJson.js";
|
|
51
|
+
import { buildRefsJson } from "./emit/refsJson.js";
|
|
52
|
+
import { buildShapeReport, formatShapeText } from "./cli/shape.js";
|
|
53
|
+
import { applyRefWaivers, checkRefs, readWaiveFile } from "./cli/refsCheck.js";
|
|
43
54
|
import { buildCoverageReport, resolveScopedSlices, resolveCoverageSliceKey } from "./cli/coverage.js";
|
|
44
55
|
import { buildCoverageJson } from "./emit/coverageJson.js";
|
|
45
56
|
import { resolveSliceStatusFacts, countOpenIssues, resolveConformanceEntry, aggregateInvariantTotals, buildStatusReport, formatStatusText, formatStatusMarkdown, buildStatusBadge, formatConformancePart, findSpecifyRoot, resolveModelVersionStatusEntry, resolveEmVersionStatusEntry, } from "./cli/status.js";
|
|
@@ -64,7 +75,7 @@ import { planGlossaryArgs } from "./cli/glossary-inputs.js";
|
|
|
64
75
|
import { buildCatalog } from "./catalog/build.js";
|
|
65
76
|
import { planCatalogArgs } from "./cli/catalog-inputs.js";
|
|
66
77
|
import { runSliceIndex } from "./cli/sliceIndex.js";
|
|
67
|
-
import { runMarkImplemented } from "./cli/markImplemented.js";
|
|
78
|
+
import { formatNextDraftNotice, runMarkImplemented } from "./cli/markImplemented.js";
|
|
68
79
|
import { runRatify, parseMeaningFlags, meaningConfirmationLine } from "./cli/ratify.js";
|
|
69
80
|
import { runReview } from "./cli/review.js";
|
|
70
81
|
import { runConformSupersede } from "./cli/conformSupersede.js";
|
|
@@ -79,6 +90,7 @@ import { buildChangelogDoc } from "./cli/changelogBuild.js";
|
|
|
79
90
|
import { runRevise } from "./cli/revise.js";
|
|
80
91
|
import { runDefer, runDeferFromIssue } from "./cli/defer.js";
|
|
81
92
|
import { runSliceConform } from "./cli/sliceConform.js";
|
|
93
|
+
import { checkPrdTriage, buildCheckPrdTriageJson, renderPrdTriageMarkdown, validatePrdTriage } from "./cli/prdTriage.js";
|
|
82
94
|
import { checkFindingsFile, buildCheckFindingsJson, lookupFindingsBesideReport, unruledFindingsInScope, FINDING_LOCI } from "./cli/findings.js";
|
|
83
95
|
import { STATE_FILE_NAME, PHASES, isPhase, loadStateFile, parseState, setPhase, setConformance, setModelVersion, setCertified, setEmVersion, setReview, isValidDateString, modelPathMismatch, } from "./cli/stateFile.js";
|
|
84
96
|
import { findCertifiedVersion, latestModelVersionNumber, modelVersionDrift, runCertifyModelVersion, runModelVersionBump, } from "./cli/modelVersion.js";
|
|
@@ -86,6 +98,7 @@ import { STARTER_EM, starterEmFor, scaffoldReadme, scaffoldStateFile, scaffoldCo
|
|
|
86
98
|
import { kebabSlug } from "./util/slug.js";
|
|
87
99
|
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";
|
|
88
100
|
import { resolveRepoRoot, detectUpgrade, applyUpgrade, checkUpgrade } from "./cli/upgrade.js";
|
|
101
|
+
import { buildStorylineSuggestion } from "./cli/storylineSuggest.js";
|
|
89
102
|
import { buildUpgradeJson } from "./emit/upgradeJson.js";
|
|
90
103
|
import { isOlder } from "./util/semver.js";
|
|
91
104
|
import { RULES } from "./model/rules.js";
|
|
@@ -101,6 +114,16 @@ import { runCodeowners } from "./system/codeowners.js";
|
|
|
101
114
|
import { buildCodeownersJson } from "./emit/codeownersJson.js";
|
|
102
115
|
import { verifySystem } from "./system/verify.js";
|
|
103
116
|
import { buildSystemJson } from "./emit/systemJson.js";
|
|
117
|
+
/** GH #253: the trailing note `em slice defer` prints after `deferred: …` on a ratified/shipped
|
|
118
|
+
* doc — the item went into the signed body, outside `ratifiedHash`, and when the stamp was still
|
|
119
|
+
* in the pre-1.17 whole-body scope it was re-stamped in the same edit (body unchanged). */
|
|
120
|
+
function deferSignedNote(result) {
|
|
121
|
+
if (!result.signed)
|
|
122
|
+
return "";
|
|
123
|
+
return result.restamped
|
|
124
|
+
? " — appended to the signed doc's Open Questions, outside ratifiedHash; ratifiedHash re-stamped to the 1.17 scope (body unchanged)"
|
|
125
|
+
: " — appended to the signed doc's Open Questions, outside ratifiedHash";
|
|
126
|
+
}
|
|
104
127
|
const program = new Command();
|
|
105
128
|
// Single source of truth for the version (also read by src/emit/json.ts for
|
|
106
129
|
// `generator.version` in `em export`): package.json, one level up from both
|
|
@@ -175,6 +198,9 @@ program
|
|
|
175
198
|
.option("-T, --format <fmt>", "output format (svg, png, pdf, ...)")
|
|
176
199
|
.option("--slice <name>", "render only this slice, redrawn in its own canonical pattern shape " +
|
|
177
200
|
"(default out: slices/<kebab-slug>.svg)")
|
|
201
|
+
.option("--storyline <name>", "render only this storyline: its slices in order, preceded by dimmed stub columns for what " +
|
|
202
|
+
"they read from outside it (and the end state of a storyline it comes `after`) " +
|
|
203
|
+
"(default out: storylines/<kebab-slug>.svg)")
|
|
178
204
|
.option("--emit-dot", "print the generated DOT instead of rendering")
|
|
179
205
|
.option("--keep-empty-lanes", "keep the API lane even when empty")
|
|
180
206
|
.action(async (file, opts) => {
|
|
@@ -182,6 +208,10 @@ program
|
|
|
182
208
|
console.error("em render: --slice cannot be combined with --emit-dot");
|
|
183
209
|
process.exit(1);
|
|
184
210
|
}
|
|
211
|
+
if (opts.storyline !== undefined && (opts.slice !== undefined || opts.emitDot)) {
|
|
212
|
+
console.error(`em render: --storyline cannot be combined with ${opts.slice !== undefined ? "--slice" : "--emit-dot"}`);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
185
215
|
const { dot, model, grid, diagnostics, refs } = compileFile(file, {
|
|
186
216
|
keepEmptyLanes: opts.keepEmptyLanes,
|
|
187
217
|
});
|
|
@@ -206,6 +236,22 @@ program
|
|
|
206
236
|
console.error("not rendering: fix the errors above");
|
|
207
237
|
process.exit(1);
|
|
208
238
|
}
|
|
239
|
+
if (opts.storyline !== undefined) {
|
|
240
|
+
const found = resolveStorylineArg(model, opts.storyline);
|
|
241
|
+
if ("error" in found) {
|
|
242
|
+
console.error(found.error);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
const out = opts.out ?? defaultStorylineOut(file, found.storyline.name);
|
|
246
|
+
const fmt = opts.format ?? formatFromPath(out);
|
|
247
|
+
await mkdir(dirname(out), { recursive: true }); // storylines/ may not exist yet
|
|
248
|
+
const sub = buildStorylineDiagram(model, found.storyline, { keepEmptyLanes: opts.keepEmptyLanes });
|
|
249
|
+
const raw = await layoutDot(sub.dot);
|
|
250
|
+
const svg = composeSvg(raw, sub.model, sub.grid, dirname(file), dirname(out));
|
|
251
|
+
await writeRendered(svg, out, fmt);
|
|
252
|
+
console.log(`rendered ${out}`);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
209
255
|
if (opts.slice !== undefined) {
|
|
210
256
|
const lookup = resolveSliceArg(opts.slice, model.slices);
|
|
211
257
|
if ("error" in lookup) {
|
|
@@ -525,6 +571,7 @@ slice
|
|
|
525
571
|
// MIL-266: with a model in hand (--wire), the doc's generated regions are filled from it;
|
|
526
572
|
// without one they carry the template's placeholders until `em slice sync` fills them.
|
|
527
573
|
let regions;
|
|
574
|
+
let alternatives = [];
|
|
528
575
|
if (opts.wire) {
|
|
529
576
|
const { model, refs, diagnostics, source } = compileFile(opts.wire);
|
|
530
577
|
printDiagnostics(diagnostics);
|
|
@@ -539,11 +586,13 @@ slice
|
|
|
539
586
|
}
|
|
540
587
|
wired = result;
|
|
541
588
|
regions = buildSliceRegions(model, [refs.sliceKeys.indexOf(key)]);
|
|
589
|
+
const sliceEvents = model.slices[refs.sliceKeys.indexOf(key)].elements.filter((e) => e.kind === "event");
|
|
590
|
+
alternatives = sliceEvents.filter((e) => e.alternativeGroup !== undefined).map((e) => e.name);
|
|
542
591
|
}
|
|
543
592
|
mkdirSync(dir, { recursive: true });
|
|
544
593
|
const content = opts.stub
|
|
545
594
|
? buildStubDocContent(name, opts.pattern, opts.swimlane)
|
|
546
|
-
: buildSliceDocContent(name, key, opts.pattern, opts.swimlane, regions);
|
|
595
|
+
: buildSliceDocContent(name, key, opts.pattern, opts.swimlane, regions, alternatives);
|
|
547
596
|
writeFileSync(path, content);
|
|
548
597
|
console.log(`wrote ${path}`);
|
|
549
598
|
if (wired) {
|
|
@@ -684,6 +733,7 @@ slice
|
|
|
684
733
|
.argument("<slice-key>", "slice export key (kebab-case)")
|
|
685
734
|
.argument("<pr-url>", "merged PR (or commit) URL")
|
|
686
735
|
.option("--on <date>", "merge date recorded as `shippedOn:`, YYYY-MM-DD (default: today, local date; MIL-284)")
|
|
736
|
+
.option("--json", "print a JSON document instead of text: { path, changed, implementedIn, nextDraft: { version, reopened[] } | null } (GH #285)")
|
|
687
737
|
.action((file, sliceKey, prUrl, opts) => {
|
|
688
738
|
if (opts.on !== undefined && !isValidDateString(opts.on)) {
|
|
689
739
|
console.error(`em slice mark-implemented: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
|
|
@@ -706,13 +756,21 @@ slice
|
|
|
706
756
|
console.error(`em slice mark-implemented: ${result.message}`);
|
|
707
757
|
process.exit(1);
|
|
708
758
|
}
|
|
759
|
+
if (opts.json) {
|
|
760
|
+
console.log(JSON.stringify({
|
|
761
|
+
path: result.path,
|
|
762
|
+
changed: result.changed,
|
|
763
|
+
implementedIn: prUrl,
|
|
764
|
+
nextDraft: result.openedDraft ? { version: result.openedDraft.version, reopened: result.openedDraft.titles } : null,
|
|
765
|
+
}, null, 2));
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
709
768
|
console.log(result.changed
|
|
710
769
|
? `marked implemented: ${result.path} (implementedIn: ${prUrl})`
|
|
711
770
|
: `already implemented (no-op): ${result.path}`);
|
|
712
|
-
if (result.
|
|
713
|
-
console.log(
|
|
714
|
-
|
|
715
|
-
`; v${result.openedDraft.version - 1} stays shipped`);
|
|
771
|
+
if (result.openedDraft) {
|
|
772
|
+
console.log("");
|
|
773
|
+
console.log(formatNextDraftNotice(result.path, result.openedDraft, prUrl));
|
|
716
774
|
}
|
|
717
775
|
});
|
|
718
776
|
slice
|
|
@@ -727,6 +785,7 @@ slice
|
|
|
727
785
|
.argument("<slice-key>", "slice export key (kebab-case)")
|
|
728
786
|
.requiredOption("--by <name>", "the reviewer's (or facilitator's) name")
|
|
729
787
|
.option("--on <date>", "review date, YYYY-MM-DD (default: today, local date)")
|
|
788
|
+
.option("--roster <path>", "ratifier roster to check --by against (GH #259; default: the nearest ratifiers.yaml above the model, none = unrestricted)")
|
|
730
789
|
.action((file, sliceKey, opts) => {
|
|
731
790
|
const { model, refs, diagnostics } = compileFile(file);
|
|
732
791
|
printDiagnostics(diagnostics);
|
|
@@ -738,6 +797,11 @@ slice
|
|
|
738
797
|
console.error(`em slice review: slice "${sliceKey}" has errors — fix them first`);
|
|
739
798
|
process.exit(1);
|
|
740
799
|
}
|
|
800
|
+
const rosterRefused = checkRosterForWrite(loadRoster(dirname(file), opts.roster, rosterContextFor(model, file)), "reviewers", opts.by, model, refs.sliceKeys.indexOf(sliceKey), computeModelKey(model, file));
|
|
801
|
+
if (rosterRefused !== null) {
|
|
802
|
+
console.error(`em slice review: ${rosterRefused}`);
|
|
803
|
+
process.exit(1);
|
|
804
|
+
}
|
|
741
805
|
const today = localIsoDate();
|
|
742
806
|
const reviewedOn = opts.on ?? today;
|
|
743
807
|
if (opts.on !== undefined && !isValidDateString(opts.on)) {
|
|
@@ -770,17 +834,26 @@ slice
|
|
|
770
834
|
"is passed. Idempotent on " +
|
|
771
835
|
"the same --by/--on pair; refuses to overwrite a different one already recorded, and (MIL-284) a " +
|
|
772
836
|
"same-pair re-run on a doc whose body moved after sign-off; never " +
|
|
773
|
-
"touches `version:` or the doc body"
|
|
837
|
+
"touches `version:` or the doc body. Inside a git work tree also records `ratifiedRef:` (HEAD, " +
|
|
838
|
+
"refusing a doc with uncommitted changes) and always `ratifiedHash:`; with --no-ref (GH #255) " +
|
|
839
|
+
"only the hash — for a repo whose squash/rebase merges would leave the ref unreachable — and " +
|
|
840
|
+
"git is not consulted")
|
|
774
841
|
.argument("<file>", "input .em file")
|
|
775
842
|
.argument("<slice-key>", "slice export key (kebab-case)")
|
|
776
843
|
.requiredOption("--by <name>", "the ratifier's name")
|
|
844
|
+
.option("--no-ref", "record ratifiedHash: only, no ratifiedRef: (GH #255) — the signed content keeps its identity, nothing " +
|
|
845
|
+
"points at a commit a squash or rebase merge would orphan; the uncommitted-changes check does not apply")
|
|
777
846
|
.option("--on <date>", "ratification date, YYYY-MM-DD (default: today, local date)")
|
|
778
|
-
.option("--
|
|
847
|
+
.option("--roster <path>", "ratifier roster to check --by against (GH #259; default: the nearest ratifiers.yaml above the model, none = unrestricted)")
|
|
848
|
+
.option("--skip-review", "ratify without a recorded review — prints a loud notice on stderr and records `reviewSkipped: <status>` on the doc " +
|
|
849
|
+
"(GH #260) so the bypass is visible to later readers, `em export` and `--slice-ready --json`; cleared by `em slice revise`")
|
|
779
850
|
.option("--meaning-unchanged", "API-first sign-off (MIL-238): this version does not change what the public contract means — " +
|
|
780
851
|
"records `meaningConfirmed: true`; required (or --contract-change) on a slice touching a `public` element")
|
|
781
852
|
.option("--contract-change <why>", "API-first sign-off (MIL-238): a consumer must read this change differently, and why — records " +
|
|
782
853
|
"`contractChange: \"<why>\"`")
|
|
783
854
|
.action((file, sliceKey, opts) => {
|
|
855
|
+
// commander: `--no-ref` sets `opts.ref === false`; unset leaves it undefined (ref recorded).
|
|
856
|
+
const noRef = opts.ref === false;
|
|
784
857
|
const flags = parseMeaningFlags(opts.meaningUnchanged, opts.contractChange);
|
|
785
858
|
if (!flags.ok) {
|
|
786
859
|
console.error(`em slice ratify: ${flags.message}`);
|
|
@@ -796,13 +869,18 @@ slice
|
|
|
796
869
|
console.error(`em slice ratify: slice "${sliceKey}" has errors — fix them first`);
|
|
797
870
|
process.exit(1);
|
|
798
871
|
}
|
|
872
|
+
const rosterRefused = checkRosterForWrite(loadRoster(dirname(file), opts.roster, rosterContextFor(model, file)), "ratifiers", opts.by, model, refs.sliceKeys.indexOf(sliceKey), computeModelKey(model, file));
|
|
873
|
+
if (rosterRefused !== null) {
|
|
874
|
+
console.error(`em slice ratify: ${rosterRefused}`);
|
|
875
|
+
process.exit(1);
|
|
876
|
+
}
|
|
799
877
|
const today = localIsoDate();
|
|
800
878
|
const ratifiedOn = opts.on ?? today;
|
|
801
879
|
if (opts.on !== undefined && !isValidDateString(opts.on)) {
|
|
802
880
|
console.error(`em slice ratify: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
|
|
803
881
|
process.exit(1);
|
|
804
882
|
}
|
|
805
|
-
const result = runRatify(model, refs, dirname(file), sliceKey, opts.by, ratifiedOn, opts.skipReview === true, flags.confirmation);
|
|
883
|
+
const result = runRatify(model, refs, dirname(file), sliceKey, opts.by, ratifiedOn, opts.skipReview === true, flags.confirmation, undefined, noRef);
|
|
806
884
|
if (!result.ok) {
|
|
807
885
|
console.error(`em slice ratify: ${result.message}`);
|
|
808
886
|
process.exit(1);
|
|
@@ -820,7 +898,8 @@ slice
|
|
|
820
898
|
console.error(`warn: ratifying "${sliceKey}" ahead of upstream slice "${upstream.sliceKey}" (status: ${upstream.status})`);
|
|
821
899
|
}
|
|
822
900
|
console.log(result.changed
|
|
823
|
-
? `ratified: ${result.path} (ratifiedBy: ${opts.by}, ratifiedOn: ${ratifiedOn}${confirmationSuffix(flags.confirmation)})`
|
|
901
|
+
? `ratified: ${result.path} (ratifiedBy: ${opts.by}, ratifiedOn: ${ratifiedOn}${confirmationSuffix(flags.confirmation)})` +
|
|
902
|
+
(noRef ? " — ratifiedHash only, no ratifiedRef (--no-ref)" : "")
|
|
824
903
|
: `already ratified (no-op): ${result.path}`);
|
|
825
904
|
warnModelVersionDrift(dirname(file), model, refs, source, `ratifying "${sliceKey}"`);
|
|
826
905
|
});
|
|
@@ -845,7 +924,7 @@ function reviseAction(commandName) {
|
|
|
845
924
|
console.error(`em slice ${commandName}: slice "${sliceKey}" has errors — fix them first`);
|
|
846
925
|
process.exit(1);
|
|
847
926
|
}
|
|
848
|
-
const result = runRevise(model, refs, dirname(file), sliceKey, opts.withdraw === true);
|
|
927
|
+
const result = runRevise(model, refs, dirname(file), sliceKey, opts.withdraw === true, opts.keepVersion === true);
|
|
849
928
|
if (!result.ok) {
|
|
850
929
|
console.error(`em slice ${commandName}: ${result.message}`);
|
|
851
930
|
process.exit(1);
|
|
@@ -861,8 +940,14 @@ function reviseAction(commandName) {
|
|
|
861
940
|
const what = result.kind === "shipped"
|
|
862
941
|
? `v${result.newVersion - 1} stays shipped; v${result.newVersion} opened as draft`
|
|
863
942
|
: result.kind === "unshipped"
|
|
864
|
-
?
|
|
865
|
-
|
|
943
|
+
? result.shippedVersion !== null
|
|
944
|
+
? `ratification of v${result.newVersion - 1} withdrawn; v${result.shippedVersion} stays shipped; v${result.newVersion} opened as draft`
|
|
945
|
+
: `ratification of v${result.newVersion - 1} withdrawn (never shipped); v${result.newVersion} opened as draft`
|
|
946
|
+
: result.kind === "unshipped-kept"
|
|
947
|
+
? result.shippedVersion !== null
|
|
948
|
+
? `ratification of v${result.newVersion} withdrawn; v${result.shippedVersion} stays shipped; reopened as draft at the same version (--keep-version)`
|
|
949
|
+
: `ratification of v${result.newVersion} withdrawn (never shipped); reopened as draft at the same version (--keep-version)`
|
|
950
|
+
: `v${result.newVersion} was never signed; reopened as draft at the same version`;
|
|
866
951
|
console.log(`revised: ${result.path} (status: draft, version: ${result.newVersion} — ${what})`);
|
|
867
952
|
if (result.reopened > 0)
|
|
868
953
|
console.log(`re-opened ${result.reopened} deferred question(s)`);
|
|
@@ -879,15 +964,19 @@ const REVISE_DESCRIPTION = "open the next version of a slice doc as a `draft` (M
|
|
|
879
964
|
"`ready-to-implement` doc (ratification withdrawn, never shipped); a `ready-to-implement` doc with no " +
|
|
880
965
|
"`ratifiedBy:` reopens as draft at the same version; `draft`/`reviewed` refuse — the next version is " +
|
|
881
966
|
"already open. The new version then takes the normal road: `em slice review --by`, `em slice ratify --by`. " +
|
|
882
|
-
"Questions queued by `em slice defer` for the new version (GH #236) are injected into its Open Questions. " +
|
|
883
967
|
"A ratified, never-shipped doc REFUSES without --withdraw (revising withdraws the only implementable version; " +
|
|
884
|
-
"
|
|
968
|
+
"record a question with `em slice defer` instead — it sits outside the hash, GH #253). " +
|
|
969
|
+
"With --keep-version (GH #262) a ratified, never-shipped doc " +
|
|
970
|
+
"reopens as a draft at the SAME `version:` — same status flip and sign-off clearing, no bump — for a project that " +
|
|
971
|
+
"holds every slice at its initial version until a first release; refused on a shipped doc";
|
|
885
972
|
slice
|
|
886
973
|
.command("revise")
|
|
887
974
|
.description(REVISE_DESCRIPTION)
|
|
888
975
|
.argument("<file>", "input .em file")
|
|
889
976
|
.argument("<slice-key>", "slice export key (kebab-case)")
|
|
890
977
|
.option("--withdraw", "GH #236: required on a ratified, never-shipped doc — confirms withdrawing the ratification of the only implementable version")
|
|
978
|
+
.option("--keep-version", "GH #262: reopen a ratified, never-shipped doc as a draft at the same `version:` (no bump) — a correction before a first " +
|
|
979
|
+
"release, not a delta; refused on a shipped doc, whose version must stay distinct from the next one")
|
|
891
980
|
.action(reviseAction("revise"));
|
|
892
981
|
slice
|
|
893
982
|
.command("reratify")
|
|
@@ -912,9 +1001,11 @@ slice
|
|
|
912
1001
|
"promotes the single open `issue \"...\"` on the slice's own elements containing <text> into " +
|
|
913
1002
|
"the doc's Open Questions as that same deferred item, mirrors it, and removes the `issue` " +
|
|
914
1003
|
"clause from the .em — the one-command path for a question raised in a review session. " +
|
|
915
|
-
"On a RATIFIED (`ready-to-implement` + `ratifiedBy:`) or shipped (`implemented`) doc (GH #
|
|
916
|
-
"
|
|
917
|
-
"
|
|
1004
|
+
"On a RATIFIED (`ready-to-implement` + `ratifiedBy:`) or shipped (`implemented`) doc (GH #253) the " +
|
|
1005
|
+
"already-deferred `[x]` item is APPENDED to the doc's Open Questions — `ratifiedHash` excludes the " +
|
|
1006
|
+
"section heading and its `deferred to v<n>` items, so the signature, `--slice-ready` and ratify's " +
|
|
1007
|
+
"re-run stay valid, and the question is visible in the ratified doc; a pre-1.17 whole-body hash is " +
|
|
1008
|
+
"re-stamped in the same edit. Refuses a doc whose body already fails its hash")
|
|
918
1009
|
.argument("<file>", "input .em file")
|
|
919
1010
|
.argument("<slice-key>", "slice export key (kebab-case)")
|
|
920
1011
|
.argument("[question]", "text identifying the unchecked Open Question (case-sensitive substring); omit when using --from-issue")
|
|
@@ -947,14 +1038,8 @@ slice
|
|
|
947
1038
|
console.error(`em slice defer: ${result.message}`);
|
|
948
1039
|
process.exit(1);
|
|
949
1040
|
}
|
|
950
|
-
if (result.queued) {
|
|
951
|
-
console.log(result.changed
|
|
952
|
-
? `queued, not edited: ${result.path} stays as ratified (v${result.queued.docVersion}) — issue "${result.issue}" → ${opts.until} queued for ${sliceKey}@v${result.queued.forVersion} in .event-modeling.md; issue clause removed from ${file}:${result.modelLine}`
|
|
953
|
-
: `already queued (no-op): ${result.path}`);
|
|
954
|
-
return;
|
|
955
|
-
}
|
|
956
1041
|
console.log(result.changed
|
|
957
|
-
? `deferred: ${result.path} (issue "${result.issue}" → ${opts.until}; issue clause removed from ${file}:${result.modelLine})`
|
|
1042
|
+
? `deferred: ${result.path} (issue "${result.issue}" → ${opts.until}; issue clause removed from ${file}:${result.modelLine})${deferSignedNote(result)}`
|
|
958
1043
|
: `already deferred (no-op): ${result.path}`);
|
|
959
1044
|
return;
|
|
960
1045
|
}
|
|
@@ -963,14 +1048,8 @@ slice
|
|
|
963
1048
|
console.error(`em slice defer: ${result.message}`);
|
|
964
1049
|
process.exit(1);
|
|
965
1050
|
}
|
|
966
|
-
if (result.queued) {
|
|
967
|
-
console.log(result.changed
|
|
968
|
-
? `queued, not edited: ${result.path} stays as ratified (v${result.queued.docVersion}) — "${question}" → ${opts.until} queued for ${sliceKey}@v${result.queued.forVersion} in .event-modeling.md; \`em slice revise\` or \`mark-implemented\` injects it into the next draft`
|
|
969
|
-
: `already queued (no-op): ${result.path}`);
|
|
970
|
-
return;
|
|
971
|
-
}
|
|
972
1051
|
console.log(result.changed
|
|
973
|
-
? `deferred: ${result.path} (${question} → ${opts.until})`
|
|
1052
|
+
? `deferred: ${result.path} (${question} → ${opts.until})${deferSignedNote(result)}`
|
|
974
1053
|
: `already deferred (no-op): ${result.path}`);
|
|
975
1054
|
});
|
|
976
1055
|
slice
|
|
@@ -986,7 +1065,9 @@ slice
|
|
|
986
1065
|
"for this slice; --skip-findings-check overrides with a loud notice.")
|
|
987
1066
|
.argument("<file>", "input .em file")
|
|
988
1067
|
.argument("<slice-key>", "slice export key (kebab-case)")
|
|
989
|
-
.requiredOption("--at <rev>", "the target-repo revision this certification sweep diffed against"
|
|
1068
|
+
.requiredOption("--at <rev>", "the target-repo revision this certification sweep diffed against — or, when what was exercised is not a commit " +
|
|
1069
|
+
"that stays reachable (a per-PR environment under squash/rebase merges, a build or image), an opaque " +
|
|
1070
|
+
"`<kind>:<id>` such as build:1234 or deploy:eu-1-2026-10-15 (GH #261); recorded verbatim, skipped by git-reachability checks")
|
|
990
1071
|
.option("--on <date>", "certification date, YYYY-MM-DD (default: today, local date)")
|
|
991
1072
|
.option("--skip-findings-check", "certify even with unruled findings in scope — prints a loud notice on stderr")
|
|
992
1073
|
.action((file, sliceKey, opts) => {
|
|
@@ -1474,6 +1555,45 @@ conformFindings
|
|
|
1474
1555
|
if (!result.ok)
|
|
1475
1556
|
process.exit(1);
|
|
1476
1557
|
});
|
|
1558
|
+
const discover = program.command("discover").description("checks for the discover skill's records (GH #295)");
|
|
1559
|
+
discover
|
|
1560
|
+
.command("triage-check")
|
|
1561
|
+
.description("validate a discovery/prd-triage.json (prdTriageSchemaVersion 1.0): shape, the shipped rubric " +
|
|
1562
|
+
"version, every statement/storyline quote appearing verbatim in the input document, and every " +
|
|
1563
|
+
"statement carrying a category, an outcome and (modeled/rewritten) a group + storyline — exit 1 " +
|
|
1564
|
+
"on errors. The discover skill writes this file directly; this is how a headless run verifies it.")
|
|
1565
|
+
.argument("<file>", "path to a discovery/prd-triage.json file")
|
|
1566
|
+
.option("--source <path>", "the input PRD/spec to check quotes against (default: the file's own `source`, relative to it)")
|
|
1567
|
+
.option("--markdown", "after a passing check, print the Markdown view of the record instead of the summary")
|
|
1568
|
+
.option("--json", "print a JSON document instead of the text report")
|
|
1569
|
+
.action((file, opts) => {
|
|
1570
|
+
const result = checkPrdTriage(file, { source: opts.source });
|
|
1571
|
+
if (opts.json) {
|
|
1572
|
+
console.log(buildCheckPrdTriageJson(result));
|
|
1573
|
+
}
|
|
1574
|
+
else if (result.ok) {
|
|
1575
|
+
if (opts.markdown) {
|
|
1576
|
+
const shape = validatePrdTriage(JSON.parse(readFileSync(file, "utf8")));
|
|
1577
|
+
if (shape.ok)
|
|
1578
|
+
process.stdout.write(renderPrdTriageMarkdown(shape.doc, shape.referenceOnlyGroups));
|
|
1579
|
+
}
|
|
1580
|
+
else {
|
|
1581
|
+
console.log(`ok — ${result.path}: ${result.storylines} storyline(s), ${result.groups} group(s), ${result.statements} statement(s), quotes verbatim in ${relative(process.cwd(), result.source) || result.source}`);
|
|
1582
|
+
if (result.referenceOnlyGroups.length > 0) {
|
|
1583
|
+
console.log("reference-only groups — operations no walkthrough touches; check these are not CRUD fill:");
|
|
1584
|
+
for (const g of result.referenceOnlyGroups)
|
|
1585
|
+
console.log(` - ${g}`);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
else {
|
|
1590
|
+
console.error(`em discover triage-check: ${result.path} — errors:`);
|
|
1591
|
+
for (const e of result.errors)
|
|
1592
|
+
console.error(` - ${e}`);
|
|
1593
|
+
}
|
|
1594
|
+
if (!result.ok)
|
|
1595
|
+
process.exit(1);
|
|
1596
|
+
});
|
|
1477
1597
|
program
|
|
1478
1598
|
.command("watch")
|
|
1479
1599
|
.description("re-render on every save")
|
|
@@ -1563,11 +1683,68 @@ program
|
|
|
1563
1683
|
"note binding, zero unchecked Open Questions, a recorded ratifiedBy, and — for a slice " +
|
|
1564
1684
|
"touching a `public` element — a current contracts/<model key>.tsp; exits non-zero if not " +
|
|
1565
1685
|
"ready (MIL-87, MIL-259, MIL-238)")
|
|
1686
|
+
.option("--require-ratified-hash", "with --slice-ready (GH #256): strict mode — a ready-to-implement doc with no `ratifiedHash:` is NOT ready " +
|
|
1687
|
+
"(`slice-ready-missing-ratified-hash`, error; `ready: false` in --json). Off by default so docs ratified " +
|
|
1688
|
+
"before em 1.15 keep passing; turn it on once legacy docs are re-ratified")
|
|
1689
|
+
.option("--strict", "promote advisory lints (GH #293: scenario-quality warnings) to errors: they fail the run and, with --slice-ready, block readiness")
|
|
1690
|
+
.option("--roster <path>", "with --slice-ready (GH #259): the ratifier roster to check `ratifiedBy` against (default: the nearest " +
|
|
1691
|
+
"ratifiers.yaml above the model; none = unrestricted). A name the roster does not permit for the slice's " +
|
|
1692
|
+
"context is `ratified-by-not-in-roster` (error)")
|
|
1693
|
+
.option("--check-signed-bodies", "opt-in (GH #257): compare every slice doc that records a `ratifiedHash:` — ready, implemented, any status — " +
|
|
1694
|
+
"to its body; a mismatch is `slice-body-changed-since-ratification` (error). A pre-1.17 whole-body hash with an " +
|
|
1695
|
+
"intact body passes; a deferral written by `em slice defer` is outside the hash. The one check that still " +
|
|
1696
|
+
"watches a shipped doc's signed content")
|
|
1697
|
+
.option("--shape", "GH #282: run only the advisory shape-family lints (storyline shape) and print rule / count / first ref / docs " +
|
|
1698
|
+
"anchor; exit 0 always. With --json: `shapeSchemaVersion` 1.0 with per-rule finding arrays")
|
|
1699
|
+
.option("--waive-file <path>", "with --shape: a baseline file (`<rule-code> <slice-key>` per line, or a bare slice key for every shape rule; `#` comments) whose shape findings are listed " +
|
|
1700
|
+
"as waived and not counted; missing file = no waivers")
|
|
1566
1701
|
.option("--json", "print a JSON document instead of text — works on a model WITH errors, unlike `em export` " +
|
|
1567
1702
|
"(MIL-128, see docs/cli.md); exit codes are unchanged")
|
|
1568
1703
|
.action((file, opts) => {
|
|
1704
|
+
if (opts.roster !== undefined && !opts.sliceReady) {
|
|
1705
|
+
console.error("em validate: --roster applies only with --slice-ready <key>");
|
|
1706
|
+
process.exit(1);
|
|
1707
|
+
}
|
|
1708
|
+
if (opts.requireRatifiedHash && !opts.sliceReady) {
|
|
1709
|
+
console.error("em validate: --require-ratified-hash applies only with --slice-ready <key>");
|
|
1710
|
+
process.exit(1);
|
|
1711
|
+
}
|
|
1712
|
+
if (opts.waiveFile && !opts.shape) {
|
|
1713
|
+
console.error("em validate: --waive-file applies only with --shape");
|
|
1714
|
+
process.exit(1);
|
|
1715
|
+
}
|
|
1716
|
+
if (opts.shape && opts.strict) {
|
|
1717
|
+
console.error("em validate: --strict applies only without --shape (shape findings never gate)");
|
|
1718
|
+
process.exit(1);
|
|
1719
|
+
}
|
|
1720
|
+
if (opts.shape && (opts.sliceReady || opts.listIssues || opts.listDivergences || opts.listPublic || opts.failOnIssues || opts.checkSignedBodies)) {
|
|
1721
|
+
console.error("em validate: --shape cannot be combined with --slice-ready, --list-*, --fail-on-issues or --check-signed-bodies");
|
|
1722
|
+
process.exit(1);
|
|
1723
|
+
}
|
|
1569
1724
|
const { model, diagnostics, refs, source } = compileFile(file);
|
|
1570
|
-
|
|
1725
|
+
if (opts.shape) {
|
|
1726
|
+
const all = computeAllDiagnostics(file, model, refs, diagnostics, source);
|
|
1727
|
+
const report = buildShapeReport(all, opts.waiveFile ? readWaiveFile(opts.waiveFile) : []);
|
|
1728
|
+
const modelErrors = all.filter((d) => d.severity === "error").length;
|
|
1729
|
+
for (const k of report.staleWaivers)
|
|
1730
|
+
console.error(`note: waiver "${k}" (${opts.waiveFile}) excuses no shape finding — ignored`);
|
|
1731
|
+
if (opts.json)
|
|
1732
|
+
process.stdout.write(buildShapeJson(file, report, modelErrors) + "\n");
|
|
1733
|
+
else
|
|
1734
|
+
console.log(formatShapeText(report, modelErrors));
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
// GH #257: an opt-in addition to the unconditional set, so --json, the text report and the
|
|
1738
|
+
// exit code all see it the same way every other diagnostic is seen.
|
|
1739
|
+
const allDiagnostics = [
|
|
1740
|
+
...computeAllDiagnostics(file, model, refs, diagnostics, source),
|
|
1741
|
+
...(opts.checkSignedBodies ? validateSignedBodies(model, refs, dirname(file)) : []),
|
|
1742
|
+
];
|
|
1743
|
+
// GH #293: --strict promotes advisory lints to errors (the --slice-ready branch below does
|
|
1744
|
+
// its own promotion inside computeSliceReadiness, from the unpromoted set).
|
|
1745
|
+
const reported = opts.strict && !opts.sliceReady
|
|
1746
|
+
? allDiagnostics.map((d) => RULES[d.code]?.advisory ? { ...d, severity: "error" } : d)
|
|
1747
|
+
: allDiagnostics;
|
|
1571
1748
|
if (opts.sliceReady) {
|
|
1572
1749
|
// MIL-87: a targeted, single-slice readiness gate, not part of the unconditional
|
|
1573
1750
|
// diagnostic set above (see sliceReadyValidate.ts's header for why). Folds in MIL-85's
|
|
@@ -1592,13 +1769,17 @@ program
|
|
|
1592
1769
|
// `slice-ready-unknown-slice` error (carrying the resolver's message).
|
|
1593
1770
|
const resolved = resolveSliceKey(model, refs, opts.sliceReady);
|
|
1594
1771
|
const key = resolved.ok ? resolved.key : opts.sliceReady;
|
|
1595
|
-
const { scoped, ready, result } = computeSliceReadiness(model, refs, dirname(file), key, contractInput, allDiagnostics
|
|
1596
|
-
|
|
1772
|
+
const { scoped, advisories, ready, result } = computeSliceReadiness(model, refs, dirname(file), key, contractInput, allDiagnostics, {
|
|
1773
|
+
requireRatifiedHash: opts.requireRatifiedHash === true,
|
|
1774
|
+
strict: opts.strict === true,
|
|
1775
|
+
rosterPath: opts.roster,
|
|
1776
|
+
});
|
|
1777
|
+
printDiagnostics([...scoped, ...advisories]);
|
|
1597
1778
|
if (opts.json) {
|
|
1598
1779
|
// MIL-128: the named gates (see computeSliceReadyGates) plus the same `scoped`
|
|
1599
1780
|
// diagnostics and `ready` verdict driving the exit code below — replaces both the
|
|
1600
1781
|
// scraped warning prose and the two hand-parsed English sentences.
|
|
1601
|
-
process.stdout.write(buildSliceReadyJson(file, key, result?.gates ?? null, scoped, ready, result?.continuationOf ?? null) + "\n");
|
|
1782
|
+
process.stdout.write(buildSliceReadyJson(file, key, result?.gates ?? null, scoped, ready, result?.continuationOf ?? null, result?.reviewSkipped ?? null, advisories) + "\n");
|
|
1602
1783
|
}
|
|
1603
1784
|
else {
|
|
1604
1785
|
console.log(ready ? `slice "${key}" is ready-to-implement` : `slice "${key}" is NOT ready-to-implement`);
|
|
@@ -1617,7 +1798,7 @@ program
|
|
|
1617
1798
|
if (opts.listIssues || opts.listDivergences || opts.listPublic) {
|
|
1618
1799
|
// Errors still fail the run below — surface them rather than exiting
|
|
1619
1800
|
// non-zero with nothing but the list on screen.
|
|
1620
|
-
const errorsOnly =
|
|
1801
|
+
const errorsOnly = reported.filter((d) => d.severity === "error");
|
|
1621
1802
|
if (opts.json) {
|
|
1622
1803
|
const markers = collectMarkers(model, refs, {
|
|
1623
1804
|
issues: opts.listIssues,
|
|
@@ -1638,15 +1819,15 @@ program
|
|
|
1638
1819
|
}
|
|
1639
1820
|
}
|
|
1640
1821
|
else if (opts.json) {
|
|
1641
|
-
printDiagnostics(
|
|
1642
|
-
process.stdout.write(buildValidateJson(file,
|
|
1822
|
+
printDiagnostics(reported);
|
|
1823
|
+
process.stdout.write(buildValidateJson(file, reported) + "\n");
|
|
1643
1824
|
}
|
|
1644
1825
|
else {
|
|
1645
|
-
printDiagnostics(
|
|
1646
|
-
if (
|
|
1826
|
+
printDiagnostics(reported);
|
|
1827
|
+
if (reported.length === 0)
|
|
1647
1828
|
console.log("ok — no issues");
|
|
1648
1829
|
}
|
|
1649
|
-
if (hasErrors(
|
|
1830
|
+
if (hasErrors(reported))
|
|
1650
1831
|
process.exit(1);
|
|
1651
1832
|
if (opts.failOnIssues && model.elements.some((el) => el.issue))
|
|
1652
1833
|
process.exit(1);
|
|
@@ -1709,6 +1890,55 @@ program
|
|
|
1709
1890
|
function describeLedgerWaiveSource(source) {
|
|
1710
1891
|
return source.source === "flag" ? "--waive" : `trailer ${source.commit.slice(0, 7)}`;
|
|
1711
1892
|
}
|
|
1893
|
+
program
|
|
1894
|
+
.command("refs")
|
|
1895
|
+
.description("check that every commit a slice doc records — `ratifiedRef:`, `shippedRef:`, a bare `conformedAt:` — still " +
|
|
1896
|
+
"resolves and is reachable from a branch (GH #258): per doc, per field, `ancestor` / `not-ancestor` / " +
|
|
1897
|
+
"`unresolved` / `undecidable` (a shallow clone never passes). Squash and rebase merges orphan the PR-branch HEAD " +
|
|
1898
|
+
"ratify recorded; nothing else in em resolves a ref. Opt-in CI check, like `em ledger` — never part of `em validate`. " +
|
|
1899
|
+
"Local object store only, no network. Kinded `conformedAt` values (build:…) are skipped; docs with no ref fields are not reported")
|
|
1900
|
+
.argument("<file>", "model .em file (compiled for its slice keys; its directory locates slices/ and the repository)")
|
|
1901
|
+
.requiredOption("--reachable-from <rev>", "the branch or commit every ref must be an ancestor of (e.g. origin/main)")
|
|
1902
|
+
.option("--waive <slice-key>", "excuse this slice's non-ancestor/unresolved/undecidable results from the exit code (repeatable); still reported", (value, previous) => [...previous, value], [])
|
|
1903
|
+
.option("--waive-file <path>", "a baseline file of slice keys to waive, one per line (`#` comments allowed); missing file = no waivers")
|
|
1904
|
+
.option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
|
|
1905
|
+
.action((file, opts) => {
|
|
1906
|
+
const { model, refs } = compileFile(file);
|
|
1907
|
+
const outcome = checkRefs(model, refs, dirname(file), opts.reachableFrom);
|
|
1908
|
+
if (!outcome.ok) {
|
|
1909
|
+
console.error(`em refs: ${outcome.message}`);
|
|
1910
|
+
process.exit(1);
|
|
1911
|
+
}
|
|
1912
|
+
const fileKeys = opts.waiveFile ? readWaiveFile(opts.waiveFile) : [];
|
|
1913
|
+
const { result, unknownWaivers } = applyRefWaivers(outcome.result, opts.waive, fileKeys, opts.waiveFile ?? null);
|
|
1914
|
+
for (const note of unknownWaivers) {
|
|
1915
|
+
console.error(`note: waiver for slice "${note.sliceKey}" (${note.source.source === "flag" ? "--waive" : note.source.path}) has nothing to excuse — ignored`);
|
|
1916
|
+
}
|
|
1917
|
+
if (result.shallow)
|
|
1918
|
+
console.error("warn: shallow clone — every ref is undecidable; check out with fetch-depth: 0");
|
|
1919
|
+
if (opts.json) {
|
|
1920
|
+
process.stdout.write(buildRefsJson(result, file) + "\n");
|
|
1921
|
+
}
|
|
1922
|
+
else {
|
|
1923
|
+
for (const r of result.results) {
|
|
1924
|
+
console.log(` ${r.status.padEnd(12)} ${r.sliceKey} ${r.field}: ${r.ref}${r.detail ? ` — ${r.detail}` : ""}`);
|
|
1925
|
+
}
|
|
1926
|
+
for (const w of result.waived) {
|
|
1927
|
+
console.log(` waived ${w.sliceKey} ${w.field}: ${w.ref} (${w.status}; waived by ${w.waivedBy.source === "flag" ? "--waive" : w.waivedBy.path})`);
|
|
1928
|
+
}
|
|
1929
|
+
const bad = result.results.filter((r) => r.status !== "ancestor").length;
|
|
1930
|
+
const waivedNote = result.waived.length > 0 ? `, ${result.waived.length} waived` : "";
|
|
1931
|
+
if (bad === 0) {
|
|
1932
|
+
console.log(`ok — every recorded ref is reachable from ${opts.reachableFrom} (${result.checkedRefs} ref(s) on ${result.checkedDocs} doc(s)${waivedNote})`);
|
|
1933
|
+
}
|
|
1934
|
+
else {
|
|
1935
|
+
console.log(`${bad} ref(s) not reachable from ${opts.reachableFrom} (${result.checkedRefs} checked on ${result.checkedDocs} doc(s)${waivedNote})`);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
// Set the code rather than process.exit(): stdout to a pipe shouldn't risk truncation.
|
|
1939
|
+
if (!result.ok)
|
|
1940
|
+
process.exitCode = 1;
|
|
1941
|
+
});
|
|
1712
1942
|
program
|
|
1713
1943
|
.command("ledger")
|
|
1714
1944
|
.description("check slice docs' version: field agrees with their content across two git revisions " +
|
|
@@ -1958,7 +2188,7 @@ program
|
|
|
1958
2188
|
.filter((e) => e !== null);
|
|
1959
2189
|
// MIL-239 (R11): the nearest system.yaml above the first input file, consumer-adaptation counted.
|
|
1960
2190
|
const system = statusSystemBlock(files[0], realGit, (m) => console.error(`warn: em status: ${m}`));
|
|
1961
|
-
const report = buildStatusReport(files, sliceFacts, openIssuesCount, invariants, conformance, statusDiagnostics, modelVersion, emVersion, system, readOpenEngagements(files));
|
|
2191
|
+
const report = buildStatusReport(files, sliceFacts, openIssuesCount, invariants, conformance, statusDiagnostics, modelVersion, emVersion, system, readOpenEngagements(files), loadRoster(dirname(files[0])).state);
|
|
1962
2192
|
let output;
|
|
1963
2193
|
if (opts.json)
|
|
1964
2194
|
output = buildStatusJson(report);
|
|
@@ -2145,9 +2375,10 @@ query
|
|
|
2145
2375
|
.option("--context <c>", "match a slice with an event in this @Context")
|
|
2146
2376
|
.option("--persona <p>", "match a slice with a ui in this @Persona")
|
|
2147
2377
|
.option("--tag <t>", "match a slice with an event carrying this tag key")
|
|
2378
|
+
.option("--storyline <name>", "match a slice declared in this `storyline` block")
|
|
2148
2379
|
.option("--json", "print a JSON document instead of the text report")
|
|
2149
2380
|
.action((files, opts) => {
|
|
2150
|
-
const filters = { pattern: opts.pattern, status: opts.status, context: opts.context, persona: opts.persona, tag: opts.tag };
|
|
2381
|
+
const filters = { pattern: opts.pattern, status: opts.status, context: opts.context, persona: opts.persona, tag: opts.tag, storyline: opts.storyline };
|
|
2151
2382
|
runQueryVerb(files, "slices", filters, opts.json, (s) => querySlices(s, filters), formatSlices);
|
|
2152
2383
|
});
|
|
2153
2384
|
query
|
|
@@ -2266,6 +2497,34 @@ engagement
|
|
|
2266
2497
|
}
|
|
2267
2498
|
process.stdout.write((opts.json ? buildEngagementPlanJson(result.plan) : formatEngagementPlanText(result.plan)) + "\n");
|
|
2268
2499
|
});
|
|
2500
|
+
engagement
|
|
2501
|
+
.command("foundation")
|
|
2502
|
+
.description("GH #240: is the foundation PR (implement.md §8) actually in the code for the events this engagement's slices " +
|
|
2503
|
+
"emit and consume? A text scan of --code: an event is present when a file carries an identifier derived from " +
|
|
2504
|
+
"its name (OrderPlaced / order_placed / ORDER_PLACED / order-placed), its model fields and tag fields are then " +
|
|
2505
|
+
"looked for in those files. Per event: present | incomplete (missing-field / missing-tag, named) | missing-event " +
|
|
2506
|
+
"— so \"foundation absent\" and \"foundation incomplete\" are different reports. Exits 1 unless every event is " +
|
|
2507
|
+
"present. Proves the contract was written down, not that it type-checks")
|
|
2508
|
+
.argument("<file>", "input .em file")
|
|
2509
|
+
.argument("<slug>", "engagement slug")
|
|
2510
|
+
.requiredOption("--code <dir>", "the target repository (or the directory the constitution's \"where a slice's code lives\" rule points at)")
|
|
2511
|
+
.option("--json", "print a JSON document instead of text (same document as the MCP engagement_foundation tool)")
|
|
2512
|
+
.action((file, slug, opts) => {
|
|
2513
|
+
if (!existsSync(opts.code) || !statSync(opts.code).isDirectory()) {
|
|
2514
|
+
console.error(`em engagement foundation: --code ${opts.code} is not a directory`);
|
|
2515
|
+
process.exit(1);
|
|
2516
|
+
}
|
|
2517
|
+
const { model, refs } = compileFile(file);
|
|
2518
|
+
const members = engagementMembersFor({ file, model, refs, baseDir: dirname(file) }, slug);
|
|
2519
|
+
if (!members.ok) {
|
|
2520
|
+
console.error(`em engagement foundation: ${members.message}`);
|
|
2521
|
+
process.exit(1);
|
|
2522
|
+
}
|
|
2523
|
+
const check = checkFoundation(model, members.members, members.idx, opts.code);
|
|
2524
|
+
process.stdout.write((opts.json ? buildEngagementFoundationJson(file, slug, check) : formatFoundationText(slug, check)) + "\n");
|
|
2525
|
+
if (!check.ok)
|
|
2526
|
+
process.exitCode = 1;
|
|
2527
|
+
});
|
|
2269
2528
|
engagement
|
|
2270
2529
|
.command("set")
|
|
2271
2530
|
.description("the only write path to the Ledger: set one slice's state (and optionally branch/base/pr); " +
|
|
@@ -2343,6 +2602,69 @@ engagement
|
|
|
2343
2602
|
writeFileSync(loaded.path, result.text);
|
|
2344
2603
|
console.log(`closed engagement "${slug}" (${loaded.path})`);
|
|
2345
2604
|
});
|
|
2605
|
+
const rosterCommand = program
|
|
2606
|
+
.command("roster")
|
|
2607
|
+
.description("the optional ratifier roster (ratifiers.yaml, GH #259) -- who may sign slice docs");
|
|
2608
|
+
rosterCommand
|
|
2609
|
+
.command("init")
|
|
2610
|
+
.description("write ratifiers.yaml beside the model, seeded from the names already recorded in the slice docs' " +
|
|
2611
|
+
"`ratifiedBy:`/`reviewedBy:` (whole-model entries; narrow them with `contexts:` by hand). Refuses " +
|
|
2612
|
+
"to overwrite an existing file unless --force; `em upgrade`'s grandfathered placeholder is not a name")
|
|
2613
|
+
.argument("<file>", "input .em file")
|
|
2614
|
+
.option("--roster <path>", "write the roster here instead of <model dir>/ratifiers.yaml")
|
|
2615
|
+
.option("--force", "overwrite an existing roster file")
|
|
2616
|
+
.action((file, opts) => {
|
|
2617
|
+
const { model, refs, diagnostics } = compileFile(file);
|
|
2618
|
+
printDiagnostics(diagnostics);
|
|
2619
|
+
const target = opts.roster ?? join(dirname(file), ROSTER_FILE);
|
|
2620
|
+
if (existsSync(target) && !opts.force) {
|
|
2621
|
+
console.error(`em roster init: ${target} already exists -- pass --force to overwrite it`);
|
|
2622
|
+
process.exit(1);
|
|
2623
|
+
}
|
|
2624
|
+
// A roster already governing this model from higher up would be shadowed by a nearer one.
|
|
2625
|
+
const governing = findRosterAbove(dirname(file));
|
|
2626
|
+
if (governing !== null && resolve(governing) !== resolve(target) && !opts.force) {
|
|
2627
|
+
console.error(`em roster init: ${governing} already governs this model -- writing ${target} would shadow it; pass --force to write it anyway`);
|
|
2628
|
+
process.exit(1);
|
|
2629
|
+
}
|
|
2630
|
+
const observed = observeSigners(model, refs, dirname(file));
|
|
2631
|
+
if (observed.ratifiers.length === 0 && observed.reviewers.length === 0) {
|
|
2632
|
+
const gfNone = grandfatheredNotice(observed.grandfathered);
|
|
2633
|
+
if (gfNone)
|
|
2634
|
+
console.log(gfNone);
|
|
2635
|
+
console.error("em roster init: no ratifiedBy/reviewedBy names found in the slice docs -- write ratifiers.yaml by hand (docs/slice-doc-schema.md#who-may-ratify)");
|
|
2636
|
+
process.exit(1);
|
|
2637
|
+
}
|
|
2638
|
+
writeFileSync(target, renderRoster(observed), "utf8");
|
|
2639
|
+
console.log(`wrote ${target} (${observed.ratifiers.length} ratifier(s), ${observed.reviewers.length} reviewer(s))`);
|
|
2640
|
+
const gf = grandfatheredNotice(observed.grandfathered);
|
|
2641
|
+
if (gf)
|
|
2642
|
+
console.log(gf);
|
|
2643
|
+
});
|
|
2644
|
+
rosterCommand
|
|
2645
|
+
.command("check")
|
|
2646
|
+
.description("check EVERY slice doc's recorded signers against the ratifier roster (GH #259) - `ratifiedBy` on " +
|
|
2647
|
+
"ready-to-implement and implemented docs, `reviewedBy` on any doc - the whole-model CI gate that " +
|
|
2648
|
+
"`--slice-ready <key>` (one ready slice) is not. Exit 1 on any violation or an invalid roster; no " +
|
|
2649
|
+
"roster file = nothing to check, exit 0. The `grandfathered ...` placeholder is exempt (counted)")
|
|
2650
|
+
.argument("<file>", "input .em file")
|
|
2651
|
+
.option("--roster <path>", "roster to check against (default: the nearest ratifiers.yaml above the model)")
|
|
2652
|
+
.option("--json", "print a JSON document (per-doc results, diagnostics with refs); exit codes unchanged")
|
|
2653
|
+
.action((file, opts) => {
|
|
2654
|
+
const { model, refs } = compileFile(file);
|
|
2655
|
+
const loaded = loadRoster(dirname(file), opts.roster, rosterContextFor(model, file));
|
|
2656
|
+
const report = checkRoster(model, refs, dirname(file), file, loaded);
|
|
2657
|
+
printDiagnostics(report.diagnostics);
|
|
2658
|
+
if (opts.json) {
|
|
2659
|
+
process.stdout.write(buildRosterCheckJson(file, report) + "\n");
|
|
2660
|
+
}
|
|
2661
|
+
else {
|
|
2662
|
+
for (const line of formatRosterCheck(report))
|
|
2663
|
+
console.log(line);
|
|
2664
|
+
}
|
|
2665
|
+
if (!report.ok)
|
|
2666
|
+
process.exitCode = 1;
|
|
2667
|
+
});
|
|
2346
2668
|
const systemCommand = program
|
|
2347
2669
|
.command("system")
|
|
2348
2670
|
// `--json` exists on both `em system` and `em system codeowners`: parent options are only
|
|
@@ -2769,7 +3091,7 @@ ci.command("init")
|
|
|
2769
3091
|
files = [
|
|
2770
3092
|
[
|
|
2771
3093
|
ciPath,
|
|
2772
|
-
planCiFile(ciPath, buildCiWorkflowFile(model, opts.tests, PKG_VERSION), ciManagedBody(model, opts.tests, PKG_VERSION), CI_WORKFLOW_MARKER, !!opts.force),
|
|
3094
|
+
planCiFile(ciPath, buildCiWorkflowFile(model, opts.tests, PKG_VERSION, findRosterAbove(dirname(resolve(model))) !== null), ciManagedBody(model, opts.tests, PKG_VERSION, findRosterAbove(dirname(resolve(model))) !== null), CI_WORKFLOW_MARKER, !!opts.force),
|
|
2773
3095
|
],
|
|
2774
3096
|
[
|
|
2775
3097
|
conformPath,
|
|
@@ -2840,6 +3162,30 @@ ci.command("init")
|
|
|
2840
3162
|
console.log(`${path} already up to date`);
|
|
2841
3163
|
}
|
|
2842
3164
|
});
|
|
3165
|
+
const storylineCmd = program.command("storyline").description("storyline blocks (GH #277): advisory helpers for adopting them");
|
|
3166
|
+
storylineCmd
|
|
3167
|
+
.command("suggest")
|
|
3168
|
+
.description("advisory (GH #290): propose `storyline` blocks for a model that has none — one per distinct slice " +
|
|
3169
|
+
"`source \"url\"`, the remaining slices grouped by the view-instance chains (`again`) they feed — " +
|
|
3170
|
+
"printed as a unified diff on stdout (apply with `patch -p1` or `git apply`); never writes the model")
|
|
3171
|
+
.argument("<file>", "input .em model file")
|
|
3172
|
+
.action((file) => {
|
|
3173
|
+
const { model, diagnostics, source: text } = compileFile(file);
|
|
3174
|
+
printDiagnostics(diagnostics);
|
|
3175
|
+
const s = buildStorylineSuggestion(model, text, file);
|
|
3176
|
+
if (s.patch === "") {
|
|
3177
|
+
console.error(`em storyline suggest: nothing to propose for ${file}`);
|
|
3178
|
+
return;
|
|
3179
|
+
}
|
|
3180
|
+
for (const st of s.storylines) {
|
|
3181
|
+
console.error(`storyline "${st.name}" — ${st.sliceIndexes.length} slice(s), from ${st.basis === "source" ? `source ${st.source}` : "a view-instance chain"}`);
|
|
3182
|
+
}
|
|
3183
|
+
if (s.ungrouped.length > 0) {
|
|
3184
|
+
console.error(`left outside every storyline (${s.ungrouped.length}): ${s.ungrouped.map((i) => model.slices[i].name).join(", ")}`);
|
|
3185
|
+
}
|
|
3186
|
+
console.error("review the patch below and apply it by hand — nothing was written");
|
|
3187
|
+
process.stdout.write(s.patch);
|
|
3188
|
+
});
|
|
2843
3189
|
program
|
|
2844
3190
|
.command("upgrade")
|
|
2845
3191
|
.description("bring a model repo authored under an older em (1.6 forward) up to the installed version: " +
|
|
@@ -2851,7 +3197,13 @@ program
|
|
|
2851
3197
|
.option("--apply", "apply every applicable mechanical step, one git commit each, then a final `Em version:` commit")
|
|
2852
3198
|
.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")
|
|
2853
3199
|
.option("--json", "print a JSON document instead of text (dry-run/--check only, never with --apply)")
|
|
3200
|
+
.option("--fold-continuations", "also run the opt-in fold-continuations step (stack consecutive continuations of one view into the first; continuationOf, doc join and index rows unchanged)")
|
|
3201
|
+
.option("--to-sugar", "(implies --fold-continuations; without --apply it is a dry run listing exactly what would fold): fold continuations into their host command slice as `view X again … shown on` lines — CHANGES continuationOf and the doc join to `<host>--again` and removes the folded slices' index rows")
|
|
2854
3202
|
.action((file, opts) => {
|
|
3203
|
+
if ((opts.foldContinuations || opts.toSugar) && opts.check) {
|
|
3204
|
+
console.error("em upgrade: --fold-continuations / --to-sugar do not combine with --check");
|
|
3205
|
+
process.exit(1);
|
|
3206
|
+
}
|
|
2855
3207
|
if (opts.apply && opts.check) {
|
|
2856
3208
|
console.error("em upgrade: --apply and --check are mutually exclusive");
|
|
2857
3209
|
process.exit(1);
|
|
@@ -2886,12 +3238,14 @@ program
|
|
|
2886
3238
|
packagedAgentsRoot: packagedAgentsRoot(),
|
|
2887
3239
|
model,
|
|
2888
3240
|
refs,
|
|
3241
|
+
...(opts.foldContinuations ? { foldContinuations: true } : {}),
|
|
3242
|
+
...(opts.toSugar ? { toSugar: true } : {}),
|
|
2889
3243
|
};
|
|
2890
3244
|
if (opts.check) {
|
|
2891
3245
|
const { ok, report, reasons } = checkUpgrade(ctx);
|
|
2892
3246
|
console.error(`em upgrade --check: from ${report.from.version}${report.from.inferred ? ` (inferred: ${report.from.basis})` : ""} → to ${report.to}`);
|
|
2893
3247
|
for (const s of report.steps) {
|
|
2894
|
-
console.error(` [${s.applicable ? "x" : " "}] ${s.id} (since ${s.sinceVersion}): ${s.reason}`);
|
|
3248
|
+
console.error(` [${s.optIn ? "~" : s.applicable ? "x" : " "}] ${s.id} (since ${s.sinceVersion}): ${s.reason}`);
|
|
2895
3249
|
}
|
|
2896
3250
|
for (const h of report.human)
|
|
2897
3251
|
console.error(` human: ${h.id}: ${h.reason}`);
|
|
@@ -2904,10 +3258,13 @@ program
|
|
|
2904
3258
|
if (opts.apply) {
|
|
2905
3259
|
const result = applyUpgrade(ctx);
|
|
2906
3260
|
for (const s of result.applied) {
|
|
2907
|
-
if (s.applied)
|
|
3261
|
+
if (s.applied) {
|
|
2908
3262
|
console.log(`applied: ${s.id} — ${s.changedFiles.join(", ")} (${s.commit})`);
|
|
3263
|
+
for (const n of s.notes ?? [])
|
|
3264
|
+
console.log(` note: ${n}`);
|
|
3265
|
+
}
|
|
2909
3266
|
else
|
|
2910
|
-
console.log(`skipped: ${s.id}`);
|
|
3267
|
+
console.log(`skipped: ${s.id}${result.report.steps.find((x) => x.id === s.id)?.optIn ? " (opt-in: not requested by a flag)" : ""}`);
|
|
2911
3268
|
}
|
|
2912
3269
|
if (!result.ok) {
|
|
2913
3270
|
console.error(result.message);
|
|
@@ -2919,6 +3276,7 @@ program
|
|
|
2919
3276
|
console.log("Em version: already up to date — no commit needed");
|
|
2920
3277
|
for (const h of result.report.human)
|
|
2921
3278
|
console.log(`human: ${h.id}: ${h.reason}`);
|
|
3279
|
+
printUpgradeShapeSection(file);
|
|
2922
3280
|
return;
|
|
2923
3281
|
}
|
|
2924
3282
|
const report = detectUpgrade(ctx);
|
|
@@ -2931,16 +3289,31 @@ program
|
|
|
2931
3289
|
console.log(`state file: ${report.stateFileError}`);
|
|
2932
3290
|
console.log("Steps:");
|
|
2933
3291
|
for (const s of report.steps) {
|
|
2934
|
-
console.log(` [${s.applicable ? "x" : " "}] ${s.id} (since ${s.sinceVersion}): ${s.reason}`);
|
|
3292
|
+
console.log(` [${s.optIn ? "~" : s.applicable ? "x" : " "}] ${s.id} (since ${s.sinceVersion}): ${s.reason}`);
|
|
2935
3293
|
}
|
|
2936
3294
|
console.log("Human:");
|
|
2937
3295
|
if (report.human.length === 0)
|
|
2938
3296
|
console.log(" (none)");
|
|
2939
3297
|
for (const h of report.human)
|
|
2940
3298
|
console.log(` ${h.id}: ${h.reason}`);
|
|
2941
|
-
const applicableCount = report.steps.filter((s) => s.applicable).length;
|
|
2942
|
-
|
|
3299
|
+
const applicableCount = report.steps.filter((s) => s.applicable && !s.optIn).length;
|
|
3300
|
+
const optInCount = report.steps.filter((s) => s.optIn).length;
|
|
3301
|
+
console.log(`${applicableCount} step(s) applicable${optInCount > 0 ? ` (+${optInCount} opt-in, marked [~], not applied by --apply)` : ""}, ${report.human.length} human item(s) — re-run with --apply to apply mechanical steps`);
|
|
3302
|
+
printUpgradeShapeSection(file);
|
|
2943
3303
|
});
|
|
3304
|
+
/** GH #290: the closing section of `em upgrade`'s text report — `em validate --shape` over the model as it
|
|
3305
|
+
* stands, so an estate sees how many advisory shape findings it carries. Never writes a waive file:
|
|
3306
|
+
* baselining shapes you accept is a decision (`--waive-file`, and a `shape-accepted:` Decisions-log bullet). */
|
|
3307
|
+
function printUpgradeShapeSection(file) {
|
|
3308
|
+
const { model, diagnostics, refs, source } = compileFile(file);
|
|
3309
|
+
const all = computeAllDiagnostics(file, model, refs, diagnostics, source);
|
|
3310
|
+
const report = buildShapeReport(all, []);
|
|
3311
|
+
console.log("");
|
|
3312
|
+
console.log("Shape (em validate --shape, advisory):");
|
|
3313
|
+
console.log(formatShapeText(report, all.filter((d) => d.severity === "error").length));
|
|
3314
|
+
console.log("Record each shape you accept as `shape-accepted: <rule> <refs> — <reason>` in `.event-modeling.md` `## Decisions log`; " +
|
|
3315
|
+
"to baseline an estate, run `em validate " + file + " --shape --waive-file <path>` (em upgrade writes no waive file).");
|
|
3316
|
+
}
|
|
2944
3317
|
// Exported so dev tooling (e.g. scripts/generate-skill-docs.ts) can introspect the registered
|
|
2945
3318
|
// commands/options without triggering a real CLI run. Guarded the same way Node's CJS
|
|
2946
3319
|
// `require.main === module` used to: only actually parse argv when this file is the process's
|