@milehimikey/em 1.13.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/em-critic.md +26 -0
- package/.claude/agents/em-implementer.md +34 -0
- package/.claude/agents/em-reviewer.md +31 -0
- package/.claude/agents/em-validator.md +27 -0
- package/.claude/skills/event-modeling/SKILL.md +52 -12
- package/.claude/skills/event-modeling-conform/SKILL.md +1 -1
- package/.claude/skills/event-modeling-conform/reference/conform.md +1 -1
- package/.claude/skills/event-modeling-design/SKILL.md +140 -18
- package/.claude/skills/event-modeling-discover/SKILL.md +1 -1
- package/.claude/skills/event-modeling-engagement/SKILL.md +192 -0
- package/.claude/skills/event-modeling-engagement/reference/engagement.md +143 -0
- package/.claude/skills/event-modeling-implement/SKILL.md +7 -4
- package/.claude/skills/event-modeling-implement/reference/implement.md +51 -12
- package/.claude/skills/event-modeling-review/SKILL.md +1 -1
- package/.claude/skills/event-modeling-shared/reference/em-dsl.md +274 -188
- package/.claude/skills/event-modeling-shared/reference/operating-principles.md +61 -0
- package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +116 -4
- package/.claude/skills/event-modeling-shared/templates/constitution.md +18 -1
- package/.claude/skills/event-modeling-shared/templates/slice.md +64 -39
- package/.claude/skills/event-modeling-shared/templates/state.md +5 -1
- package/.claude-plugin/marketplace.json +14 -0
- package/README.md +25 -11
- package/dist/catalog/apiFirst.d.ts +28 -0
- package/dist/catalog/apiFirst.js +49 -0
- package/dist/catalog/apiFirst.js.map +1 -0
- package/dist/catalog/docJoin.d.ts +13 -0
- package/dist/catalog/docJoin.js +11 -0
- package/dist/catalog/docJoin.js.map +1 -1
- package/dist/catalog/docModelConsistencyValidate.js +44 -4
- package/dist/catalog/docModelConsistencyValidate.js.map +1 -1
- package/dist/catalog/invariantsValidate.d.ts +4 -0
- package/dist/catalog/invariantsValidate.js +102 -0
- package/dist/catalog/invariantsValidate.js.map +1 -0
- package/dist/catalog/scenarios.d.ts +15 -0
- package/dist/catalog/scenarios.js +97 -0
- package/dist/catalog/scenarios.js.map +1 -0
- package/dist/catalog/sliceDoc.d.ts +8 -0
- package/dist/catalog/sliceDoc.js +9 -0
- package/dist/catalog/sliceDoc.js.map +1 -1
- package/dist/catalog/sliceReadyValidate.d.ts +31 -4
- package/dist/catalog/sliceReadyValidate.js +66 -8
- package/dist/catalog/sliceReadyValidate.js.map +1 -1
- package/dist/catalog/sliceSections.d.ts +55 -0
- package/dist/catalog/sliceSections.js +317 -0
- package/dist/catalog/sliceSections.js.map +1 -0
- package/dist/cli/agentsMd.js +7 -1
- package/dist/cli/agentsMd.js.map +1 -1
- package/dist/cli/api.d.ts +90 -0
- package/dist/cli/api.js +318 -0
- package/dist/cli/api.js.map +1 -0
- package/dist/cli/ciInit.d.ts +88 -3
- package/dist/cli/ciInit.js +507 -50
- package/dist/cli/ciInit.js.map +1 -1
- package/dist/cli/conformScope.d.ts +28 -0
- package/dist/cli/conformScope.js +67 -0
- package/dist/cli/conformScope.js.map +1 -1
- package/dist/cli/coverage.d.ts +25 -2
- package/dist/cli/coverage.js +86 -12
- package/dist/cli/coverage.js.map +1 -1
- package/dist/cli/engagement.d.ts +210 -0
- package/dist/cli/engagement.js +444 -0
- package/dist/cli/engagement.js.map +1 -0
- package/dist/cli/engagementFile.d.ts +86 -0
- package/dist/cli/engagementFile.js +255 -0
- package/dist/cli/engagementFile.js.map +1 -0
- package/dist/cli/metrics.d.ts +10 -0
- package/dist/cli/metrics.js +49 -1
- package/dist/cli/metrics.js.map +1 -1
- package/dist/cli/pluginPin.d.ts +55 -0
- package/dist/cli/pluginPin.js +188 -0
- package/dist/cli/pluginPin.js.map +1 -0
- package/dist/cli/ratify.d.ts +27 -2
- package/dist/cli/ratify.js +112 -13
- package/dist/cli/ratify.js.map +1 -1
- package/dist/cli/reratify.d.ts +11 -4
- package/dist/cli/reratify.js +89 -35
- package/dist/cli/reratify.js.map +1 -1
- package/dist/cli/scopeInputs.d.ts +48 -0
- package/dist/cli/scopeInputs.js +176 -0
- package/dist/cli/scopeInputs.js.map +1 -0
- package/dist/cli/skillCheck.d.ts +15 -1
- package/dist/cli/skillCheck.js +22 -1
- package/dist/cli/skillCheck.js.map +1 -1
- package/dist/cli/skillDirs.d.ts +5 -0
- package/dist/cli/skillDirs.js +13 -1
- package/dist/cli/skillDirs.js.map +1 -1
- package/dist/cli/skillSync.d.ts +4 -0
- package/dist/cli/skillSync.js +23 -0
- package/dist/cli/skillSync.js.map +1 -1
- package/dist/cli/sliceNew.d.ts +11 -7
- package/dist/cli/sliceNew.js +91 -18
- package/dist/cli/sliceNew.js.map +1 -1
- package/dist/cli/sliceSync.d.ts +50 -0
- package/dist/cli/sliceSync.js +170 -0
- package/dist/cli/sliceSync.js.map +1 -0
- package/dist/cli/status.d.ts +24 -1
- package/dist/cli/status.js +20 -1
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/systemInputs.d.ts +53 -10
- package/dist/cli/systemInputs.js +186 -11
- package/dist/cli/systemInputs.js.map +1 -1
- package/dist/cli/upgrade.d.ts +30 -7
- package/dist/cli/upgrade.js +434 -50
- package/dist/cli/upgrade.js.map +1 -1
- package/dist/cli/usageLog.d.ts +4 -2
- package/dist/cli/usageLog.js +4 -2
- package/dist/cli/usageLog.js.map +1 -1
- package/dist/cli.js +631 -100
- package/dist/cli.js.map +1 -1
- package/dist/emit/api.d.ts +29 -0
- package/dist/emit/api.js +220 -0
- package/dist/emit/api.js.map +1 -0
- package/dist/emit/apiCheckJson.d.ts +3 -0
- package/dist/emit/apiCheckJson.js +21 -0
- package/dist/emit/apiCheckJson.js.map +1 -0
- package/dist/emit/codeownersJson.d.ts +3 -0
- package/dist/emit/codeownersJson.js +19 -0
- package/dist/emit/codeownersJson.js.map +1 -0
- package/dist/emit/coverageJson.d.ts +5 -2
- package/dist/emit/coverageJson.js +6 -2
- package/dist/emit/coverageJson.js.map +1 -1
- package/dist/emit/diffJson.d.ts +1 -1
- package/dist/emit/diffJson.js +8 -1
- package/dist/emit/diffJson.js.map +1 -1
- package/dist/emit/engagementJson.d.ts +8 -0
- package/dist/emit/engagementJson.js +46 -0
- package/dist/emit/engagementJson.js.map +1 -0
- package/dist/emit/glossaryJson.d.ts +1 -1
- package/dist/emit/glossaryJson.js +4 -1
- package/dist/emit/glossaryJson.js.map +1 -1
- package/dist/emit/json.d.ts +17 -1
- package/dist/emit/json.js +30 -1
- package/dist/emit/json.js.map +1 -1
- package/dist/emit/metricsJson.d.ts +1 -1
- package/dist/emit/metricsJson.js +4 -1
- package/dist/emit/metricsJson.js.map +1 -1
- package/dist/emit/queryJson.d.ts +1 -1
- package/dist/emit/queryJson.js +7 -1
- package/dist/emit/queryJson.js.map +1 -1
- package/dist/emit/scopeJson.d.ts +12 -0
- package/dist/emit/scopeJson.js +26 -0
- package/dist/emit/scopeJson.js.map +1 -0
- package/dist/emit/skillCheckJson.d.ts +3 -2
- package/dist/emit/skillCheckJson.js +10 -2
- package/dist/emit/skillCheckJson.js.map +1 -1
- package/dist/emit/statusJson.d.ts +1 -1
- package/dist/emit/statusJson.js +15 -1
- package/dist/emit/statusJson.js.map +1 -1
- package/dist/emit/systemJson.d.ts +15 -8
- package/dist/emit/systemJson.js +28 -13
- package/dist/emit/systemJson.js.map +1 -1
- package/dist/emit/upgradeJson.d.ts +1 -1
- package/dist/emit/upgradeJson.js +4 -1
- package/dist/emit/upgradeJson.js.map +1 -1
- package/dist/emit/validateJson.d.ts +3 -3
- package/dist/emit/validateJson.js +10 -4
- package/dist/emit/validateJson.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +186 -37
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/diff.d.ts +4 -1
- package/dist/model/diff.js +44 -8
- package/dist/model/diff.js.map +1 -1
- package/dist/model/glossary.d.ts +2 -0
- package/dist/model/glossary.js +1 -0
- package/dist/model/glossary.js.map +1 -1
- package/dist/model/model.d.ts +55 -2
- package/dist/model/model.js +57 -0
- package/dist/model/model.js.map +1 -1
- package/dist/model/qualifiedRef.d.ts +18 -0
- package/dist/model/qualifiedRef.js +26 -0
- package/dist/model/qualifiedRef.js.map +1 -1
- package/dist/model/queryIndex.d.ts +9 -1
- package/dist/model/queryIndex.js +18 -2
- package/dist/model/queryIndex.js.map +1 -1
- package/dist/model/rules.d.ts +143 -2
- package/dist/model/rules.js +162 -3
- package/dist/model/rules.js.map +1 -1
- package/dist/model/validate.d.ts +40 -0
- package/dist/model/validate.js +147 -1
- package/dist/model/validate.js.map +1 -1
- package/dist/parser/ast.d.ts +31 -3
- package/dist/parser/parser.js +148 -14
- package/dist/parser/parser.js.map +1 -1
- package/dist/query/format.js +1 -1
- package/dist/query/format.js.map +1 -1
- package/dist/query/verbs.d.ts +6 -0
- package/dist/query/verbs.js +6 -2
- package/dist/query/verbs.js.map +1 -1
- package/dist/system/adaptation.d.ts +14 -0
- package/dist/system/adaptation.js +49 -0
- package/dist/system/adaptation.js.map +1 -0
- package/dist/system/codeowners.d.ts +77 -0
- package/dist/system/codeowners.js +242 -0
- package/dist/system/codeowners.js.map +1 -0
- package/dist/system/manifest.d.ts +10 -5
- package/dist/system/manifest.js +54 -26
- package/dist/system/manifest.js.map +1 -1
- package/dist/system/migrateManifest.d.ts +28 -0
- package/dist/system/migrateManifest.js +155 -0
- package/dist/system/migrateManifest.js.map +1 -0
- package/dist/system/scope.d.ts +46 -0
- package/dist/system/scope.js +132 -0
- package/dist/system/scope.js.map +1 -0
- package/dist/system/verify.d.ts +50 -13
- package/dist/system/verify.js +179 -58
- package/dist/system/verify.js.map +1 -1
- package/dist/templates.d.ts +2 -2
- package/dist/templates.js +23 -2
- package/dist/templates.js.map +1 -1
- package/package.json +6 -3
- package/plugin/.claude-plugin/plugin.json +18 -0
- package/plugin/agents/em-critic.md +27 -0
- package/plugin/agents/em-implementer.md +35 -0
- package/plugin/agents/em-reviewer.md +32 -0
- package/plugin/agents/em-validator.md +28 -0
- package/plugin/shared/reference/em-dsl.md +866 -0
- package/plugin/shared/reference/methodology.md +329 -0
- package/plugin/shared/reference/operating-principles.md +222 -0
- package/plugin/shared/reference/slice-doc-schema.md +566 -0
- package/plugin/shared/templates/conformance-report.md +85 -0
- package/plugin/shared/templates/constitution.md +136 -0
- package/plugin/shared/templates/model-readme.md +46 -0
- package/plugin/shared/templates/slice.md +251 -0
- package/plugin/shared/templates/state.md +77 -0
- package/plugin/skills/conform/SKILL.md +93 -0
- package/plugin/skills/conform/reference/conform.md +428 -0
- package/plugin/skills/design/SKILL.md +297 -0
- package/plugin/skills/discover/SKILL.md +90 -0
- package/plugin/skills/discover/reference/extract.md +147 -0
- package/plugin/skills/engagement/SKILL.md +193 -0
- package/plugin/skills/engagement/reference/engagement.md +144 -0
- package/plugin/skills/event-modeling/SKILL.md +132 -0
- package/plugin/skills/implement/SKILL.md +61 -0
- package/plugin/skills/implement/reference/implement.md +439 -0
- package/plugin/skills/review/SKILL.md +74 -0
- package/dist/emit/typespec.d.ts +0 -26
- package/dist/emit/typespec.js +0 -356
- package/dist/emit/typespec.js.map +0 -1
package/dist/cli/upgrade.js
CHANGED
|
@@ -21,32 +21,55 @@
|
|
|
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, writeFileSync } from "node:fs";
|
|
25
|
-
import { join } from "node:path";
|
|
24
|
+
import { existsSync, readFileSync, realpathSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
26
|
+
import { PUBLIC_SCALAR_TYPE_NAMES } from "../model/model.js";
|
|
27
|
+
import { findUnresolvedPublicFieldTypes } from "../model/validate.js";
|
|
26
28
|
import { realGit } from "./diff-inputs.js";
|
|
27
29
|
import { resolveSliceDocJoin } from "../catalog/docJoin.js";
|
|
28
30
|
import { validateOrphanedSliceDocs } from "../catalog/orphanedSliceDocValidate.js";
|
|
29
31
|
import { planMigration, verifyMigration } from "./migrateReactionShape.js";
|
|
30
|
-
import { planSkillSyncBundle, applySkillSyncBundle } from "./skillSync.js";
|
|
31
|
-
import { EM_ALL_SKILL_BUNDLE_DIRS, EM_SKILL_ANCHOR_DIR } from "./skillDirs.js";
|
|
32
|
-
import {
|
|
32
|
+
import { planSkillSyncBundle, applySkillSyncBundle, planAgentSync, applySkillSync } from "./skillSync.js";
|
|
33
|
+
import { EM_ALL_SKILL_BUNDLE_DIRS, EM_SKILL_ANCHOR_DIR, EM_AGENT_FILES } from "./skillDirs.js";
|
|
34
|
+
import { detectPlugin, checkPlugin, claudePluginsDir, mergePluginSettings, pluginInstallCommands, pluginSettingsEntries } from "./pluginPin.js";
|
|
35
|
+
import { syncAgentsMd } from "./agentsMd.js";
|
|
36
|
+
import { ciWorkflowPath, conformWorkflowPath, buildCiWorkflowFile, buildConformWorkflowFile, buildCiWorkflowFileMulti, buildConformWorkflowFileMulti, ciManagedBody, ciManagedBodyMulti, conformManagedBody, conformManagedBodyMulti, ciModelsFromManifest, planCiFile, applyCiFile, CI_WORKFLOW_MARKER, CONFORM_WORKFLOW_MARKER, } from "./ciInit.js";
|
|
33
37
|
import { findSpecifyRoot } from "./status.js";
|
|
34
|
-
import { scaffoldConstitution } from "../templates.js";
|
|
35
|
-
import { loadStateFile, parseState, setEmVersion, ensureOptionalBullets } from "./stateFile.js";
|
|
38
|
+
import { scaffoldConstitution, scaffoldStateFile } from "../templates.js";
|
|
39
|
+
import { loadStateFile, parseState, setEmVersion, ensureOptionalBullets, resolveStateFilePath } from "./stateFile.js";
|
|
36
40
|
import { fieldLineRegex, normalizeFieldValue, locateFrontmatterInner } from "./frontmatterSurgery.js";
|
|
37
41
|
import { localIsoDate } from "../util/localDate.js";
|
|
42
|
+
import { compile } from "../pipeline.js";
|
|
43
|
+
import { hasErrors } from "../model/validate.js";
|
|
44
|
+
import { LEGACY_SYSTEM_MANIFEST_SCHEMA_VERSION, parseManifest } from "../system/manifest.js";
|
|
45
|
+
import { planManifestMigration } from "../system/migrateManifest.js";
|
|
46
|
+
import { loadSource, SYSTEM_MANIFEST_FILE } from "./systemInputs.js";
|
|
38
47
|
export const UPGRADE_MIN_SUPPORTED_VERSION = "1.6.0";
|
|
39
48
|
// ---- Step 1: skill-bundle — delegates to `em skill sync`'s own plan/apply. ----
|
|
40
49
|
function vendoredSkillsRootOf(repoRoot) {
|
|
41
50
|
return join(repoRoot, ".claude", "skills");
|
|
42
51
|
}
|
|
52
|
+
function vendoredAgentsRootOf(repoRoot) {
|
|
53
|
+
return join(repoRoot, ".claude", "agents");
|
|
54
|
+
}
|
|
55
|
+
function packagedAgentsRootOf(ctx) {
|
|
56
|
+
return ctx.packagedAgentsRoot ?? join(ctx.packagedSkillsRoot, "..", "agents");
|
|
57
|
+
}
|
|
43
58
|
function detectSkillBundle(ctx) {
|
|
44
59
|
const vendoredRoot = vendoredSkillsRootOf(ctx.repoRoot);
|
|
60
|
+
// MIL-231/232: a repo that declares the plugin has no bundle to sync (any stray vendored copy is
|
|
61
|
+
// the `skill-plugin` step's business, not this one's).
|
|
62
|
+
if (detectPlugin(ctx.repoRoot))
|
|
63
|
+
return { applicable: false, reason: "plugin repo — nothing to sync" };
|
|
64
|
+
// MIL-232: the vendored bundle is about to be removed by `skill-plugin` (ordered first) — syncing it would be a throwaway commit.
|
|
65
|
+
if (detectSkillPlugin(ctx).applicable)
|
|
66
|
+
return { applicable: false, reason: "the vendored bundle is migrated to the plugin by skill-plugin — nothing to sync" };
|
|
45
67
|
if (!existsSync(join(vendoredRoot, EM_SKILL_ANCHOR_DIR))) {
|
|
46
68
|
return { applicable: false, reason: "no vendored skill bundle installed at .claude/skills/ — run `em skill install` first if you want one" };
|
|
47
69
|
}
|
|
48
70
|
const bundlePlan = planSkillSyncBundle(ctx.packagedSkillsRoot, vendoredRoot, EM_ALL_SKILL_BUNDLE_DIRS);
|
|
49
|
-
const
|
|
71
|
+
const agentPlan = planAgentSync(packagedAgentsRootOf(ctx), vendoredAgentsRootOf(ctx.repoRoot), EM_AGENT_FILES);
|
|
72
|
+
const totalChanges = bundlePlan.reduce((n, { plan }) => n + plan.changes.length, 0) + agentPlan.changes.length;
|
|
50
73
|
if (totalChanges === 0)
|
|
51
74
|
return { applicable: false, reason: "vendored skill bundle already matches the installed em" };
|
|
52
75
|
return { applicable: true, reason: `vendored skill bundle differs from the installed em in ${totalChanges} file(s)` };
|
|
@@ -54,11 +77,71 @@ function detectSkillBundle(ctx) {
|
|
|
54
77
|
function applySkillBundle(ctx) {
|
|
55
78
|
const vendoredRoot = vendoredSkillsRootOf(ctx.repoRoot);
|
|
56
79
|
const bundlePlan = planSkillSyncBundle(ctx.packagedSkillsRoot, vendoredRoot, EM_ALL_SKILL_BUNDLE_DIRS);
|
|
57
|
-
const
|
|
80
|
+
const vendoredAgents = vendoredAgentsRootOf(ctx.repoRoot);
|
|
81
|
+
const agentPlan = planAgentSync(packagedAgentsRootOf(ctx), vendoredAgents, EM_AGENT_FILES);
|
|
82
|
+
const totalChanges = bundlePlan.reduce((n, { plan }) => n + plan.changes.length, 0) + agentPlan.changes.length;
|
|
58
83
|
if (totalChanges === 0)
|
|
59
84
|
return { ok: false, message: "nothing to sync" };
|
|
60
85
|
applySkillSyncBundle(bundlePlan, ctx.packagedSkillsRoot, vendoredRoot);
|
|
61
|
-
|
|
86
|
+
applySkillSync(agentPlan, packagedAgentsRootOf(ctx), vendoredAgents);
|
|
87
|
+
const changedFiles = [
|
|
88
|
+
...bundlePlan.flatMap(({ dirName, plan }) => plan.changes.map((c) => join(".claude", "skills", dirName, c.relPath))),
|
|
89
|
+
...agentPlan.changes.map((c) => join(".claude", "agents", c.relPath)),
|
|
90
|
+
];
|
|
91
|
+
return { ok: true, changedFiles };
|
|
92
|
+
}
|
|
93
|
+
// ---- Step 1b: skill-plugin (MIL-232, R14) — vendored bundle -> pinned em plugin, one commit. ----
|
|
94
|
+
function detectSkillPlugin(ctx) {
|
|
95
|
+
const vendoredRoot = vendoredSkillsRootOf(ctx.repoRoot);
|
|
96
|
+
if (!existsSync(join(vendoredRoot, EM_SKILL_ANCHOR_DIR))) {
|
|
97
|
+
return { applicable: false, reason: "no vendored skill bundle installed at .claude/skills/ — nothing to migrate to the plugin" };
|
|
98
|
+
}
|
|
99
|
+
if (detectPlugin(ctx.repoRoot))
|
|
100
|
+
return { applicable: false, reason: "plugin already declared in .claude/settings.json" };
|
|
101
|
+
const settingsPath = join(ctx.repoRoot, ".claude", "settings.json");
|
|
102
|
+
if (existsSync(settingsPath) && mergePluginSettings(readFileSync(settingsPath, "utf8"), ctx.installedVersion) === null) {
|
|
103
|
+
return { applicable: false, reason: ".claude/settings.json is not a JSON object — fix it by hand, then re-run em upgrade" };
|
|
104
|
+
}
|
|
105
|
+
const e = pluginSettingsEntries(ctx.installedVersion);
|
|
106
|
+
return {
|
|
107
|
+
applicable: true,
|
|
108
|
+
reason: `vendored skill bundle → em plugin: remove ${EM_ALL_SKILL_BUNDLE_DIRS.map((d) => `.claude/skills/${d}`).join(", ")} and any of ${EM_AGENT_FILES.map((f) => `.claude/agents/${f}`).join(", ")}; ` +
|
|
109
|
+
`add extraKnownMarketplaces["${e.name}"] (github ${e.marketplace.source.repo}@${e.marketplace.source.ref}) and enabledPlugins["${e.enabledKey}"] to .claude/settings.json; ` +
|
|
110
|
+
`refresh the AGENTS.md managed section with the /em:* skill names`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function applySkillPlugin(ctx) {
|
|
114
|
+
const settingsPath = join(ctx.repoRoot, ".claude", "settings.json");
|
|
115
|
+
const existing = existsSync(settingsPath) ? readFileSync(settingsPath, "utf8") : null;
|
|
116
|
+
// Verify before any write: an unparseable settings.json must not cost the user their skills.
|
|
117
|
+
const merged = mergePluginSettings(existing, ctx.installedVersion);
|
|
118
|
+
if (merged === null)
|
|
119
|
+
return { ok: false, message: ".claude/settings.json is not a JSON object — nothing changed" };
|
|
120
|
+
const vendoredRoot = vendoredSkillsRootOf(ctx.repoRoot);
|
|
121
|
+
const changedFiles = [];
|
|
122
|
+
// Only the bundle's own directories; a sibling skill under .claude/skills/ is never touched.
|
|
123
|
+
for (const d of EM_ALL_SKILL_BUNDLE_DIRS) {
|
|
124
|
+
const target = join(vendoredRoot, d);
|
|
125
|
+
if (!existsSync(target))
|
|
126
|
+
continue;
|
|
127
|
+
rmSync(target, { recursive: true, force: true });
|
|
128
|
+
changedFiles.push(join(".claude", "skills", d));
|
|
129
|
+
}
|
|
130
|
+
// MIL-269 (R32): exactly the four em agent files (the plugin ships them as em:em-*); any other
|
|
131
|
+
// agent in the shared directory is never listed or touched.
|
|
132
|
+
for (const f of EM_AGENT_FILES) {
|
|
133
|
+
const target = join(vendoredAgentsRootOf(ctx.repoRoot), f);
|
|
134
|
+
if (!existsSync(target))
|
|
135
|
+
continue;
|
|
136
|
+
rmSync(target, { force: true });
|
|
137
|
+
changedFiles.push(join(".claude", "agents", f));
|
|
138
|
+
}
|
|
139
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
140
|
+
writeFileSync(settingsPath, merged);
|
|
141
|
+
changedFiles.push(join(".claude", "settings.json"));
|
|
142
|
+
const agents = syncAgentsMd(ctx.repoRoot);
|
|
143
|
+
if (agents.wrote)
|
|
144
|
+
changedFiles.push("AGENTS.md");
|
|
62
145
|
return { ok: true, changedFiles };
|
|
63
146
|
}
|
|
64
147
|
// ---- Step 2: reaction-shape — delegates to `em migrate`'s own plan/verify/apply. ----
|
|
@@ -91,13 +174,40 @@ function applyReactionShape(ctx) {
|
|
|
91
174
|
writeFileSync(ctx.modelFile, plan.rewritten);
|
|
92
175
|
return { ok: true, changedFiles: [ctx.modelFile] };
|
|
93
176
|
}
|
|
94
|
-
// ---- Step 3: state-file — ensure Model version:/Certified:
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
177
|
+
// ---- Step 3: state-file — scaffold when absent (MIL-257); else ensure Model version:/Certified:
|
|
178
|
+
// are present (MIL-218 defaults). ----
|
|
179
|
+
// `Em version:` itself is deliberately NOT this step's job when the file already exists —
|
|
180
|
+
// `runUpgradeApply` always writes it as its own dedicated final commit, whether or not this step
|
|
181
|
+
// ran (module header). A freshly scaffolded file is written already carrying the installed
|
|
182
|
+
// version (scaffoldStateFile's own `Em version:` fill), so that final commit is then a no-op.
|
|
183
|
+
/** MIL-257: the one human-readable sentence for "the state file exists but `parseState` rejected
|
|
184
|
+
* it", naming the file so the `--check` final line / `--apply` refusal is self-explanatory.
|
|
185
|
+
* `parseState`'s "missing bullet line(s): ..." reads as `is missing ...`; its format-mismatch
|
|
186
|
+
* messages (`"- **X:**" doesn't match ...`) follow a colon instead. */
|
|
187
|
+
export function describeStateFileFailure(path, parseMessage) {
|
|
188
|
+
return parseMessage.startsWith("missing ") ? `state file ${path} is ${parseMessage}` : `state file ${path}: ${parseMessage}`;
|
|
189
|
+
}
|
|
190
|
+
/** MIL-257: non-null when the state file exists but isn't parseable — shared by `detectStateFile`
|
|
191
|
+
* (so the checklist line shows the real cause rather than the migration-tolerant
|
|
192
|
+
* `Model version:`/`Certified:` item) and `applyStateFile`. */
|
|
193
|
+
function stateFileParseFailure(ctx) {
|
|
98
194
|
const loaded = loadStateFile(ctx.baseDir);
|
|
99
195
|
if (!loaded.ok)
|
|
100
|
-
return
|
|
196
|
+
return null; // absent — scaffolded, not an error (MIL-257)
|
|
197
|
+
const parsed = parseState(loaded.text);
|
|
198
|
+
return parsed.ok ? null : describeStateFileFailure(loaded.path, parsed.message);
|
|
199
|
+
}
|
|
200
|
+
function detectStateFile(ctx) {
|
|
201
|
+
const loaded = loadStateFile(ctx.baseDir);
|
|
202
|
+
if (!loaded.ok) {
|
|
203
|
+
return {
|
|
204
|
+
applicable: true,
|
|
205
|
+
reason: `no state file at ${resolveStateFilePath(ctx.baseDir)} — em upgrade will scaffold one (Current phase: discover, Current step: 1, Last conformance/Last stakeholder review: never)`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const failure = stateFileParseFailure(ctx);
|
|
209
|
+
if (failure !== null)
|
|
210
|
+
return { applicable: false, reason: failure };
|
|
101
211
|
const patched = ensureOptionalBullets(loaded.text, "1970-01-01");
|
|
102
212
|
if (!patched.ok)
|
|
103
213
|
return { applicable: false, reason: patched.message };
|
|
@@ -107,9 +217,18 @@ function detectStateFile(ctx) {
|
|
|
107
217
|
}
|
|
108
218
|
function applyStateFile(ctx) {
|
|
109
219
|
const loaded = loadStateFile(ctx.baseDir);
|
|
110
|
-
if (!loaded.ok)
|
|
111
|
-
return { ok: false, message: loaded.message };
|
|
112
220
|
const today = localIsoDate();
|
|
221
|
+
if (!loaded.ok) {
|
|
222
|
+
// MIL-257: reuse `em scaffold`'s own generator (templates.ts scaffoldStateFile) rather than a
|
|
223
|
+
// second one. `Model file:` names the model actually being upgraded (its own basename).
|
|
224
|
+
const path = resolveStateFilePath(ctx.baseDir);
|
|
225
|
+
const slug = basename(ctx.modelFile, extname(ctx.modelFile));
|
|
226
|
+
writeFileSync(path, scaffoldStateFile(ctx.model.name, slug, today, ctx.installedVersion));
|
|
227
|
+
return { ok: true, changedFiles: [path] };
|
|
228
|
+
}
|
|
229
|
+
const failure = stateFileParseFailure(ctx);
|
|
230
|
+
if (failure !== null)
|
|
231
|
+
return { ok: false, message: failure };
|
|
113
232
|
const patched = ensureOptionalBullets(loaded.text, today);
|
|
114
233
|
if (!patched.ok)
|
|
115
234
|
return { ok: false, message: patched.message };
|
|
@@ -126,32 +245,68 @@ function applyStateFile(ctx) {
|
|
|
126
245
|
// Not anchored on a literal "em coverage" — the generated line pins an exact version
|
|
127
246
|
// (`npx @milehimikey/em@1.9.0 coverage "..."`, ciInit.ts's own `em` template variable), so
|
|
128
247
|
// "em" and "coverage" are never adjacent in the real file.
|
|
129
|
-
const CI_INIT_ARGS_RE = /\bcoverage "([^"]*)" --tests "([^"]*)" --strict\b
|
|
130
|
-
function extractCiInitArgs(emCiContent) {
|
|
131
|
-
const
|
|
132
|
-
|
|
248
|
+
const CI_INIT_ARGS_RE = /\bcoverage "([^"]*)" --tests "([^"]*)" --strict\b/g;
|
|
249
|
+
function extractCiInitArgs(emCiContent, repoRoot) {
|
|
250
|
+
const all = [...emCiContent.matchAll(CI_INIT_ARGS_RE)];
|
|
251
|
+
if (all.length === 0)
|
|
252
|
+
return { args: null };
|
|
253
|
+
const named = [...new Set(all.map((m) => m[1]))];
|
|
254
|
+
const base = { model: all[0][1], testsDir: all[0][2] };
|
|
255
|
+
if (named.length < 2)
|
|
256
|
+
return { args: { ...base, models: null } };
|
|
257
|
+
// A multi-model block: regenerate it from the manifest, never from a single model.
|
|
258
|
+
const loaded = ciModelsFromManifest(repoRoot, repoRoot);
|
|
259
|
+
if (!loaded.ok) {
|
|
260
|
+
return {
|
|
261
|
+
args: null,
|
|
262
|
+
blocked: `em-ci.yml covers ${named.length} models (${named.join(", ")}) but no readable system.yaml sits at the repo root (${loaded.message}) - run \`em ci init <system.yaml>\` by hand if it needs refreshing`,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
return { args: { ...base, models: loaded.models } };
|
|
266
|
+
}
|
|
267
|
+
function ciBlockFiles(args, ctx) {
|
|
268
|
+
// MIL-231: same plugin signal `em ci init` uses, so the two never disagree about the conform lines.
|
|
269
|
+
const usePlugin = detectPlugin(ctx.repoRoot) !== null;
|
|
270
|
+
if (args.models) {
|
|
271
|
+
return {
|
|
272
|
+
ci: [buildCiWorkflowFileMulti(SYSTEM_MANIFEST_ARG, args.models, args.testsDir, ctx.installedVersion), ciManagedBodyMulti(args.models, args.testsDir, ctx.installedVersion)],
|
|
273
|
+
conform: [buildConformWorkflowFileMulti(SYSTEM_MANIFEST_ARG, args.models, ctx.installedVersion, usePlugin), conformManagedBodyMulti(args.models, ctx.installedVersion, usePlugin)],
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
ci: [buildCiWorkflowFile(args.model, args.testsDir, ctx.installedVersion), ciManagedBody(args.model, args.testsDir, ctx.installedVersion)],
|
|
278
|
+
conform: [buildConformWorkflowFile(args.model, ctx.installedVersion, usePlugin), conformManagedBody(args.model, ctx.installedVersion, usePlugin)],
|
|
279
|
+
};
|
|
133
280
|
}
|
|
281
|
+
/** Only used for the generated header's wording on a from-scratch file; `ci-block` only ever
|
|
282
|
+
* patches the managed block of a file that already exists, so this never reaches disk there. */
|
|
283
|
+
const SYSTEM_MANIFEST_ARG = "system.yaml";
|
|
134
284
|
function ciBlockPlans(ctx) {
|
|
135
285
|
const ciPath = ciWorkflowPath(ctx.repoRoot);
|
|
136
286
|
const conformPath = conformWorkflowPath(ctx.repoRoot);
|
|
137
287
|
if (!existsSync(ciPath))
|
|
138
288
|
return { ciPath, ciStale: false, conformPath, conformStale: false, args: null };
|
|
139
|
-
const
|
|
140
|
-
if (!args)
|
|
141
|
-
return { ciPath, ciStale: false, conformPath, conformStale: false, args: null };
|
|
142
|
-
const
|
|
289
|
+
const extracted = extractCiInitArgs(readFileSync(ciPath, "utf8"), ctx.repoRoot);
|
|
290
|
+
if (!extracted.args)
|
|
291
|
+
return { ciPath, ciStale: false, conformPath, conformStale: false, args: null, blocked: "blocked" in extracted ? extracted.blocked : undefined };
|
|
292
|
+
const args = extracted.args;
|
|
293
|
+
const multi = args.models !== null;
|
|
294
|
+
const files = ciBlockFiles(args, ctx);
|
|
295
|
+
const ciStatus = planCiFile(ciPath, files.ci[0], files.ci[1], CI_WORKFLOW_MARKER, false, multi);
|
|
143
296
|
const ciStale = ciStatus.kind === "stale";
|
|
144
297
|
let conformStale = false;
|
|
145
298
|
if (existsSync(conformPath)) {
|
|
146
|
-
const conformStatus = planCiFile(conformPath,
|
|
299
|
+
const conformStatus = planCiFile(conformPath, files.conform[0], files.conform[1], CONFORM_WORKFLOW_MARKER, false, multi);
|
|
147
300
|
conformStale = conformStatus.kind === "stale";
|
|
148
301
|
}
|
|
149
302
|
return { ciPath, ciStale, conformPath, conformStale, args };
|
|
150
303
|
}
|
|
151
304
|
function detectCiBlock(ctx) {
|
|
152
|
-
const { ciPath, ciStale, conformStale, args } = ciBlockPlans(ctx);
|
|
305
|
+
const { ciPath, ciStale, conformStale, args, blocked } = ciBlockPlans(ctx);
|
|
153
306
|
if (!existsSync(ciPath))
|
|
154
307
|
return { applicable: false, reason: "no .github/workflows/em-ci.yml — em upgrade never creates one" };
|
|
308
|
+
if (!args && blocked)
|
|
309
|
+
return { applicable: false, reason: blocked };
|
|
155
310
|
if (!args)
|
|
156
311
|
return { applicable: false, reason: "can't determine the <model>/--tests arguments em-ci.yml was generated with — run `em ci init <model>` by hand if it needs refreshing" };
|
|
157
312
|
if (!ciStale && !conformStale)
|
|
@@ -162,15 +317,15 @@ function applyCiBlock(ctx) {
|
|
|
162
317
|
const { ciPath, ciStale, conformPath, conformStale, args } = ciBlockPlans(ctx);
|
|
163
318
|
if (!args || (!ciStale && !conformStale))
|
|
164
319
|
return { ok: false, message: "nothing to refresh" };
|
|
320
|
+
const multi = args.models !== null;
|
|
321
|
+
const files = ciBlockFiles(args, ctx);
|
|
165
322
|
const changedFiles = [];
|
|
166
323
|
if (ciStale) {
|
|
167
|
-
|
|
168
|
-
applyCiFile(ciPath, status);
|
|
324
|
+
applyCiFile(ciPath, planCiFile(ciPath, files.ci[0], files.ci[1], CI_WORKFLOW_MARKER, false, multi));
|
|
169
325
|
changedFiles.push(ciPath);
|
|
170
326
|
}
|
|
171
327
|
if (conformStale) {
|
|
172
|
-
|
|
173
|
-
applyCiFile(conformPath, status);
|
|
328
|
+
applyCiFile(conformPath, planCiFile(conformPath, files.conform[0], files.conform[1], CONFORM_WORKFLOW_MARKER, false, multi));
|
|
174
329
|
changedFiles.push(conformPath);
|
|
175
330
|
}
|
|
176
331
|
return { ok: true, changedFiles };
|
|
@@ -199,12 +354,191 @@ function applyConstitution(ctx) {
|
|
|
199
354
|
writeFileSync(path, scaffoldConstitution(ctx.model.name));
|
|
200
355
|
return { ok: true, changedFiles: [path] };
|
|
201
356
|
}
|
|
357
|
+
// ---- Step 6: ratified-signoff (MIL-259) — `--slice-ready` now requires a recorded `ratifiedBy`,
|
|
358
|
+
// so a doc that reached `ready-to-implement` before sign-offs were recorded (pre-1.8) would
|
|
359
|
+
// suddenly read not-ready. Grandfather exactly those: `version: 1` (never reratified — a
|
|
360
|
+
// reratified doc has `version > 1` and is mid-sign-off, which only a human can resolve; see the
|
|
361
|
+
// `ready-to-implement-no-ratifiedby` human item) with no `ratifiedBy`. A doc with no `version:`
|
|
362
|
+
// at all is treated as version 1 (the schema's default). ----
|
|
363
|
+
export const GRANDFATHERED_RATIFIER = "grandfathered (unsigned before em 1.14)";
|
|
364
|
+
/** Pure text transform: inserts the grandfather sign-off after the `status:` line, using only
|
|
365
|
+
* the key lines that are missing (a stray `ratifiedOn:` is kept as-is), in the file's own EOL.
|
|
366
|
+
* `null` when the doc isn't a candidate (no frontmatter/status, or `ratifiedBy` already set). */
|
|
367
|
+
export function applyGrandfatherSignoff(raw, ratifiedOn) {
|
|
368
|
+
const range = locateFrontmatterInner(raw);
|
|
369
|
+
if (!range)
|
|
370
|
+
return null;
|
|
371
|
+
const inner = raw.slice(range.innerStart, range.innerEnd);
|
|
372
|
+
const statusMatch = fieldLineRegex("status").exec(inner);
|
|
373
|
+
if (!statusMatch)
|
|
374
|
+
return null;
|
|
375
|
+
const byMatch = fieldLineRegex("ratifiedBy").exec(inner);
|
|
376
|
+
if (byMatch && normalizeFieldValue(byMatch[2]) !== null)
|
|
377
|
+
return null;
|
|
378
|
+
const onMatch = fieldLineRegex("ratifiedOn").exec(inner);
|
|
379
|
+
const statusEnd = statusMatch.index + statusMatch[0].length;
|
|
380
|
+
const eol = inner.slice(statusEnd).startsWith("\r\n") ? "\r\n" : "\n";
|
|
381
|
+
const value = `"${GRANDFATHERED_RATIFIER}"`;
|
|
382
|
+
// Edits against `inner`, applied highest index first so earlier offsets stay valid. A blank
|
|
383
|
+
// `ratifiedBy:` line is filled in place; missing lines are inserted right after `status:`.
|
|
384
|
+
const missing = [];
|
|
385
|
+
if (!byMatch)
|
|
386
|
+
missing.push(`ratifiedBy: ${value}`);
|
|
387
|
+
if (!onMatch)
|
|
388
|
+
missing.push(`ratifiedOn: ${ratifiedOn}`);
|
|
389
|
+
const edits = [];
|
|
390
|
+
if (missing.length > 0)
|
|
391
|
+
edits.push({ index: statusEnd, oldLen: 0, next: eol + missing.join(eol) });
|
|
392
|
+
if (byMatch)
|
|
393
|
+
edits.push({ index: byMatch.index, oldLen: byMatch[0].length, next: `${byMatch[1]}${value}` });
|
|
394
|
+
edits.sort((a, b) => b.index - a.index);
|
|
395
|
+
let updated = inner;
|
|
396
|
+
for (const e of edits)
|
|
397
|
+
updated = updated.slice(0, e.index) + e.next + updated.slice(e.index + e.oldLen);
|
|
398
|
+
return raw.slice(0, range.innerStart) + updated + raw.slice(range.innerEnd);
|
|
399
|
+
}
|
|
400
|
+
function grandfatherCandidates(ctx) {
|
|
401
|
+
const seen = new Set();
|
|
402
|
+
const out = [];
|
|
403
|
+
for (const d of nonContinuationDocs(ctx)) {
|
|
404
|
+
if (d.status !== "ready-to-implement" || d.ratifiedBy)
|
|
405
|
+
continue;
|
|
406
|
+
if ((d.version ?? 1) !== 1)
|
|
407
|
+
continue;
|
|
408
|
+
if (seen.has(d.path))
|
|
409
|
+
continue;
|
|
410
|
+
seen.add(d.path);
|
|
411
|
+
out.push({ key: d.key, path: d.path });
|
|
412
|
+
}
|
|
413
|
+
return out.sort((a, b) => a.key.localeCompare(b.key));
|
|
414
|
+
}
|
|
415
|
+
function detectRatifiedSignoff(ctx) {
|
|
416
|
+
const keys = grandfatherCandidates(ctx).map((c) => c.key);
|
|
417
|
+
if (keys.length === 0)
|
|
418
|
+
return { applicable: false, reason: "no version-1 ready-to-implement doc is missing a ratifiedBy" };
|
|
419
|
+
return {
|
|
420
|
+
applicable: true,
|
|
421
|
+
reason: `${keys.length} ready-to-implement doc(s) with no recorded sign-off (--slice-ready now requires one): ${keys.join(", ")} — em upgrade will record ratifiedBy: "${GRANDFATHERED_RATIFIER}"`,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function applyRatifiedSignoff(ctx) {
|
|
425
|
+
const candidates = grandfatherCandidates(ctx);
|
|
426
|
+
if (candidates.length === 0)
|
|
427
|
+
return { ok: false, message: "nothing to grandfather" };
|
|
428
|
+
const today = localIsoDate();
|
|
429
|
+
const writes = [];
|
|
430
|
+
for (const c of candidates) {
|
|
431
|
+
const file = join(ctx.baseDir, c.path);
|
|
432
|
+
const next = applyGrandfatherSignoff(readFileSync(file, "utf8"), today);
|
|
433
|
+
if (next === null)
|
|
434
|
+
return { ok: false, message: `${c.path}: could not locate status: in the frontmatter` };
|
|
435
|
+
writes.push({ file, text: next });
|
|
436
|
+
}
|
|
437
|
+
for (const w of writes)
|
|
438
|
+
writeFileSync(w.file, w.text);
|
|
439
|
+
return { ok: true, changedFiles: writes.map((w) => w.file) };
|
|
440
|
+
}
|
|
441
|
+
// ---- Step 7: system-manifest (MIL-235, R3) — system.yaml 1.0 -> 2.0. ----
|
|
442
|
+
// The one mechanical step that writes OTHER models' files: the legacy manifest's seams become
|
|
443
|
+
// `consumes` clauses on each consuming translation and its owners become `owner "…"` on each
|
|
444
|
+
// member model's header, in the same single commit as the manifest rewrite (its `Em-Upgrade:`
|
|
445
|
+
// trailer is what lets MIL-240's scope gate exempt that cross-model change set). Planned by
|
|
446
|
+
// src/system/migrateManifest.ts over export documents; this half finds the manifest, loads the
|
|
447
|
+
// members, verifies every rewritten model still compiles clean, and only then writes anything.
|
|
448
|
+
/** The nearest `system.yaml` from `baseDir` upward, stopping at (and including) `repoRoot`. */
|
|
449
|
+
function findSystemManifest(ctx) {
|
|
450
|
+
// Real paths on both sides: git reports the repo root symlink-resolved (macOS /var -> /private/var).
|
|
451
|
+
const real = (p) => {
|
|
452
|
+
try {
|
|
453
|
+
return realpathSync(p);
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
return resolve(p);
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
const root = real(ctx.repoRoot);
|
|
460
|
+
let dir = real(ctx.baseDir);
|
|
461
|
+
for (;;) {
|
|
462
|
+
const candidate = join(dir, SYSTEM_MANIFEST_FILE);
|
|
463
|
+
if (existsSync(candidate))
|
|
464
|
+
return candidate;
|
|
465
|
+
if (dir === root)
|
|
466
|
+
return null;
|
|
467
|
+
const parent = dirname(dir);
|
|
468
|
+
const rel = relative(root, parent);
|
|
469
|
+
if (parent === dir || rel.startsWith("..") || isAbsolute(rel))
|
|
470
|
+
return null;
|
|
471
|
+
dir = parent;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
function loadLegacyManifest(ctx) {
|
|
475
|
+
const path = findSystemManifest(ctx);
|
|
476
|
+
if (path === null)
|
|
477
|
+
return { path: null, reason: `no ${SYSTEM_MANIFEST_FILE} between the model and the repository root` };
|
|
478
|
+
const text = readFileSync(path, "utf8");
|
|
479
|
+
const parsed = parseManifest(text);
|
|
480
|
+
if (!parsed.ok)
|
|
481
|
+
return { path, reason: `${path} does not parse — run \`em system ${path}\` to see why` };
|
|
482
|
+
if (parsed.manifest.systemSchemaVersion !== LEGACY_SYSTEM_MANIFEST_SCHEMA_VERSION) {
|
|
483
|
+
return { path, reason: `${path} is already systemSchemaVersion "${parsed.manifest.systemSchemaVersion}"` };
|
|
484
|
+
}
|
|
485
|
+
return { path, text, manifest: parsed.manifest };
|
|
486
|
+
}
|
|
487
|
+
function detectSystemManifest(ctx) {
|
|
488
|
+
const loaded = loadLegacyManifest(ctx);
|
|
489
|
+
if (!("manifest" in loaded))
|
|
490
|
+
return { applicable: false, reason: loaded.reason };
|
|
491
|
+
const m = loaded.manifest;
|
|
492
|
+
return {
|
|
493
|
+
applicable: true,
|
|
494
|
+
reason: `${loaded.path} is systemSchemaVersion "1.0" — em upgrade will move its ${m.seams.length} seam(s) into \`consumes\` clauses, ` +
|
|
495
|
+
`its owner(s) onto the ${m.models.length} model header(s), and rewrite it to "2.0"`,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function applySystemManifest(ctx) {
|
|
499
|
+
const loaded = loadLegacyManifest(ctx);
|
|
500
|
+
if (!("manifest" in loaded))
|
|
501
|
+
return { ok: false, message: loaded.reason };
|
|
502
|
+
const baseDir = dirname(loaded.path);
|
|
503
|
+
const members = [];
|
|
504
|
+
for (const entry of loaded.manifest.models) {
|
|
505
|
+
const file = isAbsolute(entry.source) ? entry.source : join(baseDir, entry.source);
|
|
506
|
+
const source = loadSource(file);
|
|
507
|
+
if ("error" in source)
|
|
508
|
+
return { ok: false, message: `model "${entry.key}": ${source.error}` };
|
|
509
|
+
members.push({ key: entry.key, file, text: source.sourceKind === "em" ? readFileSync(file, "utf8") : null, doc: source.doc });
|
|
510
|
+
}
|
|
511
|
+
const plan = planManifestMigration(loaded.text, loaded.manifest, members);
|
|
512
|
+
if (!plan.ok)
|
|
513
|
+
return { ok: false, message: plan.message };
|
|
514
|
+
// Verify before write: every rewritten model must still compile with no errors.
|
|
515
|
+
for (const f of plan.files) {
|
|
516
|
+
try {
|
|
517
|
+
const { diagnostics } = compile(f.text);
|
|
518
|
+
if (hasErrors(diagnostics))
|
|
519
|
+
return { ok: false, message: `the rewrite of ${f.file} would introduce validation errors — aborting, nothing written` };
|
|
520
|
+
}
|
|
521
|
+
catch (e) {
|
|
522
|
+
return { ok: false, message: `the rewrite of ${f.file} does not parse (${e.message}) — aborting, nothing written` };
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
const reparsed = parseManifest(plan.manifestText);
|
|
526
|
+
if (!reparsed.ok)
|
|
527
|
+
return { ok: false, message: `the rewritten ${loaded.path} does not parse as 2.0 — aborting, nothing written` };
|
|
528
|
+
for (const f of plan.files)
|
|
529
|
+
writeFileSync(f.file, f.text);
|
|
530
|
+
writeFileSync(loaded.path, plan.manifestText);
|
|
531
|
+
return { ok: true, changedFiles: [...plan.files.map((f) => f.file), loaded.path] };
|
|
532
|
+
}
|
|
202
533
|
export const UPGRADE_STEPS = [
|
|
534
|
+
{ id: "skill-plugin", sinceVersion: "1.14.0", detect: detectSkillPlugin, apply: applySkillPlugin },
|
|
203
535
|
{ id: "skill-bundle", sinceVersion: "1.7.0", detect: detectSkillBundle, apply: applySkillBundle },
|
|
204
536
|
{ id: "reaction-shape", sinceVersion: "1.8.0", detect: detectReactionShape, apply: applyReactionShape },
|
|
205
537
|
{ id: "state-file", sinceVersion: "1.13.0", detect: detectStateFile, apply: applyStateFile },
|
|
206
538
|
{ id: "ci-block", sinceVersion: "1.9.0", detect: detectCiBlock, apply: applyCiBlock },
|
|
207
539
|
{ id: "constitution", sinceVersion: "1.11.0", detect: detectConstitution, apply: applyConstitution },
|
|
540
|
+
{ id: "ratified-signoff", sinceVersion: "1.14.0", detect: detectRatifiedSignoff, apply: applyRatifiedSignoff },
|
|
541
|
+
{ id: "system-manifest", sinceVersion: "1.14.0", detect: detectSystemManifest, apply: applySystemManifest },
|
|
208
542
|
];
|
|
209
543
|
function elementRefOf(ctx) {
|
|
210
544
|
return (id) => ctx.refs.refById.get(id);
|
|
@@ -220,7 +554,7 @@ function nonContinuationDocs(ctx) {
|
|
|
220
554
|
const { doc, continuationOf } = resolveSliceDocJoin(ctx.model, ctx.refs, slice, key, ctx.baseDir, elementRefOf(ctx));
|
|
221
555
|
if (continuationOf !== null)
|
|
222
556
|
return;
|
|
223
|
-
results.push({ key, status: doc.status, ratifiedBy: doc.ratifiedBy });
|
|
557
|
+
results.push({ key, status: doc.status, ratifiedBy: doc.ratifiedBy, version: doc.version, path: doc.path });
|
|
224
558
|
});
|
|
225
559
|
return results;
|
|
226
560
|
}
|
|
@@ -251,14 +585,14 @@ function detectContinuationHasOwnDoc(ctx) {
|
|
|
251
585
|
}
|
|
252
586
|
function detectReadyNoRatifiedBy(ctx) {
|
|
253
587
|
const keys = nonContinuationDocs(ctx)
|
|
254
|
-
.filter((d) => d.status === "ready-to-implement" && !d.ratifiedBy)
|
|
588
|
+
.filter((d) => d.status === "ready-to-implement" && !d.ratifiedBy && (d.version ?? 1) > 1)
|
|
255
589
|
.map((d) => d.key)
|
|
256
590
|
.sort();
|
|
257
591
|
if (keys.length === 0)
|
|
258
592
|
return null;
|
|
259
593
|
return {
|
|
260
594
|
id: "ready-to-implement-no-ratifiedby",
|
|
261
|
-
reason: `${keys.length} ready-to-implement doc(s) with no ratifiedBy: ${keys.join(", ")} — run \`em slice ratify --by <name>\` on each`,
|
|
595
|
+
reason: `${keys.length} ready-to-implement doc(s) with no ratifiedBy (version > 1, reratified and awaiting a fresh sign-off): ${keys.join(", ")} — run \`em slice ratify --by <name>\` on each`,
|
|
262
596
|
};
|
|
263
597
|
}
|
|
264
598
|
function detectCoverageScopeDefault(ctx) {
|
|
@@ -301,6 +635,36 @@ function detectPredates16(ctx) {
|
|
|
301
635
|
reason: `${plan.refusals.length} old-shape reaction site(s) can't be auto-migrated — run \`em migrate\` by hand, resolve the refusal(s), then re-run \`em upgrade\``,
|
|
302
636
|
};
|
|
303
637
|
}
|
|
638
|
+
/** MIL-237: strict public types (briefing R7). Lists every field of a `public` element (or of a
|
|
639
|
+
* declared type reachable from one) whose type is not in the fixed public type table and names
|
|
640
|
+
* no declared type — the same set `em validate`'s `public-field-type-unresolved` errors on.
|
|
641
|
+
* Detect-only: choosing the right type (or dropping `public`) is a modeling decision. */
|
|
642
|
+
function detectPublicFieldTypesUnresolved(ctx) {
|
|
643
|
+
const found = findUnresolvedPublicFieldTypes(ctx.model);
|
|
644
|
+
if (found.length === 0)
|
|
645
|
+
return null;
|
|
646
|
+
const items = found.map((f) => `${f.owner.name}.${f.field}: ${f.type ?? "(no type)"}`);
|
|
647
|
+
return {
|
|
648
|
+
id: "public-field-types-unresolved",
|
|
649
|
+
reason: `${found.length} public field(s) without a public type: ${items.join(", ")} — give each a type from ` +
|
|
650
|
+
`${PUBLIC_SCALAR_TYPE_NAMES.join(", ")}, \`X[]\`, or a declared \`type\` (em 1.14 strict public types), or drop \`public\``,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
/** MIL-232: the machine-level plugin registration is per user and cannot be done by `em upgrade`.
|
|
654
|
+
* Fires when the `skill-plugin` step is pending (it will declare the plugin), or when the repo
|
|
655
|
+
* declares the plugin at the installed version but the machine registry lacks it. */
|
|
656
|
+
function detectPluginInstallLocally(ctx) {
|
|
657
|
+
const commands = pluginInstallCommands(ctx.installedVersion);
|
|
658
|
+
const snippet = `run \`${commands.join(" && ")}\` on each developer machine and in CI (the generated em-ci.yml does it)`;
|
|
659
|
+
if (detectSkillPlugin(ctx).applicable) {
|
|
660
|
+
return { id: "plugin-install-locally", reason: `the em plugin will be declared in .claude/settings.json but is registered per machine — after --apply, ${snippet}` };
|
|
661
|
+
}
|
|
662
|
+
const check = checkPlugin(ctx.repoRoot, ctx.installedVersion, claudePluginsDir());
|
|
663
|
+
if (check && check.findings.some((f) => f.code === "plugin-not-installed-locally")) {
|
|
664
|
+
return { id: "plugin-install-locally", reason: `the em plugin is declared but not registered on this machine — ${snippet}` };
|
|
665
|
+
}
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
304
668
|
const HUMAN_DETECTORS = [
|
|
305
669
|
detectNoModelVersion,
|
|
306
670
|
detectContinuationHasOwnDoc,
|
|
@@ -308,6 +672,8 @@ const HUMAN_DETECTORS = [
|
|
|
308
672
|
detectCoverageScopeDefault,
|
|
309
673
|
detectUnratifiedConstitution,
|
|
310
674
|
detectPredates16,
|
|
675
|
+
detectPublicFieldTypesUnresolved,
|
|
676
|
+
detectPluginInstallLocally,
|
|
311
677
|
];
|
|
312
678
|
export function detectHumanItems(ctx) {
|
|
313
679
|
return HUMAN_DETECTORS.map((d) => d(ctx)).filter((x) => x !== null);
|
|
@@ -360,11 +726,17 @@ export function hasGitIdentity(repoRoot, runGit = realGit) {
|
|
|
360
726
|
const email = runGit(["-C", repoRoot, "config", "user.email"]);
|
|
361
727
|
return name.status === 0 && name.stdout.trim().length > 0 && email.status === 0 && email.stdout.trim().length > 0;
|
|
362
728
|
}
|
|
363
|
-
|
|
729
|
+
/** The commit trailer key every `em upgrade --apply` commit carries (MIL-235, R2):
|
|
730
|
+
* `Em-Upgrade: <step-id>` (or `em-version` for the final stamp commit). Readers use
|
|
731
|
+
* `git log --format=%(trailers:key=Em-Upgrade,valueonly)` — MIL-240's scope gate exempts files
|
|
732
|
+
* changed only by such commits, since a mechanical migration may legitimately span models. */
|
|
733
|
+
export const EM_UPGRADE_TRAILER = "Em-Upgrade";
|
|
734
|
+
function gitCommitAll(repoRoot, message, trailerValue, runGit) {
|
|
364
735
|
const add = runGit(["-C", repoRoot, "add", "-A"]);
|
|
365
736
|
if (add.status !== 0)
|
|
366
737
|
return { ok: false, message: `git add failed: ${(add.stderr || "").trim() || "unknown error"}` };
|
|
367
|
-
|
|
738
|
+
// Second `-m` = its own paragraph, which git reads as the trailer block.
|
|
739
|
+
const commit = runGit(["-C", repoRoot, "commit", "-m", message, "-m", `${EM_UPGRADE_TRAILER}: ${trailerValue}`]);
|
|
368
740
|
if (commit.status !== 0)
|
|
369
741
|
return { ok: false, message: `git commit failed: ${(commit.stderr || "").trim() || "unknown error"}` };
|
|
370
742
|
return { ok: true };
|
|
@@ -383,14 +755,14 @@ function gitDiscardAll(repoRoot, runGit) {
|
|
|
383
755
|
export function detectUpgrade(ctx) {
|
|
384
756
|
const loaded = loadStateFile(ctx.baseDir);
|
|
385
757
|
let recorded = null;
|
|
758
|
+
// MIL-257: an ABSENT state file is no longer an error — the `state-file` step scaffolds it — so
|
|
759
|
+
// `stateFileError` is `null` for it (the pending step's own reason carries the detail). It is
|
|
760
|
+
// set only for a file that exists but doesn't parse, and now names the file.
|
|
386
761
|
let stateFileError = null;
|
|
387
|
-
if (
|
|
388
|
-
stateFileError = loaded.message;
|
|
389
|
-
}
|
|
390
|
-
else {
|
|
762
|
+
if (loaded.ok) {
|
|
391
763
|
const parsed = parseState(loaded.text);
|
|
392
764
|
if (!parsed.ok)
|
|
393
|
-
stateFileError = parsed.message;
|
|
765
|
+
stateFileError = describeStateFileFailure(loaded.path, parsed.message);
|
|
394
766
|
else
|
|
395
767
|
recorded = parsed.state.emVersion;
|
|
396
768
|
}
|
|
@@ -404,7 +776,7 @@ export function detectUpgrade(ctx) {
|
|
|
404
776
|
}
|
|
405
777
|
/**
|
|
406
778
|
* `--apply`: detect, then apply every applicable mechanical step in fixed order, one commit
|
|
407
|
-
* each (`em upgrade: <step-id> (<from> → <to>)`), stopping at the first failure with every
|
|
779
|
+
* each (`em upgrade: <step-id> (<from> → <to>)`, trailer `Em-Upgrade: <step-id>`), stopping at the first failure with every
|
|
408
780
|
* prior commit intact and the failed step's own partial writes discarded. On full success,
|
|
409
781
|
* writes `Em version: <to>` as one final commit — always, even when every step above was a
|
|
410
782
|
* no-op, UNLESS the bullet already reads `<to>` (idempotent second run makes zero commits).
|
|
@@ -440,7 +812,7 @@ export function applyUpgrade(ctx, runGit = realGit) {
|
|
|
440
812
|
return { ok: false, message: `em upgrade: step "${step.id}" failed — ${result.message}`, report, applied };
|
|
441
813
|
}
|
|
442
814
|
const message = `em upgrade: ${step.id} (${report.from.version} → ${report.to})`;
|
|
443
|
-
const commitResult = gitCommitAll(ctx.repoRoot, message, runGit);
|
|
815
|
+
const commitResult = gitCommitAll(ctx.repoRoot, message, step.id, runGit);
|
|
444
816
|
if (!commitResult.ok) {
|
|
445
817
|
return { ok: false, message: `em upgrade: step "${step.id}" applied but failed to commit — ${commitResult.message}`, report, applied };
|
|
446
818
|
}
|
|
@@ -454,7 +826,7 @@ export function applyUpgrade(ctx, runGit = realGit) {
|
|
|
454
826
|
if (stamped.ok && stamped.text !== loaded.text) {
|
|
455
827
|
writeFileSync(loaded.path, stamped.text);
|
|
456
828
|
const message = `em upgrade: em-version (${report.from.version} → ${report.to})`;
|
|
457
|
-
const commitResult = gitCommitAll(ctx.repoRoot, message, runGit);
|
|
829
|
+
const commitResult = gitCommitAll(ctx.repoRoot, message, "em-version", runGit);
|
|
458
830
|
if (!commitResult.ok) {
|
|
459
831
|
return { ok: false, message: `em upgrade: writing Em version: failed to commit — ${commitResult.message}`, report, applied };
|
|
460
832
|
}
|
|
@@ -463,14 +835,26 @@ export function applyUpgrade(ctx, runGit = realGit) {
|
|
|
463
835
|
}
|
|
464
836
|
return { ok: true, report, applied, emVersionCommit };
|
|
465
837
|
}
|
|
838
|
+
/** MIL-257: every cause that makes `--check` exit 1, each as one sentence, so the final line
|
|
839
|
+
* always says why. Empty when there is no hard incompatibility. */
|
|
840
|
+
export function hardIncompatibilityReasons(report) {
|
|
841
|
+
const reasons = [];
|
|
842
|
+
if (report.stateFileError !== null)
|
|
843
|
+
reasons.push(report.stateFileError);
|
|
844
|
+
for (const h of report.human)
|
|
845
|
+
if (h.id === "predates-1.6")
|
|
846
|
+
reasons.push(`predates-1.6: ${h.reason}`);
|
|
847
|
+
return reasons;
|
|
848
|
+
}
|
|
466
849
|
/** `--check` (what CI runs): exit-worthiness only — no writes. Hard incompatibilities are the
|
|
467
|
-
* ONLY thing that makes this fail:
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
850
|
+
* ONLY thing that makes this fail: a state file that exists but is unparseable (a MISSING one is
|
|
851
|
+
* scaffolded by the `state-file` step, MIL-257, so it is a pending step, not a failure), or a
|
|
852
|
+
* `predates-1.6` human item (an old reaction shape `em migrate` itself refuses to touch). Every
|
|
853
|
+
* other human item — including the ordinary "some steps are applicable" case — is advisory, same
|
|
854
|
+
* as running `em upgrade` without `--apply` at all. `reasons` (MIL-257) is why `ok` is false. */
|
|
471
855
|
export function checkUpgrade(ctx) {
|
|
472
856
|
const report = detectUpgrade(ctx);
|
|
473
|
-
const
|
|
474
|
-
return { ok:
|
|
857
|
+
const reasons = hardIncompatibilityReasons(report);
|
|
858
|
+
return { ok: reasons.length === 0, report, reasons };
|
|
475
859
|
}
|
|
476
860
|
//# sourceMappingURL=upgrade.js.map
|