@milehimikey/em 1.13.1 → 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 +50 -11
- package/.claude/skills/event-modeling-conform/SKILL.md +1 -1
- package/.claude/skills/event-modeling-design/SKILL.md +126 -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 +1 -1
- package/.claude/skills/event-modeling-implement/reference/implement.md +38 -11
- package/.claude/skills/event-modeling-review/SKILL.md +1 -1
- package/.claude/skills/event-modeling-shared/reference/em-dsl.md +274 -189
- package/.claude/skills/event-modeling-shared/reference/operating-principles.md +61 -0
- package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +108 -5
- 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 +82 -21
- package/dist/cli/ciInit.js +468 -64
- 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 +9 -0
- package/dist/cli/coverage.js +60 -8
- 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 +3 -2
- package/dist/cli/reratify.js +36 -14
- 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 +16 -3
- package/dist/cli/upgrade.js +366 -30
- 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 +554 -94
- 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/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/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 +163 -31
- 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 +5 -2
- 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 { basename, extname, 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
38
|
import { scaffoldConstitution, scaffoldStateFile } from "../templates.js";
|
|
35
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. ----
|
|
@@ -162,32 +245,68 @@ function applyStateFile(ctx) {
|
|
|
162
245
|
// Not anchored on a literal "em coverage" — the generated line pins an exact version
|
|
163
246
|
// (`npx @milehimikey/em@1.9.0 coverage "..."`, ciInit.ts's own `em` template variable), so
|
|
164
247
|
// "em" and "coverage" are never adjacent in the real file.
|
|
165
|
-
const CI_INIT_ARGS_RE = /\bcoverage "([^"]*)" --tests "([^"]*)" --strict\b
|
|
166
|
-
function extractCiInitArgs(emCiContent) {
|
|
167
|
-
const
|
|
168
|
-
|
|
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
|
+
};
|
|
169
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";
|
|
170
284
|
function ciBlockPlans(ctx) {
|
|
171
285
|
const ciPath = ciWorkflowPath(ctx.repoRoot);
|
|
172
286
|
const conformPath = conformWorkflowPath(ctx.repoRoot);
|
|
173
287
|
if (!existsSync(ciPath))
|
|
174
288
|
return { ciPath, ciStale: false, conformPath, conformStale: false, args: null };
|
|
175
|
-
const
|
|
176
|
-
if (!args)
|
|
177
|
-
return { ciPath, ciStale: false, conformPath, conformStale: false, args: null };
|
|
178
|
-
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);
|
|
179
296
|
const ciStale = ciStatus.kind === "stale";
|
|
180
297
|
let conformStale = false;
|
|
181
298
|
if (existsSync(conformPath)) {
|
|
182
|
-
const conformStatus = planCiFile(conformPath,
|
|
299
|
+
const conformStatus = planCiFile(conformPath, files.conform[0], files.conform[1], CONFORM_WORKFLOW_MARKER, false, multi);
|
|
183
300
|
conformStale = conformStatus.kind === "stale";
|
|
184
301
|
}
|
|
185
302
|
return { ciPath, ciStale, conformPath, conformStale, args };
|
|
186
303
|
}
|
|
187
304
|
function detectCiBlock(ctx) {
|
|
188
|
-
const { ciPath, ciStale, conformStale, args } = ciBlockPlans(ctx);
|
|
305
|
+
const { ciPath, ciStale, conformStale, args, blocked } = ciBlockPlans(ctx);
|
|
189
306
|
if (!existsSync(ciPath))
|
|
190
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 };
|
|
191
310
|
if (!args)
|
|
192
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" };
|
|
193
312
|
if (!ciStale && !conformStale)
|
|
@@ -198,15 +317,15 @@ function applyCiBlock(ctx) {
|
|
|
198
317
|
const { ciPath, ciStale, conformPath, conformStale, args } = ciBlockPlans(ctx);
|
|
199
318
|
if (!args || (!ciStale && !conformStale))
|
|
200
319
|
return { ok: false, message: "nothing to refresh" };
|
|
320
|
+
const multi = args.models !== null;
|
|
321
|
+
const files = ciBlockFiles(args, ctx);
|
|
201
322
|
const changedFiles = [];
|
|
202
323
|
if (ciStale) {
|
|
203
|
-
|
|
204
|
-
applyCiFile(ciPath, status);
|
|
324
|
+
applyCiFile(ciPath, planCiFile(ciPath, files.ci[0], files.ci[1], CI_WORKFLOW_MARKER, false, multi));
|
|
205
325
|
changedFiles.push(ciPath);
|
|
206
326
|
}
|
|
207
327
|
if (conformStale) {
|
|
208
|
-
|
|
209
|
-
applyCiFile(conformPath, status);
|
|
328
|
+
applyCiFile(conformPath, planCiFile(conformPath, files.conform[0], files.conform[1], CONFORM_WORKFLOW_MARKER, false, multi));
|
|
210
329
|
changedFiles.push(conformPath);
|
|
211
330
|
}
|
|
212
331
|
return { ok: true, changedFiles };
|
|
@@ -235,12 +354,191 @@ function applyConstitution(ctx) {
|
|
|
235
354
|
writeFileSync(path, scaffoldConstitution(ctx.model.name));
|
|
236
355
|
return { ok: true, changedFiles: [path] };
|
|
237
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
|
+
}
|
|
238
533
|
export const UPGRADE_STEPS = [
|
|
534
|
+
{ id: "skill-plugin", sinceVersion: "1.14.0", detect: detectSkillPlugin, apply: applySkillPlugin },
|
|
239
535
|
{ id: "skill-bundle", sinceVersion: "1.7.0", detect: detectSkillBundle, apply: applySkillBundle },
|
|
240
536
|
{ id: "reaction-shape", sinceVersion: "1.8.0", detect: detectReactionShape, apply: applyReactionShape },
|
|
241
537
|
{ id: "state-file", sinceVersion: "1.13.0", detect: detectStateFile, apply: applyStateFile },
|
|
242
538
|
{ id: "ci-block", sinceVersion: "1.9.0", detect: detectCiBlock, apply: applyCiBlock },
|
|
243
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 },
|
|
244
542
|
];
|
|
245
543
|
function elementRefOf(ctx) {
|
|
246
544
|
return (id) => ctx.refs.refById.get(id);
|
|
@@ -256,7 +554,7 @@ function nonContinuationDocs(ctx) {
|
|
|
256
554
|
const { doc, continuationOf } = resolveSliceDocJoin(ctx.model, ctx.refs, slice, key, ctx.baseDir, elementRefOf(ctx));
|
|
257
555
|
if (continuationOf !== null)
|
|
258
556
|
return;
|
|
259
|
-
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 });
|
|
260
558
|
});
|
|
261
559
|
return results;
|
|
262
560
|
}
|
|
@@ -287,14 +585,14 @@ function detectContinuationHasOwnDoc(ctx) {
|
|
|
287
585
|
}
|
|
288
586
|
function detectReadyNoRatifiedBy(ctx) {
|
|
289
587
|
const keys = nonContinuationDocs(ctx)
|
|
290
|
-
.filter((d) => d.status === "ready-to-implement" && !d.ratifiedBy)
|
|
588
|
+
.filter((d) => d.status === "ready-to-implement" && !d.ratifiedBy && (d.version ?? 1) > 1)
|
|
291
589
|
.map((d) => d.key)
|
|
292
590
|
.sort();
|
|
293
591
|
if (keys.length === 0)
|
|
294
592
|
return null;
|
|
295
593
|
return {
|
|
296
594
|
id: "ready-to-implement-no-ratifiedby",
|
|
297
|
-
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`,
|
|
298
596
|
};
|
|
299
597
|
}
|
|
300
598
|
function detectCoverageScopeDefault(ctx) {
|
|
@@ -337,6 +635,36 @@ function detectPredates16(ctx) {
|
|
|
337
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\``,
|
|
338
636
|
};
|
|
339
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
|
+
}
|
|
340
668
|
const HUMAN_DETECTORS = [
|
|
341
669
|
detectNoModelVersion,
|
|
342
670
|
detectContinuationHasOwnDoc,
|
|
@@ -344,6 +672,8 @@ const HUMAN_DETECTORS = [
|
|
|
344
672
|
detectCoverageScopeDefault,
|
|
345
673
|
detectUnratifiedConstitution,
|
|
346
674
|
detectPredates16,
|
|
675
|
+
detectPublicFieldTypesUnresolved,
|
|
676
|
+
detectPluginInstallLocally,
|
|
347
677
|
];
|
|
348
678
|
export function detectHumanItems(ctx) {
|
|
349
679
|
return HUMAN_DETECTORS.map((d) => d(ctx)).filter((x) => x !== null);
|
|
@@ -396,11 +726,17 @@ export function hasGitIdentity(repoRoot, runGit = realGit) {
|
|
|
396
726
|
const email = runGit(["-C", repoRoot, "config", "user.email"]);
|
|
397
727
|
return name.status === 0 && name.stdout.trim().length > 0 && email.status === 0 && email.stdout.trim().length > 0;
|
|
398
728
|
}
|
|
399
|
-
|
|
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) {
|
|
400
735
|
const add = runGit(["-C", repoRoot, "add", "-A"]);
|
|
401
736
|
if (add.status !== 0)
|
|
402
737
|
return { ok: false, message: `git add failed: ${(add.stderr || "").trim() || "unknown error"}` };
|
|
403
|
-
|
|
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}`]);
|
|
404
740
|
if (commit.status !== 0)
|
|
405
741
|
return { ok: false, message: `git commit failed: ${(commit.stderr || "").trim() || "unknown error"}` };
|
|
406
742
|
return { ok: true };
|
|
@@ -440,7 +776,7 @@ export function detectUpgrade(ctx) {
|
|
|
440
776
|
}
|
|
441
777
|
/**
|
|
442
778
|
* `--apply`: detect, then apply every applicable mechanical step in fixed order, one commit
|
|
443
|
-
* 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
|
|
444
780
|
* prior commit intact and the failed step's own partial writes discarded. On full success,
|
|
445
781
|
* writes `Em version: <to>` as one final commit — always, even when every step above was a
|
|
446
782
|
* no-op, UNLESS the bullet already reads `<to>` (idempotent second run makes zero commits).
|
|
@@ -476,7 +812,7 @@ export function applyUpgrade(ctx, runGit = realGit) {
|
|
|
476
812
|
return { ok: false, message: `em upgrade: step "${step.id}" failed — ${result.message}`, report, applied };
|
|
477
813
|
}
|
|
478
814
|
const message = `em upgrade: ${step.id} (${report.from.version} → ${report.to})`;
|
|
479
|
-
const commitResult = gitCommitAll(ctx.repoRoot, message, runGit);
|
|
815
|
+
const commitResult = gitCommitAll(ctx.repoRoot, message, step.id, runGit);
|
|
480
816
|
if (!commitResult.ok) {
|
|
481
817
|
return { ok: false, message: `em upgrade: step "${step.id}" applied but failed to commit — ${commitResult.message}`, report, applied };
|
|
482
818
|
}
|
|
@@ -490,7 +826,7 @@ export function applyUpgrade(ctx, runGit = realGit) {
|
|
|
490
826
|
if (stamped.ok && stamped.text !== loaded.text) {
|
|
491
827
|
writeFileSync(loaded.path, stamped.text);
|
|
492
828
|
const message = `em upgrade: em-version (${report.from.version} → ${report.to})`;
|
|
493
|
-
const commitResult = gitCommitAll(ctx.repoRoot, message, runGit);
|
|
829
|
+
const commitResult = gitCommitAll(ctx.repoRoot, message, "em-version", runGit);
|
|
494
830
|
if (!commitResult.ok) {
|
|
495
831
|
return { ok: false, message: `em upgrade: writing Em version: failed to commit — ${commitResult.message}`, report, applied };
|
|
496
832
|
}
|