@milehimikey/em 1.17.0 → 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 +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 +8 -3
- package/.claude/skills/event-modeling-implement/SKILL.md +1 -1
- package/.claude/skills/event-modeling-implement/reference/implement.md +8 -0
- package/.claude/skills/event-modeling-review/SKILL.md +1 -1
- package/.claude/skills/event-modeling-shared/reference/em-dsl.md +90 -5
- 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/sliceReadyValidate.d.ts +8 -0
- package/dist/catalog/sliceReadyValidate.js +35 -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 +2 -2
- package/dist/cli/engagement.js +24 -1
- package/dist/cli/engagement.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/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 +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 +426 -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 +286 -29
- 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 +1 -1
- package/dist/emit/engagementJson.js +3 -1
- 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 +5 -2
- package/dist/emit/validateJson.js +36 -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 +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/shared/reference/em-dsl.md +90 -5
- 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 +8 -3
- package/plugin/skills/event-modeling/SKILL.md +1 -1
- package/plugin/skills/implement/SKILL.md +1 -1
- package/plugin/skills/implement/reference/implement.md +8 -0
- package/plugin/skills/review/SKILL.md +1 -1
package/dist/cli/upgrade.js
CHANGED
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
// ci-block/constitution check needs. Requires the model directory to be inside a git
|
|
22
22
|
// repository — same requirement `em conform-scope` holds, for the same reason (there is no
|
|
23
23
|
// "changed since when" or "commit per step" without one).
|
|
24
|
-
import { existsSync, readFileSync, realpathSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
|
|
25
25
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
26
|
+
import { findRosterAbove, GRANDFATHERED_RATIFIER } from "../roster/roster.js";
|
|
26
27
|
import { PUBLIC_SCALAR_TYPE_NAMES } from "../model/model.js";
|
|
27
28
|
import { findUnresolvedPublicFieldTypes } from "../model/validate.js";
|
|
28
29
|
import { realGit } from "./diff-inputs.js";
|
|
@@ -38,8 +39,12 @@ import { findSpecifyRoot, locateConstitution } from "./status.js";
|
|
|
38
39
|
import { scaffoldConstitution, scaffoldStateFile } from "../templates.js";
|
|
39
40
|
import { loadStateFile, parseState, setEmVersion, ensureOptionalBullets, resolveStateFilePath } from "./stateFile.js";
|
|
40
41
|
import { fieldLineRegex, normalizeFieldValue, locateFrontmatterInner, replaceFieldValue } from "./frontmatterSurgery.js";
|
|
41
|
-
import { ratifiedHashScope, sliceDocBodyHash } from "../catalog/sliceDoc.js";
|
|
42
|
-
import { applyDeferOrAppend, drainQueueInState, finishSignedDocEdit, isSignedDoc, readQueue } from "./defer.js";
|
|
42
|
+
import { parseSliceDoc, ratifiedHashScope, sliceDocBodyHash } from "../catalog/sliceDoc.js";
|
|
43
|
+
import { applyDeferOrAppend, drainQueueInState, finishSignedDocEdit, isSignedDoc, readQueue, rewriteParkingLotFragments } from "./defer.js";
|
|
44
|
+
import { runSliceSync } from "./sliceSync.js";
|
|
45
|
+
import { runSliceIndex } from "./sliceIndex.js";
|
|
46
|
+
import { planCollapseAlternativeEvents, planFoldContinuations, planStackContinuations } from "./upgradeReshape.js";
|
|
47
|
+
import { observeSigners } from "../roster/init.js";
|
|
43
48
|
import { STATE_FILE_NAME } from "./stateFile.js";
|
|
44
49
|
import { localIsoDate } from "../util/localDate.js";
|
|
45
50
|
import { compile } from "../pipeline.js";
|
|
@@ -277,7 +282,10 @@ function ciBlockFiles(args, ctx) {
|
|
|
277
282
|
};
|
|
278
283
|
}
|
|
279
284
|
return {
|
|
280
|
-
ci: [
|
|
285
|
+
ci: [
|
|
286
|
+
buildCiWorkflowFile(args.model, args.testsDir, ctx.installedVersion, findRosterAbove(dirname(resolve(ctx.repoRoot, args.model))) !== null),
|
|
287
|
+
ciManagedBody(args.model, args.testsDir, ctx.installedVersion, findRosterAbove(dirname(resolve(ctx.repoRoot, args.model))) !== null),
|
|
288
|
+
],
|
|
281
289
|
conform: [buildConformWorkflowFile(args.model, ctx.installedVersion, usePlugin), conformManagedBody(args.model, ctx.installedVersion, usePlugin)],
|
|
282
290
|
};
|
|
283
291
|
}
|
|
@@ -363,7 +371,7 @@ function applyConstitution(ctx) {
|
|
|
363
371
|
// reratified doc has `version > 1` and is mid-sign-off, which only a human can resolve; see the
|
|
364
372
|
// `ready-to-implement-no-ratifiedby` human item) with no `ratifiedBy`. A doc with no `version:`
|
|
365
373
|
// at all is treated as version 1 (the schema's default). ----
|
|
366
|
-
export
|
|
374
|
+
export { GRANDFATHERED_RATIFIER };
|
|
367
375
|
/** Pure text transform: inserts the grandfather sign-off after the `status:` line, using only
|
|
368
376
|
* the key lines that are missing (a stray `ratifiedOn:` is kept as-is), in the file's own EOL.
|
|
369
377
|
* `null` when the doc isn't a candidate (no frontmatter/status, or `ratifiedBy` already set). */
|
|
@@ -547,6 +555,298 @@ function applyRatifiedHashScope(ctx) {
|
|
|
547
555
|
}
|
|
548
556
|
return { ok: true, changedFiles };
|
|
549
557
|
}
|
|
558
|
+
// ---- Step 8-: collapse-alternative-events (GH #290 / #278) and fold-continuations (GH #290 / #287) ----
|
|
559
|
+
// The two steps that rewrite the `.em` itself (planned by upgradeReshape.ts: text surgery, verified by
|
|
560
|
+
// recompiling before anything is written). They run before the doc/index steps, which re-read the
|
|
561
|
+
// model `applyUpgrade` recompiles after each step. `fold-continuations` is OPT-IN (see below).
|
|
562
|
+
function readModelText(ctx) {
|
|
563
|
+
return readFileSync(ctx.modelFile, "utf8");
|
|
564
|
+
}
|
|
565
|
+
function freshCompile(ctx) {
|
|
566
|
+
try {
|
|
567
|
+
const c = compile(readModelText(ctx));
|
|
568
|
+
return { model: c.model, refs: c.refs };
|
|
569
|
+
}
|
|
570
|
+
catch {
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
/** Signed docs (ratified or shipped) whose generated regions would be stale against `nextText`, i.e. that
|
|
575
|
+
* a rewrite leaves behind a signature the regions no longer match. Read-only. */
|
|
576
|
+
function signedDocsStaleAfter(ctx, nextText) {
|
|
577
|
+
const staleSigned = (src) => {
|
|
578
|
+
const out = new Map();
|
|
579
|
+
let c;
|
|
580
|
+
try {
|
|
581
|
+
c = compile(src);
|
|
582
|
+
}
|
|
583
|
+
catch {
|
|
584
|
+
return out;
|
|
585
|
+
}
|
|
586
|
+
const r = runSliceSync(c.model, c.refs, ctx.baseDir, undefined, true);
|
|
587
|
+
if (!r.ok)
|
|
588
|
+
return out;
|
|
589
|
+
for (const d of r.docs) {
|
|
590
|
+
if (d.status === "stale" && isSignedDoc(readFileSync(join(ctx.baseDir, d.path), "utf8"))) {
|
|
591
|
+
out.set(d.path, `${d.path} (${d.regions.filter((g) => g.status === "stale").map((g) => g.name).join(", ")})`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return out;
|
|
595
|
+
};
|
|
596
|
+
const before = staleSigned(readModelText(ctx));
|
|
597
|
+
return [...staleSigned(nextText)].filter(([path]) => !before.has(path)).map(([, label]) => label); // newly stale only
|
|
598
|
+
}
|
|
599
|
+
const CARRIED_SECTIONS = /^##\s+(Scenarios|Invariants|Open Questions|Decisions(?: log)?)\s*$/i;
|
|
600
|
+
/** Sections of a retiring doc whose AUTHORED content a human must carry to the kept doc (generated
|
|
601
|
+
* regions are rebuilt from the model, so they are not content). */
|
|
602
|
+
function carriedSections(raw) {
|
|
603
|
+
const body = raw.replace(/<!-- GENERATED:[^>]*:start[^>]*-->[\s\S]*?<!-- GENERATED:[^>]*:end -->/g, "");
|
|
604
|
+
const lines = body.split("\n");
|
|
605
|
+
const found = [];
|
|
606
|
+
for (let i = 0; i < lines.length; i++) {
|
|
607
|
+
const m = CARRIED_SECTIONS.exec(lines[i]);
|
|
608
|
+
if (!m)
|
|
609
|
+
continue;
|
|
610
|
+
let j = i + 1;
|
|
611
|
+
let content = false;
|
|
612
|
+
for (; j < lines.length && !/^#{1,2}\s/.test(lines[j]); j++)
|
|
613
|
+
if (lines[j].trim() !== "" && !lines[j].includes("{{"))
|
|
614
|
+
content = true;
|
|
615
|
+
if (content)
|
|
616
|
+
found.push(m[1]);
|
|
617
|
+
}
|
|
618
|
+
return found;
|
|
619
|
+
}
|
|
620
|
+
/** Where a collapsed slice's retired doc goes: beside the model, outside `slices/` (so it is not an
|
|
621
|
+
* orphaned slice doc), keeping its file name. */
|
|
622
|
+
const ARCHIVE_DIR = join("archive", "slices");
|
|
623
|
+
function retiredDocs(ctx, groups) {
|
|
624
|
+
const out = [];
|
|
625
|
+
const refOf = elementRefOf(ctx);
|
|
626
|
+
const joinOf = (key) => {
|
|
627
|
+
const i = ctx.refs.sliceKeys.indexOf(key);
|
|
628
|
+
return i === -1 ? null : resolveSliceDocJoin(ctx.model, ctx.refs, ctx.model.slices[i], key, ctx.baseDir, refOf).doc;
|
|
629
|
+
};
|
|
630
|
+
for (const g of groups) {
|
|
631
|
+
const kept = joinOf(g.keys[0]);
|
|
632
|
+
const keptDoc = kept && kept.found && kept.reason === null ? kept.path : null;
|
|
633
|
+
const keptVersion = kept && keptDoc ? kept.version : null;
|
|
634
|
+
for (const key of g.keys.slice(1)) {
|
|
635
|
+
const bound = joinOf(key);
|
|
636
|
+
const rel = bound && bound.found && bound.reason === null ? bound.path : `slices/${key}.md`;
|
|
637
|
+
const from = join(ctx.baseDir, rel);
|
|
638
|
+
if (!existsSync(from) || (keptDoc !== null && rel === keptDoc))
|
|
639
|
+
continue;
|
|
640
|
+
const raw = readFileSync(from, "utf8");
|
|
641
|
+
const range = locateFrontmatterInner(raw);
|
|
642
|
+
const retiredVersion = Number(/^version:\s*(\d+)/m.exec(raw)?.[1] ?? "1");
|
|
643
|
+
const target = keptDoc !== null ? basename(keptDoc, ".md") : g.keys[0];
|
|
644
|
+
let next = raw;
|
|
645
|
+
if (range) {
|
|
646
|
+
const fenceEnd = raw.indexOf("\n", range.innerEnd) + 1 || raw.length;
|
|
647
|
+
const banner = `\n> **SUPERSEDED** by \`${target}\` (\`superseded-by\`): this slice was collapsed into one recording alternative events (GH #278). Kept for history; fold anything worth keeping into \`${keptDoc ?? `slices/${g.keys[0]}.md`}\`.\n`;
|
|
648
|
+
next = raw.slice(0, range.innerEnd) + `superseded-by: ${target}@v${keptVersion ?? 1}\n` + raw.slice(range.innerEnd, fenceEnd) + banner + raw.slice(fenceEnd);
|
|
649
|
+
}
|
|
650
|
+
out.push({ from, to: join(ctx.baseDir, ARCHIVE_DIR, basename(rel)), key, keptKey: g.keys[0], keptDoc, keptVersion, retiredVersion, text: next, carried: carriedSections(raw) });
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return out;
|
|
654
|
+
}
|
|
655
|
+
function carriedNotes(retired) {
|
|
656
|
+
return retired.filter((r) => r.carried.length > 0).map((r) => `${ARCHIVE_DIR}/${basename(r.to)} holds ${r.carried.join(", ")} — fold these into ${r.keptDoc ?? `slices/${r.keptKey}.md`}`);
|
|
657
|
+
}
|
|
658
|
+
function detectCollapseAlternativeEvents(ctx) {
|
|
659
|
+
const fresh = freshCompile(ctx);
|
|
660
|
+
const plan = fresh ? planCollapseAlternativeEvents(fresh.model, fresh.refs, readModelText(ctx)) : null;
|
|
661
|
+
if (plan === null)
|
|
662
|
+
return { applicable: false, reason: "no run of adjacent slices repeats one command and differs only in the recorded event" };
|
|
663
|
+
const desc = plan.groups.map((g) => `${g.slices.map((n) => `"${n}"`).join(" + ")} → "${g.slices[0]}" recording ${g.form === "inline" ? g.events.join(" | ") : `oneof { ${g.events.join(", ")} }`}`);
|
|
664
|
+
const retired = retiredDocs(ctx, plan.groups);
|
|
665
|
+
const notes = carriedNotes(retired);
|
|
666
|
+
const stale = signedDocsStaleAfter(ctx, plan.next);
|
|
667
|
+
return {
|
|
668
|
+
applicable: true,
|
|
669
|
+
reason: `${plan.groups.length} run(s) of repeated-command slices collapse to alternative events: ${desc.join("; ")} — em upgrade will rewrite them (the first slice's key is kept; the others' docs move to ${ARCHIVE_DIR}/ with superseded-by and a SUPERSEDED banner, and the kept doc gains merged-from)` +
|
|
670
|
+
(notes.length > 0 ? `; ${notes.join("; ")}` : "") +
|
|
671
|
+
(stale.length > 0 ? `; signed doc(s) will not be synced and go stale: ${stale.join(", ")} — em slice revise <key>, then em slice sync` : ""),
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
/** Frontmatter-only edit: adds `key: value` before the closing fence (a no-op without frontmatter). */
|
|
675
|
+
function addFrontmatterLine(raw, line) {
|
|
676
|
+
const range = locateFrontmatterInner(raw);
|
|
677
|
+
return range ? raw.slice(0, range.innerEnd) + line + "\n" + raw.slice(range.innerEnd) : raw;
|
|
678
|
+
}
|
|
679
|
+
function applyCollapseAlternativeEvents(ctx) {
|
|
680
|
+
const fresh = freshCompile(ctx);
|
|
681
|
+
const text = readModelText(ctx);
|
|
682
|
+
const plan = fresh ? planCollapseAlternativeEvents(fresh.model, fresh.refs, text) : null;
|
|
683
|
+
if (plan === null)
|
|
684
|
+
return { ok: false, message: "the model no longer collapses cleanly" };
|
|
685
|
+
const changed = [ctx.modelFile];
|
|
686
|
+
const retired = retiredDocs(ctx, plan.groups); // computed on the OLD model (joins, versions)
|
|
687
|
+
const notes = carriedNotes(retired);
|
|
688
|
+
writeFileSync(ctx.modelFile, plan.next, "utf8");
|
|
689
|
+
for (const r of retired) {
|
|
690
|
+
mkdirSync(dirname(r.to), { recursive: true });
|
|
691
|
+
writeFileSync(r.to, r.text, "utf8");
|
|
692
|
+
rmSync(r.from);
|
|
693
|
+
changed.push(r.from, r.to);
|
|
694
|
+
}
|
|
695
|
+
// Lineage back-link on the kept doc: `merged-from: <retired>@v<N>` (frontmatter only, outside the
|
|
696
|
+
// ratified hash). A SIGNED kept doc is not touched; `lineage-backlink-missing` lists it.
|
|
697
|
+
for (const r of retired) {
|
|
698
|
+
if (r.keptDoc === null)
|
|
699
|
+
continue;
|
|
700
|
+
const abs = join(ctx.baseDir, r.keptDoc);
|
|
701
|
+
const raw = readFileSync(abs, "utf8");
|
|
702
|
+
if (isSignedDoc(raw))
|
|
703
|
+
continue;
|
|
704
|
+
const ref = `${basename(r.to, ".md")}@v${r.retiredVersion}`;
|
|
705
|
+
const existing = /^merged-from:\s*(.*)$/m.exec(raw);
|
|
706
|
+
const next = existing ? raw.replace(/^merged-from:\s*(.*)$/m, (_m, v) => `merged-from: ${String(v).trim() === "" ? ref : `${String(v).trim()}, ${ref}`}`) : addFrontmatterLine(raw, `merged-from: ${ref}`);
|
|
707
|
+
writeFileSync(abs, next, "utf8");
|
|
708
|
+
if (!changed.includes(abs))
|
|
709
|
+
changed.push(abs);
|
|
710
|
+
}
|
|
711
|
+
// Regenerate the kept slice's event region (one `Records one of:` region now) unless its doc is signed
|
|
712
|
+
// (then `slice-doc-stale-signed` is the human item that says what to do).
|
|
713
|
+
const after = freshCompile(ctx);
|
|
714
|
+
if (after) {
|
|
715
|
+
for (const g of plan.groups) {
|
|
716
|
+
const kept = retired.find((r) => r.keptKey === g.keys[0])?.keptDoc ?? `slices/${g.keys[0]}.md`;
|
|
717
|
+
const doc = join(ctx.baseDir, kept);
|
|
718
|
+
if (existsSync(doc) && !isSignedDoc(readFileSync(doc, "utf8"))) {
|
|
719
|
+
const r = runSliceSync(after.model, after.refs, ctx.baseDir, g.keys[0], false);
|
|
720
|
+
if (r.ok && r.docs.some((d) => d.status === "synced") && !changed.includes(doc))
|
|
721
|
+
changed.push(doc);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return { ok: true, changedFiles: changed, notes };
|
|
726
|
+
}
|
|
727
|
+
// ---- fold-continuations is OPT-IN (`em upgrade --apply --fold-continuations`) ----
|
|
728
|
+
// Default form (a): consecutive continuations of ONE view stack into the first slice (its key, name,
|
|
729
|
+
// `continuationOf`, doc join and index row stay). `--to-sugar` instead moves them into the host
|
|
730
|
+
// command slice as `view X again … shown on` lines; that CHANGES `continuationOf` and the doc join to
|
|
731
|
+
// the host (`<host>--again`) and removes the index rows.
|
|
732
|
+
const FOLD_FLAG = "em upgrade --apply --fold-continuations";
|
|
733
|
+
function foldPlan(ctx) {
|
|
734
|
+
const fresh = freshCompile(ctx);
|
|
735
|
+
if (!fresh)
|
|
736
|
+
return { kind: "stack", plan: null };
|
|
737
|
+
const text = readModelText(ctx);
|
|
738
|
+
return ctx.toSugar === true
|
|
739
|
+
? { kind: "sugar", plan: planFoldContinuations(fresh.model, fresh.refs, text) }
|
|
740
|
+
: { kind: "stack", plan: planStackContinuations(fresh.model, fresh.refs, text) };
|
|
741
|
+
}
|
|
742
|
+
function detectFoldContinuations(ctx) {
|
|
743
|
+
const f = foldPlan(ctx);
|
|
744
|
+
if (f.plan === null)
|
|
745
|
+
return { applicable: false, reason: "no consecutive hand-named continuation slices of one view (stack) — or, with --to-sugar, no continuation directly after its host command — in a foldable shape" };
|
|
746
|
+
if (f.kind === "sugar" && f.plan.groups.length === 0) {
|
|
747
|
+
const r = f.plan.refused;
|
|
748
|
+
return { applicable: false, reason: `--to-sugar: nothing folded; refused: ${r.map((x) => `${x.hostKey} (${x.reason})`).join("; ")}` };
|
|
749
|
+
}
|
|
750
|
+
const optIn = ctx.foldContinuations === true || ctx.toSugar === true;
|
|
751
|
+
const prefix = optIn ? "" : `OPT-IN, not applied by a plain --apply (run \`${FOLD_FLAG}\`): `;
|
|
752
|
+
const stale = signedDocsStaleAfter(ctx, f.plan.next);
|
|
753
|
+
const staleNote = stale.length > 0 ? `; signed doc(s) whose generated regions (e.g. the view's \`via\` annotations) go stale and will not be synced: ${stale.join(", ")} — em slice revise <key>, then em slice sync` : "";
|
|
754
|
+
if (f.kind === "stack") {
|
|
755
|
+
const p = f.plan;
|
|
756
|
+
const desc = p.groups.map((g) => `${[g.kept, ...g.dropped].map((n) => `"${n}"`).join(" + ")} → one continuation slice "${g.kept}" (key ${g.keptKey} kept; dropped keys: ${g.droppedKeys.join(", ")})`);
|
|
757
|
+
const skipped = p.skipped.length > 0 ? `; left unstacked: ${p.skipped.map((s) => `${s.key} (${s.reason})`).join(", ")}` : "";
|
|
758
|
+
return { applicable: true, reason: `${prefix}${desc.join("; ")} — continuationOf, the doc join and the kept key's index row are unchanged${skipped}${staleNote}` };
|
|
759
|
+
}
|
|
760
|
+
const p = f.plan;
|
|
761
|
+
const desc = p.groups.map((g) => `${g.folded.map((n) => `"${n}"`).join(" + ")} (keys ${g.foldedKeys.join(", ")}) → \`view … again\` lines in "${g.host}" (column ${g.hostKey}--again)`);
|
|
762
|
+
const refusedNote = p.refused.length > 0 ? `; refused: ${p.refused.map((x) => `${x.hostKey} (${x.reason})`).join("; ")}` : "";
|
|
763
|
+
return { applicable: true, reason: `${prefix}--to-sugar: ${desc.join("; ")}${refusedNote} — CHANGES continuationOf and the doc join to the host (${p.groups.map((g) => `${g.hostKey}--again`).join(", ")}) and removes the index rows of ${p.groups.flatMap((g) => g.foldedKeys).join(", ")} (a stray own doc of a folded slice is left alone)${staleNote}` };
|
|
764
|
+
}
|
|
765
|
+
function applyFoldContinuations(ctx) {
|
|
766
|
+
const f = foldPlan(ctx);
|
|
767
|
+
if (f.plan === null || ("groups" in f.plan && f.plan.groups.length === 0))
|
|
768
|
+
return { ok: false, message: "the model no longer folds cleanly" };
|
|
769
|
+
writeFileSync(ctx.modelFile, f.plan.next, "utf8");
|
|
770
|
+
return { ok: true, changedFiles: [ctx.modelFile] };
|
|
771
|
+
}
|
|
772
|
+
function staleViewDocs(ctx) {
|
|
773
|
+
const open = [];
|
|
774
|
+
const signed = [];
|
|
775
|
+
const r = runSliceSync(ctx.model, ctx.refs, ctx.baseDir, undefined, true);
|
|
776
|
+
if (!r.ok)
|
|
777
|
+
return { open, signed };
|
|
778
|
+
for (const d of r.docs) {
|
|
779
|
+
if (d.status !== "stale" || !d.regions.some((g) => g.status === "stale" && g.name.startsWith("em-slice-view")))
|
|
780
|
+
continue;
|
|
781
|
+
(isSignedDoc(readFileSync(join(ctx.baseDir, d.path), "utf8")) ? signed : open).push({ key: d.key, path: d.path });
|
|
782
|
+
}
|
|
783
|
+
return { open, signed };
|
|
784
|
+
}
|
|
785
|
+
function detectViewDocSourcesStale(ctx) {
|
|
786
|
+
const { open, signed } = staleViewDocs(ctx);
|
|
787
|
+
const left = signed.length > 0 ? `; left alone (ratified or shipped — em slice revise, then em slice sync): ${signed.map((d) => d.path).join(", ")}` : "";
|
|
788
|
+
if (open.length === 0) {
|
|
789
|
+
return { applicable: false, reason: `no bound slice doc's view region is stale against the model${left}` };
|
|
790
|
+
}
|
|
791
|
+
return {
|
|
792
|
+
applicable: true,
|
|
793
|
+
reason: `em slice sync --check would change the view region of ${open.length} doc(s): ${open.map((d) => d.path).join(", ")} — em upgrade will run em slice sync on them${left}`,
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
function applyViewDocSourcesStale(ctx) {
|
|
797
|
+
const { open } = staleViewDocs(ctx);
|
|
798
|
+
const changedFiles = [];
|
|
799
|
+
for (const d of open) {
|
|
800
|
+
const r = runSliceSync(ctx.model, ctx.refs, ctx.baseDir, d.key, false);
|
|
801
|
+
if (!r.ok)
|
|
802
|
+
return { ok: false, message: r.message };
|
|
803
|
+
changedFiles.push(join(ctx.baseDir, d.path));
|
|
804
|
+
}
|
|
805
|
+
return { ok: true, changedFiles };
|
|
806
|
+
}
|
|
807
|
+
// ---- Step 8b: slice-index-stale (GH #290 / #275) — the README Slices table. ----
|
|
808
|
+
function detectSliceIndexStale(ctx) {
|
|
809
|
+
const r = runSliceIndex(ctx.model, ctx.refs, ctx.modelFile, true);
|
|
810
|
+
if (r.ok)
|
|
811
|
+
return { applicable: false, reason: "the README Slices table is current" };
|
|
812
|
+
if (r.kind === "stale")
|
|
813
|
+
return { applicable: true, reason: `${join(ctx.baseDir, "README.md")}'s Slices table is stale (em slice index --check) — em upgrade will regenerate it` };
|
|
814
|
+
return { applicable: false, reason: r.kind === "missing-readme" ? "no README.md beside the model — nothing to index" : "README.md has no GENERATED:slices markers — nothing to index" };
|
|
815
|
+
}
|
|
816
|
+
function applySliceIndexStale(ctx) {
|
|
817
|
+
const r = runSliceIndex(ctx.model, ctx.refs, ctx.modelFile, false);
|
|
818
|
+
if (!r.ok)
|
|
819
|
+
return { ok: false, message: r.message };
|
|
820
|
+
return { ok: true, changedFiles: r.wrote ? [r.readmePath] : [] };
|
|
821
|
+
}
|
|
822
|
+
// ---- Step 8c: parking-lot-fragments (GH #290 / #283) — substring bullets become the item's label. ----
|
|
823
|
+
function parkingLotDocs(ctx) {
|
|
824
|
+
const docs = new Map();
|
|
825
|
+
for (const d of nonContinuationDocs(ctx)) {
|
|
826
|
+
const file = join(ctx.baseDir, d.path);
|
|
827
|
+
if (d.path !== "" && existsSync(file))
|
|
828
|
+
docs.set(d.key, readFileSync(file, "utf8"));
|
|
829
|
+
}
|
|
830
|
+
return docs;
|
|
831
|
+
}
|
|
832
|
+
function detectParkingLotFragments(ctx) {
|
|
833
|
+
const statePath = join(ctx.baseDir, STATE_FILE_NAME);
|
|
834
|
+
if (!existsSync(statePath))
|
|
835
|
+
return { applicable: false, reason: `no ${STATE_FILE_NAME} beside the model` };
|
|
836
|
+
const { rewrites } = rewriteParkingLotFragments(readFileSync(statePath, "utf8"), parkingLotDocs(ctx));
|
|
837
|
+
if (rewrites.length === 0)
|
|
838
|
+
return { applicable: false, reason: "no parking-lot bullet is a fragment of an Open Questions item" };
|
|
839
|
+
return {
|
|
840
|
+
applicable: true,
|
|
841
|
+
reason: `${rewrites.length} parking-lot bullet(s) hold a fragment of an Open Questions item (the pre-1.18 em slice defer shape): ${rewrites.map((r) => `"${r.from}" → "${r.to}"`).join("; ")} — em upgrade will rewrite each to the item's label`,
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
function applyParkingLotFragments(ctx) {
|
|
845
|
+
const statePath = join(ctx.baseDir, STATE_FILE_NAME);
|
|
846
|
+
const { content } = rewriteParkingLotFragments(readFileSync(statePath, "utf8"), parkingLotDocs(ctx));
|
|
847
|
+
writeFileSync(statePath, content, "utf8");
|
|
848
|
+
return { ok: true, changedFiles: [statePath] };
|
|
849
|
+
}
|
|
550
850
|
// ---- Step 8: system-manifest (MIL-235, R3) — system.yaml 1.0 -> 2.0. ----
|
|
551
851
|
// The one mechanical step that writes OTHER models' files: the legacy manifest's seams become
|
|
552
852
|
// `consumes` clauses on each consuming translation and its owners become `owner "…"` on each
|
|
@@ -648,6 +948,11 @@ export const UPGRADE_STEPS = [
|
|
|
648
948
|
{ id: "constitution", sinceVersion: "1.11.0", detect: detectConstitution, apply: applyConstitution },
|
|
649
949
|
{ id: "ratified-signoff", sinceVersion: "1.14.0", detect: detectRatifiedSignoff, apply: applyRatifiedSignoff },
|
|
650
950
|
{ id: "ratified-hash-scope", sinceVersion: "1.17.0", detect: detectRatifiedHashScope, apply: applyRatifiedHashScope },
|
|
951
|
+
{ id: "collapse-alternative-events", sinceVersion: "1.18.0", detect: detectCollapseAlternativeEvents, apply: applyCollapseAlternativeEvents },
|
|
952
|
+
{ id: "fold-continuations", sinceVersion: "1.18.0", detect: detectFoldContinuations, apply: applyFoldContinuations, optedIn: (c) => c.foldContinuations === true || c.toSugar === true },
|
|
953
|
+
{ id: "view-doc-sources-stale", sinceVersion: "1.18.0", detect: detectViewDocSourcesStale, apply: applyViewDocSourcesStale },
|
|
954
|
+
{ id: "slice-index-stale", sinceVersion: "1.18.0", detect: detectSliceIndexStale, apply: applySliceIndexStale },
|
|
955
|
+
{ id: "parking-lot-fragments", sinceVersion: "1.18.0", detect: detectParkingLotFragments, apply: applyParkingLotFragments },
|
|
651
956
|
{ id: "system-manifest", sinceVersion: "1.14.0", detect: detectSystemManifest, apply: applySystemManifest },
|
|
652
957
|
];
|
|
653
958
|
function elementRefOf(ctx) {
|
|
@@ -780,6 +1085,101 @@ function detectPluginInstallLocally(ctx) {
|
|
|
780
1085
|
}
|
|
781
1086
|
return null;
|
|
782
1087
|
}
|
|
1088
|
+
/** GH #290: a SIGNED doc (ratified or shipped) whose generated regions are stale against the model —
|
|
1089
|
+
* `view-doc-sources-stale` and `collapse-alternative-events` leave it alone because syncing would move
|
|
1090
|
+
* its `ratifiedHash`. Raised for any region (view, event, command, invariants). */
|
|
1091
|
+
function detectSliceDocStaleSigned(ctx) {
|
|
1092
|
+
const r = runSliceSync(ctx.model, ctx.refs, ctx.baseDir, undefined, true);
|
|
1093
|
+
if (!r.ok)
|
|
1094
|
+
return null;
|
|
1095
|
+
const docs = r.docs.filter((d) => d.status === "stale" && isSignedDoc(readFileSync(join(ctx.baseDir, d.path), "utf8")));
|
|
1096
|
+
if (docs.length === 0)
|
|
1097
|
+
return null;
|
|
1098
|
+
return {
|
|
1099
|
+
id: "slice-doc-stale-signed",
|
|
1100
|
+
reason: `${docs.map((d) => `${d.path} (${d.regions.filter((g) => g.status === "stale").map((g) => g.name).join(", ")})`).join("; ")} — ratified or shipped and stale against the model; run \`em slice revise ${docs.map((d) => d.key).join(" / ")}\`, then \`em slice sync\``,
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
/** GH #290 / #278: a retired doc under archive/slices/ names its successor in `superseded-by`, but the
|
|
1104
|
+
* successor (a signed doc `em upgrade` did not touch) lacks the `merged-from` back-link. */
|
|
1105
|
+
function detectLineageBacklinkMissing(ctx) {
|
|
1106
|
+
const dir = join(ctx.baseDir, ARCHIVE_DIR);
|
|
1107
|
+
if (!existsSync(dir))
|
|
1108
|
+
return null;
|
|
1109
|
+
const missing = [];
|
|
1110
|
+
for (const f of readdirSync(dir).filter((n) => n.endsWith(".md")).sort()) {
|
|
1111
|
+
const retired = parseSliceDoc(readFileSync(join(dir, f), "utf8"));
|
|
1112
|
+
for (const ref of retired.supersededBy) {
|
|
1113
|
+
if (!ref.sliceKey)
|
|
1114
|
+
continue;
|
|
1115
|
+
const target = join(ctx.baseDir, "slices", `${ref.sliceKey}.md`);
|
|
1116
|
+
if (!existsSync(target))
|
|
1117
|
+
continue;
|
|
1118
|
+
const doc = parseSliceDoc(readFileSync(target, "utf8"));
|
|
1119
|
+
const key = basename(f, ".md");
|
|
1120
|
+
if (!doc.mergedFrom.some((m) => m.sliceKey === key))
|
|
1121
|
+
missing.push(`slices/${ref.sliceKey}.md needs merged-from: ${key}@v${retired.version ?? 1}`);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
if (missing.length === 0)
|
|
1125
|
+
return null;
|
|
1126
|
+
return { id: "lineage-backlink-missing", reason: `${missing.join("; ")} (the doc is signed or was edited since; add the line to its frontmatter — it is outside the ratified hash)` };
|
|
1127
|
+
}
|
|
1128
|
+
/** GH #290 / #287: `fold-continuations` is detected and listed by every run but needs the flag. */
|
|
1129
|
+
function detectFoldContinuationsOptIn(ctx) {
|
|
1130
|
+
if (ctx.foldContinuations === true || ctx.toSugar === true)
|
|
1131
|
+
return null;
|
|
1132
|
+
const d = detectFoldContinuations(ctx);
|
|
1133
|
+
return d.applicable ? { id: "fold-continuations-opt-in", reason: d.reason } : null;
|
|
1134
|
+
}
|
|
1135
|
+
function compareVersions(a, b) {
|
|
1136
|
+
const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
|
|
1137
|
+
const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
|
|
1138
|
+
for (let i = 0; i < 3; i++)
|
|
1139
|
+
if ((pa[i] ?? 0) !== (pb[i] ?? 0))
|
|
1140
|
+
return (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
1141
|
+
return 0;
|
|
1142
|
+
}
|
|
1143
|
+
/** The version the repo is upgrading FROM (recorded `Em version:` bullet, else inferred). */
|
|
1144
|
+
function recordedFromVersion(ctx) {
|
|
1145
|
+
const loaded = loadStateFile(ctx.baseDir);
|
|
1146
|
+
let recorded = null;
|
|
1147
|
+
if (loaded.ok) {
|
|
1148
|
+
const parsed = parseState(loaded.text);
|
|
1149
|
+
if (parsed.ok)
|
|
1150
|
+
recorded = parsed.state.emVersion;
|
|
1151
|
+
}
|
|
1152
|
+
return resolveFromVersion(ctx, recorded).version;
|
|
1153
|
+
}
|
|
1154
|
+
/** GH #290 / #259: signatures are a human decision, so the roster is never generated silently. */
|
|
1155
|
+
function detectRosterNotInitialised(ctx) {
|
|
1156
|
+
if (findRosterAbove(ctx.baseDir) !== null)
|
|
1157
|
+
return null;
|
|
1158
|
+
const { ratifiers } = observeSigners(ctx.model, ctx.refs, ctx.baseDir);
|
|
1159
|
+
if (ratifiers.length === 0)
|
|
1160
|
+
return null;
|
|
1161
|
+
return {
|
|
1162
|
+
id: "roster-not-initialised",
|
|
1163
|
+
reason: `${ratifiers.length} ratifier(s) have signed slice docs (${ratifiers.join(", ")}) and there is no ratifiers.yaml — decide who may sign, then run \`em roster init ${basename(ctx.modelFile)}\` to seed one from those names and review it before committing`,
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
/** GH #290 / #279: information only. Since 1.18.0 the same event recorded by several commands (same
|
|
1167
|
+
* name and context) is one logical event and `duplicate-name` no longer fires; nothing to change. */
|
|
1168
|
+
function detectSharedEventNowLegal(ctx) {
|
|
1169
|
+
// The transition only: a repo already stamped 1.18.0+ has seen this note.
|
|
1170
|
+
if (compareVersions(recordedFromVersion(ctx), "1.18.0") >= 0)
|
|
1171
|
+
return null;
|
|
1172
|
+
const shared = new Set();
|
|
1173
|
+
for (const el of ctx.model.elements)
|
|
1174
|
+
if (el.kind === "event" && el.logicalId !== el.id)
|
|
1175
|
+
shared.add(el.name);
|
|
1176
|
+
if (shared.size === 0)
|
|
1177
|
+
return null;
|
|
1178
|
+
return {
|
|
1179
|
+
id: "shared-event-now-legal",
|
|
1180
|
+
reason: `information only: ${[...shared].join(", ")} ${shared.size === 1 ? "is" : "are"} recorded by more than one command — legal since 1.18.0 (one logical event; duplicate-name no longer fires), no model change needed`,
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
783
1183
|
const HUMAN_DETECTORS = [
|
|
784
1184
|
detectNoModelVersion,
|
|
785
1185
|
detectContinuationHasOwnDoc,
|
|
@@ -789,6 +1189,11 @@ const HUMAN_DETECTORS = [
|
|
|
789
1189
|
detectPredates16,
|
|
790
1190
|
detectPublicFieldTypesUnresolved,
|
|
791
1191
|
detectPluginInstallLocally,
|
|
1192
|
+
detectSliceDocStaleSigned,
|
|
1193
|
+
detectLineageBacklinkMissing,
|
|
1194
|
+
detectFoldContinuationsOptIn,
|
|
1195
|
+
detectSharedEventNowLegal,
|
|
1196
|
+
detectRosterNotInitialised,
|
|
792
1197
|
];
|
|
793
1198
|
export function detectHumanItems(ctx) {
|
|
794
1199
|
return HUMAN_DETECTORS.map((d) => d(ctx)).filter((x) => x !== null);
|
|
@@ -884,7 +1289,8 @@ export function detectUpgrade(ctx) {
|
|
|
884
1289
|
const from = resolveFromVersion(ctx, recorded);
|
|
885
1290
|
const steps = UPGRADE_STEPS.map((s) => {
|
|
886
1291
|
const d = s.detect(ctx);
|
|
887
|
-
|
|
1292
|
+
const optIn = s.optedIn !== undefined && !s.optedIn(ctx) && d.applicable;
|
|
1293
|
+
return { id: s.id, sinceVersion: s.sinceVersion, applicable: d.applicable, human: d.human === true, reason: d.reason, ...(optIn ? { optIn: true } : {}) };
|
|
888
1294
|
});
|
|
889
1295
|
const human = detectHumanItems(ctx);
|
|
890
1296
|
return { from, to: ctx.installedVersion, steps, human, stateFileError };
|
|
@@ -921,6 +1327,10 @@ export function applyUpgrade(ctx, runGit = realGit) {
|
|
|
921
1327
|
applied.push({ id: step.id, applied: false, changedFiles: [], commit: null });
|
|
922
1328
|
continue;
|
|
923
1329
|
}
|
|
1330
|
+
if (step.optedIn && !step.optedIn(ctx)) {
|
|
1331
|
+
applied.push({ id: step.id, applied: false, changedFiles: [], commit: null });
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
924
1334
|
const result = step.apply(ctx);
|
|
925
1335
|
if (!result.ok) {
|
|
926
1336
|
gitDiscardAll(ctx.repoRoot, runGit);
|
|
@@ -931,8 +1341,17 @@ export function applyUpgrade(ctx, runGit = realGit) {
|
|
|
931
1341
|
if (!commitResult.ok) {
|
|
932
1342
|
return { ok: false, message: `em upgrade: step "${step.id}" applied but failed to commit — ${commitResult.message}`, report, applied };
|
|
933
1343
|
}
|
|
934
|
-
applied.push({ id: step.id, applied: true, changedFiles: result.changedFiles, commit: message });
|
|
1344
|
+
applied.push({ id: step.id, applied: true, changedFiles: result.changedFiles, commit: message, ...(result.notes && result.notes.length > 0 ? { notes: result.notes } : {}) });
|
|
1345
|
+
// A step may have rewritten the model (or its docs): later steps must see the new one.
|
|
1346
|
+
const fresh = freshCompile(ctx);
|
|
1347
|
+
if (fresh) {
|
|
1348
|
+
ctx.model = fresh.model;
|
|
1349
|
+
ctx.refs = fresh.refs;
|
|
1350
|
+
}
|
|
935
1351
|
}
|
|
1352
|
+
// The pre-apply human list can be stale (a rewritten model, a doc now out of sync): report the upgraded
|
|
1353
|
+
// repo's, computed before the `Em version:` stamp so transition-only notes still show.
|
|
1354
|
+
report.human = detectHumanItems(ctx);
|
|
936
1355
|
const loaded = loadStateFile(ctx.baseDir);
|
|
937
1356
|
let emVersionCommit = null;
|
|
938
1357
|
if (loaded.ok) {
|