@opengsd/gsd-core 1.19.0 → 1.20.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-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-code-fixer.compact.md +5 -5
- package/agents/gsd-code-fixer.md +9 -4
- package/agents/gsd-code-reviewer.compact.md +49 -8
- package/agents/gsd-code-reviewer.md +53 -14
- package/agents/gsd-executor.md +8 -4
- package/commands/gsd/update.md +5 -2
- package/gsd-core/bin/gsd-tools.cjs +73 -23
- package/gsd-core/bin/lib/capability-lifecycle.cjs +1 -12
- package/gsd-core/bin/lib/capability-registry.cjs +284 -93
- package/gsd-core/bin/lib/capability-validator.cjs +15 -0
- package/gsd-core/bin/lib/commands.cjs +27 -17
- package/gsd-core/bin/lib/config-loader.cjs +3 -3
- package/gsd-core/bin/lib/core-utils.cjs +11 -0
- package/gsd-core/bin/lib/frontmatter.cjs +21 -3
- package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +2 -2
- package/gsd-core/bin/lib/gate-context-drift.cjs +1 -1
- package/gsd-core/bin/lib/gate-phase-context.cjs +7 -8
- package/gsd-core/bin/lib/gate-schema-drift.cjs +1 -1
- package/gsd-core/bin/lib/git-info-exclude.cjs +1 -6
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +13 -9
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +9 -7
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +3 -3
- package/gsd-core/bin/lib/init.cjs +180 -67
- package/gsd-core/bin/lib/io.cjs +6 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +34 -1
- package/gsd-core/bin/lib/markdown-table.cjs +23 -0
- package/gsd-core/bin/lib/mcp-server.cjs +8 -5
- package/gsd-core/bin/lib/milestone.cjs +18 -50
- package/gsd-core/bin/lib/phase-entry-grammar.cjs +119 -0
- package/gsd-core/bin/lib/phase-id-display.cjs +2 -0
- package/gsd-core/bin/lib/phase-id.cjs +609 -35
- package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -74
- package/gsd-core/bin/lib/phase-locator.cjs +328 -19
- package/gsd-core/bin/lib/phase.cjs +2990 -666
- package/gsd-core/bin/lib/planning-document.cjs +26 -5
- package/gsd-core/bin/lib/planning-inspect.cjs +27 -35
- package/gsd-core/bin/lib/planning-snapshot.cjs +12 -7
- package/gsd-core/bin/lib/planning-workspace.cjs +3 -3
- package/gsd-core/bin/lib/read-only-git-env.cjs +23 -2
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +2 -2
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +22 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +125 -18
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +11 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +730 -144
- package/gsd-core/bin/lib/roadmap-progress.cjs +136 -0
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +8 -5
- package/gsd-core/bin/lib/roadmap.cjs +358 -106
- package/gsd-core/bin/lib/shell-command-projection.cjs +36 -9
- package/gsd-core/bin/lib/smart-entry.cjs +22 -11
- package/gsd-core/bin/lib/state-contract.cjs +11 -7
- package/gsd-core/bin/lib/state-document.cjs +332 -23
- package/gsd-core/bin/lib/state-transition.cjs +39 -10
- package/gsd-core/bin/lib/state.cjs +498 -179
- package/gsd-core/bin/lib/template.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +3 -2
- package/gsd-core/bin/lib/vendor/re2js.cjs +863 -577
- package/gsd-core/bin/lib/vendor/re2js.d.cts +284 -219
- package/gsd-core/bin/lib/verify.cjs +2 -2
- package/gsd-core/bin/lib/workstream-inventory.cjs +26 -20
- package/gsd-core/bin/verify-reapply-patches.cjs +411 -10
- package/gsd-core/references/cicd-review-patterns.md +45 -0
- package/gsd-core/references/necessity-review.md +48 -0
- package/gsd-core/workflows/code-review-fix.md +46 -5
- package/gsd-core/workflows/code-review.md +3 -0
- package/gsd-core/workflows/execute-phase.md +1 -1
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +13 -9
- package/gsd-core/workflows/plan-phase.md +1 -1
- package/gsd-core/workflows/pr-branch.md +2 -0
- package/gsd-core/workflows/reapply-patches.md +104 -0
- package/gsd-core/workflows/review.md +54 -141
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +57 -9
- package/gsd-core/workflows/ship.md +12 -0
- package/hooks/dist/gsd-graphify-update.sh +6 -0
- package/hooks/dist/gsd-statusline.js +3 -2
- package/hooks/dist/gsd-windsurf-pre-write.js +4 -4
- package/hooks/dist/gsd-workflow-guard.js +2 -1
- package/hooks/dist/gsd-worktree-path-guard.js +4 -4
- package/hooks/dist/lib/git-probe.js +16 -0
- package/hooks/gsd-graphify-update.sh +6 -0
- package/hooks/gsd-statusline.js +3 -2
- package/hooks/gsd-windsurf-pre-write.js +4 -4
- package/hooks/gsd-workflow-guard.js +2 -1
- package/hooks/gsd-worktree-path-guard.js +4 -4
- package/hooks/lib/git-probe.js +16 -0
- package/package.json +3 -3
- package/pi/package-agents/gsd-code-fixer.md +9 -4
- package/pi/package-agents/gsd-code-reviewer.md +53 -14
- package/pi/package-agents/gsd-executor.md +8 -4
- package/pi/package-agents/manifest.json +3 -3
- package/pi/package-skills/gsd-update/SKILL.md +4 -1
- package/scripts/ci-next-health.cjs +246 -23
- package/scripts/docs-guard-registry.cjs +2 -0
- package/scripts/gen-features.cjs +14 -2
- package/scripts/lib/drift-scan.cjs +27 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +6 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +11 -0
- package/scripts/lint-phase-enumeration-drift.cjs +8 -4
- package/scripts/lint-roadmap-progress-drift.cjs +421 -0
- package/scripts/mutation-matrix.cjs +23 -1
- package/scripts/sync-next-version.cjs +38 -6
- package/skills/gsd-update/SKILL.md +5 -2
- package/vscode/package.json +1 -1
|
@@ -1874,6 +1874,11 @@ const KNOWN_REVIEWER_FIELDS = new Set([
|
|
|
1874
1874
|
// effort", which emits no effort argument at all.
|
|
1875
1875
|
'effortConfigKey',
|
|
1876
1876
|
'defaultEffort',
|
|
1877
|
+
// `contextWindowTokens` / `contextWindowConfigKey` added by #4559, same optional/backward-compatible
|
|
1878
|
+
// shape as `promptBudgetKey`: a manifest authored before they existed keeps validating, and an
|
|
1879
|
+
// absent or non-positive window resolves to "no derived prompt cap" at the invocation boundary.
|
|
1880
|
+
'contextWindowTokens',
|
|
1881
|
+
'contextWindowConfigKey',
|
|
1877
1882
|
'handler',
|
|
1878
1883
|
]);
|
|
1879
1884
|
|
|
@@ -2505,6 +2510,16 @@ function validateReviewerBodyFields(cap) {
|
|
|
2505
2510
|
);
|
|
2506
2511
|
}
|
|
2507
2512
|
|
|
2513
|
+
// #4559: optional. Absent or null is "no declared window"; a present key must be a dotted
|
|
2514
|
+
// config key string. The window's VALUE is narrowed (never rejected) by `resolveLaneCap`.
|
|
2515
|
+
if (r.contextWindowConfigKey !== undefined && r.contextWindowConfigKey !== null
|
|
2516
|
+
&& (typeof r.contextWindowConfigKey !== 'string' || r.contextWindowConfigKey.length === 0)) {
|
|
2517
|
+
errors.push(
|
|
2518
|
+
ctx + ' reviewer.contextWindowConfigKey must be a dotted config key or null ' +
|
|
2519
|
+
'(got: ' + describeValue(r.contextWindowConfigKey) + ')',
|
|
2520
|
+
);
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2508
2523
|
// ── handler (D6) — closed first-party enum; null is the default ────────────
|
|
2509
2524
|
if (r.handler !== null && !VALID_LANE_HANDLERS.has(r.handler)) {
|
|
2510
2525
|
errors.push(
|
|
@@ -31,13 +31,13 @@ const coreUtilsMod = require("./core-utils.cjs");
|
|
|
31
31
|
const { toPosixPath, generateSlugInternal, extractOneLinerFromBody } = coreUtilsMod;
|
|
32
32
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
33
33
|
const phaseIdMod = require("./phase-id.cjs");
|
|
34
|
-
const { normalizePhaseName, comparePhaseNum, extractPhaseToken, PHASE_NUMBER_TOKEN_SOURCE, isSentinelPhaseId, renderPhaseBranchName, parsePhaseId, renderPhaseId, phaseHeadingPrefixSrcFor, PHASE_HEADING_BASELINE, } = phaseIdMod;
|
|
34
|
+
const { customPhaseQuery, customDirectoryOwner, isCustomPhaseId, normalizePhaseName, comparePhaseNum, extractPhaseToken, PHASE_NUMBER_TOKEN_SOURCE, isSentinelPhaseId, renderPhaseBranchName, parsePhaseId, renderPhaseId, phaseHeadingPrefixSrcFor, PHASE_HEADING_BASELINE, } = phaseIdMod;
|
|
35
35
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
36
36
|
const phaseIdDisplayMod = require("./phase-id-display.cjs");
|
|
37
37
|
const { renderBracketMilestoneDisplay } = phaseIdDisplayMod;
|
|
38
38
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
39
39
|
const phaseLocatorMod = require("./phase-locator.cjs");
|
|
40
|
-
const { getArchivedPhaseDirs, findPhaseInternal, listMilestonePhaseDirs } = phaseLocatorMod;
|
|
40
|
+
const { phaseDirectoryOptions, getArchivedPhaseDirs, findPhaseInternal, listMilestonePhaseDirs } = phaseLocatorMod;
|
|
41
41
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
42
42
|
const roadmapParserMod = require("./roadmap-parser.cjs");
|
|
43
43
|
const { extractCurrentMilestone, stripShippedMilestones: _stripShippedMilestones, getMilestoneInfo, getRoadmapPhaseInternal } = roadmapParserMod;
|
|
@@ -1822,7 +1822,7 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify, filesRemoved) {
|
|
|
1822
1822
|
// phase — the phase arm must never branch-mutate for it (isSentinelPhaseId
|
|
1823
1823
|
// is the invariant's single owner, src/phase-id.cts).
|
|
1824
1824
|
if (phaseNum && !isSentinelPhaseId(phaseNum)) {
|
|
1825
|
-
const phaseInfo = findPhaseInternal(cwd, phaseNum);
|
|
1825
|
+
const phaseInfo = findPhaseInternal(cwd, phaseNum, undefined, phaseDirectoryOptions(cwd, config));
|
|
1826
1826
|
if (phaseInfo) {
|
|
1827
1827
|
// #4126: shared with init.cts's cmdInitExecutePhase branch_name field
|
|
1828
1828
|
// via the one canonical renderer (src/phase-id.cts) so an undeliverable
|
|
@@ -3053,6 +3053,7 @@ function cmdProgressRender(cwd, format, raw) {
|
|
|
3053
3053
|
? renderBracketMilestoneDisplay(milestone?.version, loadConfig(cwd).project_code)
|
|
3054
3054
|
: null;
|
|
3055
3055
|
const milestoneVersion = milestoneDisplay ?? milestone?.version ?? null;
|
|
3056
|
+
const directoryContext = phaseDirectoryOptions(cwd);
|
|
3056
3057
|
const phases = [];
|
|
3057
3058
|
let totalPlans = 0;
|
|
3058
3059
|
let totalSummaries = 0;
|
|
@@ -3091,9 +3092,11 @@ function cmdProgressRender(cwd, format, raw) {
|
|
|
3091
3092
|
}
|
|
3092
3093
|
}
|
|
3093
3094
|
else {
|
|
3095
|
+
const custom = customPhaseQuery(dir, directoryContext);
|
|
3096
|
+
const owner = custom && customDirectoryOwner(dir, custom);
|
|
3094
3097
|
const dm = dir.match(/^(\d+(?:\.\d+)*)-?(.*)/);
|
|
3095
|
-
phaseNum = dm ? dm[1] : dir;
|
|
3096
|
-
phaseName = dm && dm[2] ? dm[2].replace(/-/g, ' ') : '';
|
|
3098
|
+
phaseNum = owner ? owner.id : dm ? dm[1] : dir;
|
|
3099
|
+
phaseName = owner ? owner.slug.replace(/-/g, ' ') : dm && dm[2] ? dm[2].replace(/-/g, ' ') : '';
|
|
3097
3100
|
}
|
|
3098
3101
|
// #3183/#5060: canonical plan/summary counts and lifecycle status
|
|
3099
3102
|
// (root+nested, superseded-excluded, canonical pairing) from the
|
|
@@ -3212,7 +3215,7 @@ function cmdTodoMatchPhase(cwd, phase, raw) {
|
|
|
3212
3215
|
.map(w => w.replace(/[^a-z0-9]/g, ''))
|
|
3213
3216
|
.filter(w => w.length > 2 && !stopWords.has(w)));
|
|
3214
3217
|
// Find phase directory to get expected file paths
|
|
3215
|
-
const phaseInfoDisk = findPhaseInternal(cwd, phase);
|
|
3218
|
+
const phaseInfoDisk = findPhaseInternal(cwd, phase, undefined, phaseDirectoryOptions(cwd));
|
|
3216
3219
|
const phasePlans = [];
|
|
3217
3220
|
if (phaseInfoDisk && phaseInfoDisk['found']) {
|
|
3218
3221
|
try {
|
|
@@ -3394,7 +3397,7 @@ function cmdScaffold(cwd, type, options, raw) {
|
|
|
3394
3397
|
const padded = phase ? normalizePhaseName(phase) : '00';
|
|
3395
3398
|
const today = clock_cjs_1.realClock.localToday();
|
|
3396
3399
|
// Find phase directory
|
|
3397
|
-
const phaseInfo = phase ? findPhaseInternal(cwd, phase) : null;
|
|
3400
|
+
const phaseInfo = phase && type !== 'phase-dir' ? findPhaseInternal(cwd, phase, undefined, phaseDirectoryOptions(cwd)) : null;
|
|
3398
3401
|
const phaseDir = phaseInfo ? node_path_1.default.join(cwd, phaseInfo['directory']) : null;
|
|
3399
3402
|
if (phase && !phaseDir && type !== 'phase-dir') {
|
|
3400
3403
|
error(`Phase ${phase} directory not found`);
|
|
@@ -3422,14 +3425,14 @@ function cmdScaffold(cwd, type, options, raw) {
|
|
|
3422
3425
|
}
|
|
3423
3426
|
const slug = generateSlugInternal(name);
|
|
3424
3427
|
// #3287: apply project_code prefix to stay consistent with phase.add/phase.insert
|
|
3425
|
-
const scaffoldConfig =
|
|
3426
|
-
const
|
|
3427
|
-
const
|
|
3428
|
-
|
|
3429
|
-
const
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3428
|
+
const scaffoldConfig = phaseDirectoryOptions(cwd, undefined, '');
|
|
3429
|
+
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
3430
|
+
const roadmap = scaffoldConfig.phaseNaming === 'custom' && node_fs_1.default.existsSync(roadmapPath)
|
|
3431
|
+
? node_fs_1.default.readFileSync(roadmapPath, 'utf8') : '';
|
|
3432
|
+
const [{ dirPath }] = phaseLocatorMod.createPhaseDirectories(cwd, [{
|
|
3433
|
+
id: phase, slug: slug || '',
|
|
3434
|
+
directoryId: scaffoldConfig.phaseNaming === 'custom' && isCustomPhaseId(phase) ? phase : padded,
|
|
3435
|
+
}], { ...scaffoldConfig, roadmap }, false);
|
|
3433
3436
|
output({ created: true, directory: toPosixPath(node_path_1.default.relative(cwd, dirPath)), path: dirPath }, raw, dirPath);
|
|
3434
3437
|
return;
|
|
3435
3438
|
}
|
|
@@ -3728,12 +3731,19 @@ function cmdStats(cwd, format, raw) {
|
|
|
3728
3731
|
*/
|
|
3729
3732
|
function cmdCheckCommit(cwd, raw) {
|
|
3730
3733
|
const config = loadConfig(cwd);
|
|
3731
|
-
|
|
3734
|
+
// #5548: execGit scrubs the repo-location vars, but inside the pre-commit hook git sets them
|
|
3735
|
+
// (GIT_DIR/GIT_WORK_TREE of an explicit --git-dir commit, GIT_INDEX_FILE of a `-a` / `<path>`
|
|
3736
|
+
// commit) to name the repo being committed: hand them back or the guard finds no repo and allows.
|
|
3737
|
+
const hookEnv = {};
|
|
3738
|
+
for (const k of shell_command_projection_cjs_1.GIT_LOCATION_ENV_VARS)
|
|
3739
|
+
if (process.env[k] !== undefined)
|
|
3740
|
+
hookEnv[k] = process.env[k];
|
|
3741
|
+
const stagedResult = (0, shell_command_projection_cjs_1.execGit)(['diff', '--cached', '--name-only', '-z'], { cwd, env: hookEnv });
|
|
3732
3742
|
if (stagedResult.exitCode === 0) {
|
|
3733
3743
|
const files = stagedResult.stdout.split('\0').filter(Boolean);
|
|
3734
3744
|
const planningFiles = files.filter(f => f.startsWith('.planning/'));
|
|
3735
3745
|
if (planningFiles.length > 0) {
|
|
3736
|
-
const policy = resolveCommitDocsPolicy(config, detectPhaseNumberFromFiles(planningFiles), () => isGitIgnored(cwd, '.planning'));
|
|
3746
|
+
const policy = resolveCommitDocsPolicy(config, detectPhaseNumberFromFiles(planningFiles), () => isGitIgnored(cwd, '.planning', hookEnv));
|
|
3737
3747
|
if (!policy.resolved) {
|
|
3738
3748
|
error(`commit_docs is false but ${planningFiles.length} .planning/ file(s) are staged:\n` +
|
|
3739
3749
|
planningFiles.map(f => ` ${f}`).join('\n') +
|
|
@@ -213,16 +213,16 @@ function _deepMergeConfig(base, overlay) {
|
|
|
213
213
|
const _warnedUnknownConfigKeys = new Set();
|
|
214
214
|
// ─── Git utilities ────────────────────────────────────────────────────────────
|
|
215
215
|
const _gitIgnoredCache = new Map();
|
|
216
|
-
function isGitIgnored(cwd, targetPath) {
|
|
216
|
+
function isGitIgnored(cwd, targetPath, env) {
|
|
217
217
|
// #2206: strip trailing slashes — `git check-ignore` has a quirk where a
|
|
218
218
|
// CRLF .gitignore with blank lines falsely reports a trailing-slash path
|
|
219
219
|
// (e.g. `.planning/`) as ignored. Normalizing here protects every call site.
|
|
220
220
|
const normalized = targetPath.replace(/\/+$/, '');
|
|
221
|
-
const key = cwd + '::' + normalized;
|
|
221
|
+
const key = cwd + '::' + normalized + (env ? '::env:' + JSON.stringify(Object.entries(env).sort()) : '');
|
|
222
222
|
if (_gitIgnoredCache.has(key))
|
|
223
223
|
return _gitIgnoredCache.get(key);
|
|
224
224
|
// --no-index checks .gitignore rules regardless of whether the file is tracked.
|
|
225
|
-
const result = (0, shell_command_projection_cjs_1.execGit)(['check-ignore', '-q', '--no-index', '--', normalized], { cwd });
|
|
225
|
+
const result = (0, shell_command_projection_cjs_1.execGit)(['check-ignore', '-q', '--no-index', '--', normalized], { cwd, env });
|
|
226
226
|
const ignored = result.exitCode === 0;
|
|
227
227
|
_gitIgnoredCache.set(key, ignored);
|
|
228
228
|
return ignored;
|
|
@@ -42,6 +42,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
42
42
|
const frontmatter_fence_cjs_1 = require("./frontmatter-fence.cjs");
|
|
43
43
|
// pattern.cjs is a leaf module (escapeRegex only), so this adds no cycle.
|
|
44
44
|
const pattern_cjs_1 = require("./pattern.cjs");
|
|
45
|
+
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
45
46
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
46
47
|
const phaseIdModule = require("./phase-id.cjs");
|
|
47
48
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -553,6 +554,15 @@ function planRowPattern(planId, opts = {}) {
|
|
|
553
554
|
const lead = opts.anchored ? '^(\\s*-\\s*\\[)' : '(-\\s*\\[)';
|
|
554
555
|
return new RegExp(`${lead}${box}(\\]\\s*(?:\\*\\*)?${(0, pattern_cjs_1.escapeRegex)(planId)}${PLAN_ID_ROW_BOUNDARY_SOURCE}(?:\\*\\*)?)`, 'i');
|
|
555
556
|
}
|
|
557
|
+
/**
|
|
558
|
+
* Tick the first line-anchored unchecked row for `planId` in `text` (the
|
|
559
|
+
* `phase complete` / `roadmap update-plan-progress` bullet flip): a bullet
|
|
560
|
+
* mutation through `updateBullet`, so never a fenced line or mid-line match.
|
|
561
|
+
*/
|
|
562
|
+
function tickAnchoredPlanRow(text, planId) {
|
|
563
|
+
const row = planRowPattern(planId, { anchored: true });
|
|
564
|
+
return (0, markdown_sectionizer_cjs_1.updateBullet)(text, (_bulletText, rawLine) => row.test(rawLine), (rawLine) => rawLine.replace(row, '$1x$2'));
|
|
565
|
+
}
|
|
556
566
|
module.exports = {
|
|
557
567
|
toPosixPath,
|
|
558
568
|
normalizeLineEndings,
|
|
@@ -570,4 +580,5 @@ module.exports = {
|
|
|
570
580
|
findOrphanSummaries,
|
|
571
581
|
summaryPairsWithAnyPlan,
|
|
572
582
|
planRowPattern,
|
|
583
|
+
tickAnchoredPlanRow,
|
|
573
584
|
};
|
|
@@ -851,6 +851,17 @@ function frontmatterListEntries(content, key) {
|
|
|
851
851
|
* no closed, parseable frontmatter mapping or the key is not an own key of it.
|
|
852
852
|
*/
|
|
853
853
|
function rawFrontmatterField(content, key) {
|
|
854
|
+
const mapping = rawFrontmatterMapping(content);
|
|
855
|
+
if (!mapping || !Object.prototype.hasOwnProperty.call(mapping, key))
|
|
856
|
+
return null;
|
|
857
|
+
return { value: mapping[key] };
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* The top-level frontmatter mapping VERBATIM, off `rawFrontmatterField`'s guarded parse path.
|
|
861
|
+
* Null when the document has no closed, parseable frontmatter mapping — so a caller can tell
|
|
862
|
+
* "parsed, without that key" (#4998 review) from "no usable frontmatter".
|
|
863
|
+
*/
|
|
864
|
+
function rawFrontmatterMapping(content) {
|
|
854
865
|
const found = frontmatterRegion(content);
|
|
855
866
|
if (!found || !found.terminated)
|
|
856
867
|
return null;
|
|
@@ -867,9 +878,7 @@ function rawFrontmatterField(content, key) {
|
|
|
867
878
|
}
|
|
868
879
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
869
880
|
return null;
|
|
870
|
-
|
|
871
|
-
return null;
|
|
872
|
-
return { value: raw[key] };
|
|
881
|
+
return raw;
|
|
873
882
|
}
|
|
874
883
|
/**
|
|
875
884
|
* Escape a string for emission inside a YAML double-quoted scalar (#1779). ADR-3473 §8.1
|
|
@@ -940,6 +949,14 @@ function scalarNeedsDoubleQuoting(s) {
|
|
|
940
949
|
// round-trip.
|
|
941
950
|
if (/[\uD800-\uDFFF]/.test(s))
|
|
942
951
|
return true;
|
|
952
|
+
// #4998: U+2028/U+2029 are not YAML line breaks, so a bare one round-trips
|
|
953
|
+
// through js-yaml, but every line-anchored reader of a planning document
|
|
954
|
+
// (`^key:\s*(.+)` with the `m` flag) ends the line there and reads only the
|
|
955
|
+
// part before it. (A value holding `:` was already quoted by the callers, so
|
|
956
|
+
// no key could be forged this way.) The quoted path escapes them (`\L`,
|
|
957
|
+
// `\P`), which no such reader splits on.
|
|
958
|
+
if (/[\u2028\u2029]/.test(s))
|
|
959
|
+
return true;
|
|
943
960
|
return false;
|
|
944
961
|
}
|
|
945
962
|
/**
|
|
@@ -1806,6 +1823,7 @@ module.exports = {
|
|
|
1806
1823
|
// parse path — the decision-coverage gates read `must_haves`/`truths`/`objective` and the
|
|
1807
1824
|
// SUMMARY `files_modified` list through it instead of a hand-rolled `^key:` scan / regex.
|
|
1808
1825
|
rawFrontmatterField,
|
|
1826
|
+
rawFrontmatterMapping,
|
|
1809
1827
|
// #5139: one key's block as unparsed text, for a citation scan that must survive frontmatter
|
|
1810
1828
|
// the YAML parser refuses.
|
|
1811
1829
|
frontmatterKeyBlockText,
|
|
@@ -39,7 +39,7 @@ const planningWorkspaceMod = require("./planning-workspace.cjs");
|
|
|
39
39
|
const { planningDir } = planningWorkspaceMod;
|
|
40
40
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
41
41
|
const phaseLocatorMod = require("./phase-locator.cjs");
|
|
42
|
-
const { findPhaseInternal } = phaseLocatorMod;
|
|
42
|
+
const { phaseDirectoryOptions, findPhaseInternal } = phaseLocatorMod;
|
|
43
43
|
const security_cjs_1 = require("./security.cjs");
|
|
44
44
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
45
45
|
const roadmapModule = require("./roadmap.cjs");
|
|
@@ -171,7 +171,7 @@ function evaluateApiCoverageVerifyPre(input) {
|
|
|
171
171
|
let resolvedDir = null;
|
|
172
172
|
let phaseNumber = '';
|
|
173
173
|
if (token) {
|
|
174
|
-
const found = findPhaseInternal(projectDir, token);
|
|
174
|
+
const found = findPhaseInternal(projectDir, token, undefined, phaseDirectoryOptions(projectDir));
|
|
175
175
|
if (found && found.directory) {
|
|
176
176
|
// findPhaseInternal's `directory` is relative to the project root; anchor it to `projectDir`
|
|
177
177
|
// (not the process cwd) so the gate reads the same files in-process as it does from the CLI.
|
|
@@ -85,7 +85,7 @@ function runContextDriftGate(cwd, phaseArg) {
|
|
|
85
85
|
}
|
|
86
86
|
// Same phase-directory resolution rule the schema-drift gate uses (#1571, #2528):
|
|
87
87
|
// matchPhaseDirs, never a naive substring test.
|
|
88
|
-
const phaseDir = (0, gate_phase_context_cjs_1.resolvePhaseDirByToken)(phasesDir, phaseArg);
|
|
88
|
+
const phaseDir = (0, gate_phase_context_cjs_1.resolvePhaseDirByToken)(cwd, phasesDir, phaseArg);
|
|
89
89
|
if (!phaseDir) {
|
|
90
90
|
return unresolvable();
|
|
91
91
|
}
|
|
@@ -34,10 +34,7 @@ const planningWorkspaceMod = require("./planning-workspace.cjs");
|
|
|
34
34
|
const { planningDir } = planningWorkspaceMod;
|
|
35
35
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
36
36
|
const phaseLocatorMod = require("./phase-locator.cjs");
|
|
37
|
-
const { findPhaseInternal } = phaseLocatorMod;
|
|
38
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
39
|
-
const phaseIdMod = require("./phase-id.cjs");
|
|
40
|
-
const { normalizePhaseName, matchPhaseDirs } = phaseIdMod;
|
|
37
|
+
const { phaseDirectoryOptions, findPhaseInternal, resolvePhaseDirectoryLookup, matchPhaseDirsForLookup } = phaseLocatorMod;
|
|
41
38
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
42
39
|
const roadmapModule = require("./roadmap.cjs");
|
|
43
40
|
const { getRoadmapPhaseWithFallback } = roadmapModule;
|
|
@@ -78,7 +75,7 @@ function unresolvableProbeVerdict(readError) {
|
|
|
78
75
|
function resolvePhaseDir(projectDir, phase) {
|
|
79
76
|
let result;
|
|
80
77
|
try {
|
|
81
|
-
result = findPhaseInternal(projectDir, phase);
|
|
78
|
+
result = findPhaseInternal(projectDir, phase, undefined, phaseDirectoryOptions(projectDir));
|
|
82
79
|
}
|
|
83
80
|
catch (err) {
|
|
84
81
|
return (0, gate_evidence_cjs_1.evidenceFromError)(err, `phase ${phase}`);
|
|
@@ -109,11 +106,13 @@ function resolvePhaseDir(projectDir, phase) {
|
|
|
109
106
|
* `resolvePhaseDir` would widen what those gates inspect, so the two stay apart and each states its
|
|
110
107
|
* scope. Throws when `phasesDir` cannot be listed; both callers sit inside their non-blocking catch.
|
|
111
108
|
*/
|
|
112
|
-
function resolvePhaseDirByToken(phasesDir, phaseArg) {
|
|
113
|
-
const
|
|
109
|
+
function resolvePhaseDirByToken(cwd, phasesDir, phaseArg, directoryContext = phaseDirectoryOptions(cwd)) {
|
|
110
|
+
const lookup = resolvePhaseDirectoryLookup(cwd, phaseArg, undefined, directoryContext);
|
|
114
111
|
const dirEntries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
|
|
115
112
|
const dirNames = dirEntries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
116
|
-
|
|
113
|
+
// #4304: both drift gates resolve a directory in the current checkout.
|
|
114
|
+
// Bracket is the only opt-in grammar; all other conventions remain legacy.
|
|
115
|
+
const matched = matchPhaseDirsForLookup(dirNames, lookup, undefined, directoryContext).matches[0];
|
|
117
116
|
if (matched)
|
|
118
117
|
return node_path_1.default.join(phasesDir, matched);
|
|
119
118
|
const contained = (0, security_cjs_1.tryWithinRoot)(phaseArg, phasesDir);
|
|
@@ -68,7 +68,7 @@ function runSchemaDriftGate(cwd, phaseArg, skipFlag) {
|
|
|
68
68
|
// matching "11-expansion"), making the drift gate inspect the wrong phase.
|
|
69
69
|
// This shares the one selection rule with find-phase / verify
|
|
70
70
|
// phase-completeness rather than restating it. (#1571, #2528)
|
|
71
|
-
const phaseDir = (0, gate_phase_context_cjs_1.resolvePhaseDirByToken)(phasesDir, phaseArg);
|
|
71
|
+
const phaseDir = (0, gate_phase_context_cjs_1.resolvePhaseDirByToken)(cwd, phasesDir, phaseArg);
|
|
72
72
|
if (!phaseDir) {
|
|
73
73
|
// An unresolvable phase is "could not look" (#5170): there is no phase directory to evaluate.
|
|
74
74
|
return schemaDriftUnreadable(`Phase directory not found: ${phaseArg}`);
|
|
@@ -32,14 +32,9 @@ function excludeFromGit(dir, filename) {
|
|
|
32
32
|
// gitignore cannot express a newline in a pattern, and git's line output could not be split.
|
|
33
33
|
if (/[\r\n]/.test(dir))
|
|
34
34
|
return;
|
|
35
|
-
// cwd must pick the repo: drop inherited repository-location variables (e.g. inside a git hook).
|
|
36
|
-
const env = { GIT_DIR: undefined, GIT_WORK_TREE: undefined, GIT_INDEX_FILE: undefined, GIT_COMMON_DIR: undefined };
|
|
37
35
|
// The prefix goes last: stdout is trimmed, and a non-empty prefix ends in `/`, so only an
|
|
38
36
|
// empty prefix (dir is the git root) loses its line; a leading space survives.
|
|
39
|
-
const revParse = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '--git-path', 'info/exclude', '--show-prefix'], {
|
|
40
|
-
cwd: dir,
|
|
41
|
-
env: env,
|
|
42
|
-
});
|
|
37
|
+
const revParse = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '--git-path', 'info/exclude', '--show-prefix'], { cwd: dir });
|
|
43
38
|
const [excludeRel, prefix = ''] = (0, text_lines_cjs_1.splitLines)(String(revParse.stdout));
|
|
44
39
|
if (revParse.exitCode !== 0 || !excludeRel)
|
|
45
40
|
return;
|
|
@@ -54,7 +54,7 @@ const validateMod = require("../validate.cjs");
|
|
|
54
54
|
const { isPhaseDirName } = validateMod;
|
|
55
55
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
56
56
|
const phaseIdMod = require("../phase-id.cjs");
|
|
57
|
-
const { extractPhaseToken, normalizePhaseName, comparePhaseNum } = phaseIdMod;
|
|
57
|
+
const { extractPhaseToken, normalizePhaseName, phaseKeyFromDir, comparePhaseNum } = phaseIdMod;
|
|
58
58
|
// #5060: the Phase Status Module owns the status-label ladder.
|
|
59
59
|
const phase_status_cjs_1 = require("../phase-status.cjs");
|
|
60
60
|
// #5291: W030 asks whether a raw report status has a known repair. The lookup is
|
|
@@ -63,8 +63,11 @@ const verification_cjs_1 = require("../verification.cjs");
|
|
|
63
63
|
// ─── W005 — phase directory doesn't follow NN-name format (verify.cts:1893-1902) ─
|
|
64
64
|
function checkW005(snapshot) {
|
|
65
65
|
const diagnostics = [];
|
|
66
|
+
const context = snapshot.directoryContext ?? phaseIdMod.phaseDirectoryContext(snapshot.config.value ?? {}, '', snapshot.roadmapDeclaredPhases.value.map(({ phaseId }) => phaseId));
|
|
66
67
|
for (const name of snapshot.phaseDirs.value) {
|
|
67
|
-
|
|
68
|
+
const custom = snapshot.phaseIdConvention === 'bracket' ? null : phaseIdMod.customPhaseQuery(name, context);
|
|
69
|
+
const owner = custom && phaseIdMod.customDirectoryOwner(name, custom);
|
|
70
|
+
if (!owner && !isPhaseDirName(name, snapshot.phaseIdConvention)) {
|
|
68
71
|
diagnostics.push({
|
|
69
72
|
code: 'W005',
|
|
70
73
|
severity: SEVERITY.WARNING,
|
|
@@ -77,17 +80,18 @@ function checkW005(snapshot) {
|
|
|
77
80
|
}
|
|
78
81
|
// ─── W023 — phase directories collide on normalized key (verify.cts:1904-1950) ─
|
|
79
82
|
//
|
|
80
|
-
// Groups
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
// `comparePhaseNum` + a `localeCompare` tiebreak, mirroring
|
|
83
|
+
// Groups directories by the canonical identity owner, preserving recorded
|
|
84
|
+
// custom IDs instead of collapsing their shared token prefixes. Sorted with
|
|
85
|
+
// contextual `comparePhaseNum` + a `localeCompare` tiebreak, mirroring
|
|
84
86
|
// `verify.cts:1930-1932`'s deterministic-output rationale. See the file-level
|
|
85
87
|
// comment for the disclosed "described" fidelity reduction.
|
|
86
88
|
function checkW023(snapshot) {
|
|
89
|
+
const context = snapshot.directoryContext;
|
|
87
90
|
const groups = new Map();
|
|
88
91
|
for (const name of snapshot.phaseDirs.value) {
|
|
89
|
-
const
|
|
90
|
-
|
|
92
|
+
const key = context?.phaseNaming === 'custom'
|
|
93
|
+
? phaseKeyFromDir(name, undefined, context)
|
|
94
|
+
: normalizePhaseName(extractPhaseToken(name));
|
|
91
95
|
const list = groups.get(key);
|
|
92
96
|
if (list)
|
|
93
97
|
list.push(name);
|
|
@@ -101,7 +105,7 @@ function checkW023(snapshot) {
|
|
|
101
105
|
continue;
|
|
102
106
|
const described = dirs
|
|
103
107
|
.slice()
|
|
104
|
-
.sort((a, b) => comparePhaseNum(a, b) || String(a).localeCompare(String(b)))
|
|
108
|
+
.sort((a, b) => comparePhaseNum(a, b, context) || String(a).localeCompare(String(b)))
|
|
105
109
|
.map((d) => {
|
|
106
110
|
const phase = phaseByDir.get(d);
|
|
107
111
|
const plans = phase ? phase.planCount : 0;
|
|
@@ -104,7 +104,7 @@ const planningScopeMod = require("../planning-scope.cjs");
|
|
|
104
104
|
const { SCOPE } = planningScopeMod;
|
|
105
105
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
106
106
|
const phaseIdMod = require("../phase-id.cjs");
|
|
107
|
-
const { matchPhaseDirs,
|
|
107
|
+
const { matchPhaseDirs, phaseLookupQuery, extractPhaseToken, isSentinelPhaseId, isSentinelPhaseDir } = phaseIdMod;
|
|
108
108
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
109
109
|
const validateMod = require("../validate.cjs");
|
|
110
110
|
const { phaseVariants } = validateMod;
|
|
@@ -116,7 +116,7 @@ const { phaseVariants } = validateMod;
|
|
|
116
116
|
* does the roadmap claim, for W007) call this — one matcher, reused, per the
|
|
117
117
|
* file-level comment.
|
|
118
118
|
*/
|
|
119
|
-
function dirsForPhase(dirs, phaseId, convention) {
|
|
119
|
+
function dirsForPhase(dirs, phaseId, convention, directoryContext) {
|
|
120
120
|
// #612: `convention` reaches BOTH operands of this pairing or it pairs
|
|
121
121
|
// nothing. The phases come from a bracket-aware heading scan and the dirs
|
|
122
122
|
// from a bracket-aware disk scan, so a convention-less selector here leaves
|
|
@@ -125,7 +125,7 @@ function dirsForPhase(dirs, phaseId, convention) {
|
|
|
125
125
|
// disk" AND every bracket directory reads as unclaimed. `matchPhaseDirs`
|
|
126
126
|
// forwards it to its primary `phaseTokenMatches`; handed nothing it is
|
|
127
127
|
// byte-identical, so no legacy repo's pairing changes.
|
|
128
|
-
const canonical = matchPhaseDirs(dirs,
|
|
128
|
+
const canonical = matchPhaseDirs(dirs, phaseLookupQuery(phaseId, directoryContext), convention, undefined, directoryContext).matches;
|
|
129
129
|
if (canonical.length > 0)
|
|
130
130
|
return canonical;
|
|
131
131
|
// Bug 2 (#3309 W006/W007 migration cluster, found while fixing the
|
|
@@ -187,6 +187,7 @@ function checkW006(snapshot) {
|
|
|
187
187
|
return [];
|
|
188
188
|
const dirs = snapshot.allPhaseDirNames.value;
|
|
189
189
|
const convention = snapshot.phaseIdConvention;
|
|
190
|
+
const directoryContext = snapshot.directoryContext ?? phaseIdMod.phaseDirectoryContext(snapshot.config.value ?? {}, '', snapshot.roadmapDeclaredPhases.value.map(({ phaseId }) => phaseId));
|
|
190
191
|
const archivedTokens = new Set(snapshot.archivedPhaseTokens.value);
|
|
191
192
|
// No separate scope guard: `roadmapSentinelPhaseTokens` and
|
|
192
193
|
// `roadmapDeclaredPhases` are produced by ONE builder call
|
|
@@ -219,7 +220,7 @@ function checkW006(snapshot) {
|
|
|
219
220
|
// not change its result.
|
|
220
221
|
if (sentinelTokens.has(phaseId) || isSentinelPhaseId(phaseId))
|
|
221
222
|
continue;
|
|
222
|
-
if (dirsForPhase(dirs, phaseId, convention).length > 0)
|
|
223
|
+
if (dirsForPhase(dirs, phaseId, convention, directoryContext).length > 0)
|
|
223
224
|
continue;
|
|
224
225
|
// Bug 1 (#3309 W006/W007 migration cluster): a phase whose ONLY
|
|
225
226
|
// directory lives under a milestone archive
|
|
@@ -247,10 +248,10 @@ function checkW006(snapshot) {
|
|
|
247
248
|
// ─── W007 — an on-disk phase directory has no matching ROADMAP entry ──────
|
|
248
249
|
// (verify.cts:2086-2101)
|
|
249
250
|
/** Every directory in `dirs` that ANY declared roadmap phase resolves to. */
|
|
250
|
-
function computeClaimedDirs(dirs, declaredPhases, convention) {
|
|
251
|
+
function computeClaimedDirs(dirs, declaredPhases, convention, directoryContext) {
|
|
251
252
|
const claimed = new Set();
|
|
252
253
|
for (const { phaseId } of declaredPhases) {
|
|
253
|
-
for (const dir of dirsForPhase(dirs, phaseId, convention))
|
|
254
|
+
for (const dir of dirsForPhase(dirs, phaseId, convention, directoryContext))
|
|
254
255
|
claimed.add(dir);
|
|
255
256
|
}
|
|
256
257
|
return claimed;
|
|
@@ -261,7 +262,8 @@ function checkW007(snapshot) {
|
|
|
261
262
|
return [];
|
|
262
263
|
const dirs = snapshot.allPhaseDirNames.value;
|
|
263
264
|
const convention = snapshot.phaseIdConvention;
|
|
264
|
-
const
|
|
265
|
+
const directoryContext = snapshot.directoryContext ?? phaseIdMod.phaseDirectoryContext(snapshot.config.value ?? {}, '', snapshot.roadmapDeclaredPhases.value.map(({ phaseId }) => phaseId));
|
|
266
|
+
const claimedDirs = computeClaimedDirs(dirs, snapshot.roadmapDeclaredPhases.value, convention, directoryContext);
|
|
265
267
|
const diagnostics = [];
|
|
266
268
|
for (const dirName of dirs) {
|
|
267
269
|
// `extractPhaseToken` is the phase-id.cts owner `PHASE_TOKEN_FROM_DIR_RE`
|
|
@@ -46,7 +46,7 @@ const healthDiagnosticMod = require("../health-diagnostic-types.cjs");
|
|
|
46
46
|
const { SEVERITY, adviseRemedy } = healthDiagnosticMod;
|
|
47
47
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
48
48
|
const phaseIdMod = require("../phase-id.cjs");
|
|
49
|
-
const { getMilestoneFromPhaseId, matchPhaseDirs,
|
|
49
|
+
const { getMilestoneFromPhaseId, matchPhaseDirs, phaseLookupQuery, extractPhaseToken, PHASE_NUMBER_TOKEN_SOURCE } = phaseIdMod;
|
|
50
50
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- the single owner of the persisted status vocabulary
|
|
51
51
|
const stateDocumentMod = require("../state-document.cjs");
|
|
52
52
|
const { normalizeStateStatus } = stateDocumentMod;
|
|
@@ -332,8 +332,8 @@ const RULE_W026 = {
|
|
|
332
332
|
// field's own doc comment on `PlanningSnapshot` for why
|
|
333
333
|
// `roadmapDeclaredPhases`'s `milestone` attribution is the wrong fit).
|
|
334
334
|
const unstarted = [];
|
|
335
|
+
const directoryContext = snapshot.directoryContext ?? phaseIdMod.phaseDirectoryContext(snapshot.config.value ?? {}, '', snapshot.currentMilestoneRoadmapPhaseIds.value);
|
|
335
336
|
for (const phaseId of snapshot.currentMilestoneRoadmapPhaseIds.value) {
|
|
336
|
-
const normalized = normalizePhaseName(phaseId);
|
|
337
337
|
// `allPhaseDirNames` — every directory under `phases/`, UNWINDOWED by
|
|
338
338
|
// ROADMAP-declaration membership — mirrors the original's own
|
|
339
339
|
// unwindowed `phaseDirNames2` (`verify.cts:2372-2382`, a direct
|
|
@@ -346,7 +346,7 @@ const RULE_W026 = {
|
|
|
346
346
|
// moved the read onto the shared selector; the convention rides along,
|
|
347
347
|
// since the selector's primary match IS the `phaseTokenMatches` call this
|
|
348
348
|
// line used to make directly.
|
|
349
|
-
const hasDirectory = matchPhaseDirs(snapshot.allPhaseDirNames.value,
|
|
349
|
+
const hasDirectory = matchPhaseDirs(snapshot.allPhaseDirNames.value, phaseLookupQuery(phaseId, directoryContext), snapshot.phaseIdConvention, undefined, directoryContext).matches.length > 0;
|
|
350
350
|
if (!hasDirectory)
|
|
351
351
|
unstarted.push(phaseId);
|
|
352
352
|
}
|