@opengsd/gsd-core 1.20.0 → 1.21.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/.opencode/plugins/gsd-core.js +33 -2
- package/agents/gsd-executor.md +1 -1
- package/agents/gsd-plan-checker.md +1 -1
- package/agents/gsd-planner.md +3 -3
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +265 -161
- package/commands/gsd/autonomous.md +1 -1
- package/commands/gsd/new-milestone.md +2 -2
- package/commands/gsd/new-project.md +2 -2
- package/gsd-core/bin/gsd-tools.cjs +554 -823
- package/gsd-core/bin/lib/capability-activation.cjs +8 -2
- package/gsd-core/bin/lib/capability-command-router.cjs +6 -2
- package/gsd-core/bin/lib/capability-ledger.cjs +1 -1
- package/gsd-core/bin/lib/capability-lock.cjs +19 -27
- package/gsd-core/bin/lib/capability-registry.cjs +74 -71
- package/gsd-core/bin/lib/capability-validator.cjs +1 -0
- package/gsd-core/bin/lib/capability-writer.cjs +7 -4
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +142 -62
- package/gsd-core/bin/lib/claude-orchestration.cjs +214 -122
- package/gsd-core/bin/lib/cli-surface.cjs +545 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +356 -103
- package/gsd-core/bin/lib/commands.cjs +99 -2
- package/gsd-core/bin/lib/config-file-read.cjs +85 -0
- package/gsd-core/bin/lib/config-loader.cjs +23 -15
- package/gsd-core/bin/lib/config.cjs +139 -101
- package/gsd-core/bin/lib/configuration.cjs +41 -2
- package/gsd-core/bin/lib/criterion-baseline.cjs +346 -0
- package/gsd-core/bin/lib/criterion-replay.cjs +380 -0
- package/gsd-core/bin/lib/estimate-cli.cjs +5 -1
- package/gsd-core/bin/lib/gap-checker.cjs +12 -1
- package/gsd-core/bin/lib/gate-evaluation-scope.cjs +593 -27
- package/gsd-core/bin/lib/gate-phase-context.cjs +16 -4
- package/gsd-core/bin/lib/graphify.cjs +19 -2
- package/gsd-core/bin/lib/gsd-tool-dir.cjs +134 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +5 -1
- package/gsd-core/bin/lib/init.cjs +20 -18
- package/gsd-core/bin/lib/install-effort-resolver.cjs +24 -4
- package/gsd-core/bin/lib/install-engine.cjs +20 -28
- package/gsd-core/bin/lib/install-fs-adapter.cjs +5 -1
- package/gsd-core/bin/lib/install-manifest-record.cjs +86 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -3
- package/gsd-core/bin/lib/install-profiles.cjs +4 -2
- package/gsd-core/bin/lib/io.cjs +6 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +62 -31
- package/gsd-core/bin/lib/markdown-table.cjs +39 -1
- package/gsd-core/bin/lib/milestone.cjs +8 -6
- package/gsd-core/bin/lib/model-resolver.cjs +8 -4
- package/gsd-core/bin/lib/phase-entry-grammar.cjs +14 -0
- package/gsd-core/bin/lib/phase-id.cjs +26 -9
- package/gsd-core/bin/lib/phase-locator.cjs +8 -3
- package/gsd-core/bin/lib/phase.cjs +170 -45
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +46 -23
- package/gsd-core/bin/lib/planning-snapshot.cjs +10 -6
- package/gsd-core/bin/lib/planning-workspace.cjs +20 -10
- package/gsd-core/bin/lib/profile-output.cjs +9 -6
- package/gsd-core/bin/lib/project-context.cjs +439 -0
- package/gsd-core/bin/lib/project-root.cjs +9 -230
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +1 -1
- package/gsd-core/bin/lib/report-parser.cjs +16 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +357 -50
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +24 -5
- package/gsd-core/bin/lib/roadmap.cjs +2 -2
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +317 -134
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +81 -3
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +6 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +279 -8
- package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +1010 -10
- package/gsd-core/bin/lib/stale-bake-guard.cjs +4 -1
- package/gsd-core/bin/lib/state-transition.cjs +103 -46
- package/gsd-core/bin/lib/state.cjs +448 -63
- package/gsd-core/bin/lib/surface.cjs +4 -7
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +10 -3
- package/gsd-core/bin/lib/verify-command-router.cjs +6 -1
- package/gsd-core/bin/lib/verify.cjs +49 -6
- package/gsd-core/bin/lib/workstream-inventory.cjs +4 -4
- package/gsd-core/bin/lib/worktree-base-ref.cjs +32 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +8 -111
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/autonomous-smart-discuss.md +99 -18
- package/gsd-core/references/checkpoints.md +1 -1
- package/gsd-core/references/planner-guidance.md +11 -0
- package/gsd-core/references/planner-human-verify-mode.md +1 -1
- package/gsd-core/references/planner-reversibility.md +2 -0
- package/gsd-core/references/planner-verify-command-grounding.md +22 -6
- package/gsd-core/workflows/autonomous.md +4 -4
- package/gsd-core/workflows/code-review-fix.md +5 -7
- package/gsd-core/workflows/execute-phase.md +21 -7
- package/gsd-core/workflows/plan-phase/steps/plan-pre-dispatch.md +101 -0
- package/gsd-core/workflows/plan-phase.md +18 -19
- package/hooks/dist/gsd-agent-isolation-guard.js +4 -3
- package/hooks/dist/gsd-config-reload.js +2 -2
- package/hooks/dist/gsd-context-monitor.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +9 -6
- package/hooks/dist/gsd-read-injection-scanner.js +2 -2
- package/hooks/dist/gsd-secret-read-guard.js +141 -6
- package/hooks/dist/gsd-statusline.js +3 -2
- package/hooks/dist/gsd-workflow-guard.js +2 -1
- package/hooks/dist/lib/config-read.js +49 -0
- package/hooks/dist/lib/git-cmd.js +3 -1
- package/hooks/dist/lib/git-head-tree.js +474 -0
- package/hooks/dist/lib/isolation-sentinel.js +168 -13
- package/hooks/dist/lib/secret-read-exemptions.js +161 -0
- package/hooks/gsd-agent-isolation-guard.js +4 -3
- package/hooks/gsd-config-reload.js +2 -2
- package/hooks/gsd-context-monitor.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +9 -6
- package/hooks/gsd-read-injection-scanner.js +2 -2
- package/hooks/gsd-secret-read-guard.js +141 -6
- package/hooks/gsd-statusline.js +3 -2
- package/hooks/gsd-workflow-guard.js +2 -1
- package/hooks/lib/config-read.js +49 -0
- package/hooks/lib/git-cmd.js +3 -1
- package/hooks/lib/git-head-tree.js +474 -0
- package/hooks/lib/isolation-sentinel.js +168 -13
- package/hooks/lib/secret-read-exemptions.js +161 -0
- package/package.json +7 -2
- package/pi/package-agents/gsd-executor.md +1 -1
- package/pi/package-agents/gsd-plan-checker.md +1 -1
- package/pi/package-agents/gsd-planner.md +3 -3
- package/pi/package-agents/gsd-verifier.md +2 -2
- package/pi/package-agents/manifest.json +4 -4
- package/pi/package-skills/gsd-autonomous/SKILL.md +1 -1
- package/pi/package-skills/gsd-new-milestone/SKILL.md +1 -1
- package/pi/package-skills/gsd-new-project/SKILL.md +1 -1
- package/scripts/changeset/cli.cjs +2 -2
- package/scripts/changeset/lint.cjs +28 -3
- package/scripts/changeset/serialize.cjs +12 -2
- package/scripts/ci-compact-v8-coverage.cjs +204 -0
- package/scripts/ci-fix-next-rerun.cjs +149 -0
- package/scripts/ci-next-health.cjs +52 -9
- package/scripts/ci-test-scope.cjs +77 -71
- package/scripts/command-contract-helpers.cjs +56 -7
- package/scripts/docs-guard-registry.cjs +3 -0
- package/scripts/gen-cli-surface-manifest.cjs +287 -0
- package/scripts/gen-platform-conformance-tier.cjs +258 -19
- package/scripts/lib/macos-conformance-tier.generated.cjs +59 -47
- package/scripts/lib/platform-conformance-tier.generated.cjs +23 -64
- package/scripts/lib/shellcheck-fetch.cjs +71 -4
- package/scripts/lint-removed-but-needed.cjs +562 -50
- package/scripts/merge-commit-pr.cjs +186 -0
- package/scripts/release-ancestry.cjs +86 -0
- package/scripts/release-notes/changelog-release-notes.cjs +345 -0
- package/scripts/release-notes/discord-release-summary.cjs +82 -1
- package/scripts/release-pipeline-pr.cjs +92 -0
- package/scripts/require-issue-link-policy.cjs +18 -9
- package/scripts/sync-next-version.cjs +11 -2
- package/scripts/timeout-evidence-from-ci.cjs +964 -0
- package/skills/gsd-autonomous/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-new-project/SKILL.md +1 -1
- package/vscode/package.json +1 -1
|
@@ -25,6 +25,7 @@ const node_crypto_1 = require("node:crypto");
|
|
|
25
25
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
26
26
|
const node_path_1 = __importDefault(require("node:path"));
|
|
27
27
|
const pattern_cjs_1 = require("./pattern.cjs");
|
|
28
|
+
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
28
29
|
const phase_entry_grammar_cjs_1 = require("./phase-entry-grammar.cjs");
|
|
29
30
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
30
31
|
const phaseIdModule = require("./phase-id.cjs");
|
|
@@ -60,7 +61,7 @@ const { rawFrontmatterMapping } = frontmatterMod;
|
|
|
60
61
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
61
62
|
const unusableInputMod = require("./unusable-input.cjs");
|
|
62
63
|
const { UNUSABLE_REASON, warnUnusableInput } = unusableInputMod;
|
|
63
|
-
const
|
|
64
|
+
const markdown_sectionizer_cjs_2 = require("./markdown-sectionizer.cjs");
|
|
64
65
|
const markdown_table_cjs_1 = require("./markdown-table.cjs");
|
|
65
66
|
const roadmap_progress_cjs_1 = require("./roadmap-progress.cjs");
|
|
66
67
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -73,10 +74,93 @@ const { SCOPE } = planningScopeMod;
|
|
|
73
74
|
* (`extractCurrentMilestone`, `currentMilestoneRawRanges`,
|
|
74
75
|
* `isMilestoneShippedInRoadmap`) previously kept byte-identical copies.
|
|
75
76
|
*/
|
|
76
|
-
const
|
|
77
|
+
const CLOSED_STATUS_WORD_SOURCE = '\\b(?:CLOSED|ARCHIVED|ABANDONED|SHIPPED|FAILED)\\b';
|
|
78
|
+
const CLOSED_STATUS_MARK_SOURCE = '\u{2705}|\u{1f5c4}';
|
|
79
|
+
// Same source, same flags as the pre-#5726 literal: no `u`, so `i` keeps ASCII-only folding.
|
|
80
|
+
const MILESTONE_CLOSED_MARKER_PATTERN = new RegExp(`${CLOSED_STATUS_WORD_SOURCE}|${CLOSED_STATUS_MARK_SOURCE}`, 'i');
|
|
77
81
|
const MILESTONE_ACTIVE_MARKER_PATTERN = /\b(?:STARTED|ACTIVE|WIP)\b|in\s+progress|🚧|🔄/i;
|
|
82
|
+
const CLOSED_STATUS_WORD_RE = new RegExp(CLOSED_STATUS_WORD_SOURCE, 'i');
|
|
83
|
+
const CLOSED_STATUS_MARK_RE = new RegExp(CLOSED_STATUS_MARK_SOURCE, 'u');
|
|
84
|
+
/**
|
|
85
|
+
* THE closed-milestone predicate (#5726, ADR-3180): selection, masking, the
|
|
86
|
+
* archive strip and the shipped refusal all call this one function. It is the
|
|
87
|
+
* pre-#5726 rule, a closed marker and no active marker, with ONE exception: a
|
|
88
|
+
* check mark that appears only inside a parenthetical naming another version
|
|
89
|
+
* (`Next (after v2.0 prep` + check mark + `)`) does not close the heading. A
|
|
90
|
+
* status word counts anywhere, including inside a version parenthetical
|
|
91
|
+
* (`(shipped, replaces v0.9)`).
|
|
92
|
+
*/
|
|
78
93
|
function isClosedMilestoneHeading(headingText) {
|
|
79
|
-
|
|
94
|
+
if (!MILESTONE_CLOSED_MARKER_PATTERN.test(headingText) || MILESTONE_ACTIVE_MARKER_PATTERN.test(headingText))
|
|
95
|
+
return false;
|
|
96
|
+
return CLOSED_STATUS_WORD_RE.test(headingText) || hasClosedStatusMark(headingText);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Does a check mark close the heading? One linear pass splits the heading into
|
|
100
|
+
* its top-level text and its top-level parentheticals. A parenthetical is exempt
|
|
101
|
+
* only when the heading's own version (text before the first parenthetical)
|
|
102
|
+
* exists and every version the parenthetical names is unrelated to it; versions
|
|
103
|
+
* are normalized by one helper (case-insensitive, dotted segments, suffix dropped,
|
|
104
|
+
* a dotted prefix counts as the same). An unbalanced or nested parenthesis, a
|
|
105
|
+
* missing own version or a version after the group disables the exemption, so such
|
|
106
|
+
* a heading reads exactly as before #5726.
|
|
107
|
+
*/
|
|
108
|
+
function hasClosedStatusMark(headingText) {
|
|
109
|
+
let depth = 0;
|
|
110
|
+
let start = 0;
|
|
111
|
+
let outside = '';
|
|
112
|
+
let ownText = '';
|
|
113
|
+
const groups = [];
|
|
114
|
+
for (let i = 0; i < headingText.length; i++) {
|
|
115
|
+
const ch = headingText[i];
|
|
116
|
+
if (ch === '(') {
|
|
117
|
+
depth += 1;
|
|
118
|
+
if (depth > 1)
|
|
119
|
+
return CLOSED_STATUS_MARK_RE.test(headingText);
|
|
120
|
+
if (groups.length === 0)
|
|
121
|
+
ownText = outside;
|
|
122
|
+
start = i + 1;
|
|
123
|
+
}
|
|
124
|
+
else if (ch === ')') {
|
|
125
|
+
depth -= 1;
|
|
126
|
+
if (depth < 0)
|
|
127
|
+
return CLOSED_STATUS_MARK_RE.test(headingText);
|
|
128
|
+
if (depth === 0)
|
|
129
|
+
groups.push(headingText.slice(start, i));
|
|
130
|
+
}
|
|
131
|
+
else if (depth === 0) {
|
|
132
|
+
outside += ch;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (depth !== 0)
|
|
136
|
+
return CLOSED_STATUS_MARK_RE.test(headingText);
|
|
137
|
+
if (CLOSED_STATUS_MARK_RE.test(outside))
|
|
138
|
+
return true;
|
|
139
|
+
const own = versionSegments(ownText);
|
|
140
|
+
return groups.some((group) => {
|
|
141
|
+
if (!CLOSED_STATUS_MARK_RE.test(group))
|
|
142
|
+
return false;
|
|
143
|
+
const named = versionsIn(group);
|
|
144
|
+
// Fail closed: exempt only when the heading's own version precedes the group, the
|
|
145
|
+
// group names a version, and every version it names is unrelated to the own one.
|
|
146
|
+
return own === null || named.length === 0 || named.some((v) => versionsRelated(own, v));
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
const HEADING_VERSION_RE = /\bv\d+(?:\.\d+)*/gi;
|
|
150
|
+
/** `v` + digits + dotted segments, lowercased as numbers; any suffix (`-beta`, `.rc`) is dropped. */
|
|
151
|
+
function versionsIn(text) {
|
|
152
|
+
return (text.match(HEADING_VERSION_RE) ?? []).map((v) => v.slice(1).split('.').map(Number));
|
|
153
|
+
}
|
|
154
|
+
function versionSegments(text) {
|
|
155
|
+
return versionsIn(text)[0] ?? null;
|
|
156
|
+
}
|
|
157
|
+
/** Equal, or one a dotted-segment prefix of the other (`v2` / `v2.0` / `v2.0.1`). */
|
|
158
|
+
function versionsRelated(a, b) {
|
|
159
|
+
const n = Math.min(a.length, b.length);
|
|
160
|
+
for (let i = 0; i < n; i++)
|
|
161
|
+
if (a[i] !== b[i])
|
|
162
|
+
return false;
|
|
163
|
+
return true;
|
|
80
164
|
}
|
|
81
165
|
/**
|
|
82
166
|
* Is this `<details>` block a CLOSED milestone? One test for the read-side strip
|
|
@@ -97,7 +181,7 @@ function isClosedMilestoneBlock(block) {
|
|
|
97
181
|
* Strip shipped milestone content wrapped in <details> blocks.
|
|
98
182
|
*/
|
|
99
183
|
function stripShippedMilestones(content) {
|
|
100
|
-
return (0,
|
|
184
|
+
return (0, markdown_sectionizer_cjs_2.stripTaggedBlocks)(content, 'details');
|
|
101
185
|
}
|
|
102
186
|
/**
|
|
103
187
|
* #5543: hide every CLOSED-milestone `<details>` block (`<details>` opener, a
|
|
@@ -220,6 +304,132 @@ function isMilestoneShippedInRoadmap(content, version) {
|
|
|
220
304
|
}
|
|
221
305
|
return false;
|
|
222
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* #5726: `content` with every fenced code block and HTML comment blanked to
|
|
309
|
+
* spaces, so offsets and line structure are unchanged but no heading inside a
|
|
310
|
+
* fence or comment can be located. `\n` stays at its index; every other
|
|
311
|
+
* character (`\r` included) becomes a space; no `<!--` appears that the input
|
|
312
|
+
* did not have at that index.
|
|
313
|
+
*
|
|
314
|
+
* ONE linear scan in document order; the construct that starts first wins:
|
|
315
|
+
*
|
|
316
|
+
* - At a line start, a fence opener (the `matchFenceDelimiter` grammar: 0-3 spaces
|
|
317
|
+
* of indent, 3+ backticks or tildes, a backtick opener's info string holds no
|
|
318
|
+
* backtick) blanks through its matching closer line (same character, at least
|
|
319
|
+
* the opener's length, nothing but whitespace after), or to EOF when none
|
|
320
|
+
* exists. A `<!--` inside a fence opens nothing.
|
|
321
|
+
* - Otherwise the next `<!--` at `p` opens a comment, and its `-->` is searched
|
|
322
|
+
* from `p + 2`, so `<!-->` and `<!--->` are complete comments (CommonMark
|
|
323
|
+
* 0.31). With a closer, `p .. closer + 3` is blanked and may span lines; a
|
|
324
|
+
* fence line inside it is part of the comment.
|
|
325
|
+
* - With no closer anywhere after it: if the `<!--` opens its line (0-3 spaces
|
|
326
|
+
* before it, CommonMark HTML block type 2) everything to EOF is blanked; if
|
|
327
|
+
* it is mid-line nothing is hidden and the scan resumes after `p + 4`.
|
|
328
|
+
*
|
|
329
|
+
* Each of the next `<!--`, `-->` and `\n` is searched through a cached finder
|
|
330
|
+
* (the scan position only moves forward, so a hit at or past the position is
|
|
331
|
+
* still the next hit, and a miss stays a miss), keeping the whole pass linear.
|
|
332
|
+
*/
|
|
333
|
+
function blankFencesAndComments(content) {
|
|
334
|
+
const n = content.length;
|
|
335
|
+
const finder = (needle) => {
|
|
336
|
+
let hit = -2;
|
|
337
|
+
return (from) => {
|
|
338
|
+
if (hit === -1)
|
|
339
|
+
return -1;
|
|
340
|
+
if (hit < from)
|
|
341
|
+
hit = content.indexOf(needle, from);
|
|
342
|
+
return hit;
|
|
343
|
+
};
|
|
344
|
+
};
|
|
345
|
+
const nextOpen = finder('<!--');
|
|
346
|
+
const nextClose = finder('-->');
|
|
347
|
+
const nextNewline = finder('\n');
|
|
348
|
+
const fenceOn = (from, to) => (0, markdown_sectionizer_cjs_1.matchFenceDelimiter)(content.slice(from, to));
|
|
349
|
+
const lineEnd = (from) => {
|
|
350
|
+
const nl = nextNewline(from);
|
|
351
|
+
return nl === -1 ? n : nl;
|
|
352
|
+
};
|
|
353
|
+
const pieces = [];
|
|
354
|
+
let at = 0;
|
|
355
|
+
const hide = (from, to) => {
|
|
356
|
+
pieces.push(content.slice(at, from), content.slice(from, to).replace(/[^\n]/g, ' '));
|
|
357
|
+
at = to;
|
|
358
|
+
};
|
|
359
|
+
let pos = 0;
|
|
360
|
+
let lineStart = 0;
|
|
361
|
+
let atLineStart = true;
|
|
362
|
+
while (pos < n) {
|
|
363
|
+
const le = lineEnd(pos);
|
|
364
|
+
if (atLineStart) {
|
|
365
|
+
const opener = fenceOn(pos, le);
|
|
366
|
+
if (opener !== null && opener.canOpen) {
|
|
367
|
+
let closeEnd = n;
|
|
368
|
+
for (let ls = le + 1; ls < n;) {
|
|
369
|
+
const e = lineEnd(ls);
|
|
370
|
+
const m = fenceOn(ls, e);
|
|
371
|
+
if (m !== null && m.closes(opener)) {
|
|
372
|
+
closeEnd = e;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
ls = e + 1;
|
|
376
|
+
}
|
|
377
|
+
hide(pos, closeEnd);
|
|
378
|
+
pos = closeEnd + 1;
|
|
379
|
+
lineStart = pos;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const p = nextOpen(pos);
|
|
384
|
+
if (p === -1 || p >= le) {
|
|
385
|
+
pos = le + 1;
|
|
386
|
+
lineStart = pos;
|
|
387
|
+
atLineStart = true;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
const close = nextClose(p + 2);
|
|
391
|
+
if (close !== -1) {
|
|
392
|
+
const end = close + 3;
|
|
393
|
+
hide(p, end);
|
|
394
|
+
if (le < end)
|
|
395
|
+
lineStart = content.lastIndexOf('\n', end - 1) + 1;
|
|
396
|
+
pos = end;
|
|
397
|
+
atLineStart = false;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (p - lineStart <= 3 && /^ *$/.test(content.slice(lineStart, p))) {
|
|
401
|
+
hide(p, n);
|
|
402
|
+
return pieces.join('');
|
|
403
|
+
}
|
|
404
|
+
pos = p + 4;
|
|
405
|
+
atLineStart = false;
|
|
406
|
+
}
|
|
407
|
+
pieces.push(content.slice(at));
|
|
408
|
+
return pieces.join('');
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* #5726: is the milestone window `extractCurrentMilestoneScoped` selected for
|
|
412
|
+
* `version` a SHIPPED one? Decided on the heading the owner selects
|
|
413
|
+
* (`selectActiveMilestoneHeading`: the first non-closed match, else the first),
|
|
414
|
+
* located in fence- and comment-blanked content so an example inside a fence or
|
|
415
|
+
* comment cannot stand in for the real heading.
|
|
416
|
+
*/
|
|
417
|
+
function selectedMilestoneHeadingShipped(content, version, phaseIdConvention) {
|
|
418
|
+
const visible = blankFencesAndComments(content);
|
|
419
|
+
let matches = locateMilestoneHeadings(visible, version);
|
|
420
|
+
if (matches.length === 0)
|
|
421
|
+
matches = bracketFallbackHeadingMatches(visible, version, phaseIdConvention);
|
|
422
|
+
if (matches.length === 0)
|
|
423
|
+
return false;
|
|
424
|
+
return isClosedMilestoneHeading(selectActiveMilestoneHeading(visible, version, matches)[1]);
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* #5726: is the `<summary>` the archive-span window was cut from a shipped one?
|
|
428
|
+
* Read from fence- and comment-blanked content, tags removed.
|
|
429
|
+
*/
|
|
430
|
+
function archiveSummaryShipped(summaryInner) {
|
|
431
|
+
return isClosedMilestoneHeading(summaryInner.replace(/<[^>]{0,200}>/g, ''));
|
|
432
|
+
}
|
|
223
433
|
// #2761 B1: matches a bracket MILESTONE heading's intro (`[GSD.02]`) at the
|
|
224
434
|
// START of a heading's text, capturing the bracket id in group 1. Shared by
|
|
225
435
|
// isBracketMilestoneBoundary below — the ONE recognizer for "is this heading
|
|
@@ -357,7 +567,7 @@ function computeMilestoneSectionEnd(content, headingText, headingStart, addition
|
|
|
357
567
|
const afterHeading = headingStart + headingText.length;
|
|
358
568
|
// Use tokenizeHeadings (fence-aware, offsets into original content) to find
|
|
359
569
|
// the next stop boundary without re-implementing fence detection. T4 seam migration.
|
|
360
|
-
const headings = headingTokens ?? (0,
|
|
570
|
+
const headings = headingTokens ?? (0, markdown_sectionizer_cjs_2.tokenizeHeadings)(content);
|
|
361
571
|
for (const h of headings) {
|
|
362
572
|
if (h.offset <= headingStart)
|
|
363
573
|
continue;
|
|
@@ -516,6 +726,25 @@ function isMilestoneBoundedInRoadmap(content, version) {
|
|
|
516
726
|
function hasVersionedMilestones(content) {
|
|
517
727
|
return /^#{1,3}\s+.*v\d+\.\d+/mi.test(content);
|
|
518
728
|
}
|
|
729
|
+
/**
|
|
730
|
+
* #5726: does this ROADMAP carry a RECOGNIZED versioned milestone heading — a
|
|
731
|
+
* non-Phase level 1-3 heading with a version token, outside fenced code and
|
|
732
|
+
* HTML comments? Stricter than `hasVersionedMilestones` (a raw line regex that
|
|
733
|
+
* a Phase title such as `### Phase 1: Migrate to v2.0 API`, a fenced example or
|
|
734
|
+
* a commented-out heading all satisfy), so it is NOT a replacement: that
|
|
735
|
+
* predicate feeds `classifyMilestoneWindow`'s rows 3/4 and its behavior stays
|
|
736
|
+
* as is. This one gates only the write refusal for an unresolved STATE
|
|
737
|
+
* milestone, which must not fire on a roadmap that has no real milestone
|
|
738
|
+
* heading at all. Fences and comments are blanked by `blankFencesAndComments`,
|
|
739
|
+
* and `tokenizeHeadings` then reads the headings;
|
|
740
|
+
* `isRecognizedMilestoneHeading` (Phase exclusion, level bound) and
|
|
741
|
+
* `listMilestoneHeadings` (the version-bearing grammar) judge them.
|
|
742
|
+
*/
|
|
743
|
+
function hasRecognizedVersionedMilestoneHeading(content) {
|
|
744
|
+
const visible = blankFencesAndComments(content);
|
|
745
|
+
return (0, markdown_sectionizer_cjs_2.tokenizeHeadings)(visible).some((heading) => isRecognizedMilestoneHeading(heading.text, heading.level)
|
|
746
|
+
&& listMilestoneHeadings(`${'#'.repeat(heading.level)} ${heading.text}`).length > 0);
|
|
747
|
+
}
|
|
519
748
|
// This file's milestone-heading vocabulary: a version token (`v1.2`-style),
|
|
520
749
|
// a ✅/🚧/📋 status marker, or the word "Milestone". Tested against a
|
|
521
750
|
// non-Phase heading's own text by `hasMilestoneSectioning` below — this
|
|
@@ -610,7 +839,7 @@ const MILESTONE_HEADING_SIGNAL_PATTERN = /v\d+\.\d+|✅|📋|🚧|\bMilestone\b/
|
|
|
610
839
|
function countMilestoneHeadings(content) {
|
|
611
840
|
const isPhaseHeading = (text) => /^Phase\s+\S/i.test(text);
|
|
612
841
|
let milestoneHeadingCount = 0;
|
|
613
|
-
for (const heading of (0,
|
|
842
|
+
for (const heading of (0, markdown_sectionizer_cjs_2.tokenizeHeadings)(content)) {
|
|
614
843
|
if (heading.level < 1 || heading.level > 3)
|
|
615
844
|
continue;
|
|
616
845
|
if (isPhaseHeading(heading.text))
|
|
@@ -690,7 +919,7 @@ function sliceMilestoneWindow(content, version) {
|
|
|
690
919
|
*/
|
|
691
920
|
function milestoneSections(content) {
|
|
692
921
|
const sections = [];
|
|
693
|
-
const headings = (0,
|
|
922
|
+
const headings = (0, markdown_sectionizer_cjs_2.tokenizeHeadings)(content);
|
|
694
923
|
for (const heading of headings) {
|
|
695
924
|
if (heading.level < 1 || heading.level > 3)
|
|
696
925
|
continue;
|
|
@@ -797,7 +1026,7 @@ function hasPhaseEntries(markdown, phaseIdConvention) {
|
|
|
797
1026
|
// #3641: the widened grammar engages ONLY when the resolved convention is
|
|
798
1027
|
// 'bracket' — a project that has not opted in runs the legacy pattern
|
|
799
1028
|
// alone, byte-identically.
|
|
800
|
-
for (const h of (0,
|
|
1029
|
+
for (const h of (0, markdown_sectionizer_cjs_2.tokenizeHeadings)(markdown)) {
|
|
801
1030
|
if (h.level < 2 || h.level > 4)
|
|
802
1031
|
continue;
|
|
803
1032
|
if (isPhaseEntryHeading(h.text, phaseIdConvention))
|
|
@@ -808,7 +1037,7 @@ function hasPhaseEntries(markdown, phaseIdConvention) {
|
|
|
808
1037
|
// doc showing the convention) counts as a real phase entry. Strip fences
|
|
809
1038
|
// through the canonical seam before testing, matching tokenizeHeadings'
|
|
810
1039
|
// fence-awareness above.
|
|
811
|
-
if (BULLET_PHASE_LINE_PATTERN.test((0,
|
|
1040
|
+
if (BULLET_PHASE_LINE_PATTERN.test((0, markdown_sectionizer_cjs_2.stripFencedCode)(markdown).text))
|
|
812
1041
|
return true;
|
|
813
1042
|
// #3577: a markdown-table phase listing also declares phases.
|
|
814
1043
|
return collectTablePhaseRows(markdown).length > 0;
|
|
@@ -827,7 +1056,7 @@ const PHASE_LISTING_HEADER_RE = /^\|?\s*phase(?:\s*(?:#|no\.?|number))?\s*\|/i;
|
|
|
827
1056
|
const PHASE_NAME_HEADER_RE = /^(?:phase\s+)?name$/i;
|
|
828
1057
|
const TABLE_PHASE_ID_RE = /^[A-Za-z]?\d[\w.-]*$/;
|
|
829
1058
|
function collectTablePhaseRows(window) {
|
|
830
|
-
const unfenced = (0,
|
|
1059
|
+
const unfenced = (0, markdown_sectionizer_cjs_2.stripFencedCode)(window).text;
|
|
831
1060
|
const lines = unfenced.split(/\r?\n/);
|
|
832
1061
|
const rows = [];
|
|
833
1062
|
for (let i = 0; i + 1 < lines.length; i++) {
|
|
@@ -873,7 +1102,7 @@ function collectTablePhaseRows(window) {
|
|
|
873
1102
|
* synthesis it was never meant to coexist with.
|
|
874
1103
|
*/
|
|
875
1104
|
function hasPhaseListingTableHeader(content) {
|
|
876
|
-
const unfenced = (0,
|
|
1105
|
+
const unfenced = (0, markdown_sectionizer_cjs_2.stripFencedCode)(content).text;
|
|
877
1106
|
return unfenced.split(/\r?\n/).some((line) => PHASE_LISTING_HEADER_RE.test(line));
|
|
878
1107
|
}
|
|
879
1108
|
/**
|
|
@@ -906,7 +1135,7 @@ function scanMilestonePhaseIdSets(window, convention, context) {
|
|
|
906
1135
|
const capturing = convention === 'bracket';
|
|
907
1136
|
const bracketGroups = capturing ? 1 : 0;
|
|
908
1137
|
const phaseHeadingPattern = new RegExp(`^${phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.ANY_BRACKET, convention, capturing)}(${context?.phaseNaming === 'custom' && convention !== 'bracket' ? phaseIdModule.phaseScanTokenSource(context) : '[\\w][\\w.-]*'})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:`, 'i');
|
|
909
|
-
for (const h of (0,
|
|
1138
|
+
for (const h of (0, markdown_sectionizer_cjs_2.tokenizeHeadings)(window)) {
|
|
910
1139
|
if (h.level < 2 || h.level > 4)
|
|
911
1140
|
continue;
|
|
912
1141
|
const pm = phaseHeadingPattern.exec(h.text);
|
|
@@ -929,7 +1158,7 @@ function scanMilestonePhaseIdSets(window, convention, context) {
|
|
|
929
1158
|
// collapsing to a zero-count pass-all filter.
|
|
930
1159
|
let bm;
|
|
931
1160
|
const scanner = new RegExp(BULLET_PHASE_LINE_PATTERN.source, 'gim');
|
|
932
|
-
const unfenced = (0,
|
|
1161
|
+
const unfenced = (0, markdown_sectionizer_cjs_2.stripFencedCode)(window).text;
|
|
933
1162
|
while ((bm = scanner.exec(unfenced)) !== null) {
|
|
934
1163
|
if (!/^999\b/.test(bm[1]))
|
|
935
1164
|
ids.add(bm[1]);
|
|
@@ -999,7 +1228,7 @@ function scanMilestonePhaseIds(window, convention) {
|
|
|
999
1228
|
*/
|
|
1000
1229
|
function findMilestoneScopeHeadingLines(text, convention) {
|
|
1001
1230
|
const out = [];
|
|
1002
|
-
for (const h of (0,
|
|
1231
|
+
for (const h of (0, markdown_sectionizer_cjs_2.tokenizeHeadings)(text)) {
|
|
1003
1232
|
if (h.level > 3)
|
|
1004
1233
|
continue;
|
|
1005
1234
|
if (/^Phase\s+\S/i.test(h.text))
|
|
@@ -1090,7 +1319,7 @@ function bracketFallbackHeadingMatches(content, version, bracketScopeConvention)
|
|
|
1090
1319
|
// For every survivor, rebuilding [fullLine, fullLine] plus `.index`
|
|
1091
1320
|
// retains the match shape and first-match order expected downstream.
|
|
1092
1321
|
const bracketMilestoneHeadingRe = new RegExp(`^${bracketMilestoneIntroSrcFor(milestoneInt)}`, 'i');
|
|
1093
|
-
return (0,
|
|
1322
|
+
return (0, markdown_sectionizer_cjs_2.tokenizeHeadings)(content)
|
|
1094
1323
|
.filter((h) => h.level <= 3 && content[h.offset] === '#' && bracketMilestoneHeadingRe.test(h.text))
|
|
1095
1324
|
.map((h) => {
|
|
1096
1325
|
const lineEnd = content.indexOf('\n', h.offset);
|
|
@@ -1146,12 +1375,15 @@ function bracketAwareMilestoneSection(content, selected, sectionStart, bracketBo
|
|
|
1146
1375
|
}
|
|
1147
1376
|
/**
|
|
1148
1377
|
* #5038: single owner for classifying a `milestone:` scalar as STATE.md spells
|
|
1149
|
-
* it. The frontmatter parser (FAILSAFE_SCHEMA)
|
|
1150
|
-
*
|
|
1151
|
-
*
|
|
1152
|
-
*
|
|
1153
|
-
*
|
|
1154
|
-
*
|
|
1378
|
+
* it. The frontmatter parser (FAILSAFE_SCHEMA) sees YAML null as text, so every
|
|
1379
|
+
* reader classifies it here. The input is an already DECODED scalar (both the
|
|
1380
|
+
* frontmatter path and the raw line read in `readStateMilestoneScalar` decode
|
|
1381
|
+
* through the same FAILSAFE loader), so a quoted `"null"` is indistinguishable
|
|
1382
|
+
* from a bare `null` after decoding and reads as explicit null. A decoded value
|
|
1383
|
+
* of `null`, `Null`, `NULL` or `~` is explicit null on both paths. A decoded
|
|
1384
|
+
* `null # x` (quoted on disk) is a literal version; an unquoted
|
|
1385
|
+
* `milestone: null # x` decodes to `null` and is an explicit null. A blank value
|
|
1386
|
+
* is absent, not null.
|
|
1155
1387
|
*/
|
|
1156
1388
|
function classifyMilestoneScalar(raw) {
|
|
1157
1389
|
if (typeof raw !== 'string')
|
|
@@ -1159,19 +1391,31 @@ function classifyMilestoneScalar(raw) {
|
|
|
1159
1391
|
const trimmed = raw.trim();
|
|
1160
1392
|
if (trimmed === '')
|
|
1161
1393
|
return { explicitNull: false, version: null };
|
|
1162
|
-
|
|
1163
|
-
if (/^(?:null|Null|NULL|~)$/.test(bare))
|
|
1394
|
+
if (/^(?:null|Null|NULL|~)$/.test(trimmed))
|
|
1164
1395
|
return { explicitNull: true, version: null };
|
|
1396
|
+
// The input is already a DECODED scalar (#4998): a quote or ` #` inside it is
|
|
1397
|
+
// literal content, so the version is the trimmed text.
|
|
1165
1398
|
return { explicitNull: false, version: trimmed };
|
|
1166
1399
|
}
|
|
1400
|
+
/**
|
|
1401
|
+
* #5726: do two spellings name the same milestone version? Case-insensitive, the
|
|
1402
|
+
* leading `v` optional (`1.0` / `v1.0`); a suffix is significant (`v1.0-closing`
|
|
1403
|
+
* is a different version from `v1.0`).
|
|
1404
|
+
*/
|
|
1405
|
+
function sameMilestoneVersion(a, b) {
|
|
1406
|
+
const fold = (v) => v.trim().replace(/^v(?=\d)/i, '').toLowerCase();
|
|
1407
|
+
return fold(a) === fold(b);
|
|
1408
|
+
}
|
|
1167
1409
|
/**
|
|
1168
1410
|
* #4998: STATE.md's `milestone:` scalar as the frontmatter parser DECODES it.
|
|
1169
1411
|
* The raw `^milestone:` line read captured the on-disk YAML text — quotes and
|
|
1170
1412
|
* escapes included — and that text was re-serialized as the value, adding one
|
|
1171
|
-
* escape layer per `state.*` write. Falls back to
|
|
1172
|
-
* when there is no parseable frontmatter, or its `milestone` is not text
|
|
1173
|
-
* that
|
|
1174
|
-
*
|
|
1413
|
+
* escape layer per `state.*` write. Falls back to a `^milestone:` line read
|
|
1414
|
+
* only when there is no parseable frontmatter, or its `milestone` is not text;
|
|
1415
|
+
* that fallback line is decoded through the same FAILSAFE loader as
|
|
1416
|
+
* frontmatter (#5726). Frontmatter that parses without a `milestone` key names
|
|
1417
|
+
* none: a `milestone:` line further down (a fenced example) is not one (#4998
|
|
1418
|
+
* review, Minor 5).
|
|
1175
1419
|
*/
|
|
1176
1420
|
function readStateMilestoneScalar(stateRaw) {
|
|
1177
1421
|
const fm = rawFrontmatterMapping(stateRaw);
|
|
@@ -1183,8 +1427,20 @@ function readStateMilestoneScalar(stateRaw) {
|
|
|
1183
1427
|
// A non-string value keeps the pre-#4998 line read, which finds this
|
|
1184
1428
|
// frontmatter line first.
|
|
1185
1429
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1430
|
+
// `[ \t]*(\S.*)` cannot cross a newline: an empty value must not borrow the next
|
|
1431
|
+
// line (the `---` closing the frontmatter), so it reads as absent.
|
|
1432
|
+
const m = stateRaw.match(/^milestone:[ \t]*(\S.*)/m);
|
|
1433
|
+
if (!m)
|
|
1434
|
+
return null;
|
|
1435
|
+
// #5726: this branch holds on-disk YAML text. Decode it with the SAME guarded
|
|
1436
|
+
// FAILSAFE loader the frontmatter path uses (a one-key synthetic frontmatter
|
|
1437
|
+
// block), so quotes, YAML escapes and a trailing ` # comment` decode identically
|
|
1438
|
+
// on both paths by construction. An undecodable or non-string result keeps the
|
|
1439
|
+
// trimmed raw text.
|
|
1440
|
+
const text = m[1].trim();
|
|
1441
|
+
const decoded = rawFrontmatterMapping(`---\nmilestone: ${text}\n---\n`);
|
|
1442
|
+
const value = decoded ? decoded['milestone'] : undefined;
|
|
1443
|
+
return typeof value === 'string' ? value : text;
|
|
1188
1444
|
}
|
|
1189
1445
|
/**
|
|
1190
1446
|
* Extract the current milestone section from ROADMAP.md by positive lookup,
|
|
@@ -1221,6 +1477,9 @@ function extractCurrentMilestoneScoped(content, cwd, ws, phaseIdConvention) {
|
|
|
1221
1477
|
}
|
|
1222
1478
|
let version = null;
|
|
1223
1479
|
let stateAssertsNoMilestone = false;
|
|
1480
|
+
// #5726: set only when the version came from STATE.md's `milestone:` field,
|
|
1481
|
+
// not from the in-progress bullet guess below.
|
|
1482
|
+
let versionFromState = false;
|
|
1224
1483
|
try {
|
|
1225
1484
|
const statePath = node_path_1.default.join(planningDir(cwd, ws), 'STATE.md');
|
|
1226
1485
|
const stateRaw = (0, shell_command_projection_cjs_1.platformReadSync)(statePath);
|
|
@@ -1229,6 +1488,7 @@ function extractCurrentMilestoneScoped(content, cwd, ws, phaseIdConvention) {
|
|
|
1229
1488
|
if (milestoneScalar !== null) {
|
|
1230
1489
|
const classified = classifyMilestoneScalar(milestoneScalar);
|
|
1231
1490
|
version = classified.version;
|
|
1491
|
+
versionFromState = classified.version !== null;
|
|
1232
1492
|
stateAssertsNoMilestone = classified.explicitNull;
|
|
1233
1493
|
}
|
|
1234
1494
|
}
|
|
@@ -1247,6 +1507,11 @@ function extractCurrentMilestoneScoped(content, cwd, ws, phaseIdConvention) {
|
|
|
1247
1507
|
// twin copy in `getMilestonePhaseFilter`, the intra-owner-file duplicate
|
|
1248
1508
|
// review caught since the drift guard exempts this file by construction).
|
|
1249
1509
|
const versionedMilestonesPresent = hasVersionedMilestones(content);
|
|
1510
|
+
// #5726: STATE.md's milestone is one the ROADMAP itself marks shipped. Decided
|
|
1511
|
+
// below, from the heading (or archive `<summary>`) the window was actually cut
|
|
1512
|
+
// from, so a line that merely mentions the version cannot read as shipped.
|
|
1513
|
+
// Reported by the window's one owner.
|
|
1514
|
+
const shippedOf = (shipped) => shipped && versionFromState && version !== null ? { shippedMilestone: version } : {};
|
|
1250
1515
|
if (!version) {
|
|
1251
1516
|
const value = stripShippedMilestones(content);
|
|
1252
1517
|
return {
|
|
@@ -1296,7 +1561,7 @@ function extractCurrentMilestoneScoped(content, cwd, ws, phaseIdConvention) {
|
|
|
1296
1561
|
const anyMilestoneOrDetails = /^#{1,3}\s+(?!Phase\s+\S)(?:.*v\d+\.\d+|✅|📋|🚧|🔄)|<details/im;
|
|
1297
1562
|
const firstMilestoneMatch = content.match(anyMilestoneOrDetails);
|
|
1298
1563
|
const preambleCutoff = firstMilestoneMatch ? firstMilestoneMatch.index : detailsOpenIdx;
|
|
1299
|
-
const preamble = (0,
|
|
1564
|
+
const preamble = (0, markdown_sectionizer_cjs_2.stripTaggedBlocks)(content.slice(0, preambleCutoff), 'details')
|
|
1300
1565
|
// #5007 (Phase 6 / ADR-4910 §8): shared owner, see
|
|
1301
1566
|
// PHASE_HEADING_BLOCK_STRIP_RE above.
|
|
1302
1567
|
.replace(PHASE_HEADING_BLOCK_STRIP_RE, '')
|
|
@@ -1312,21 +1577,30 @@ function extractCurrentMilestoneScoped(content, cwd, ws, phaseIdConvention) {
|
|
|
1312
1577
|
windowHasPhaseEntries: hasPhaseEntries(value, phaseIdConvention),
|
|
1313
1578
|
documentHasPhaseEntries,
|
|
1314
1579
|
}),
|
|
1580
|
+
...shippedOf(archiveSummaryShipped(blankFencesAndComments(content).match(summaryPattern)?.[1] ?? '')),
|
|
1315
1581
|
};
|
|
1316
1582
|
}
|
|
1317
1583
|
}
|
|
1318
1584
|
const value = stripShippedMilestones(content);
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1585
|
+
const scope = classifyMilestoneWindow({
|
|
1586
|
+
readable: true,
|
|
1587
|
+
versionResolved,
|
|
1588
|
+
hasVersionedMilestones: versionedMilestonesPresent,
|
|
1589
|
+
headingFound: false,
|
|
1590
|
+
windowHasPhaseEntries: hasPhaseEntries(value, phaseIdConvention),
|
|
1591
|
+
documentHasPhaseEntries,
|
|
1592
|
+
});
|
|
1593
|
+
// #5726 (row 5): STATE.md names a milestone and the ROADMAP, which carries
|
|
1594
|
+
// versioned milestone headings, has none for it. `value` is then the whole
|
|
1595
|
+
// unshipped roadmap, so a write verb must not allocate against it. Reported
|
|
1596
|
+
// only here, by the window's one owner; a free-form legacy ROADMAP (no
|
|
1597
|
+
// versioned headings) and a bullet-guessed version stay a plain UNSCOPED.
|
|
1598
|
+
// The gate counts only RECOGNIZED milestone headings, so a version token in
|
|
1599
|
+
// a Phase title, a fence or an HTML comment does not read as a milestone.
|
|
1600
|
+
if (scope === SCOPE.UNSCOPED && versionFromState && hasRecognizedVersionedMilestoneHeading(content)) {
|
|
1601
|
+
return { value, scope, unresolvedMilestone: version };
|
|
1602
|
+
}
|
|
1603
|
+
return { value, scope };
|
|
1330
1604
|
}
|
|
1331
1605
|
const allMatches = headingMatches;
|
|
1332
1606
|
const isClosed = isClosedMilestoneHeading;
|
|
@@ -1340,7 +1614,7 @@ function extractCurrentMilestoneScoped(content, cwd, ws, phaseIdConvention) {
|
|
|
1340
1614
|
// #2761 B3: tokenize once for both the centralized section-end owner and
|
|
1341
1615
|
// the bracket preamble scan below. This keeps both boundary decisions
|
|
1342
1616
|
// fence-aware without restoring the local section walker retired by #3184.
|
|
1343
|
-
const currentMilestoneHeadings = (0,
|
|
1617
|
+
const currentMilestoneHeadings = (0, markdown_sectionizer_cjs_2.tokenizeHeadings)(content);
|
|
1344
1618
|
const { sectionEnd, selectedBracketId, bracketBoundary } = bracketAwareMilestoneSection(content, selected, sectionStart, bracketBoundaryActive, currentMilestoneHeadings);
|
|
1345
1619
|
const anyMilestonePattern = /^#{1,3}\s+(?!Phase\s+\S)(?:.*v\d+\.\d+|✅|📋|🚧)/im;
|
|
1346
1620
|
let earliestMilestoneIndex;
|
|
@@ -1528,7 +1802,7 @@ function extractCurrentMilestoneScoped(content, cwd, ws, phaseIdConvention) {
|
|
|
1528
1802
|
// forcing the two sites onto an identical terminal token that would be a
|
|
1529
1803
|
// behavior change here.
|
|
1530
1804
|
const currentSectionHasPhaseDetails = new RegExp(`^#{2,4}\\s*${phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.LABEL_ONLY)}\\S`, 'im').test(currentSection);
|
|
1531
|
-
const preambleBase = (0,
|
|
1805
|
+
const preambleBase = (0, markdown_sectionizer_cjs_2.stripTaggedBlocks)(beforeMilestones, 'details');
|
|
1532
1806
|
// #3235: the conditional wraps the REPLACE, not the pattern. This used to select between the
|
|
1533
1807
|
// strip regex and a `/$/` sentinel, which made the do-not-strip branch an identity replacement
|
|
1534
1808
|
// (CodeQL js/identity-replacement, alert 53) -- correct, but it left both branches sharing one
|
|
@@ -1553,6 +1827,7 @@ function extractCurrentMilestoneScoped(content, cwd, ws, phaseIdConvention) {
|
|
|
1553
1827
|
windowHasPhaseEntries: hasPhaseEntries(value, phaseIdConvention),
|
|
1554
1828
|
documentHasPhaseEntries,
|
|
1555
1829
|
}),
|
|
1830
|
+
...shippedOf(selectedMilestoneHeadingShipped(content, version, bracketScopeConvention)),
|
|
1556
1831
|
};
|
|
1557
1832
|
}
|
|
1558
1833
|
/**
|
|
@@ -1605,7 +1880,7 @@ function blankCodeAndComments(content) {
|
|
|
1605
1880
|
// closes a block.
|
|
1606
1881
|
const lines = content.split('\n');
|
|
1607
1882
|
const blank = (t) => t.replace(/[^\r\n]/g, ' ');
|
|
1608
|
-
for (const f of (0,
|
|
1883
|
+
for (const f of (0, markdown_sectionizer_cjs_2.scanFencedBlocks)(lines)) {
|
|
1609
1884
|
// An unclosed fence is bounded at the next heading, not EOF, so later archives stay visible.
|
|
1610
1885
|
let last = f.closeLineIdx;
|
|
1611
1886
|
if (last === -1) {
|
|
@@ -1622,7 +1897,7 @@ function blankCodeAndComments(content) {
|
|
|
1622
1897
|
const joined = lines.join('\n');
|
|
1623
1898
|
let codeBlanked = '';
|
|
1624
1899
|
let at = 0;
|
|
1625
|
-
for (const sp of (0,
|
|
1900
|
+
for (const sp of (0, markdown_sectionizer_cjs_2.scanInlineCodeSpans)(joined)) {
|
|
1626
1901
|
codeBlanked += joined.slice(at, sp.start) + blank(joined.slice(sp.start, sp.end));
|
|
1627
1902
|
at = sp.end;
|
|
1628
1903
|
}
|
|
@@ -1636,7 +1911,7 @@ function plainClosedSections(content, overlapsActive, activeVersion) {
|
|
|
1636
1911
|
// A closed milestone written as a plain heading section (no `<details>`) is
|
|
1637
1912
|
// archive too. It ends at the next heading of the same or shallower level, so
|
|
1638
1913
|
// a following `## Progress` table stays visible.
|
|
1639
|
-
const headingTokens = (0,
|
|
1914
|
+
const headingTokens = (0, markdown_sectionizer_cjs_2.tokenizeHeadings)(content);
|
|
1640
1915
|
const spans = [];
|
|
1641
1916
|
for (let i = 0; i < headingTokens.length; i++) {
|
|
1642
1917
|
const h = headingTokens[i];
|
|
@@ -1736,6 +2011,31 @@ function scanArchiveSpans(content, active) {
|
|
|
1736
2011
|
spans.push(...plainClosedSections(content, overlapsActive, activeVersion));
|
|
1737
2012
|
return { spans, inActive, ambiguous };
|
|
1738
2013
|
}
|
|
2014
|
+
/**
|
|
2015
|
+
* #5679: the milestone version that owns `offset`. An offset inside a `<details>`
|
|
2016
|
+
* archive (or a plain closed-milestone section) belongs to that block's
|
|
2017
|
+
* `<summary>` (or heading) version, wherever the block sits relative to the
|
|
2018
|
+
* active heading; any other offset belongs to the last milestone heading before
|
|
2019
|
+
* it. Blocks come from `scanArchiveSpans` (fence/comment-aware, depth-paired),
|
|
2020
|
+
* innermost first, so a notes block inside an archive does not hide the archive.
|
|
2021
|
+
* Null when neither names a version.
|
|
2022
|
+
*/
|
|
2023
|
+
function milestoneVersionAtOffset(content, offset) {
|
|
2024
|
+
const containing = scanArchiveSpans(content, []).spans
|
|
2025
|
+
.filter((span) => span.start <= offset && offset < span.end)
|
|
2026
|
+
.sort((a, b) => b.start - a.start);
|
|
2027
|
+
for (const span of containing) {
|
|
2028
|
+
const head = content.slice(span.start, span.start + 1200);
|
|
2029
|
+
// Only a <details> span owns its <summary>; a plain closed-milestone section must not borrow a later block's.
|
|
2030
|
+
const summary = /^<details\b/i.test(head) ? /<summary[^>]*>([\s\S]{0,1000}?)<\/summary>/i.exec(head) : null;
|
|
2031
|
+
const text = summary ? summary[1].replace(/<[^>]{0,200}>/g, '') : head.split('\n', 1)[0];
|
|
2032
|
+
const version = text.match(VERSION_TOKEN)?.[0];
|
|
2033
|
+
if (version)
|
|
2034
|
+
return version;
|
|
2035
|
+
}
|
|
2036
|
+
const preceding = listMilestoneHeadings(content.slice(0, offset));
|
|
2037
|
+
return preceding.length > 0 ? preceding[preceding.length - 1].version : null;
|
|
2038
|
+
}
|
|
1739
2039
|
/**
|
|
1740
2040
|
* #5553: hide every archived `<details>` block, and every closed milestone
|
|
1741
2041
|
* written as a plain heading section, behind a placeholder; `restore` puts them
|
|
@@ -1866,7 +2166,7 @@ function withPhaseSection(content, phaseId, edit, levelBounded = false) {
|
|
|
1866
2166
|
const cut = body.search(/^#{1,6}[ \t]+(?:\[[^\]\n]{1,200}\][ \t]*)?Phase\s+\S/m);
|
|
1867
2167
|
return cut === -1 ? edit(body) : edit(body.slice(0, cut)) + body.slice(cut);
|
|
1868
2168
|
};
|
|
1869
|
-
return (0,
|
|
2169
|
+
return (0, markdown_sectionizer_cjs_2.withSection)(content, (h) => headingRe.test(h.text), ownBody, { levelBounded });
|
|
1870
2170
|
}
|
|
1871
2171
|
// ─── Roadmap phase lookup ─────────────────────────────────────────────────────
|
|
1872
2172
|
// #2199: a bullet/checkbox phase entry, e.g. `- [ ] **Phase 36 — Authentication**`
|
|
@@ -1883,7 +2183,7 @@ function bulletPhaseLineFor(phaseNum, phaseSource) {
|
|
|
1883
2183
|
function findRoadmapPhaseInContent(content, phaseNum, phaseSource) {
|
|
1884
2184
|
// #1729: OPTIONAL_PHASE_TAG_SOURCE after the number tolerates a pre-colon ( ) tag.
|
|
1885
2185
|
const headingPattern = new RegExp(`^(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+${phaseSource ?? phaseMarkdownRegexSource(phaseNum)}${OPTIONAL_PHASE_TAG_SOURCE}:\\s*(.+)$`, 'i');
|
|
1886
|
-
const headings = (0,
|
|
2186
|
+
const headings = (0, markdown_sectionizer_cjs_2.tokenizeHeadings)(content);
|
|
1887
2187
|
const headingIndex = headings.findIndex((heading) => headingPattern.test(heading.text));
|
|
1888
2188
|
if (headingIndex === -1)
|
|
1889
2189
|
return null;
|
|
@@ -1891,7 +2191,7 @@ function findRoadmapPhaseInContent(content, phaseNum, phaseSource) {
|
|
|
1891
2191
|
const headerMatch = heading.text.match(headingPattern);
|
|
1892
2192
|
if (!headerMatch)
|
|
1893
2193
|
return null;
|
|
1894
|
-
const phaseName = headerMatch[1]
|
|
2194
|
+
const phaseName = (0, phase_entry_grammar_cjs_1.stripInsertedMarker)(headerMatch[1]);
|
|
1895
2195
|
const nextHeading = headings.slice(headingIndex + 1).find((candidate) => candidate.level <= heading.level);
|
|
1896
2196
|
const sectionEnd = nextHeading ? nextHeading.offset : content.length;
|
|
1897
2197
|
const section = content.slice(heading.offset, sectionEnd).trim();
|
|
@@ -2617,7 +2917,7 @@ function currentMilestoneRawRanges(content, cwd, phaseIdConvention) {
|
|
|
2617
2917
|
const selected = selectActiveMilestoneHeading(content, version, headingMatches);
|
|
2618
2918
|
const sectionStart = selected.index ?? 0;
|
|
2619
2919
|
const bracketBoundaryActive = bracketScopeConvention === 'bracket';
|
|
2620
|
-
const currentMilestoneHeadings = (0,
|
|
2920
|
+
const currentMilestoneHeadings = (0, markdown_sectionizer_cjs_2.tokenizeHeadings)(content);
|
|
2621
2921
|
const { sectionEnd, bracketBoundary } = bracketAwareMilestoneSection(content, selected, sectionStart, bracketBoundaryActive, currentMilestoneHeadings);
|
|
2622
2922
|
const selectedVersionToken = selected[1].match(/v\d+(?:\.\d+)+(?:[-.][A-Za-z0-9]+)*/i)?.[0];
|
|
2623
2923
|
const detailsVersionBoundary = selectedVersionToken
|
|
@@ -2860,6 +3160,7 @@ module.exports = {
|
|
|
2860
3160
|
getMilestoneInfo,
|
|
2861
3161
|
classifyMilestoneScalar,
|
|
2862
3162
|
readStateMilestoneScalar,
|
|
3163
|
+
sameMilestoneVersion,
|
|
2863
3164
|
getMilestonePhaseFilter,
|
|
2864
3165
|
currentMilestoneRawRanges,
|
|
2865
3166
|
withPhaseSection,
|
|
@@ -2915,6 +3216,12 @@ module.exports = {
|
|
|
2915
3216
|
// #4304: destructive ROADMAP writers classify details blocks through
|
|
2916
3217
|
// the same closed-summary rule as stripClosedMilestoneDetails.
|
|
2917
3218
|
isClosedMilestoneDetails,
|
|
3219
|
+
// #5679: the owning milestone version of a ROADMAP offset, archive-aware.
|
|
3220
|
+
milestoneVersionAtOffset,
|
|
3221
|
+
// #5679: same-length copy with fenced blocks, code spans and HTML comments blanked.
|
|
3222
|
+
blankCodeAndComments,
|
|
3223
|
+
// #5726: fences and HTML comments blanked (same length, newlines kept).
|
|
3224
|
+
blankFencesAndComments,
|
|
2918
3225
|
// #4144 round 5 Blocker 1: the readers' own phase-heading grammar, so the
|
|
2919
3226
|
// bracket roadmap migrator's "phase-like but unparsed" refusal
|
|
2920
3227
|
// (src/roadmap-upgrade.cts) can gate on it instead of a private, looser
|