@milehimikey/em 1.17.0 → 1.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/em-implementer.md +7 -4
- package/.claude/agents/em-validator.md +2 -2
- package/.claude/skills/event-modeling/SKILL.md +1 -1
- package/.claude/skills/event-modeling-conform/SKILL.md +1 -1
- 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 +21 -9
- package/.claude/skills/event-modeling-implement/SKILL.md +1 -1
- package/.claude/skills/event-modeling-implement/reference/implement.md +18 -0
- package/.claude/skills/event-modeling-review/SKILL.md +4 -2
- package/.claude/skills/event-modeling-shared/reference/em-dsl.md +94 -6
- package/.claude/skills/event-modeling-shared/reference/methodology.md +268 -0
- package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +96 -2
- package/.claude/skills/event-modeling-shared/templates/slice.md +11 -3
- package/.claude-plugin/marketplace.json +2 -2
- 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/sliceDoc.d.ts +4 -0
- package/dist/catalog/sliceDoc.js +16 -7
- package/dist/catalog/sliceDoc.js.map +1 -1
- package/dist/catalog/sliceReadyValidate.d.ts +11 -2
- package/dist/catalog/sliceReadyValidate.js +62 -3
- 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 +28 -0
- package/dist/cli/defer.js +87 -4
- package/dist/cli/defer.js.map +1 -1
- package/dist/cli/engagement.d.ts +13 -2
- package/dist/cli/engagement.js +60 -5
- package/dist/cli/engagement.js.map +1 -1
- package/dist/cli/engagementFile.d.ts +2 -0
- package/dist/cli/engagementFile.js +3 -1
- package/dist/cli/engagementFile.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 +393 -0
- package/dist/cli/prdTriage.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/review.d.ts +17 -2
- package/dist/cli/review.js +39 -10
- package/dist/cli/review.js.map +1 -1
- package/dist/cli/revise.d.ts +2 -0
- package/dist/cli/revise.js +7 -2
- 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/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 +21 -1
- package/dist/cli/status.js +25 -3
- 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 +516 -7
- 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 +311 -35
- 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 -3
- package/dist/emit/engagementJson.js +11 -4
- package/dist/emit/engagementJson.js.map +1 -1
- package/dist/emit/json.d.ts +21 -1
- package/dist/emit/json.js +26 -2
- package/dist/emit/json.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/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 +9 -5
- package/dist/emit/validateJson.js +40 -5
- 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 +46 -11
- 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 +149 -0
- package/dist/model/rules.js +144 -0
- 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 +2 -0
- package/dist/parser/lexer.js +4 -0
- 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/agents/em-implementer.md +7 -4
- package/plugin/agents/em-validator.md +2 -2
- package/plugin/shared/reference/em-dsl.md +94 -6
- package/plugin/shared/reference/methodology.md +268 -0
- package/plugin/shared/reference/slice-doc-schema.md +96 -2
- package/plugin/shared/templates/slice.md +11 -3
- package/plugin/skills/conform/SKILL.md +1 -1
- 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 +21 -9
- 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 -0
- package/plugin/skills/review/SKILL.md +4 -2
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,7 +21,7 @@ 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";
|
|
@@ -34,7 +35,12 @@ import { validateOrphanedSliceDocs } from "./catalog/orphanedSliceDocValidate.js
|
|
|
34
35
|
import { validateModelVersionStale } from "./catalog/modelVersionValidate.js";
|
|
35
36
|
import { validateInvariants } from "./catalog/invariantsValidate.js";
|
|
36
37
|
import { computeSliceReadiness } from "./catalog/sliceReadyValidate.js";
|
|
37
|
-
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, buildUnitIndex, effectiveTracking, engagementMembersFor, engagementPlanFor, engagementStatusFor, formatEngagementPlanText, formatEngagementStatusText, } from "./cli/engagement.js";
|
|
38
44
|
import { LEDGER_STATES, engagementPath, loadEngagement, readOpenEngagements } from "./cli/engagementFile.js";
|
|
39
45
|
import { buildEngagementFoundationJson, buildEngagementPlanJson, buildEngagementStatusJson } from "./emit/engagementJson.js";
|
|
40
46
|
import { checkFoundation, formatFoundationText } from "./cli/engagementFoundation.js";
|
|
@@ -43,6 +49,7 @@ import { checkLedger, readLedgerWaiverTrailers, applyLedgerWaivers } from "./cli
|
|
|
43
49
|
import { planMigration, verifyMigration } from "./cli/migrateReactionShape.js";
|
|
44
50
|
import { buildLedgerJson } from "./emit/ledgerJson.js";
|
|
45
51
|
import { buildRefsJson } from "./emit/refsJson.js";
|
|
52
|
+
import { buildShapeReport, formatShapeText } from "./cli/shape.js";
|
|
46
53
|
import { applyRefWaivers, checkRefs, readWaiveFile } from "./cli/refsCheck.js";
|
|
47
54
|
import { buildCoverageReport, resolveScopedSlices, resolveCoverageSliceKey } from "./cli/coverage.js";
|
|
48
55
|
import { buildCoverageJson } from "./emit/coverageJson.js";
|
|
@@ -68,7 +75,7 @@ import { planGlossaryArgs } from "./cli/glossary-inputs.js";
|
|
|
68
75
|
import { buildCatalog } from "./catalog/build.js";
|
|
69
76
|
import { planCatalogArgs } from "./cli/catalog-inputs.js";
|
|
70
77
|
import { runSliceIndex } from "./cli/sliceIndex.js";
|
|
71
|
-
import { runMarkImplemented } from "./cli/markImplemented.js";
|
|
78
|
+
import { formatNextDraftNotice, runMarkImplemented } from "./cli/markImplemented.js";
|
|
72
79
|
import { runRatify, parseMeaningFlags, meaningConfirmationLine } from "./cli/ratify.js";
|
|
73
80
|
import { runReview } from "./cli/review.js";
|
|
74
81
|
import { runConformSupersede } from "./cli/conformSupersede.js";
|
|
@@ -83,6 +90,7 @@ import { buildChangelogDoc } from "./cli/changelogBuild.js";
|
|
|
83
90
|
import { runRevise } from "./cli/revise.js";
|
|
84
91
|
import { runDefer, runDeferFromIssue } from "./cli/defer.js";
|
|
85
92
|
import { runSliceConform } from "./cli/sliceConform.js";
|
|
93
|
+
import { checkPrdTriage, buildCheckPrdTriageJson, renderPrdTriageMarkdown, validatePrdTriage } from "./cli/prdTriage.js";
|
|
86
94
|
import { checkFindingsFile, buildCheckFindingsJson, lookupFindingsBesideReport, unruledFindingsInScope, FINDING_LOCI } from "./cli/findings.js";
|
|
87
95
|
import { STATE_FILE_NAME, PHASES, isPhase, loadStateFile, parseState, setPhase, setConformance, setModelVersion, setCertified, setEmVersion, setReview, isValidDateString, modelPathMismatch, } from "./cli/stateFile.js";
|
|
88
96
|
import { findCertifiedVersion, latestModelVersionNumber, modelVersionDrift, runCertifyModelVersion, runModelVersionBump, } from "./cli/modelVersion.js";
|
|
@@ -90,6 +98,7 @@ import { STARTER_EM, starterEmFor, scaffoldReadme, scaffoldStateFile, scaffoldCo
|
|
|
90
98
|
import { kebabSlug } from "./util/slug.js";
|
|
91
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";
|
|
92
100
|
import { resolveRepoRoot, detectUpgrade, applyUpgrade, checkUpgrade } from "./cli/upgrade.js";
|
|
101
|
+
import { buildStorylineSuggestion } from "./cli/storylineSuggest.js";
|
|
93
102
|
import { buildUpgradeJson } from "./emit/upgradeJson.js";
|
|
94
103
|
import { isOlder } from "./util/semver.js";
|
|
95
104
|
import { RULES } from "./model/rules.js";
|
|
@@ -189,6 +198,9 @@ program
|
|
|
189
198
|
.option("-T, --format <fmt>", "output format (svg, png, pdf, ...)")
|
|
190
199
|
.option("--slice <name>", "render only this slice, redrawn in its own canonical pattern shape " +
|
|
191
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)")
|
|
192
204
|
.option("--emit-dot", "print the generated DOT instead of rendering")
|
|
193
205
|
.option("--keep-empty-lanes", "keep the API lane even when empty")
|
|
194
206
|
.action(async (file, opts) => {
|
|
@@ -196,6 +208,10 @@ program
|
|
|
196
208
|
console.error("em render: --slice cannot be combined with --emit-dot");
|
|
197
209
|
process.exit(1);
|
|
198
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
|
+
}
|
|
199
215
|
const { dot, model, grid, diagnostics, refs } = compileFile(file, {
|
|
200
216
|
keepEmptyLanes: opts.keepEmptyLanes,
|
|
201
217
|
});
|
|
@@ -220,6 +236,22 @@ program
|
|
|
220
236
|
console.error("not rendering: fix the errors above");
|
|
221
237
|
process.exit(1);
|
|
222
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
|
+
}
|
|
223
255
|
if (opts.slice !== undefined) {
|
|
224
256
|
const lookup = resolveSliceArg(opts.slice, model.slices);
|
|
225
257
|
if ("error" in lookup) {
|
|
@@ -539,6 +571,7 @@ slice
|
|
|
539
571
|
// MIL-266: with a model in hand (--wire), the doc's generated regions are filled from it;
|
|
540
572
|
// without one they carry the template's placeholders until `em slice sync` fills them.
|
|
541
573
|
let regions;
|
|
574
|
+
let alternatives = [];
|
|
542
575
|
if (opts.wire) {
|
|
543
576
|
const { model, refs, diagnostics, source } = compileFile(opts.wire);
|
|
544
577
|
printDiagnostics(diagnostics);
|
|
@@ -553,11 +586,13 @@ slice
|
|
|
553
586
|
}
|
|
554
587
|
wired = result;
|
|
555
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);
|
|
556
591
|
}
|
|
557
592
|
mkdirSync(dir, { recursive: true });
|
|
558
593
|
const content = opts.stub
|
|
559
594
|
? buildStubDocContent(name, opts.pattern, opts.swimlane)
|
|
560
|
-
: buildSliceDocContent(name, key, opts.pattern, opts.swimlane, regions);
|
|
595
|
+
: buildSliceDocContent(name, key, opts.pattern, opts.swimlane, regions, alternatives);
|
|
561
596
|
writeFileSync(path, content);
|
|
562
597
|
console.log(`wrote ${path}`);
|
|
563
598
|
if (wired) {
|
|
@@ -698,6 +733,7 @@ slice
|
|
|
698
733
|
.argument("<slice-key>", "slice export key (kebab-case)")
|
|
699
734
|
.argument("<pr-url>", "merged PR (or commit) URL")
|
|
700
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)")
|
|
701
737
|
.action((file, sliceKey, prUrl, opts) => {
|
|
702
738
|
if (opts.on !== undefined && !isValidDateString(opts.on)) {
|
|
703
739
|
console.error(`em slice mark-implemented: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
|
|
@@ -720,13 +756,21 @@ slice
|
|
|
720
756
|
console.error(`em slice mark-implemented: ${result.message}`);
|
|
721
757
|
process.exit(1);
|
|
722
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
|
+
}
|
|
723
768
|
console.log(result.changed
|
|
724
769
|
? `marked implemented: ${result.path} (implementedIn: ${prUrl})`
|
|
725
770
|
: `already implemented (no-op): ${result.path}`);
|
|
726
|
-
if (result.
|
|
727
|
-
console.log(
|
|
728
|
-
|
|
729
|
-
`; v${result.openedDraft.version - 1} stays shipped`);
|
|
771
|
+
if (result.openedDraft) {
|
|
772
|
+
console.log("");
|
|
773
|
+
console.log(formatNextDraftNotice(result.path, result.openedDraft, prUrl));
|
|
730
774
|
}
|
|
731
775
|
});
|
|
732
776
|
slice
|
|
@@ -735,12 +779,16 @@ slice
|
|
|
735
779
|
"— the FIRST of the two human gates (MIL-201, docs/process.md#the-slice-lifecycle-gates): " +
|
|
736
780
|
"the room walked this slice and every open question it raised is resolved. Ratification is " +
|
|
737
781
|
"a separate, later gate (`em slice ratify`). Legal from `status: draft`; idempotent on the " +
|
|
738
|
-
"same --by/--on pair; refuses to overwrite a different one already recorded
|
|
739
|
-
"
|
|
782
|
+
"same --by/--on pair; refuses to overwrite a different one already recorded (--again re-records " +
|
|
783
|
+
"it); refuses a doc with an unchecked `## Open Questions` item unless --allow-open (GH #333); " +
|
|
784
|
+
"refuses a doc already `ready-to-implement`/`implemented`; never touches `version:` or the doc body")
|
|
740
785
|
.argument("<file>", "input .em file")
|
|
741
786
|
.argument("<slice-key>", "slice export key (kebab-case)")
|
|
742
787
|
.requiredOption("--by <name>", "the reviewer's (or facilitator's) name")
|
|
743
788
|
.option("--on <date>", "review date, YYYY-MM-DD (default: today, local date)")
|
|
789
|
+
.option("--roster <path>", "ratifier roster to check --by against (GH #259; default: the nearest ratifiers.yaml above the model, none = unrestricted)")
|
|
790
|
+
.option("--again", "re-record reviewedBy/reviewedOn on a doc already `status: reviewed` (prints old -> new); still refused on ready-to-implement/implemented")
|
|
791
|
+
.option("--allow-open", "record the review although `## Open Questions` has unchecked items (a deliberately partial walk); prints a notice with the open count")
|
|
744
792
|
.action((file, sliceKey, opts) => {
|
|
745
793
|
const { model, refs, diagnostics } = compileFile(file);
|
|
746
794
|
printDiagnostics(diagnostics);
|
|
@@ -752,19 +800,32 @@ slice
|
|
|
752
800
|
console.error(`em slice review: slice "${sliceKey}" has errors — fix them first`);
|
|
753
801
|
process.exit(1);
|
|
754
802
|
}
|
|
803
|
+
const rosterRefused = checkRosterForWrite(loadRoster(dirname(file), opts.roster, rosterContextFor(model, file)), "reviewers", opts.by, model, refs.sliceKeys.indexOf(sliceKey), computeModelKey(model, file));
|
|
804
|
+
if (rosterRefused !== null) {
|
|
805
|
+
console.error(`em slice review: ${rosterRefused}`);
|
|
806
|
+
process.exit(1);
|
|
807
|
+
}
|
|
755
808
|
const today = localIsoDate();
|
|
756
809
|
const reviewedOn = opts.on ?? today;
|
|
757
810
|
if (opts.on !== undefined && !isValidDateString(opts.on)) {
|
|
758
811
|
console.error(`em slice review: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
|
|
759
812
|
process.exit(1);
|
|
760
813
|
}
|
|
761
|
-
const result = runReview(model, refs, dirname(file), sliceKey, opts.by, reviewedOn
|
|
814
|
+
const result = runReview(model, refs, dirname(file), sliceKey, opts.by, reviewedOn, {
|
|
815
|
+
again: opts.again === true,
|
|
816
|
+
allowOpen: opts.allowOpen === true,
|
|
817
|
+
});
|
|
762
818
|
if (!result.ok) {
|
|
763
819
|
console.error(`em slice review: ${result.message}`);
|
|
764
820
|
process.exit(1);
|
|
765
821
|
}
|
|
822
|
+
if (result.allowedOpen !== undefined) {
|
|
823
|
+
console.error(`notice: --allow-open — reviewing "${sliceKey}" with ${result.allowedOpen} unchecked Open Questions item(s)`);
|
|
824
|
+
}
|
|
766
825
|
console.log(result.changed
|
|
767
|
-
?
|
|
826
|
+
? result.previous
|
|
827
|
+
? `re-reviewed: ${result.path} (reviewedBy: ${result.previous.by} -> ${opts.by}, reviewedOn: ${result.previous.on} -> ${reviewedOn})`
|
|
828
|
+
: `reviewed: ${result.path} (reviewedBy: ${opts.by}, reviewedOn: ${reviewedOn})`
|
|
768
829
|
: `already reviewed (no-op): ${result.path}`);
|
|
769
830
|
});
|
|
770
831
|
/** MIL-238: `, meaningConfirmed: true` / `, contractChange: "<why>"` for a success line, or "". */
|
|
@@ -794,6 +855,7 @@ slice
|
|
|
794
855
|
.option("--no-ref", "record ratifiedHash: only, no ratifiedRef: (GH #255) — the signed content keeps its identity, nothing " +
|
|
795
856
|
"points at a commit a squash or rebase merge would orphan; the uncommitted-changes check does not apply")
|
|
796
857
|
.option("--on <date>", "ratification date, YYYY-MM-DD (default: today, local date)")
|
|
858
|
+
.option("--roster <path>", "ratifier roster to check --by against (GH #259; default: the nearest ratifiers.yaml above the model, none = unrestricted)")
|
|
797
859
|
.option("--skip-review", "ratify without a recorded review — prints a loud notice on stderr and records `reviewSkipped: <status>` on the doc " +
|
|
798
860
|
"(GH #260) so the bypass is visible to later readers, `em export` and `--slice-ready --json`; cleared by `em slice revise`")
|
|
799
861
|
.option("--meaning-unchanged", "API-first sign-off (MIL-238): this version does not change what the public contract means — " +
|
|
@@ -818,6 +880,11 @@ slice
|
|
|
818
880
|
console.error(`em slice ratify: slice "${sliceKey}" has errors — fix them first`);
|
|
819
881
|
process.exit(1);
|
|
820
882
|
}
|
|
883
|
+
const rosterRefused = checkRosterForWrite(loadRoster(dirname(file), opts.roster, rosterContextFor(model, file)), "ratifiers", opts.by, model, refs.sliceKeys.indexOf(sliceKey), computeModelKey(model, file));
|
|
884
|
+
if (rosterRefused !== null) {
|
|
885
|
+
console.error(`em slice ratify: ${rosterRefused}`);
|
|
886
|
+
process.exit(1);
|
|
887
|
+
}
|
|
821
888
|
const today = localIsoDate();
|
|
822
889
|
const ratifiedOn = opts.on ?? today;
|
|
823
890
|
if (opts.on !== undefined && !isValidDateString(opts.on)) {
|
|
@@ -884,9 +951,13 @@ function reviseAction(commandName) {
|
|
|
884
951
|
const what = result.kind === "shipped"
|
|
885
952
|
? `v${result.newVersion - 1} stays shipped; v${result.newVersion} opened as draft`
|
|
886
953
|
: result.kind === "unshipped"
|
|
887
|
-
?
|
|
954
|
+
? result.shippedVersion !== null
|
|
955
|
+
? `ratification of v${result.newVersion - 1} withdrawn; v${result.shippedVersion} stays shipped; v${result.newVersion} opened as draft`
|
|
956
|
+
: `ratification of v${result.newVersion - 1} withdrawn (never shipped); v${result.newVersion} opened as draft`
|
|
888
957
|
: result.kind === "unshipped-kept"
|
|
889
|
-
?
|
|
958
|
+
? result.shippedVersion !== null
|
|
959
|
+
? `ratification of v${result.newVersion} withdrawn; v${result.shippedVersion} stays shipped; reopened as draft at the same version (--keep-version)`
|
|
960
|
+
: `ratification of v${result.newVersion} withdrawn (never shipped); reopened as draft at the same version (--keep-version)`
|
|
890
961
|
: `v${result.newVersion} was never signed; reopened as draft at the same version`;
|
|
891
962
|
console.log(`revised: ${result.path} (status: draft, version: ${result.newVersion} — ${what})`);
|
|
892
963
|
if (result.reopened > 0)
|
|
@@ -1495,6 +1566,45 @@ conformFindings
|
|
|
1495
1566
|
if (!result.ok)
|
|
1496
1567
|
process.exit(1);
|
|
1497
1568
|
});
|
|
1569
|
+
const discover = program.command("discover").description("checks for the discover skill's records (GH #295)");
|
|
1570
|
+
discover
|
|
1571
|
+
.command("triage-check")
|
|
1572
|
+
.description("validate a discovery/prd-triage.json (prdTriageSchemaVersion 1.0): shape, the shipped rubric " +
|
|
1573
|
+
"version, every statement/storyline quote appearing verbatim in the input document, and every " +
|
|
1574
|
+
"statement carrying a category, an outcome and (modeled/rewritten) a group + storyline — exit 1 " +
|
|
1575
|
+
"on errors. The discover skill writes this file directly; this is how a headless run verifies it.")
|
|
1576
|
+
.argument("<file>", "path to a discovery/prd-triage.json file")
|
|
1577
|
+
.option("--source <path>", "the input PRD/spec to check quotes against (default: the file's own `source`, relative to it)")
|
|
1578
|
+
.option("--markdown", "after a passing check, print the Markdown view of the record instead of the summary")
|
|
1579
|
+
.option("--json", "print a JSON document instead of the text report")
|
|
1580
|
+
.action((file, opts) => {
|
|
1581
|
+
const result = checkPrdTriage(file, { source: opts.source });
|
|
1582
|
+
if (opts.json) {
|
|
1583
|
+
console.log(buildCheckPrdTriageJson(result));
|
|
1584
|
+
}
|
|
1585
|
+
else if (result.ok) {
|
|
1586
|
+
if (opts.markdown) {
|
|
1587
|
+
const shape = validatePrdTriage(JSON.parse(readFileSync(file, "utf8")));
|
|
1588
|
+
if (shape.ok)
|
|
1589
|
+
process.stdout.write(renderPrdTriageMarkdown(shape.doc, shape.referenceOnlyGroups));
|
|
1590
|
+
}
|
|
1591
|
+
else {
|
|
1592
|
+
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}`);
|
|
1593
|
+
if (result.referenceOnlyGroups.length > 0) {
|
|
1594
|
+
console.log("reference-only groups — operations no walkthrough touches; check these are not CRUD fill:");
|
|
1595
|
+
for (const g of result.referenceOnlyGroups)
|
|
1596
|
+
console.log(` - ${g}`);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
else {
|
|
1601
|
+
console.error(`em discover triage-check: ${result.path} — errors:`);
|
|
1602
|
+
for (const e of result.errors)
|
|
1603
|
+
console.error(` - ${e}`);
|
|
1604
|
+
}
|
|
1605
|
+
if (!result.ok)
|
|
1606
|
+
process.exit(1);
|
|
1607
|
+
});
|
|
1498
1608
|
program
|
|
1499
1609
|
.command("watch")
|
|
1500
1610
|
.description("re-render on every save")
|
|
@@ -1587,24 +1697,65 @@ program
|
|
|
1587
1697
|
.option("--require-ratified-hash", "with --slice-ready (GH #256): strict mode — a ready-to-implement doc with no `ratifiedHash:` is NOT ready " +
|
|
1588
1698
|
"(`slice-ready-missing-ratified-hash`, error; `ready: false` in --json). Off by default so docs ratified " +
|
|
1589
1699
|
"before em 1.15 keep passing; turn it on once legacy docs are re-ratified")
|
|
1700
|
+
.option("--strict", "promote advisory lints (GH #293: scenario-quality warnings) to errors: they fail the run and, with --slice-ready, block readiness")
|
|
1701
|
+
.option("--roster <path>", "with --slice-ready (GH #259): the ratifier roster to check `ratifiedBy` against (default: the nearest " +
|
|
1702
|
+
"ratifiers.yaml above the model; none = unrestricted). A name the roster does not permit for the slice's " +
|
|
1703
|
+
"context is `ratified-by-not-in-roster` (error)")
|
|
1590
1704
|
.option("--check-signed-bodies", "opt-in (GH #257): compare every slice doc that records a `ratifiedHash:` — ready, implemented, any status — " +
|
|
1591
1705
|
"to its body; a mismatch is `slice-body-changed-since-ratification` (error). A pre-1.17 whole-body hash with an " +
|
|
1592
1706
|
"intact body passes; a deferral written by `em slice defer` is outside the hash. The one check that still " +
|
|
1593
1707
|
"watches a shipped doc's signed content")
|
|
1708
|
+
.option("--shape", "GH #282: run only the advisory shape-family lints (storyline shape) and print rule / count / first ref / docs " +
|
|
1709
|
+
"anchor; exit 0 always. With --json: `shapeSchemaVersion` 1.0 with per-rule finding arrays")
|
|
1710
|
+
.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 " +
|
|
1711
|
+
"as waived and not counted; missing file = no waivers")
|
|
1594
1712
|
.option("--json", "print a JSON document instead of text — works on a model WITH errors, unlike `em export` " +
|
|
1595
1713
|
"(MIL-128, see docs/cli.md); exit codes are unchanged")
|
|
1596
1714
|
.action((file, opts) => {
|
|
1715
|
+
if (opts.roster !== undefined && !opts.sliceReady) {
|
|
1716
|
+
console.error("em validate: --roster applies only with --slice-ready <key>");
|
|
1717
|
+
process.exit(1);
|
|
1718
|
+
}
|
|
1597
1719
|
if (opts.requireRatifiedHash && !opts.sliceReady) {
|
|
1598
1720
|
console.error("em validate: --require-ratified-hash applies only with --slice-ready <key>");
|
|
1599
1721
|
process.exit(1);
|
|
1600
1722
|
}
|
|
1723
|
+
if (opts.waiveFile && !opts.shape) {
|
|
1724
|
+
console.error("em validate: --waive-file applies only with --shape");
|
|
1725
|
+
process.exit(1);
|
|
1726
|
+
}
|
|
1727
|
+
if (opts.shape && opts.strict) {
|
|
1728
|
+
console.error("em validate: --strict applies only without --shape (shape findings never gate)");
|
|
1729
|
+
process.exit(1);
|
|
1730
|
+
}
|
|
1731
|
+
if (opts.shape && (opts.sliceReady || opts.listIssues || opts.listDivergences || opts.listPublic || opts.failOnIssues || opts.checkSignedBodies)) {
|
|
1732
|
+
console.error("em validate: --shape cannot be combined with --slice-ready, --list-*, --fail-on-issues or --check-signed-bodies");
|
|
1733
|
+
process.exit(1);
|
|
1734
|
+
}
|
|
1601
1735
|
const { model, diagnostics, refs, source } = compileFile(file);
|
|
1736
|
+
if (opts.shape) {
|
|
1737
|
+
const all = computeAllDiagnostics(file, model, refs, diagnostics, source);
|
|
1738
|
+
const report = buildShapeReport(all, opts.waiveFile ? readWaiveFile(opts.waiveFile) : []);
|
|
1739
|
+
const modelErrors = all.filter((d) => d.severity === "error").length;
|
|
1740
|
+
for (const k of report.staleWaivers)
|
|
1741
|
+
console.error(`note: waiver "${k}" (${opts.waiveFile}) excuses no shape finding — ignored`);
|
|
1742
|
+
if (opts.json)
|
|
1743
|
+
process.stdout.write(buildShapeJson(file, report, modelErrors) + "\n");
|
|
1744
|
+
else
|
|
1745
|
+
console.log(formatShapeText(report, modelErrors));
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1602
1748
|
// GH #257: an opt-in addition to the unconditional set, so --json, the text report and the
|
|
1603
1749
|
// exit code all see it the same way every other diagnostic is seen.
|
|
1604
1750
|
const allDiagnostics = [
|
|
1605
1751
|
...computeAllDiagnostics(file, model, refs, diagnostics, source),
|
|
1606
1752
|
...(opts.checkSignedBodies ? validateSignedBodies(model, refs, dirname(file)) : []),
|
|
1607
1753
|
];
|
|
1754
|
+
// GH #293: --strict promotes advisory lints to errors (the --slice-ready branch below does
|
|
1755
|
+
// its own promotion inside computeSliceReadiness, from the unpromoted set).
|
|
1756
|
+
const reported = opts.strict && !opts.sliceReady
|
|
1757
|
+
? allDiagnostics.map((d) => RULES[d.code]?.advisory ? { ...d, severity: "error" } : d)
|
|
1758
|
+
: allDiagnostics;
|
|
1608
1759
|
if (opts.sliceReady) {
|
|
1609
1760
|
// MIL-87: a targeted, single-slice readiness gate, not part of the unconditional
|
|
1610
1761
|
// diagnostic set above (see sliceReadyValidate.ts's header for why). Folds in MIL-85's
|
|
@@ -1629,15 +1780,17 @@ program
|
|
|
1629
1780
|
// `slice-ready-unknown-slice` error (carrying the resolver's message).
|
|
1630
1781
|
const resolved = resolveSliceKey(model, refs, opts.sliceReady);
|
|
1631
1782
|
const key = resolved.ok ? resolved.key : opts.sliceReady;
|
|
1632
|
-
const { scoped, ready, result } = computeSliceReadiness(model, refs, dirname(file), key, contractInput, allDiagnostics, {
|
|
1783
|
+
const { scoped, advisories, ready, result } = computeSliceReadiness(model, refs, dirname(file), key, contractInput, allDiagnostics, {
|
|
1633
1784
|
requireRatifiedHash: opts.requireRatifiedHash === true,
|
|
1785
|
+
strict: opts.strict === true,
|
|
1786
|
+
rosterPath: opts.roster,
|
|
1634
1787
|
});
|
|
1635
|
-
printDiagnostics(scoped);
|
|
1788
|
+
printDiagnostics([...scoped, ...advisories]);
|
|
1636
1789
|
if (opts.json) {
|
|
1637
1790
|
// MIL-128: the named gates (see computeSliceReadyGates) plus the same `scoped`
|
|
1638
1791
|
// diagnostics and `ready` verdict driving the exit code below — replaces both the
|
|
1639
1792
|
// scraped warning prose and the two hand-parsed English sentences.
|
|
1640
|
-
process.stdout.write(buildSliceReadyJson(file, key, result?.gates ?? null, scoped, ready, result?.continuationOf ?? null, result?.reviewSkipped ?? null) + "\n");
|
|
1793
|
+
process.stdout.write(buildSliceReadyJson(file, key, result?.gates ?? null, scoped, ready, result?.continuationOf ?? null, result?.reviewSkipped ?? null, advisories) + "\n");
|
|
1641
1794
|
}
|
|
1642
1795
|
else {
|
|
1643
1796
|
console.log(ready ? `slice "${key}" is ready-to-implement` : `slice "${key}" is NOT ready-to-implement`);
|
|
@@ -1656,7 +1809,7 @@ program
|
|
|
1656
1809
|
if (opts.listIssues || opts.listDivergences || opts.listPublic) {
|
|
1657
1810
|
// Errors still fail the run below — surface them rather than exiting
|
|
1658
1811
|
// non-zero with nothing but the list on screen.
|
|
1659
|
-
const errorsOnly =
|
|
1812
|
+
const errorsOnly = reported.filter((d) => d.severity === "error");
|
|
1660
1813
|
if (opts.json) {
|
|
1661
1814
|
const markers = collectMarkers(model, refs, {
|
|
1662
1815
|
issues: opts.listIssues,
|
|
@@ -1677,15 +1830,15 @@ program
|
|
|
1677
1830
|
}
|
|
1678
1831
|
}
|
|
1679
1832
|
else if (opts.json) {
|
|
1680
|
-
printDiagnostics(
|
|
1681
|
-
process.stdout.write(buildValidateJson(file,
|
|
1833
|
+
printDiagnostics(reported);
|
|
1834
|
+
process.stdout.write(buildValidateJson(file, reported) + "\n");
|
|
1682
1835
|
}
|
|
1683
1836
|
else {
|
|
1684
|
-
printDiagnostics(
|
|
1685
|
-
if (
|
|
1837
|
+
printDiagnostics(reported);
|
|
1838
|
+
if (reported.length === 0)
|
|
1686
1839
|
console.log("ok — no issues");
|
|
1687
1840
|
}
|
|
1688
|
-
if (hasErrors(
|
|
1841
|
+
if (hasErrors(reported))
|
|
1689
1842
|
process.exit(1);
|
|
1690
1843
|
if (opts.failOnIssues && model.elements.some((el) => el.issue))
|
|
1691
1844
|
process.exit(1);
|
|
@@ -2046,7 +2199,7 @@ program
|
|
|
2046
2199
|
.filter((e) => e !== null);
|
|
2047
2200
|
// MIL-239 (R11): the nearest system.yaml above the first input file, consumer-adaptation counted.
|
|
2048
2201
|
const system = statusSystemBlock(files[0], realGit, (m) => console.error(`warn: em status: ${m}`));
|
|
2049
|
-
const report = buildStatusReport(files, sliceFacts, openIssuesCount, invariants, conformance, statusDiagnostics, modelVersion, emVersion, system, readOpenEngagements(files));
|
|
2202
|
+
const report = buildStatusReport(files, sliceFacts, openIssuesCount, invariants, conformance, statusDiagnostics, modelVersion, emVersion, system, readOpenEngagements(files), loadRoster(dirname(files[0])).state);
|
|
2050
2203
|
let output;
|
|
2051
2204
|
if (opts.json)
|
|
2052
2205
|
output = buildStatusJson(report);
|
|
@@ -2233,9 +2386,10 @@ query
|
|
|
2233
2386
|
.option("--context <c>", "match a slice with an event in this @Context")
|
|
2234
2387
|
.option("--persona <p>", "match a slice with a ui in this @Persona")
|
|
2235
2388
|
.option("--tag <t>", "match a slice with an event carrying this tag key")
|
|
2389
|
+
.option("--storyline <name>", "match a slice declared in this `storyline` block")
|
|
2236
2390
|
.option("--json", "print a JSON document instead of the text report")
|
|
2237
2391
|
.action((files, opts) => {
|
|
2238
|
-
const filters = { pattern: opts.pattern, status: opts.status, context: opts.context, persona: opts.persona, tag: opts.tag };
|
|
2392
|
+
const filters = { pattern: opts.pattern, status: opts.status, context: opts.context, persona: opts.persona, tag: opts.tag, storyline: opts.storyline };
|
|
2239
2393
|
runQueryVerb(files, "slices", filters, opts.json, (s) => querySlices(s, filters), formatSlices);
|
|
2240
2394
|
});
|
|
2241
2395
|
query
|
|
@@ -2294,6 +2448,7 @@ engagement
|
|
|
2294
2448
|
.option("--parallel <n>", "how many slices of one level may be in flight at once", "3")
|
|
2295
2449
|
.option("--by <name>", "who created the engagement (createdBy)")
|
|
2296
2450
|
.option("--tracking <key>", "an issue-tracker key or URL the slice PRs cite (a slice doc's own tracking: wins)")
|
|
2451
|
+
.option("--pr-title <template>", "PR title template for the slice PRs; {key} = tracking key, {slice} = slice name, {sliceKey} = slice key (default \"{key}: {slice}\", or \"{slice}\" without a key)")
|
|
2297
2452
|
.option("--force", "overwrite an existing engagement file with this slug")
|
|
2298
2453
|
.action((file, slug, opts) => {
|
|
2299
2454
|
const fail = (m) => {
|
|
@@ -2311,13 +2466,20 @@ engagement
|
|
|
2311
2466
|
const parallel = /^\d+$/.test(opts.parallel) ? Number(opts.parallel) : NaN;
|
|
2312
2467
|
const { model, refs } = compileFile(file);
|
|
2313
2468
|
const path = engagementPath(file, slug);
|
|
2314
|
-
const result = buildNewEngagement({ file, model, refs, baseDir: dirname(file) }, { slug, selector, parallel, createdBy: opts.by ?? null, tracking: opts.tracking ?? null, created: localIsoDate() });
|
|
2469
|
+
const result = buildNewEngagement({ file, model, refs, baseDir: dirname(file) }, { slug, selector, parallel, createdBy: opts.by ?? null, tracking: opts.tracking ?? null, prTitle: opts.prTitle ?? null, created: localIsoDate() });
|
|
2315
2470
|
if (!result.ok)
|
|
2316
2471
|
return fail(result.message);
|
|
2317
2472
|
if (existsSync(path) && !opts.force)
|
|
2318
2473
|
fail(`engagement "${slug}" already exists (${path}) — pass --force to overwrite`);
|
|
2319
2474
|
if (result.warning)
|
|
2320
2475
|
console.error(result.warning);
|
|
2476
|
+
if (opts.prTitle !== undefined && opts.prTitle.includes("{key}")) {
|
|
2477
|
+
const uidx = buildUnitIndex(model, refs, dirname(file));
|
|
2478
|
+
const keyless = result.units.filter((u) => effectiveTracking(uidx.docTracking.get(u) ?? null, opts.tracking?.trim() || null) === null);
|
|
2479
|
+
if (keyless.length > 0) {
|
|
2480
|
+
console.error(`em engagement new: --pr-title uses {key}; slices without a tracking key fall back to the default title "{slice}" (the slice name): ${keyless.join(", ")} — pass --tracking or set tracking: in the slice docs`);
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2321
2483
|
mkdirSync(dirname(path), { recursive: true });
|
|
2322
2484
|
writeFileSync(path, result.text);
|
|
2323
2485
|
console.log(`wrote ${path} (${result.units.length} slice${result.units.length === 1 ? "" : "s"}: ${result.units.join(", ")})`);
|
|
@@ -2459,6 +2621,69 @@ engagement
|
|
|
2459
2621
|
writeFileSync(loaded.path, result.text);
|
|
2460
2622
|
console.log(`closed engagement "${slug}" (${loaded.path})`);
|
|
2461
2623
|
});
|
|
2624
|
+
const rosterCommand = program
|
|
2625
|
+
.command("roster")
|
|
2626
|
+
.description("the optional ratifier roster (ratifiers.yaml, GH #259) -- who may sign slice docs");
|
|
2627
|
+
rosterCommand
|
|
2628
|
+
.command("init")
|
|
2629
|
+
.description("write ratifiers.yaml beside the model, seeded from the names already recorded in the slice docs' " +
|
|
2630
|
+
"`ratifiedBy:`/`reviewedBy:` (whole-model entries; narrow them with `contexts:` by hand). Refuses " +
|
|
2631
|
+
"to overwrite an existing file unless --force; `em upgrade`'s grandfathered placeholder is not a name")
|
|
2632
|
+
.argument("<file>", "input .em file")
|
|
2633
|
+
.option("--roster <path>", "write the roster here instead of <model dir>/ratifiers.yaml")
|
|
2634
|
+
.option("--force", "overwrite an existing roster file")
|
|
2635
|
+
.action((file, opts) => {
|
|
2636
|
+
const { model, refs, diagnostics } = compileFile(file);
|
|
2637
|
+
printDiagnostics(diagnostics);
|
|
2638
|
+
const target = opts.roster ?? join(dirname(file), ROSTER_FILE);
|
|
2639
|
+
if (existsSync(target) && !opts.force) {
|
|
2640
|
+
console.error(`em roster init: ${target} already exists -- pass --force to overwrite it`);
|
|
2641
|
+
process.exit(1);
|
|
2642
|
+
}
|
|
2643
|
+
// A roster already governing this model from higher up would be shadowed by a nearer one.
|
|
2644
|
+
const governing = findRosterAbove(dirname(file));
|
|
2645
|
+
if (governing !== null && resolve(governing) !== resolve(target) && !opts.force) {
|
|
2646
|
+
console.error(`em roster init: ${governing} already governs this model -- writing ${target} would shadow it; pass --force to write it anyway`);
|
|
2647
|
+
process.exit(1);
|
|
2648
|
+
}
|
|
2649
|
+
const observed = observeSigners(model, refs, dirname(file));
|
|
2650
|
+
if (observed.ratifiers.length === 0 && observed.reviewers.length === 0) {
|
|
2651
|
+
const gfNone = grandfatheredNotice(observed.grandfathered);
|
|
2652
|
+
if (gfNone)
|
|
2653
|
+
console.log(gfNone);
|
|
2654
|
+
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)");
|
|
2655
|
+
process.exit(1);
|
|
2656
|
+
}
|
|
2657
|
+
writeFileSync(target, renderRoster(observed), "utf8");
|
|
2658
|
+
console.log(`wrote ${target} (${observed.ratifiers.length} ratifier(s), ${observed.reviewers.length} reviewer(s))`);
|
|
2659
|
+
const gf = grandfatheredNotice(observed.grandfathered);
|
|
2660
|
+
if (gf)
|
|
2661
|
+
console.log(gf);
|
|
2662
|
+
});
|
|
2663
|
+
rosterCommand
|
|
2664
|
+
.command("check")
|
|
2665
|
+
.description("check EVERY slice doc's recorded signers against the ratifier roster (GH #259) - `ratifiedBy` on " +
|
|
2666
|
+
"ready-to-implement and implemented docs, `reviewedBy` on any doc - the whole-model CI gate that " +
|
|
2667
|
+
"`--slice-ready <key>` (one ready slice) is not. Exit 1 on any violation or an invalid roster; no " +
|
|
2668
|
+
"roster file = nothing to check, exit 0. The `grandfathered ...` placeholder is exempt (counted)")
|
|
2669
|
+
.argument("<file>", "input .em file")
|
|
2670
|
+
.option("--roster <path>", "roster to check against (default: the nearest ratifiers.yaml above the model)")
|
|
2671
|
+
.option("--json", "print a JSON document (per-doc results, diagnostics with refs); exit codes unchanged")
|
|
2672
|
+
.action((file, opts) => {
|
|
2673
|
+
const { model, refs } = compileFile(file);
|
|
2674
|
+
const loaded = loadRoster(dirname(file), opts.roster, rosterContextFor(model, file));
|
|
2675
|
+
const report = checkRoster(model, refs, dirname(file), file, loaded);
|
|
2676
|
+
printDiagnostics(report.diagnostics);
|
|
2677
|
+
if (opts.json) {
|
|
2678
|
+
process.stdout.write(buildRosterCheckJson(file, report) + "\n");
|
|
2679
|
+
}
|
|
2680
|
+
else {
|
|
2681
|
+
for (const line of formatRosterCheck(report))
|
|
2682
|
+
console.log(line);
|
|
2683
|
+
}
|
|
2684
|
+
if (!report.ok)
|
|
2685
|
+
process.exitCode = 1;
|
|
2686
|
+
});
|
|
2462
2687
|
const systemCommand = program
|
|
2463
2688
|
.command("system")
|
|
2464
2689
|
// `--json` exists on both `em system` and `em system codeowners`: parent options are only
|
|
@@ -2885,7 +3110,7 @@ ci.command("init")
|
|
|
2885
3110
|
files = [
|
|
2886
3111
|
[
|
|
2887
3112
|
ciPath,
|
|
2888
|
-
planCiFile(ciPath, buildCiWorkflowFile(model, opts.tests, PKG_VERSION), ciManagedBody(model, opts.tests, PKG_VERSION), CI_WORKFLOW_MARKER, !!opts.force),
|
|
3113
|
+
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),
|
|
2889
3114
|
],
|
|
2890
3115
|
[
|
|
2891
3116
|
conformPath,
|
|
@@ -2956,6 +3181,30 @@ ci.command("init")
|
|
|
2956
3181
|
console.log(`${path} already up to date`);
|
|
2957
3182
|
}
|
|
2958
3183
|
});
|
|
3184
|
+
const storylineCmd = program.command("storyline").description("storyline blocks (GH #277): advisory helpers for adopting them");
|
|
3185
|
+
storylineCmd
|
|
3186
|
+
.command("suggest")
|
|
3187
|
+
.description("advisory (GH #290): propose `storyline` blocks for a model that has none — one per distinct slice " +
|
|
3188
|
+
"`source \"url\"`, the remaining slices grouped by the view-instance chains (`again`) they feed — " +
|
|
3189
|
+
"printed as a unified diff on stdout (apply with `patch -p1` or `git apply`); never writes the model")
|
|
3190
|
+
.argument("<file>", "input .em model file")
|
|
3191
|
+
.action((file) => {
|
|
3192
|
+
const { model, diagnostics, source: text } = compileFile(file);
|
|
3193
|
+
printDiagnostics(diagnostics);
|
|
3194
|
+
const s = buildStorylineSuggestion(model, text, file);
|
|
3195
|
+
if (s.patch === "") {
|
|
3196
|
+
console.error(`em storyline suggest: nothing to propose for ${file}`);
|
|
3197
|
+
return;
|
|
3198
|
+
}
|
|
3199
|
+
for (const st of s.storylines) {
|
|
3200
|
+
console.error(`storyline "${st.name}" — ${st.sliceIndexes.length} slice(s), from ${st.basis === "source" ? `source ${st.source}` : "a view-instance chain"}`);
|
|
3201
|
+
}
|
|
3202
|
+
if (s.ungrouped.length > 0) {
|
|
3203
|
+
console.error(`left outside every storyline (${s.ungrouped.length}): ${s.ungrouped.map((i) => model.slices[i].name).join(", ")}`);
|
|
3204
|
+
}
|
|
3205
|
+
console.error("review the patch below and apply it by hand — nothing was written");
|
|
3206
|
+
process.stdout.write(s.patch);
|
|
3207
|
+
});
|
|
2959
3208
|
program
|
|
2960
3209
|
.command("upgrade")
|
|
2961
3210
|
.description("bring a model repo authored under an older em (1.6 forward) up to the installed version: " +
|
|
@@ -2967,7 +3216,13 @@ program
|
|
|
2967
3216
|
.option("--apply", "apply every applicable mechanical step, one git commit each, then a final `Em version:` commit")
|
|
2968
3217
|
.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")
|
|
2969
3218
|
.option("--json", "print a JSON document instead of text (dry-run/--check only, never with --apply)")
|
|
3219
|
+
.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)")
|
|
3220
|
+
.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")
|
|
2970
3221
|
.action((file, opts) => {
|
|
3222
|
+
if ((opts.foldContinuations || opts.toSugar) && opts.check) {
|
|
3223
|
+
console.error("em upgrade: --fold-continuations / --to-sugar do not combine with --check");
|
|
3224
|
+
process.exit(1);
|
|
3225
|
+
}
|
|
2971
3226
|
if (opts.apply && opts.check) {
|
|
2972
3227
|
console.error("em upgrade: --apply and --check are mutually exclusive");
|
|
2973
3228
|
process.exit(1);
|
|
@@ -3002,12 +3257,14 @@ program
|
|
|
3002
3257
|
packagedAgentsRoot: packagedAgentsRoot(),
|
|
3003
3258
|
model,
|
|
3004
3259
|
refs,
|
|
3260
|
+
...(opts.foldContinuations ? { foldContinuations: true } : {}),
|
|
3261
|
+
...(opts.toSugar ? { toSugar: true } : {}),
|
|
3005
3262
|
};
|
|
3006
3263
|
if (opts.check) {
|
|
3007
3264
|
const { ok, report, reasons } = checkUpgrade(ctx);
|
|
3008
3265
|
console.error(`em upgrade --check: from ${report.from.version}${report.from.inferred ? ` (inferred: ${report.from.basis})` : ""} → to ${report.to}`);
|
|
3009
3266
|
for (const s of report.steps) {
|
|
3010
|
-
console.error(` [${s.applicable ? "x" : " "}] ${s.id} (since ${s.sinceVersion}): ${s.reason}`);
|
|
3267
|
+
console.error(` [${s.optIn ? "~" : s.applicable ? "x" : " "}] ${s.id} (since ${s.sinceVersion}): ${s.reason}`);
|
|
3011
3268
|
}
|
|
3012
3269
|
for (const h of report.human)
|
|
3013
3270
|
console.error(` human: ${h.id}: ${h.reason}`);
|
|
@@ -3020,10 +3277,13 @@ program
|
|
|
3020
3277
|
if (opts.apply) {
|
|
3021
3278
|
const result = applyUpgrade(ctx);
|
|
3022
3279
|
for (const s of result.applied) {
|
|
3023
|
-
if (s.applied)
|
|
3280
|
+
if (s.applied) {
|
|
3024
3281
|
console.log(`applied: ${s.id} — ${s.changedFiles.join(", ")} (${s.commit})`);
|
|
3282
|
+
for (const n of s.notes ?? [])
|
|
3283
|
+
console.log(` note: ${n}`);
|
|
3284
|
+
}
|
|
3025
3285
|
else
|
|
3026
|
-
console.log(`skipped: ${s.id}`);
|
|
3286
|
+
console.log(`skipped: ${s.id}${result.report.steps.find((x) => x.id === s.id)?.optIn ? " (opt-in: not requested by a flag)" : ""}`);
|
|
3027
3287
|
}
|
|
3028
3288
|
if (!result.ok) {
|
|
3029
3289
|
console.error(result.message);
|
|
@@ -3035,6 +3295,7 @@ program
|
|
|
3035
3295
|
console.log("Em version: already up to date — no commit needed");
|
|
3036
3296
|
for (const h of result.report.human)
|
|
3037
3297
|
console.log(`human: ${h.id}: ${h.reason}`);
|
|
3298
|
+
printUpgradeShapeSection(file);
|
|
3038
3299
|
return;
|
|
3039
3300
|
}
|
|
3040
3301
|
const report = detectUpgrade(ctx);
|
|
@@ -3047,16 +3308,31 @@ program
|
|
|
3047
3308
|
console.log(`state file: ${report.stateFileError}`);
|
|
3048
3309
|
console.log("Steps:");
|
|
3049
3310
|
for (const s of report.steps) {
|
|
3050
|
-
console.log(` [${s.applicable ? "x" : " "}] ${s.id} (since ${s.sinceVersion}): ${s.reason}`);
|
|
3311
|
+
console.log(` [${s.optIn ? "~" : s.applicable ? "x" : " "}] ${s.id} (since ${s.sinceVersion}): ${s.reason}`);
|
|
3051
3312
|
}
|
|
3052
3313
|
console.log("Human:");
|
|
3053
3314
|
if (report.human.length === 0)
|
|
3054
3315
|
console.log(" (none)");
|
|
3055
3316
|
for (const h of report.human)
|
|
3056
3317
|
console.log(` ${h.id}: ${h.reason}`);
|
|
3057
|
-
const applicableCount = report.steps.filter((s) => s.applicable).length;
|
|
3058
|
-
|
|
3318
|
+
const applicableCount = report.steps.filter((s) => s.applicable && !s.optIn).length;
|
|
3319
|
+
const optInCount = report.steps.filter((s) => s.optIn).length;
|
|
3320
|
+
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`);
|
|
3321
|
+
printUpgradeShapeSection(file);
|
|
3059
3322
|
});
|
|
3323
|
+
/** GH #290: the closing section of `em upgrade`'s text report — `em validate --shape` over the model as it
|
|
3324
|
+
* stands, so an estate sees how many advisory shape findings it carries. Never writes a waive file:
|
|
3325
|
+
* baselining shapes you accept is a decision (`--waive-file`, and a `shape-accepted:` Decisions-log bullet). */
|
|
3326
|
+
function printUpgradeShapeSection(file) {
|
|
3327
|
+
const { model, diagnostics, refs, source } = compileFile(file);
|
|
3328
|
+
const all = computeAllDiagnostics(file, model, refs, diagnostics, source);
|
|
3329
|
+
const report = buildShapeReport(all, []);
|
|
3330
|
+
console.log("");
|
|
3331
|
+
console.log("Shape (em validate --shape, advisory):");
|
|
3332
|
+
console.log(formatShapeText(report, all.filter((d) => d.severity === "error").length));
|
|
3333
|
+
console.log("Record each shape you accept as `shape-accepted: <rule> <refs> — <reason>` in `.event-modeling.md` `## Decisions log`; " +
|
|
3334
|
+
"to baseline an estate, run `em validate " + file + " --shape --waive-file <path>` (em upgrade writes no waive file).");
|
|
3335
|
+
}
|
|
3060
3336
|
// Exported so dev tooling (e.g. scripts/generate-skill-docs.ts) can introspect the registered
|
|
3061
3337
|
// commands/options without triggering a real CLI run. Guarded the same way Node's CJS
|
|
3062
3338
|
// `require.main === module` used to: only actually parse argv when this file is the process's
|