@opengsd/gsd-core 1.3.0 → 1.4.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/gsd-advisor-researcher.md +1 -20
- package/agents/gsd-ai-researcher.md +1 -20
- package/agents/gsd-domain-researcher.md +1 -20
- package/agents/gsd-executor.md +1 -1
- package/agents/gsd-phase-researcher.md +92 -166
- package/agents/gsd-planner.md +9 -36
- package/agents/gsd-project-researcher.md +62 -141
- package/agents/gsd-ui-researcher.md +2 -21
- package/agents/gsd-verifier.md +8 -2
- package/bin/install.js +85 -4
- package/commands/gsd/graphify.md +11 -6
- package/commands/gsd/import.md +6 -2
- package/commands/gsd/plan-phase.md +2 -2
- package/gsd-core/bin/check-latest-version.cjs +3 -2
- package/gsd-core/bin/gsd-tools.cjs +238 -32
- package/gsd-core/bin/lib/check-command-router.cjs +1 -0
- package/gsd-core/bin/lib/cli-exit.cjs +42 -0
- package/gsd-core/bin/lib/command-routing-hub.cjs +1 -1
- package/gsd-core/bin/lib/commands.cjs +5 -4
- package/gsd-core/bin/lib/config.cjs +28 -4
- package/gsd-core/bin/lib/core.cjs +72 -28
- package/gsd-core/bin/lib/graphify.cjs +2 -2
- package/gsd-core/bin/lib/init-command-router.cjs +2 -2
- package/gsd-core/bin/lib/init.cjs +19 -3
- package/gsd-core/bin/lib/installer-migrations.cjs +61 -22
- package/gsd-core/bin/lib/intel.cjs +3 -20
- package/gsd-core/bin/lib/package-legitimacy.cjs +368 -0
- package/gsd-core/bin/lib/phase.cjs +3 -3
- package/gsd-core/bin/lib/research-provider.cjs +137 -0
- package/gsd-core/bin/lib/research-store.cjs +167 -0
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +4 -19
- package/gsd-core/bin/lib/security.cjs +73 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +3 -0
- package/gsd-core/bin/lib/validate.cjs +2 -2
- package/gsd-core/bin/lib/verification-command-router.cjs +31 -0
- package/gsd-core/bin/lib/verification.cjs +193 -0
- package/gsd-core/bin/lib/verify.cjs +2 -2
- package/gsd-core/bin/lib/workstream-inventory.cjs +1 -1
- package/gsd-core/bin/lib/worktree-base-ref.cjs +325 -0
- package/gsd-core/bin/lib/worktree-safety.cjs +31 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -1
- package/gsd-core/bin/verify-reapply-patches.cjs +8 -11
- package/gsd-core/references/planner-load-graph-context.md +36 -0
- package/gsd-core/references/planning-config.md +3 -1
- package/gsd-core/references/research-documentation-lookup.md +29 -0
- package/gsd-core/references/research-philosophy.md +29 -0
- package/gsd-core/references/research-verification-protocol.md +27 -0
- package/gsd-core/workflows/execute-phase.md +19 -8
- package/gsd-core/workflows/help/modes/full.md +2 -2
- package/gsd-core/workflows/ingest-docs.md +3 -2
- package/gsd-core/workflows/plan-phase.md +14 -10
- package/gsd-core/workflows/plan-review-convergence.md +3 -3
- package/gsd-core/workflows/review.md +22 -5
- package/gsd-core/workflows/ship.md +5 -8
- package/gsd-core/workflows/spec-phase.md +2 -1
- package/gsd-core/workflows/update.md +2 -1
- package/hooks/dist/gsd-context-monitor.js +1 -1
- package/hooks/dist/gsd-workflow-guard.js +1 -0
- package/hooks/dist/gsd-worktree-path-guard.js +1 -1
- package/hooks/gsd-context-monitor.js +1 -1
- package/hooks/gsd-workflow-guard.js +1 -0
- package/hooks/gsd-worktree-path-guard.js +1 -1
- package/package.json +4 -1
- package/scripts/affected-tests-lib.cjs +3 -2
- package/scripts/changeset/cli.cjs +183 -28
- package/scripts/changeset/lint.cjs +5 -4
- package/scripts/changeset/new.cjs +4 -4
- package/scripts/check-alias-drift.cjs +77 -71
- package/scripts/check-env.cjs +185 -179
- package/scripts/check-npm-integrity.cjs +115 -109
- package/scripts/ci-guard-runner.cjs +11 -5
- package/scripts/ci-prepare-test-scope.cjs +27 -22
- package/scripts/ci-rebase-check.cjs +46 -45
- package/scripts/ci-test-scope.cjs +6 -4
- package/scripts/diff-touches-shipped-paths.cjs +52 -44
- package/scripts/gen-inventory-manifest.cjs +38 -32
- package/scripts/gen-research-agents.cjs +276 -0
- package/scripts/lib/cli-exit.cjs +56 -0
- package/scripts/lint-command-contract.cjs +28 -22
- package/scripts/lint-descriptions.cjs +32 -28
- package/scripts/lint-docs-required.cjs +4 -4
- package/scripts/lint-legacy-dir-name.cjs +56 -52
- package/scripts/lint-pr-check-project-dir.cjs +3 -1
- package/scripts/lint-shell-command-projection-drift.cjs +27 -22
- package/scripts/lint-skill-deps.cjs +31 -26
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/lint-test-file-count.cjs +5 -4
- package/scripts/mutation-matrix.cjs +6 -3
- package/scripts/prompt-injection-scan.sh +1 -1
- package/scripts/release-notes/format-github-release-notes.cjs +8 -3
- package/scripts/release-tarball-smoke.cjs +6 -4
- package/scripts/research-profiles.cjs +149 -0
- package/scripts/run-affected-tests.cjs +2 -1
- package/scripts/run-cross-platform-tests.cjs +11 -7
- package/scripts/run-tests.cjs +8 -7
- package/scripts/strip-prose-atrefs.cjs +1 -1
- package/scripts/sync-runtime-launcher.cjs +0 -3
- package/scripts/verify-npm-publish.cjs +14 -26
|
@@ -506,6 +506,7 @@ function loadConfig(cwd, options = {}) {
|
|
|
506
506
|
effort: (parsed['effort']) || null,
|
|
507
507
|
fast_mode: (parsed['fast_mode']) || null,
|
|
508
508
|
agent_skills: (parsed['agent_skills']) || {},
|
|
509
|
+
agent_skills_security: (parsed['agent_skills_security']) || null,
|
|
509
510
|
manager: (parsed['manager']) || {},
|
|
510
511
|
response_language: get('response_language') || null,
|
|
511
512
|
claude_md_path: get('claude_md_path') || null,
|
|
@@ -994,35 +995,38 @@ function extractCurrentMilestone(content, cwd) {
|
|
|
994
995
|
const firstMatch = allMatches[0];
|
|
995
996
|
const selected = allMatches.find((m) => !isClosed(m[1])) || firstMatch;
|
|
996
997
|
const sectionStart = selected.index;
|
|
997
|
-
const
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
998
|
+
const computeSectionEnd = (headingText, headingStart) => {
|
|
999
|
+
const level = (headingText.match(/^(#{1,3})\s/) ?? ['', '#'])[1].length;
|
|
1000
|
+
const rest = content.slice(headingStart + headingText.length);
|
|
1001
|
+
const stopPattern = new RegExp(`^#{1,${level}}\\s+(?!Phase\\s+\\S)(?:.*v\\d+\\.\\d+|✅|📋|🚧)`, 'i');
|
|
1002
|
+
let end = content.length;
|
|
1003
|
+
let fc = null;
|
|
1004
|
+
let fl = 0;
|
|
1005
|
+
let off = 0;
|
|
1006
|
+
for (const line of rest.split('\n')) {
|
|
1007
|
+
const fm = line.match(/^\s{0,3}((?:`{3,}|~{3,}))(.*)/);
|
|
1008
|
+
if (fm) {
|
|
1009
|
+
const ch = fm[1][0];
|
|
1010
|
+
const ln = fm[1].length;
|
|
1011
|
+
const trailing = fm[2] || '';
|
|
1012
|
+
if (!fc) {
|
|
1013
|
+
fc = ch;
|
|
1014
|
+
fl = ln;
|
|
1015
|
+
}
|
|
1016
|
+
else if (ch === fc && ln >= fl && /^\s*$/.test(trailing)) {
|
|
1017
|
+
fc = null;
|
|
1018
|
+
fl = 0;
|
|
1019
|
+
}
|
|
1014
1020
|
}
|
|
1015
|
-
else if (
|
|
1016
|
-
|
|
1017
|
-
|
|
1021
|
+
else if (!fc && stopPattern.test(line)) {
|
|
1022
|
+
end = headingStart + headingText.length + off;
|
|
1023
|
+
break;
|
|
1018
1024
|
}
|
|
1025
|
+
off += line.length + 1;
|
|
1019
1026
|
}
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
}
|
|
1024
|
-
charOffset += line.length + 1;
|
|
1025
|
-
}
|
|
1027
|
+
return end;
|
|
1028
|
+
};
|
|
1029
|
+
const sectionEnd = computeSectionEnd(selected[0], sectionStart);
|
|
1026
1030
|
const anyMilestonePattern = /^#{1,3}\s+(?!Phase\s+\S)(?:.*v\d+\.\d+|✅|📋|🚧)/im;
|
|
1027
1031
|
const firstMilestoneMatch = content.match(anyMilestonePattern);
|
|
1028
1032
|
const preambleCutoff = firstMilestoneMatch
|
|
@@ -1030,11 +1034,36 @@ function extractCurrentMilestone(content, cwd) {
|
|
|
1030
1034
|
: firstMatch.index;
|
|
1031
1035
|
const beforeMilestones = content.slice(0, preambleCutoff);
|
|
1032
1036
|
const currentSection = content.slice(sectionStart, sectionEnd);
|
|
1037
|
+
// Multi-milestone roadmaps split each added milestone across two version-bearing
|
|
1038
|
+
// headings: a `## Phases` checklist subsection (early) and a dedicated
|
|
1039
|
+
// `## Milestone … (Phase Details)` section (late) holding the `### Phase N:`
|
|
1040
|
+
// detail headers. The scope window above stops at the next version-bearing
|
|
1041
|
+
// heading — the current milestone's OWN Phase Details heading — leaving those
|
|
1042
|
+
// detail headers outside `currentSection`. Append that section so phase
|
|
1043
|
+
// resolution and counting see the current milestone's phases. Anchor the lookup
|
|
1044
|
+
// to the SELECTED heading's specific version token (boundary-aware, so a
|
|
1045
|
+
// `v3.0` state does not match a `v3.0-A` sub-milestone) so sibling milestones
|
|
1046
|
+
// that share a version prefix do not cross-pollinate. (#730)
|
|
1047
|
+
const selectedVersionToken = selected[1].match(/v\d+(?:\.\d+)+(?:[-.][A-Za-z0-9]+)*/i)?.[0];
|
|
1048
|
+
const detailsVersionBoundary = selectedVersionToken
|
|
1049
|
+
? new RegExp(`${escapeRegex(selectedVersionToken)}(?![\\w.-])`, 'i')
|
|
1050
|
+
: null;
|
|
1051
|
+
let detailsSection = '';
|
|
1052
|
+
const detailsMatch = allMatches.find((m) => /\(Phase\s+Details\)/i.test(m[1]) &&
|
|
1053
|
+
!isClosed(m[1]) &&
|
|
1054
|
+
(!detailsVersionBoundary || detailsVersionBoundary.test(m[1])) &&
|
|
1055
|
+
(m.index ?? 0) >= sectionEnd);
|
|
1056
|
+
if (detailsMatch) {
|
|
1057
|
+
const detailsStart = detailsMatch.index ?? 0;
|
|
1058
|
+
detailsSection = content.slice(detailsStart, computeSectionEnd(detailsMatch[0], detailsStart));
|
|
1059
|
+
}
|
|
1033
1060
|
const preamble = beforeMilestones
|
|
1034
1061
|
.replace(/<details>[\s\S]*?<\/details>/gi, '')
|
|
1035
1062
|
.replace(/^#{2,4}\s*Phase\s+[\w][\w.-]*\s*:[^\n]*(?:\n(?!#{1,6}\s)[^\n]*)*\n?/gim, '')
|
|
1036
1063
|
.replace(/^#{1,4}\s*Phase Details\b[^\n]*\n?/gim, '');
|
|
1037
|
-
return
|
|
1064
|
+
return detailsSection
|
|
1065
|
+
? preamble + currentSection + '\n' + detailsSection
|
|
1066
|
+
: preamble + currentSection;
|
|
1038
1067
|
}
|
|
1039
1068
|
/**
|
|
1040
1069
|
* Replace a pattern only in the current milestone section of ROADMAP.md.
|
|
@@ -1399,7 +1428,12 @@ const VALID_GRANULARITIES = new Set(['coarse', 'standard', 'fine']);
|
|
|
1399
1428
|
/**
|
|
1400
1429
|
* Resolve the planning granularity for a phase type (#68).
|
|
1401
1430
|
*/
|
|
1402
|
-
function resolveGranularityInternal(cwd, phaseType) {
|
|
1431
|
+
function resolveGranularityInternal(cwd, phaseType, override) {
|
|
1432
|
+
if (override !== undefined && override !== null && override !== '') {
|
|
1433
|
+
if (VALID_GRANULARITIES.has(override)) {
|
|
1434
|
+
return override;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1403
1437
|
const config = loadConfig(cwd);
|
|
1404
1438
|
const configGranularities = config['granularities'];
|
|
1405
1439
|
const perPhase = (phaseType && configGranularities && typeof configGranularities === 'object')
|
|
@@ -1418,6 +1452,15 @@ function resolveGranularityInternal(cwd, phaseType) {
|
|
|
1418
1452
|
}
|
|
1419
1453
|
return 'standard';
|
|
1420
1454
|
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Validate a CLI granularity override at the command boundary. Empty/null/undefined
|
|
1457
|
+
* are treated as "no override" (no-op). An invalid non-empty value calls `fail`.
|
|
1458
|
+
*/
|
|
1459
|
+
function assertValidGranularityOverride(override, fail) {
|
|
1460
|
+
if (override !== undefined && override !== null && override !== '' && !VALID_GRANULARITIES.has(override)) {
|
|
1461
|
+
fail(`invalid granularity '${override}' (valid: ${[...VALID_GRANULARITIES].join(', ')})`);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1421
1464
|
/**
|
|
1422
1465
|
* #3024 — Resolve a model for a specific dynamic-routing attempt.
|
|
1423
1466
|
*/
|
|
@@ -1956,6 +1999,7 @@ module.exports = {
|
|
|
1956
1999
|
resolveModelForTier,
|
|
1957
2000
|
resolveGranularityInternal,
|
|
1958
2001
|
VALID_GRANULARITIES,
|
|
2002
|
+
assertValidGranularityOverride,
|
|
1959
2003
|
resolveEffortInternal,
|
|
1960
2004
|
resolveFastModeInternal,
|
|
1961
2005
|
resolveEffortForTier,
|
|
@@ -35,7 +35,7 @@ function isGraphifyEnabled(planningDir) {
|
|
|
35
35
|
return true;
|
|
36
36
|
return false;
|
|
37
37
|
}
|
|
38
|
-
catch
|
|
38
|
+
catch {
|
|
39
39
|
return false;
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -157,7 +157,7 @@ function safeReadJson(filePath) {
|
|
|
157
157
|
return null;
|
|
158
158
|
return JSON.parse(node_fs_1.default.readFileSync(filePath, 'utf8'));
|
|
159
159
|
}
|
|
160
|
-
catch
|
|
160
|
+
catch {
|
|
161
161
|
return null;
|
|
162
162
|
}
|
|
163
163
|
}
|
|
@@ -34,8 +34,8 @@ function routeInitCommand({ init, args, cwd, raw, error }) {
|
|
|
34
34
|
init.cmdInitExecutePhase(cwd, args[2], raw, { validate: namedArgs['validate'], tdd: namedArgs['tdd'] });
|
|
35
35
|
},
|
|
36
36
|
'plan-phase': () => {
|
|
37
|
-
const namedArgs = (0, command_arg_projection_cjs_1.parseNamedArgs)(args, [], ['validate', 'tdd']);
|
|
38
|
-
init.cmdInitPlanPhase(cwd, args[2], raw, { validate: namedArgs['validate'], tdd: namedArgs['tdd'] });
|
|
37
|
+
const namedArgs = (0, command_arg_projection_cjs_1.parseNamedArgs)(args, ['granularity'], ['validate', 'tdd']);
|
|
38
|
+
init.cmdInitPlanPhase(cwd, args[2], raw, { validate: namedArgs['validate'], tdd: namedArgs['tdd'], granularity: namedArgs['granularity'] });
|
|
39
39
|
},
|
|
40
40
|
'new-project': () => init.cmdInitNewProject(cwd, raw),
|
|
41
41
|
'new-milestone': () => init.cmdInitNewMilestone(cwd, raw),
|
|
@@ -28,7 +28,7 @@ const security_cjs_1 = require("./security.cjs");
|
|
|
28
28
|
const runtime_homes_cjs_1 = require("./runtime-homes.cjs");
|
|
29
29
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- frontmatter.cjs is an export= CommonJS module
|
|
30
30
|
const frontmatterMod = require("./frontmatter.cjs");
|
|
31
|
-
const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, gitWorktreeInfoInternal, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, normalizePhaseName, toPosixPath, output, error, checkAgentsInstalled, phaseTokenMatches, } = core;
|
|
31
|
+
const { loadConfig, resolveModelInternal, resolveGranularityInternal, assertValidGranularityOverride, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, gitWorktreeInfoInternal, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, normalizePhaseName, toPosixPath, output, error, checkAgentsInstalled, phaseTokenMatches, } = core;
|
|
32
32
|
const { planningPaths, planningDir, planningRoot, findContextMdIn, } = planningWorkspace;
|
|
33
33
|
const { determinePhaseStatus } = commandsMod;
|
|
34
34
|
const { extractFrontmatter } = frontmatterMod;
|
|
@@ -297,11 +297,15 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
|
|
|
297
297
|
expectedPhaseDirPlan = toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningPaths(cwd).phases, dirName)));
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
|
+
const granularityOverride = options['granularity'];
|
|
301
|
+
assertValidGranularityOverride(granularityOverride, error);
|
|
302
|
+
const granularity = resolveGranularityInternal(cwd, 'planning', granularityOverride || undefined);
|
|
300
303
|
const result = {
|
|
301
304
|
researcher_model: resolveModelInternal(cwd, 'gsd-phase-researcher'),
|
|
302
305
|
planner_model: resolveModelInternal(cwd, 'gsd-planner'),
|
|
303
306
|
checker_model: resolveModelInternal(cwd, 'gsd-plan-checker'),
|
|
304
307
|
tdd_mode: options['tdd'] || config.tdd_mode || false,
|
|
308
|
+
granularity,
|
|
305
309
|
research_enabled: config.research,
|
|
306
310
|
plan_checker_enabled: config.plan_checker,
|
|
307
311
|
nyquist_validation_enabled: config.nyquist_validation,
|
|
@@ -1521,6 +1525,11 @@ function buildAgentSkillsBlock(config, agentType, projectRoot) {
|
|
|
1521
1525
|
skillPaths = [skillPaths];
|
|
1522
1526
|
if (!Array.isArray(skillPaths) || skillPaths.length === 0)
|
|
1523
1527
|
return '';
|
|
1528
|
+
// Hoist trusted roots computation before the loop: loadTrustedGlobalRoots does
|
|
1529
|
+
// realpathSync I/O and should run at most once per call, not once per failing skill.
|
|
1530
|
+
// It returns [] cheaply when no roots are configured, so the realpath cost only
|
|
1531
|
+
// occurs when the caller has actually set trusted_global_roots.
|
|
1532
|
+
const trustedGlobalRoots = (0, security_cjs_1.loadTrustedGlobalRoots)(config);
|
|
1524
1533
|
const validPaths = [];
|
|
1525
1534
|
for (const skillPath of skillPaths) {
|
|
1526
1535
|
if (typeof skillPath !== 'string')
|
|
@@ -1548,8 +1557,15 @@ function buildAgentSkillsBlock(config, agentType, projectRoot) {
|
|
|
1548
1557
|
}
|
|
1549
1558
|
const pathCheck = (0, security_cjs_1.validatePath)(globalSkillMd, globalSkillsBase, { allowAbsolute: true });
|
|
1550
1559
|
if (!pathCheck['safe']) {
|
|
1551
|
-
|
|
1552
|
-
|
|
1560
|
+
const acceptedViaTrustedRoot = trustedGlobalRoots.some((root) => {
|
|
1561
|
+
const rootCheck = (0, security_cjs_1.validatePath)(globalSkillMd, root, { allowAbsolute: true });
|
|
1562
|
+
return Boolean(rootCheck['safe']);
|
|
1563
|
+
});
|
|
1564
|
+
if (!acceptedViaTrustedRoot) {
|
|
1565
|
+
process.stderr.write(`[agent-skills] WARNING: Global skill "${skillName}" failed path check (symlink escape?) — skipping\n`);
|
|
1566
|
+
continue;
|
|
1567
|
+
}
|
|
1568
|
+
process.stderr.write(`[agent-skills] NOTE: Global skill "${skillName}" accepted via trusted_global_roots (resolves outside the default skills dir)\n`);
|
|
1553
1569
|
}
|
|
1554
1570
|
validPaths.push({ ref: `${globalSkillDir}/SKILL.md`, display: displayPath });
|
|
1555
1571
|
continue;
|
|
@@ -175,16 +175,46 @@ function migrationChecksum(migration) {
|
|
|
175
175
|
};
|
|
176
176
|
return `sha256:${sha256Text(JSON.stringify(serializable))}`;
|
|
177
177
|
}
|
|
178
|
-
|
|
178
|
+
// Rewrite the stored checksum of any already-applied entry whose id drifted, so the
|
|
179
|
+
// drift is reconciled durably and not re-detected on every subsequent run (issue #670).
|
|
180
|
+
// Returns the number of entries actually changed (so callers know whether a write is needed).
|
|
181
|
+
function reconcileDriftedChecksums(appliedEntries, checksumDrift) {
|
|
182
|
+
if (!Array.isArray(checksumDrift) || checksumDrift.length === 0)
|
|
183
|
+
return 0;
|
|
184
|
+
const reconcile = new Map(checksumDrift.map((d) => [d.id, d.currentChecksum]));
|
|
185
|
+
let changed = 0;
|
|
186
|
+
for (let i = 0; i < appliedEntries.length; i++) {
|
|
187
|
+
const existing = appliedEntries[i];
|
|
188
|
+
if (existing && typeof existing.id === 'string' && reconcile.has(existing.id)) {
|
|
189
|
+
const next = reconcile.get(existing.id);
|
|
190
|
+
if (existing.checksum !== next) {
|
|
191
|
+
appliedEntries[i] = { ...existing, checksum: next };
|
|
192
|
+
changed += 1;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return changed;
|
|
197
|
+
}
|
|
198
|
+
function collectAppliedChecksumDrift(applied, migrations) {
|
|
199
|
+
const drift = [];
|
|
179
200
|
for (const migration of migrations) {
|
|
180
201
|
const entry = applied.get(migration.id);
|
|
181
202
|
if (!entry || !entry.checksum)
|
|
182
203
|
continue;
|
|
183
|
-
const
|
|
184
|
-
if (entry.checksum !==
|
|
185
|
-
|
|
204
|
+
const currentChecksum = migrationChecksum(migration);
|
|
205
|
+
if (entry.checksum !== currentChecksum) {
|
|
206
|
+
// An already-applied migration is never re-run (it is filtered out of `pending`),
|
|
207
|
+
// so a checksum drift here is functionally inert. A prior release may have edited a
|
|
208
|
+
// shipped migration body (see issue #670). Surface it for reconciliation instead of
|
|
209
|
+
// hard-aborting the user's upgrade.
|
|
210
|
+
drift.push({
|
|
211
|
+
id: migration.id,
|
|
212
|
+
storedChecksum: entry.checksum,
|
|
213
|
+
currentChecksum,
|
|
214
|
+
});
|
|
186
215
|
}
|
|
187
216
|
}
|
|
217
|
+
return drift;
|
|
188
218
|
}
|
|
189
219
|
function migrationMatchesContext(migration, { runtime, scope }) {
|
|
190
220
|
if (Array.isArray(migration.runtimes) && migration.runtimes.length > 0) {
|
|
@@ -387,7 +417,7 @@ function planInstallerMigrations({ configDir, runtime = null, scope = null, migr
|
|
|
387
417
|
const validatedMigrations = migrations.map((migration) => (0, installer_migration_authoring_cjs_1.validateInstallerMigrationRecord)(migration));
|
|
388
418
|
const scopedMigrations = validatedMigrations.filter((migration) => migrationMatchesContext(migration, { runtime, scope }));
|
|
389
419
|
const applied = appliedMigrationEntries(state);
|
|
390
|
-
|
|
420
|
+
const checksumDrift = collectAppliedChecksumDrift(applied, scopedMigrations);
|
|
391
421
|
const pending = scopedMigrations.filter((migration) => !applied.has(migration.id));
|
|
392
422
|
const actions = [];
|
|
393
423
|
const blocked = [];
|
|
@@ -474,6 +504,7 @@ function planInstallerMigrations({ configDir, runtime = null, scope = null, migr
|
|
|
474
504
|
pendingMigrations: pending,
|
|
475
505
|
actions,
|
|
476
506
|
blocked,
|
|
507
|
+
checksumDrift,
|
|
477
508
|
};
|
|
478
509
|
}
|
|
479
510
|
function uniqueActionMigrationIds(actions) {
|
|
@@ -630,6 +661,7 @@ function applyInstallerMigrationPlan({ configDir, plan, now = () => new Date().t
|
|
|
630
661
|
const state = readInstallState(configDir);
|
|
631
662
|
const applied = appliedMigrationIds(state);
|
|
632
663
|
const nextApplied = [...state.appliedMigrations];
|
|
664
|
+
reconcileDriftedChecksums(nextApplied, plan.checksumDrift);
|
|
633
665
|
const actionsByMigrationId = new Map();
|
|
634
666
|
for (const action of plan.actions) {
|
|
635
667
|
if (action.migrationId && !actionsByMigrationId.has(action.migrationId)) {
|
|
@@ -684,30 +716,36 @@ function applyInstallerMigrationPlan({ configDir, plan, now = () => new Date().t
|
|
|
684
716
|
}
|
|
685
717
|
}
|
|
686
718
|
function markPendingMigrationsApplied({ configDir, plan, now = () => new Date().toISOString(), }) {
|
|
687
|
-
if (!plan
|
|
719
|
+
if (!plan)
|
|
720
|
+
return [];
|
|
721
|
+
const hasPending = Array.isArray(plan.pendingMigrationIds) && plan.pendingMigrationIds.length > 0;
|
|
722
|
+
const hasDrift = Array.isArray(plan.checksumDrift) && plan.checksumDrift.length > 0;
|
|
723
|
+
if (!hasPending && !hasDrift)
|
|
688
724
|
return [];
|
|
689
|
-
}
|
|
690
725
|
const appliedAt = now();
|
|
691
726
|
const state = readInstallState(configDir);
|
|
692
727
|
const applied = appliedMigrationIds(state);
|
|
693
|
-
const checksumsByMigrationId = new Map();
|
|
694
|
-
for (const migration of plan.pendingMigrations || []) {
|
|
695
|
-
checksumsByMigrationId.set(migration.id, migrationChecksum(migration));
|
|
696
|
-
}
|
|
697
728
|
const nextApplied = [...state.appliedMigrations];
|
|
729
|
+
const reconciledCount = reconcileDriftedChecksums(nextApplied, plan.checksumDrift);
|
|
698
730
|
const newlyApplied = [];
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
731
|
+
if (hasPending) {
|
|
732
|
+
const checksumsByMigrationId = new Map();
|
|
733
|
+
for (const migration of plan.pendingMigrations || []) {
|
|
734
|
+
checksumsByMigrationId.set(migration.id, migrationChecksum(migration));
|
|
735
|
+
}
|
|
736
|
+
for (const id of plan.pendingMigrationIds) {
|
|
737
|
+
if (applied.has(id))
|
|
738
|
+
continue;
|
|
739
|
+
nextApplied.push({
|
|
740
|
+
id,
|
|
741
|
+
appliedAt,
|
|
742
|
+
journal: null,
|
|
743
|
+
checksum: checksumsByMigrationId.get(id) || null,
|
|
744
|
+
});
|
|
745
|
+
newlyApplied.push(id);
|
|
746
|
+
}
|
|
709
747
|
}
|
|
710
|
-
if (newlyApplied.length > 0) {
|
|
748
|
+
if (newlyApplied.length > 0 || reconciledCount > 0) {
|
|
711
749
|
writeInstallState(configDir, {
|
|
712
750
|
schemaVersion: 1,
|
|
713
751
|
appliedMigrations: nextApplied,
|
|
@@ -776,6 +814,7 @@ module.exports = {
|
|
|
776
814
|
applyInstallerMigrationPlan,
|
|
777
815
|
classifyArtifact,
|
|
778
816
|
discoverInstallerMigrations,
|
|
817
|
+
migrationChecksum,
|
|
779
818
|
planInstallerMigrations,
|
|
780
819
|
readInstallManifest,
|
|
781
820
|
readInstallState,
|
|
@@ -59,7 +59,7 @@ function isIntelEnabled(planningDir) {
|
|
|
59
59
|
return true;
|
|
60
60
|
return false;
|
|
61
61
|
}
|
|
62
|
-
catch
|
|
62
|
+
catch {
|
|
63
63
|
return false;
|
|
64
64
|
}
|
|
65
65
|
}
|
|
@@ -86,7 +86,7 @@ function safeReadJson(filePath) {
|
|
|
86
86
|
return null;
|
|
87
87
|
return JSON.parse(raw);
|
|
88
88
|
}
|
|
89
|
-
catch
|
|
89
|
+
catch {
|
|
90
90
|
return null;
|
|
91
91
|
}
|
|
92
92
|
}
|
|
@@ -101,7 +101,7 @@ function hashFile(filePath) {
|
|
|
101
101
|
return null;
|
|
102
102
|
return node_crypto_1.default.createHash('sha256').update(content).digest('hex');
|
|
103
103
|
}
|
|
104
|
-
catch
|
|
104
|
+
catch {
|
|
105
105
|
return null;
|
|
106
106
|
}
|
|
107
107
|
}
|
|
@@ -147,23 +147,6 @@ function matchesInValue(value, lowerTerm) {
|
|
|
147
147
|
}
|
|
148
148
|
return false;
|
|
149
149
|
}
|
|
150
|
-
/**
|
|
151
|
-
* Search for a term in arch.md text content.
|
|
152
|
-
* Returns matching lines.
|
|
153
|
-
*/
|
|
154
|
-
function searchArchMd(filePath, term) {
|
|
155
|
-
try {
|
|
156
|
-
const content = (0, shell_command_projection_cjs_1.platformReadSync)(filePath);
|
|
157
|
-
if (content === null)
|
|
158
|
-
return [];
|
|
159
|
-
const lowerTerm = term.toLowerCase();
|
|
160
|
-
const lines = content.split(/\r?\n/);
|
|
161
|
-
return lines.filter(line => line.toLowerCase().includes(lowerTerm));
|
|
162
|
-
}
|
|
163
|
-
catch (_e) {
|
|
164
|
-
return [];
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
150
|
/**
|
|
168
151
|
* Query intel files for a search term.
|
|
169
152
|
* Searches across all JSON intel files (keys and values) and arch.md (text lines).
|