@opengsd/gsd-core 1.19.0 → 1.20.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/.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/bin/install.js +36 -40
- package/commands/gsd/new-milestone.md +2 -2
- package/commands/gsd/new-project.md +2 -2
- package/commands/gsd/update.md +5 -2
- package/gsd-core/bin/gsd-tools.cjs +177 -93
- 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 +46 -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/gsd-tool-dir.cjs +134 -0
- 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 +181 -68
- package/gsd-core/bin/lib/install-engine.cjs +9 -9
- package/gsd-core/bin/lib/install-manifest-record.cjs +86 -0
- 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 +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 +627 -36
- 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 +3039 -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 +759 -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/runtime-artifact-conversion.cjs +310 -130
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +6 -0
- 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 +51 -15
- package/gsd-core/bin/lib/state.cjs +499 -180
- 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 +27 -21
- package/gsd-core/bin/lib/worktree-base-ref.cjs +32 -2
- 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-agent-isolation-guard.js +1 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +1 -1
- 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/dist/lib/isolation-sentinel.js +77 -2
- package/hooks/gsd-agent-isolation-guard.js +1 -1
- package/hooks/gsd-cursor-subagent-start.js +1 -1
- 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/hooks/lib/isolation-sentinel.js +77 -2
- 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-new-milestone/SKILL.md +1 -1
- package/pi/package-skills/gsd-new-project/SKILL.md +1 -1
- package/pi/package-skills/gsd-update/SKILL.md +4 -1
- package/scripts/changeset/lint.cjs +28 -3
- 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 +10 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +15 -0
- package/scripts/lint-phase-enumeration-drift.cjs +8 -4
- package/scripts/lint-roadmap-progress-drift.cjs +421 -0
- package/scripts/merge-commit-pr.cjs +186 -0
- package/scripts/mutation-matrix.cjs +23 -1
- package/scripts/release-ancestry.cjs +86 -0
- package/scripts/release-pipeline-pr.cjs +92 -0
- package/scripts/require-issue-link-policy.cjs +18 -9
- package/scripts/sync-next-version.cjs +48 -7
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-new-project/SKILL.md +1 -1
- package/skills/gsd-update/SKILL.md +5 -2
- package/vscode/package.json +1 -1
|
@@ -8,19 +8,28 @@
|
|
|
8
8
|
* from the prior hand-written .cjs; only types are added.
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
-
exports.KNOWN_STATUS_PATTERNS = exports.KNOWN_TEMPLATE_DEFAULTS = exports.STATUS_ANCHORED_PATTERNS = exports.STATUS_EXACT_TOKENS = void 0;
|
|
11
|
+
exports.KNOWN_STATUS_PATTERNS = exports.KNOWN_TEMPLATE_DEFAULTS = exports.STATUS_ANCHORED_PATTERNS = exports.STATUS_EXACT_TOKENS = exports.FIELD_LINE_BREAK_RE = void 0;
|
|
12
12
|
exports.toFiniteNumber = toFiniteNumber;
|
|
13
13
|
exports.isUnfilledFieldValue = isUnfilledFieldValue;
|
|
14
14
|
exports.leadingCalendarDate = leadingCalendarDate;
|
|
15
15
|
exports.isRealCalendarDate = isRealCalendarDate;
|
|
16
16
|
exports.stateFieldContinuation = stateFieldContinuation;
|
|
17
|
+
exports.editedLineContinuation = editedLineContinuation;
|
|
18
|
+
exports.readableEditedBoldLabel = readableEditedBoldLabel;
|
|
19
|
+
exports.markdownFencedBlocks = markdownFencedBlocks;
|
|
20
|
+
exports.maskFencedLines = maskFencedLines;
|
|
21
|
+
exports.writeOutsideFences = writeOutsideFences;
|
|
22
|
+
exports.readableBoldLabels = readableBoldLabels;
|
|
17
23
|
exports.fieldContinuationEnd = fieldContinuationEnd;
|
|
24
|
+
exports.insertMissingSessionFields = insertMissingSessionFields;
|
|
18
25
|
exports.stateExtractField = stateExtractField;
|
|
19
26
|
exports.stateFieldValue = stateFieldValue;
|
|
20
27
|
exports.stateCurrentPositionSlice = stateCurrentPositionSlice;
|
|
21
28
|
exports.stateReplaceField = stateReplaceField;
|
|
22
29
|
exports.stateReplaceFieldWithFallback = stateReplaceFieldWithFallback;
|
|
23
30
|
exports.stateReplaceFieldInSession = stateReplaceFieldInSession;
|
|
31
|
+
exports.isSessionHeading = isSessionHeading;
|
|
32
|
+
exports.isSessionContinuityHeading = isSessionContinuityHeading;
|
|
24
33
|
exports.normalizeStateStatus = normalizeStateStatus;
|
|
25
34
|
exports.computeProgressPercent = computeProgressPercent;
|
|
26
35
|
exports.shouldPreserveExistingProgress = shouldPreserveExistingProgress;
|
|
@@ -32,6 +41,9 @@ const phase_lifecycle_cjs_1 = require("./phase-lifecycle.cjs");
|
|
|
32
41
|
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
33
42
|
const pattern_cjs_1 = require("./pattern.cjs");
|
|
34
43
|
const planning_document_cjs_1 = require("./planning-document.cjs");
|
|
44
|
+
Object.defineProperty(exports, "FIELD_LINE_BREAK_RE", { enumerable: true, get: function () { return planning_document_cjs_1.FIELD_LINE_BREAK_RE; } });
|
|
45
|
+
const frontmatter_fence_cjs_1 = require("./frontmatter-fence.cjs");
|
|
46
|
+
const state_md_schema_cjs_1 = require("./state-md-schema.cjs");
|
|
35
47
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-scope.cjs is an export= CommonJS module
|
|
36
48
|
const planningScopeMod = require("./planning-scope.cjs");
|
|
37
49
|
const { SCOPE } = planningScopeMod;
|
|
@@ -324,8 +336,19 @@ function isRealCalendarDate(year, month, day) {
|
|
|
324
336
|
* produce false S009 fires on well-formed content: an indented code block, an
|
|
325
337
|
* HTML block, and a setext underline (`===`, which the `[-*_]{3,}` rule does not
|
|
326
338
|
* cover — it only knows `-`, `*` and `_`).
|
|
339
|
+
*
|
|
340
|
+
* #4998: a fence, an ATX heading (an empty `##` included), a blockquote or a
|
|
341
|
+
* thematic break opens with up to three leading spaces, as Markdown allows —
|
|
342
|
+
* such a line directly below a field was read as its continuation, so
|
|
343
|
+
* record-session skipped a field that does not wrap. Four spaces or a tab is
|
|
344
|
+
* the indented-code arm. Other indented structure (an indented bullet) stays a
|
|
345
|
+
* continuation on purpose: it fails toward skipping the field, never toward
|
|
346
|
+
* splitting a note from the field it annotates.
|
|
327
347
|
*/
|
|
328
|
-
|
|
348
|
+
// The backtick fence is spelled `\x60{3}`: a literal run of backticks in a
|
|
349
|
+
// regex literal opens a template string for the source scanners in
|
|
350
|
+
// scripts/ (lint-state-field-drift), which then misread the rest of the file.
|
|
351
|
+
const MD_STRUCTURE_LINE_RE = /^(?:\||>| {0,3}(?:#{1,6}(?:\s|$)|\x60{3}|~~~|>|[-*_]{3,}\s*$)|[-*_]{3,}\s*$|=+\s*$|[-*+]\s|\d+[.)]\s|\[[^\]]+\]:|<|(?: {4}|\t))/;
|
|
329
352
|
/**
|
|
330
353
|
* A setext heading's underline — `===` or `---` on its own line. The line ABOVE
|
|
331
354
|
* one of these is a heading TITLE, which is indistinguishable from prose on its
|
|
@@ -334,7 +357,36 @@ const MD_STRUCTURE_LINE_RE = /^(?:#{1,6}\s|\||>|```|~~~|[-*_]{3,}\s*$|=+\s*$|[-*
|
|
|
334
357
|
* continuation text (#3696 review round 2).
|
|
335
358
|
*/
|
|
336
359
|
const SETEXT_UNDERLINE_RE = /^(?:=+|-+)\s*$/;
|
|
337
|
-
|
|
360
|
+
// #4998: a sibling field may be indented as the field above it may be
|
|
361
|
+
// (`stateExtractField`'s bold rung tolerates leading whitespace); up to three
|
|
362
|
+
// spaces, as for MD_STRUCTURE_LINE_RE — four is indented code either way. The
|
|
363
|
+
// label admits `.`, `(`, `)` and `/` as well (`Phase 3.1 status:`,
|
|
364
|
+
// `Next (planned):`): a sibling read as a continuation made record-session
|
|
365
|
+
// refuse a field that does not wrap.
|
|
366
|
+
const STATE_SIBLING_FIELD_LINE_RE = /^ {0,3}\*{0,2}[A-Za-z][A-Za-z0-9 _.()/-]*\*{0,2}:{1,2}\*{0,2}(?:\s|$)/;
|
|
367
|
+
/**
|
|
368
|
+
* #4998 review (trek-e 5638530738, Minor 2): the labels STATE.md defines —
|
|
369
|
+
* every body source the frontmatter sync reads, plus the session and position
|
|
370
|
+
* fields the template writes. Lower-cased.
|
|
371
|
+
*/
|
|
372
|
+
const STATE_FIELD_LABELS = new Set([
|
|
373
|
+
...Object.values(state_md_schema_cjs_1.STATE_FIELD_SCHEMA).flatMap((row) => row.bodySource ?? []),
|
|
374
|
+
'Last session', 'Last Date', 'Resume file', 'Phase', 'Plan', 'Progress', 'Current focus',
|
|
375
|
+
].map((label) => label.toLowerCase()));
|
|
376
|
+
/**
|
|
377
|
+
* A plain `words: text` line below a field is a sibling field or a wrapped
|
|
378
|
+
* tail (`records and verified: all good`); its shape cannot tell which. A
|
|
379
|
+
* line is a sibling beyond doubt only when its label is bold or one STATE.md
|
|
380
|
+
* defines. Each caller resolves the ambiguous rest toward its own safe side:
|
|
381
|
+
* the record-session wrap check (`editedLineContinuation`) reads it as a
|
|
382
|
+
* continuation and leaves the field whole; the validator and the writer that
|
|
383
|
+
* removes a continuation (`fieldContinuationEnd`) end the value there, so a
|
|
384
|
+
* sibling is never deleted.
|
|
385
|
+
*/
|
|
386
|
+
function isUnambiguousSiblingLine(line) {
|
|
387
|
+
const m = /^ {0,3}(\*\*)?([A-Za-z][A-Za-z0-9 _.()/-]*?)(?:\*\*)?:{1,2}(?:\*\*)?(?:\s|$)/.exec(line);
|
|
388
|
+
return m !== null && (m[1] !== undefined || STATE_FIELD_LABELS.has(m[2].trim().toLowerCase()));
|
|
389
|
+
}
|
|
338
390
|
/**
|
|
339
391
|
* Return the prose that FOLLOWS a single-line field but plainly belongs to it —
|
|
340
392
|
* i.e. the remainder `stateExtractField` silently drops when a writer emits a
|
|
@@ -373,17 +425,197 @@ function stateFieldContinuation(content, fieldName) {
|
|
|
373
425
|
if (!match)
|
|
374
426
|
return null;
|
|
375
427
|
// `(.+)` stops at the line terminator, so the field's line ends where the
|
|
376
|
-
// match does.
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
428
|
+
// match does.
|
|
429
|
+
return continuationAfter(content, match.index + match[0].length);
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* #4998: where the line a single-line field write changed starts — found from
|
|
433
|
+
* the edit itself, the first byte where `before` and `after` differ, so it is
|
|
434
|
+
* always the occurrence the writer touched, whichever label grammar or
|
|
435
|
+
* occurrence it matched. The bytes before that point are common to both, so
|
|
436
|
+
* the offset holds in either string. Callers handle `before === after`.
|
|
437
|
+
*/
|
|
438
|
+
function editedLineStart(before, after) {
|
|
439
|
+
const limit = Math.min(before.length, after.length);
|
|
440
|
+
let i = 0;
|
|
441
|
+
while (i < limit && before.charCodeAt(i) === after.charCodeAt(i))
|
|
442
|
+
i++;
|
|
443
|
+
// `lastIndexOf` clamps a negative start to 0, so i = 0 would find a
|
|
444
|
+
// newline AT byte 0 and misplace the edited line onto the next one.
|
|
445
|
+
return i === 0 ? 0 : before.lastIndexOf('\n', i - 1) + 1;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* #4998: the continuation (as `stateFieldContinuation` defines it) of the line
|
|
449
|
+
* a single-line field write CHANGED (`editedLineStart`). `null` when nothing
|
|
450
|
+
* changed — an identical-value write leaves every byte, the continuation
|
|
451
|
+
* included, where it was — when the edited line is the last line, or when it
|
|
452
|
+
* is a pipe-table row (a table cell cannot wrap).
|
|
453
|
+
*/
|
|
454
|
+
function editedLineContinuation(before, after) {
|
|
455
|
+
if (before === after)
|
|
456
|
+
return null;
|
|
457
|
+
const lineStart = editedLineStart(before, after);
|
|
458
|
+
const lineEnd = before.indexOf('\n', lineStart);
|
|
459
|
+
// The edited line only: trimming the rest of the document would let a blank
|
|
460
|
+
// edited line borrow a later table row's pipe.
|
|
461
|
+
const line = before.slice(lineStart, lineEnd === -1 ? before.length : lineEnd).replace(/\r$/, '');
|
|
462
|
+
if (line.trimStart().startsWith('|'))
|
|
463
|
+
return null;
|
|
464
|
+
// A line break INSIDE the edited line (a bare CR, U+2028, U+2029) already
|
|
465
|
+
// wraps the value for the `m`-flag readers, which stop there: the text after
|
|
466
|
+
// it is a continuation, exactly as a following LF line is (Codex review round 3).
|
|
467
|
+
const inner = line.search(planning_document_cjs_1.FIELD_LINE_BREAK_RE);
|
|
468
|
+
if (inner !== -1)
|
|
469
|
+
return line.slice(inner + 1).split(planning_document_cjs_1.FIELD_LINE_BREAK_RE).map((part) => part.trim()).join(' ').trim();
|
|
470
|
+
return lineEnd === -1 ? null : continuationAfter(before, lineEnd, false);
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* #4998: `after` with the line a single-line write changed respelled from
|
|
474
|
+
* `**Label**:` to `**Label:**` — the spelling `stateExtractField` reads — so a
|
|
475
|
+
* value `state record-session` just wrote reads back into the frontmatter
|
|
476
|
+
* sync. `stateReplaceField` keeps the author's spelling by contract (#5007),
|
|
477
|
+
* and that contract stands for every other caller: this is applied only by
|
|
478
|
+
* the one writer whose lines the sync must read back, and only to the one
|
|
479
|
+
* line it wrote. Widening the reader to `**Label**:` instead was tried and
|
|
480
|
+
* reverted — it made the sync read fenced examples. Any other line is
|
|
481
|
+
* untouched.
|
|
482
|
+
*/
|
|
483
|
+
function readableEditedBoldLabel(before, after) {
|
|
484
|
+
if (before === after)
|
|
485
|
+
return after;
|
|
486
|
+
const lineStart = editedLineStart(before, after);
|
|
487
|
+
const lineEnd = after.indexOf('\n', lineStart);
|
|
488
|
+
const end = lineEnd === -1 ? after.length : lineEnd;
|
|
489
|
+
const line = after.slice(lineStart, end).replace(/^([ \t]*)\*\*([^*\r\n]+)\*\*:/, '$1**$2:**');
|
|
490
|
+
return after.slice(0, lineStart) + line + after.slice(end);
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Index of the closing `---` of a closed frontmatter block at the top of a
|
|
494
|
+
* whole document, or -1 (always -1 outside `document` scope).
|
|
495
|
+
*/
|
|
496
|
+
function lastFrontmatterLine(lines, scope) {
|
|
497
|
+
if (!scope.document)
|
|
498
|
+
return -1;
|
|
499
|
+
const text = lines.join('\n');
|
|
500
|
+
const fm = (0, frontmatter_fence_cjs_1.locateFrontmatterFence)(text);
|
|
501
|
+
return fm && fm.closed ? text.slice(0, fm.closingStart).split('\n').length - 1 : -1;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* #4998: `scanFencedBlocks` over the Markdown of `lines` only. In a whole
|
|
505
|
+
* document the frontmatter block is YAML, where a ``` line is data (a block
|
|
506
|
+
* scalar), not a fence; its lines are blanked for the scan, so they never open
|
|
507
|
+
* a fence that shifts every boundary after them (Codex review). Line indices
|
|
508
|
+
* are those of `lines`.
|
|
509
|
+
*/
|
|
510
|
+
function markdownFencedBlocks(lines, scope = {}) {
|
|
511
|
+
const last = lastFrontmatterLine(lines, scope);
|
|
512
|
+
return last === -1 ? (0, markdown_sectionizer_cjs_1.scanFencedBlocks)(lines) : (0, markdown_sectionizer_cjs_1.scanFencedBlocks)(lines.map((line, i) => (i <= last ? '' : line)));
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Per line of `lines`: whether a field reader or writer must not see it — a
|
|
516
|
+
* fenced block's line (delimiters included) or, in a whole document, a line
|
|
517
|
+
* of frontmatter between its `---` delimiters (YAML data that a Markdown
|
|
518
|
+
* scanner would otherwise read as a heading, a fence or a field).
|
|
519
|
+
*/
|
|
520
|
+
function maskedLineFlags(lines, scope) {
|
|
521
|
+
const masked = lines.map(() => false);
|
|
522
|
+
for (const block of markdownFencedBlocks(lines, scope)) {
|
|
523
|
+
const end = block.closeLineIdx === -1 ? lines.length - 1 : block.closeLineIdx;
|
|
524
|
+
for (let i = block.openLineIdx; i <= end; i++)
|
|
525
|
+
masked[i] = true;
|
|
526
|
+
}
|
|
527
|
+
const last = lastFrontmatterLine(lines, scope);
|
|
528
|
+
for (let i = 1; i < last; i++)
|
|
529
|
+
masked[i] = true;
|
|
530
|
+
return masked;
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* #4998: `content` with every fenced line blanked to spaces — same length,
|
|
534
|
+
* same line terminators — for reading a field the way `writeOutsideFences`
|
|
535
|
+
* writes it: an example line in a fence is not a field. With
|
|
536
|
+
* `{ document: true }` the frontmatter's content lines are blanked too (its
|
|
537
|
+
* `---` delimiters kept), so scanners that do not know frontmatter — the
|
|
538
|
+
* heading tokenizer behind `collectSection`, `parsePlanningDoc` — see only
|
|
539
|
+
* the Markdown body (Codex review rounds 2 and 3).
|
|
540
|
+
*/
|
|
541
|
+
function maskFencedLines(content, scope = {}) {
|
|
542
|
+
const lines = content.split('\n');
|
|
543
|
+
const masked = maskedLineFlags(lines, scope);
|
|
544
|
+
return lines.map((line, i) => (masked[i] ? line.replace(/[^\r]/g, ' ') : line)).join('\n');
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* #4998: apply a single-line field write (`stateReplaceField` and kin) to
|
|
548
|
+
* `content` as if its fenced blocks were absent, so a fenced example line is
|
|
549
|
+
* never the occurrence a write replaces — the same rule
|
|
550
|
+
* `insertMissingSessionFields` follows, through the same scanner. `write` runs
|
|
551
|
+
* against a masked copy (`maskFencedLines`); its one edit is then spliced into
|
|
552
|
+
* the real text at the same offsets. Returns what `write` returns for "no
|
|
553
|
+
* match" (`null`, or the input unchanged as `content`), and `null` if the
|
|
554
|
+
* edit cannot be carried back exactly (it touched a fenced line or changed
|
|
555
|
+
* the line structure) — the stateReplaceField no-match contract, never a
|
|
556
|
+
* partial write.
|
|
557
|
+
*/
|
|
558
|
+
function writeOutsideFences(content, write, scope = {}) {
|
|
559
|
+
// Outside a whole document a leading `---` is a horizontal rule
|
|
560
|
+
// (`FenceScope`), but the field writers parse what they are given as a
|
|
561
|
+
// document, where it opens frontmatter and hides the fields below it: a
|
|
562
|
+
// section starting with a rule got a duplicate field inserted (Codex
|
|
563
|
+
// review). They are handed the text after a line break, which is removed again.
|
|
564
|
+
const run = (text) => {
|
|
565
|
+
if (scope.document)
|
|
566
|
+
return write(text);
|
|
567
|
+
const out = write(`\n${text}`);
|
|
568
|
+
return out !== null && out.startsWith('\n') ? out.slice(1) : null;
|
|
569
|
+
};
|
|
570
|
+
const masked = maskFencedLines(content, scope);
|
|
571
|
+
if (masked === content)
|
|
572
|
+
return run(content);
|
|
573
|
+
const result = run(masked);
|
|
574
|
+
if (result === null)
|
|
575
|
+
return null;
|
|
576
|
+
if (result === masked)
|
|
577
|
+
return content;
|
|
578
|
+
const limit = Math.min(masked.length, result.length);
|
|
579
|
+
let p = 0;
|
|
580
|
+
while (p < limit && masked.charCodeAt(p) === result.charCodeAt(p))
|
|
581
|
+
p++;
|
|
582
|
+
let s = 0;
|
|
583
|
+
while (s < limit - p && masked.charCodeAt(masked.length - 1 - s) === result.charCodeAt(result.length - 1 - s))
|
|
584
|
+
s++;
|
|
585
|
+
// The span the write replaced must lie on unfenced lines — an edit that
|
|
586
|
+
// touched a fenced line cannot be carried back, even when the masked and
|
|
587
|
+
// real bytes there happen to agree (a whitespace-only fenced line).
|
|
588
|
+
const lineOf = (offset) => content.slice(0, offset).split('\n').length - 1;
|
|
589
|
+
const fenced = maskedLineFlags(content.split('\n'), scope);
|
|
590
|
+
for (let line = lineOf(p); line <= lineOf(content.length - s); line++)
|
|
591
|
+
if (fenced[line])
|
|
592
|
+
return null;
|
|
593
|
+
const spliced = content.slice(0, p) + result.slice(p, result.length - s) + content.slice(content.length - s);
|
|
594
|
+
return maskFencedLines(spliced, scope) === result ? spliced : null;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* #4998: `content` with every `**Label**:` line respelled `**Label:**`, for
|
|
598
|
+
* READING a field the way the writer finds it: `stateReplaceField` matches
|
|
599
|
+
* both spellings, `stateExtractField` only the second, so a `**Resume
|
|
600
|
+
* file**: plan.md` read as absent while the writer could still replace it.
|
|
601
|
+
* Never written back.
|
|
602
|
+
*/
|
|
603
|
+
function readableBoldLabels(content) {
|
|
604
|
+
// allow-adhoc-markdown: a read-only copy for stateExtractField, never written back — not a document mutation
|
|
605
|
+
return content.replace(/^([ \t]*)\*\*([^*\r\n]+)\*\*:/gm, '$1**$2:**');
|
|
606
|
+
}
|
|
607
|
+
/** The continuation lines following the line that ends at `afterValue`. */
|
|
608
|
+
function continuationAfter(content, afterValue, ambiguousLabelEnds = true) {
|
|
609
|
+
// JS `.` excludes \r as well as \n, so on a CRLF document the \r sits just
|
|
610
|
+
// AFTER a `(.+)` match rather than inside it — hence the strip below before
|
|
611
|
+
// testing for the newline.
|
|
380
612
|
const rest = content.slice(afterValue).replace(/^\r/, '');
|
|
381
613
|
// #5460: U+2028 / U+2029 are line starts to the `m`-flag readers (and stop `(.+)`), so they end
|
|
382
614
|
// the value's line exactly as `\n` does.
|
|
383
615
|
if (!/^[\n\u2028\u2029]/.test(rest))
|
|
384
616
|
return null; // end of file: nothing follows
|
|
385
617
|
const lines = rest.slice(1).split(LINE_SEPARATOR_RE).map((line) => line.replace(/\r$/, ''));
|
|
386
|
-
const continuation = lines.slice(0, continuationLineCount(lines)).map((line) => line.trim());
|
|
618
|
+
const continuation = lines.slice(0, continuationLineCount(lines, ambiguousLabelEnds)).map((line) => line.trim());
|
|
387
619
|
return continuation.length ? continuation.join(' ') : null;
|
|
388
620
|
}
|
|
389
621
|
/**
|
|
@@ -391,7 +623,7 @@ function stateFieldContinuation(content, fieldName) {
|
|
|
391
623
|
* `stateFieldContinuation` reports on, shared so a writer that replaces the value removes exactly
|
|
392
624
|
* the lines a validator would call its continuation (#4945). `lines` are CR-free.
|
|
393
625
|
*/
|
|
394
|
-
function continuationLineCount(lines) {
|
|
626
|
+
function continuationLineCount(lines, ambiguousLabelEnds = true) {
|
|
395
627
|
let count = 0;
|
|
396
628
|
for (let i = 0; i < lines.length; i++) {
|
|
397
629
|
const line = lines[i];
|
|
@@ -399,7 +631,9 @@ function continuationLineCount(lines) {
|
|
|
399
631
|
break;
|
|
400
632
|
if (MD_STRUCTURE_LINE_RE.test(line))
|
|
401
633
|
break;
|
|
402
|
-
|
|
634
|
+
// A label line the shape cannot place is a sibling only where the caller
|
|
635
|
+
// fails safe that way (`isUnambiguousSiblingLine`).
|
|
636
|
+
if (STATE_SIBLING_FIELD_LINE_RE.test(line) && (ambiguousLabelEnds || isUnambiguousSiblingLine(line)))
|
|
403
637
|
break;
|
|
404
638
|
// Look ahead one line: a setext underline below makes THIS line a heading
|
|
405
639
|
// title, so stop before consuming it rather than after.
|
|
@@ -437,6 +671,63 @@ function fieldContinuationEnd(content, valueEnd) {
|
|
|
437
671
|
removed -= 1;
|
|
438
672
|
return valueEnd + removed;
|
|
439
673
|
}
|
|
674
|
+
/**
|
|
675
|
+
* #4998: insert each needed field of `fields` — given in template order — into
|
|
676
|
+
* `body`, beside the sibling it follows in that order (below the sibling's own
|
|
677
|
+
* wrapped lines, never between them), else before the sibling it precedes,
|
|
678
|
+
* else at the top in bold. Plain beside a plain sibling, else bold, in the
|
|
679
|
+
* spellings `stateExtractField` reads back. Every existing line is kept, in
|
|
680
|
+
* order, and lines inside a fenced block are examples: never an anchor, never
|
|
681
|
+
* an insertion point. An inserted line ends as the body's lines do (CRLF when
|
|
682
|
+
* the body uses CRLF).
|
|
683
|
+
*/
|
|
684
|
+
function insertMissingSessionFields(body, fields) {
|
|
685
|
+
const lines = body.split('\n');
|
|
686
|
+
const fenced = maskedLineFlags(lines, {});
|
|
687
|
+
const cr = body.includes('\r\n') ? '\r' : '';
|
|
688
|
+
// The spellings stateReplaceField writes: `**Label:**`, `**Label**:`, `Label:`.
|
|
689
|
+
const lineOf = (label) => {
|
|
690
|
+
const re = new RegExp(`^[ \\t]*(?:\\*\\*)?${(0, pattern_cjs_1.escapeRegex)(label)}(?::\\*\\*|\\*\\*:|:)(?:\\s|$)`, 'i');
|
|
691
|
+
return lines.findIndex((l, i) => !fenced[i] && re.test(l));
|
|
692
|
+
};
|
|
693
|
+
// Where field k goes: below the nearest earlier sibling present (past its
|
|
694
|
+
// wrapped lines, stopping before a fence the continuation scan does not
|
|
695
|
+
// recognize), else above the nearest later one, else the top.
|
|
696
|
+
const placement = (k) => {
|
|
697
|
+
for (let j = k - 1; j >= 0; j--) {
|
|
698
|
+
const anchor = lineOf(fields[j].label);
|
|
699
|
+
if (anchor === -1)
|
|
700
|
+
continue;
|
|
701
|
+
// An ambiguous `words: text` line is taken as the anchor's tail, as the
|
|
702
|
+
// wrap check takes it: inserting inside a wrapped value splits it
|
|
703
|
+
// (Codex review); inserting below a sibling only moves the new line.
|
|
704
|
+
const tailEnd = anchor + 1 + continuationLineCount(lines.slice(anchor + 1).map((l) => l.replace(/\r$/, '')), false);
|
|
705
|
+
let at = anchor + 1;
|
|
706
|
+
while (at < tailEnd && !fenced[at])
|
|
707
|
+
at++;
|
|
708
|
+
return { at, anchor };
|
|
709
|
+
}
|
|
710
|
+
for (let j = k + 1; j < fields.length; j++) {
|
|
711
|
+
const anchor = lineOf(fields[j].label);
|
|
712
|
+
if (anchor !== -1)
|
|
713
|
+
return { at: anchor, anchor };
|
|
714
|
+
}
|
|
715
|
+
return { at: 0, anchor: -1 };
|
|
716
|
+
};
|
|
717
|
+
fields.forEach(({ label, needed, value }, k) => {
|
|
718
|
+
if (!needed)
|
|
719
|
+
return;
|
|
720
|
+
const { at, anchor } = placement(k);
|
|
721
|
+
const bold = anchor === -1 || lines[anchor].trimStart().startsWith('**');
|
|
722
|
+
const line = (bold ? `**${label}:** ` : `${label}: `) + value;
|
|
723
|
+
// Appended after an unterminated last line: that line gains the CR.
|
|
724
|
+
if (at === lines.length)
|
|
725
|
+
lines[at - 1] += cr;
|
|
726
|
+
lines.splice(at, 0, at === lines.length ? line : line + cr);
|
|
727
|
+
fenced.splice(at, 0, false);
|
|
728
|
+
});
|
|
729
|
+
return lines.join('\n');
|
|
730
|
+
}
|
|
440
731
|
function stateExtractField(content, fieldName) {
|
|
441
732
|
const escaped = (0, pattern_cjs_1.escapeRegex)(fieldName);
|
|
442
733
|
// Bold line-start format: **FieldName:** value. Leading same-line whitespace
|
|
@@ -644,14 +935,13 @@ function stateReplaceField(content, fieldName, newValue) {
|
|
|
644
935
|
// is the same, already-precedented widening as the "Depends on"
|
|
645
936
|
// migration (src/phase.cts) — intentional, not scope creep.
|
|
646
937
|
//
|
|
647
|
-
// A parse failure
|
|
648
|
-
// a line break (\r/\n — would forge sibling structure on splice, the same
|
|
649
|
-
// hazard `setFieldValue` refuses unconditionally at #4917/ADR-4910
|
|
650
|
-
// Decision 2 & 4; this local splice has no seam call to inherit that
|
|
651
|
-
// refusal from, so it is re-checked here) falls through to the
|
|
938
|
+
// A parse failure or an absent/case-mismatched label falls through to the
|
|
652
939
|
// plain/pipe-table branches below unchanged — the same silent
|
|
653
940
|
// per-occurrence no-op-on-no-match contract the removed regex had for any
|
|
654
|
-
// input it didn't match.
|
|
941
|
+
// input it didn't match. A value containing a line break
|
|
942
|
+
// (`FIELD_LINE_BREAK_RE` — would forge sibling structure on splice, the
|
|
943
|
+
// same hazard `setFieldValue` refuses at #4917/ADR-4910 Decision 2 & 4)
|
|
944
|
+
// is refused by every rung (#4998): the function returns null.
|
|
655
945
|
const parsed = (0, planning_document_cjs_1.parsePlanningDoc)(content, 'STATE.md');
|
|
656
946
|
if (parsed.ok) {
|
|
657
947
|
let boldField = null;
|
|
@@ -675,11 +965,16 @@ function stateReplaceField(content, fieldName, newValue) {
|
|
|
675
965
|
const newValueStr = `${newValue}`;
|
|
676
966
|
const needsSeparator = newValueStr.length > 0 && !/[ \t]$/.test(spacingText);
|
|
677
967
|
const writeValue = `${needsSeparator ? ' ' : ''}${newValueStr}`;
|
|
678
|
-
if (
|
|
968
|
+
if (!planning_document_cjs_1.FIELD_LINE_BREAK_RE.test(writeValue)) {
|
|
679
969
|
return (content.slice(0, boldField.valueSpan.start) + writeValue + content.slice(boldField.trailingSpan.end));
|
|
680
970
|
}
|
|
681
971
|
}
|
|
682
972
|
}
|
|
973
|
+
// #4998: the plain and table rungs refuse a line break too. Spliced in, it
|
|
974
|
+
// forges sibling lines exactly as the bold rung's refusal above prevents;
|
|
975
|
+
// falling through to them only moved the forgery one rung down.
|
|
976
|
+
if (planning_document_cjs_1.FIELD_LINE_BREAK_RE.test(`${newValue}`))
|
|
977
|
+
return null;
|
|
683
978
|
// Plain line-start format: FieldName: value (same same-line confinement as above)
|
|
684
979
|
const plainPattern = new RegExp(`(^${escaped}:[ \\t]*)(.*)`, 'im');
|
|
685
980
|
if (plainPattern.test(content)) {
|
|
@@ -722,14 +1017,28 @@ function stateReplaceFieldWithFallback(content, primary, fallback, value) {
|
|
|
722
1017
|
* Replace-only (no insertion): returns `content` unchanged when no session
|
|
723
1018
|
* section exists or the field is absent from it, so a STATE.md layout without
|
|
724
1019
|
* the line keeps its shape and the post-sync preservation pass decides the
|
|
725
|
-
* frontmatter value (see #3374).
|
|
1020
|
+
* frontmatter value (see #3374). Lines inside a fenced block are examples and
|
|
1021
|
+
* are never the occurrence replaced (#4998, `writeOutsideFences`).
|
|
726
1022
|
*/
|
|
727
1023
|
function stateReplaceFieldInSession(content, primary, fallback, value) {
|
|
728
|
-
const
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
return (0, markdown_sectionizer_cjs_1.withSection)(content, target, (sectionBody) =>
|
|
1024
|
+
const hasCanonicalSession = (0, markdown_sectionizer_cjs_1.collectSection)(content, isSessionHeading, { levelBounded: true }) !== null;
|
|
1025
|
+
const target = hasCanonicalSession ? isSessionHeading : isSessionContinuityHeading;
|
|
1026
|
+
// #4998: a fenced example line is never the field written, as the session
|
|
1027
|
+
// readers (state.cts sessionFieldScope) never read one.
|
|
1028
|
+
return (0, markdown_sectionizer_cjs_1.withSection)(content, target, (sectionBody) => writeOutsideFences(sectionBody, (visible) => stateReplaceFieldWithFallback(visible, primary, fallback, value)) ?? sectionBody);
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* The two session headings, as every session reader and writer recognises
|
|
1032
|
+
* them: one grammar (the `collectSection` tokenizer), so a heading the readers
|
|
1033
|
+
* take is never one a writer misses. #4998 review: `state record-session`
|
|
1034
|
+
* detected the section with its own regex, which rejected `## Session`, and
|
|
1035
|
+
* appended a duplicate section.
|
|
1036
|
+
*/
|
|
1037
|
+
function isSessionHeading(h) {
|
|
1038
|
+
return h.level === 2 && h.text.trim().toLowerCase() === 'session';
|
|
1039
|
+
}
|
|
1040
|
+
function isSessionContinuityHeading(h) {
|
|
1041
|
+
return h.level === 2 && h.text.trim().toLowerCase() === 'session continuity';
|
|
733
1042
|
}
|
|
734
1043
|
/**
|
|
735
1044
|
* #4186: the DECLARED raw-status vocabulary `normalizeStateStatus` recognizes.
|
|
@@ -19,6 +19,7 @@ exports.STATE_MD_SECTIONS = exports.FRONTMATTER_BODY_SOURCE = exports.FIELD_CLAS
|
|
|
19
19
|
exports.formatProgressMachineSegment = formatProgressMachineSegment;
|
|
20
20
|
exports.stateReplaceProgressPercent = stateReplaceProgressPercent;
|
|
21
21
|
exports.beginFrontmatterReassembly = beginFrontmatterReassembly;
|
|
22
|
+
exports.isSessionScopedBodyField = isSessionScopedBodyField;
|
|
22
23
|
exports.getFrontmatterBodySource = getFrontmatterBodySource;
|
|
23
24
|
exports.frontmatterKeyForBodyField = frontmatterKeyForBodyField;
|
|
24
25
|
exports.getFieldClassification = getFieldClassification;
|
|
@@ -37,10 +38,11 @@ const state_document_cjs_1 = require("./state-document.cjs");
|
|
|
37
38
|
const state_document_cjs_2 = require("./state-document.cjs");
|
|
38
39
|
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
39
40
|
const phase_lifecycle_cjs_1 = require("./phase-lifecycle.cjs");
|
|
41
|
+
const roadmap_progress_cjs_1 = require("./roadmap-progress.cjs");
|
|
40
42
|
const pattern_cjs_1 = require("./pattern.cjs");
|
|
41
43
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id.cjs is an export= CommonJS module
|
|
42
44
|
const phaseIdMod = require("./phase-id.cjs");
|
|
43
|
-
const { comparePhaseNum } = phaseIdMod;
|
|
45
|
+
const { comparePhaseNum, parsePhaseFromProse } = phaseIdMod;
|
|
44
46
|
// #4129: the completion-ratio kernel for the resync-arm ratchet's percent
|
|
45
47
|
// (planning-scope's SCOPE — state-document's own dependency, no cycle here:
|
|
46
48
|
// state-document never imports this module).
|
|
@@ -250,6 +252,13 @@ exports.FRONTMATTER_BODY_SOURCE = Object.freeze(FRONTMATTER_BODY_SOURCE_KEY_ORDE
|
|
|
250
252
|
* docstring). `updateCore` was still doing the whole-body replace.
|
|
251
253
|
*/
|
|
252
254
|
const SESSION_SCOPED_KEYS = new Set(['stopped_at', 'paused_at']);
|
|
255
|
+
/**
|
|
256
|
+
* #4998 review: the `## Session` fields with no frontmatter key. `state-snapshot`
|
|
257
|
+
* reads them from the session section only, with fenced blocks blanked, so
|
|
258
|
+
* `state update` / `state patch` write them there too — never a fenced example
|
|
259
|
+
* above the live line, and never an archive section's copy.
|
|
260
|
+
*/
|
|
261
|
+
const SESSION_BODY_FIELDS = new Set(['resume file', 'last session', 'last date']);
|
|
253
262
|
/**
|
|
254
263
|
* The `(primary, fallback)` label pair for a session-scoped frontmatter KEY.
|
|
255
264
|
*/
|
|
@@ -261,7 +270,8 @@ function sessionLabelsForKey(key) {
|
|
|
261
270
|
}
|
|
262
271
|
/**
|
|
263
272
|
* The same pair, resolved from a BODY LABEL the caller named (`Stopped At`,
|
|
264
|
-
* `Stopped at`, `Paused At
|
|
273
|
+
* `Stopped at`, `Paused At`, or a SESSION_BODY_FIELDS label as named). `null`
|
|
274
|
+
* for anything else.
|
|
265
275
|
*
|
|
266
276
|
* Deliberately does NOT accept a frontmatter key. An earlier cut resolved both
|
|
267
277
|
* spellings through one function and used it for the write, which made
|
|
@@ -272,7 +282,13 @@ function sessionLabelsForKey(key) {
|
|
|
272
282
|
*/
|
|
273
283
|
function sessionLabelsForBodyField(field) {
|
|
274
284
|
const key = frontmatterKeyForBodyField(field);
|
|
275
|
-
|
|
285
|
+
if (key !== null)
|
|
286
|
+
return sessionLabelsForKey(key);
|
|
287
|
+
return SESSION_BODY_FIELDS.has(field.trim().toLowerCase()) ? { primary: field, fallback: null } : null;
|
|
288
|
+
}
|
|
289
|
+
/** #4998 review: whether `state update` / `state patch` write the body field `field` inside the session section. */
|
|
290
|
+
function isSessionScopedBodyField(field) {
|
|
291
|
+
return sessionLabelsForBodyField(field) !== null;
|
|
276
292
|
}
|
|
277
293
|
/**
|
|
278
294
|
* Would a session-scoped write actually land? Asks by attempting the real write
|
|
@@ -1025,7 +1041,7 @@ function beginPhaseCore(content, intent, deps) {
|
|
|
1025
1041
|
tryField('Current Phase Name', intent.phaseName);
|
|
1026
1042
|
}
|
|
1027
1043
|
tryField('Current Plan', String(startPlan));
|
|
1028
|
-
if (intent.planCount) {
|
|
1044
|
+
if (intent.planCount != null) {
|
|
1029
1045
|
tryField('Total Plans in Phase', String(intent.planCount));
|
|
1030
1046
|
}
|
|
1031
1047
|
// **Current focus:** body text line (#1104).
|
|
@@ -1163,7 +1179,7 @@ function mutateCurrentPositionFirstTime(body, intent, today, updated) {
|
|
|
1163
1179
|
sectionBody = replaced;
|
|
1164
1180
|
}
|
|
1165
1181
|
// Plan line.
|
|
1166
|
-
const planValue = `${intent.startPlan} of ${intent.planCount
|
|
1182
|
+
const planValue = `${intent.startPlan} of ${intent.planCount ?? '?'}`;
|
|
1167
1183
|
if (/^Plan:/m.test(sectionBody)) {
|
|
1168
1184
|
sectionBody = sectionBody.replace(/^Plan:.*$/m, `Plan: ${planValue}`);
|
|
1169
1185
|
}
|
|
@@ -1708,8 +1724,10 @@ function nextPhaseWording(disk) {
|
|
|
1708
1724
|
* position; the caller combines this with "the phase is already complete in
|
|
1709
1725
|
* ROADMAP.md" so a first-time out-of-order completion (#2028) still
|
|
1710
1726
|
* transitions. An unparseable or absent current phase is "not past".
|
|
1727
|
+
* #5676: under `milestone-prefixed` the id is the whole `M-NN` (`3-02`, not `3`); that read goes
|
|
1728
|
+
* through `parsePhaseFromProse` (the #5626 grammar owner), so the two cannot drift.
|
|
1711
1729
|
*/
|
|
1712
|
-
function isStatePastPhase(content, phaseNum, sourcePath) {
|
|
1730
|
+
function isStatePastPhase(content, phaseNum, sourcePath, convention) {
|
|
1713
1731
|
const fm = extractFrontmatter(content, sourcePath);
|
|
1714
1732
|
const fromFm = fm['current_phase'];
|
|
1715
1733
|
let raw = typeof fromFm === 'string' || typeof fromFm === 'number' ? String(fromFm) : null;
|
|
@@ -1718,10 +1736,15 @@ function isStatePastPhase(content, phaseNum, sourcePath) {
|
|
|
1718
1736
|
raw = (0, state_document_cjs_1.stateExtractField)(body, 'Current Phase') || (0, state_document_cjs_1.stateExtractField)(body, 'Phase') || null;
|
|
1719
1737
|
}
|
|
1720
1738
|
// `Phase 4` / `phase 4 of 9` name phase 4 as surely as `4 of 9` does.
|
|
1721
|
-
|
|
1722
|
-
if (
|
|
1739
|
+
let id = null;
|
|
1740
|
+
if (raw !== null) {
|
|
1741
|
+
id = convention === 'milestone-prefixed'
|
|
1742
|
+
? parsePhaseFromProse(raw, 'milestone-prefixed').phase
|
|
1743
|
+
: (/^\s*(?:phase\s+)?(\d+(?:\.\d+)*)/i.exec(raw)?.[1] ?? null);
|
|
1744
|
+
}
|
|
1745
|
+
if (id === null)
|
|
1723
1746
|
return false;
|
|
1724
|
-
return comparePhaseNum(
|
|
1747
|
+
return comparePhaseNum(id, String(phaseNum)) > 0;
|
|
1725
1748
|
}
|
|
1726
1749
|
/**
|
|
1727
1750
|
* Apply a `completePhase` transition to STATE.md content.
|
|
@@ -1754,8 +1777,8 @@ function completePhaseCore(content, intent, deps) {
|
|
|
1754
1777
|
// else the milestone STATE asserts. It scopes the roadmap-derived counters
|
|
1755
1778
|
// below to that milestone's Progress-table rows, the same milestone
|
|
1756
1779
|
// `phase complete` scopes its frontmatter progress to.
|
|
1757
|
-
const currentMilestone = (0,
|
|
1758
|
-
?? (0,
|
|
1780
|
+
const currentMilestone = (0, roadmap_progress_cjs_1.milestoneVersionOf)(intent.milestone)
|
|
1781
|
+
?? (0, roadmap_progress_cjs_1.milestoneVersionOf)(extractFrontmatter(content, deps.sourcePath)['milestone']);
|
|
1759
1782
|
// Consult the field-classification table for the frontmatter keys this
|
|
1760
1783
|
// transition touches (same guard beginPhaseCore applies). A missing row is a
|
|
1761
1784
|
// substrate defect — fail loudly rather than silently re-encoding policy.
|
|
@@ -2350,7 +2373,20 @@ function patchCore(content, intent) {
|
|
|
2350
2373
|
// `^field:` pattern — e.g. `status` matching both the frontmatter key
|
|
2351
2374
|
// and a `Status:` body line). Frontmatter is only consulted when the
|
|
2352
2375
|
// body has no match for this key.
|
|
2353
|
-
|
|
2376
|
+
// #4998: a session field is written the way `state update` writes it —
|
|
2377
|
+
// inside the session section, never a fenced example there — so the
|
|
2378
|
+
// patch agrees with the session readers the frontmatter sync uses.
|
|
2379
|
+
const sessionWriteLabels = sessionLabelsForBodyField(field);
|
|
2380
|
+
let replaced;
|
|
2381
|
+
if (sessionWriteLabels) {
|
|
2382
|
+
const inSession = (0, state_document_cjs_1.stateReplaceFieldInSession)(body, sessionWriteLabels.primary, sessionWriteLabels.fallback, value);
|
|
2383
|
+
// Unchanged is a miss only when the field is absent: one already holding
|
|
2384
|
+
// the value matched (#4488), as stateReplaceField reports it.
|
|
2385
|
+
replaced = inSession !== body || sessionSourceExists(body, sessionWriteLabels) ? inSession : null;
|
|
2386
|
+
}
|
|
2387
|
+
else {
|
|
2388
|
+
replaced = (0, state_document_cjs_1.stateReplaceField)(body, field, value);
|
|
2389
|
+
}
|
|
2354
2390
|
if (replaced !== null) {
|
|
2355
2391
|
body = replaced;
|
|
2356
2392
|
updated.push(field);
|
|
@@ -2416,10 +2452,10 @@ function updateCore(content, intent) {
|
|
|
2416
2452
|
const sessionWriteLabels = sessionLabelsForBodyField(intent.field);
|
|
2417
2453
|
let result;
|
|
2418
2454
|
if (sessionWriteLabels) {
|
|
2419
|
-
// Replace-only by contract:
|
|
2420
|
-
// session section, which is a miss, not a write.
|
|
2455
|
+
// Replace-only by contract: the session writer never inserts a missing field.
|
|
2421
2456
|
const replaced = (0, state_document_cjs_1.stateReplaceFieldInSession)(body, sessionWriteLabels.primary, sessionWriteLabels.fallback, intent.value);
|
|
2422
|
-
|
|
2457
|
+
// A field already holding the value matched; unchanged with no field is the miss (#4488).
|
|
2458
|
+
result = replaced !== body || sessionSourceExists(body, sessionWriteLabels) ? replaced : null;
|
|
2423
2459
|
}
|
|
2424
2460
|
else {
|
|
2425
2461
|
result = (0, state_document_cjs_1.stateReplaceField)(body, intent.field, intent.value);
|