@opengsd/gsd-core 1.19.0 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-code-fixer.compact.md +5 -5
  4. package/agents/gsd-code-fixer.md +9 -4
  5. package/agents/gsd-code-reviewer.compact.md +49 -8
  6. package/agents/gsd-code-reviewer.md +53 -14
  7. package/agents/gsd-executor.md +8 -4
  8. package/commands/gsd/update.md +5 -2
  9. package/gsd-core/bin/gsd-tools.cjs +73 -23
  10. package/gsd-core/bin/lib/capability-lifecycle.cjs +1 -12
  11. package/gsd-core/bin/lib/capability-registry.cjs +284 -93
  12. package/gsd-core/bin/lib/capability-validator.cjs +15 -0
  13. package/gsd-core/bin/lib/commands.cjs +27 -17
  14. package/gsd-core/bin/lib/config-loader.cjs +3 -3
  15. package/gsd-core/bin/lib/core-utils.cjs +11 -0
  16. package/gsd-core/bin/lib/frontmatter.cjs +21 -3
  17. package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +2 -2
  18. package/gsd-core/bin/lib/gate-context-drift.cjs +1 -1
  19. package/gsd-core/bin/lib/gate-phase-context.cjs +7 -8
  20. package/gsd-core/bin/lib/gate-schema-drift.cjs +1 -1
  21. package/gsd-core/bin/lib/git-info-exclude.cjs +1 -6
  22. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +13 -9
  23. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +9 -7
  24. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +3 -3
  25. package/gsd-core/bin/lib/init.cjs +180 -67
  26. package/gsd-core/bin/lib/io.cjs +6 -0
  27. package/gsd-core/bin/lib/markdown-sectionizer.cjs +34 -1
  28. package/gsd-core/bin/lib/markdown-table.cjs +23 -0
  29. package/gsd-core/bin/lib/mcp-server.cjs +8 -5
  30. package/gsd-core/bin/lib/milestone.cjs +18 -50
  31. package/gsd-core/bin/lib/phase-entry-grammar.cjs +119 -0
  32. package/gsd-core/bin/lib/phase-id-display.cjs +2 -0
  33. package/gsd-core/bin/lib/phase-id.cjs +609 -35
  34. package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -74
  35. package/gsd-core/bin/lib/phase-locator.cjs +328 -19
  36. package/gsd-core/bin/lib/phase.cjs +2990 -666
  37. package/gsd-core/bin/lib/planning-document.cjs +26 -5
  38. package/gsd-core/bin/lib/planning-inspect.cjs +27 -35
  39. package/gsd-core/bin/lib/planning-snapshot.cjs +12 -7
  40. package/gsd-core/bin/lib/planning-workspace.cjs +3 -3
  41. package/gsd-core/bin/lib/read-only-git-env.cjs +23 -2
  42. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +2 -2
  43. package/gsd-core/bin/lib/review-lane-descriptor.cjs +22 -0
  44. package/gsd-core/bin/lib/review-lane-invocation.cjs +125 -18
  45. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +11 -9
  46. package/gsd-core/bin/lib/roadmap-parser.cjs +730 -144
  47. package/gsd-core/bin/lib/roadmap-progress.cjs +136 -0
  48. package/gsd-core/bin/lib/roadmap-upgrade.cjs +8 -5
  49. package/gsd-core/bin/lib/roadmap.cjs +358 -106
  50. package/gsd-core/bin/lib/shell-command-projection.cjs +36 -9
  51. package/gsd-core/bin/lib/smart-entry.cjs +22 -11
  52. package/gsd-core/bin/lib/state-contract.cjs +11 -7
  53. package/gsd-core/bin/lib/state-document.cjs +332 -23
  54. package/gsd-core/bin/lib/state-transition.cjs +39 -10
  55. package/gsd-core/bin/lib/state.cjs +498 -179
  56. package/gsd-core/bin/lib/template.cjs +2 -2
  57. package/gsd-core/bin/lib/validate.cjs +3 -2
  58. package/gsd-core/bin/lib/vendor/re2js.cjs +863 -577
  59. package/gsd-core/bin/lib/vendor/re2js.d.cts +284 -219
  60. package/gsd-core/bin/lib/verify.cjs +2 -2
  61. package/gsd-core/bin/lib/workstream-inventory.cjs +26 -20
  62. package/gsd-core/bin/verify-reapply-patches.cjs +411 -10
  63. package/gsd-core/references/cicd-review-patterns.md +45 -0
  64. package/gsd-core/references/necessity-review.md +48 -0
  65. package/gsd-core/workflows/code-review-fix.md +46 -5
  66. package/gsd-core/workflows/code-review.md +3 -0
  67. package/gsd-core/workflows/execute-phase.md +1 -1
  68. package/gsd-core/workflows/execute-plan.md +1 -1
  69. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +13 -9
  70. package/gsd-core/workflows/plan-phase.md +1 -1
  71. package/gsd-core/workflows/pr-branch.md +2 -0
  72. package/gsd-core/workflows/reapply-patches.md +104 -0
  73. package/gsd-core/workflows/review.md +54 -141
  74. package/gsd-core/workflows/secure-phase.md +1 -1
  75. package/gsd-core/workflows/settings-advanced.md +57 -9
  76. package/gsd-core/workflows/ship.md +12 -0
  77. package/hooks/dist/gsd-graphify-update.sh +6 -0
  78. package/hooks/dist/gsd-statusline.js +3 -2
  79. package/hooks/dist/gsd-windsurf-pre-write.js +4 -4
  80. package/hooks/dist/gsd-workflow-guard.js +2 -1
  81. package/hooks/dist/gsd-worktree-path-guard.js +4 -4
  82. package/hooks/dist/lib/git-probe.js +16 -0
  83. package/hooks/gsd-graphify-update.sh +6 -0
  84. package/hooks/gsd-statusline.js +3 -2
  85. package/hooks/gsd-windsurf-pre-write.js +4 -4
  86. package/hooks/gsd-workflow-guard.js +2 -1
  87. package/hooks/gsd-worktree-path-guard.js +4 -4
  88. package/hooks/lib/git-probe.js +16 -0
  89. package/package.json +3 -3
  90. package/pi/package-agents/gsd-code-fixer.md +9 -4
  91. package/pi/package-agents/gsd-code-reviewer.md +53 -14
  92. package/pi/package-agents/gsd-executor.md +8 -4
  93. package/pi/package-agents/manifest.json +3 -3
  94. package/pi/package-skills/gsd-update/SKILL.md +4 -1
  95. package/scripts/ci-next-health.cjs +246 -23
  96. package/scripts/docs-guard-registry.cjs +2 -0
  97. package/scripts/gen-features.cjs +14 -2
  98. package/scripts/lib/drift-scan.cjs +27 -0
  99. package/scripts/lib/macos-conformance-tier.generated.cjs +6 -0
  100. package/scripts/lib/platform-conformance-tier.generated.cjs +11 -0
  101. package/scripts/lint-phase-enumeration-drift.cjs +8 -4
  102. package/scripts/lint-roadmap-progress-drift.cjs +421 -0
  103. package/scripts/mutation-matrix.cjs +23 -1
  104. package/scripts/sync-next-version.cjs +38 -6
  105. package/skills/gsd-update/SKILL.md +5 -2
  106. package/vscode/package.json +1 -1
@@ -38,7 +38,7 @@ const stateContract = require("./state-contract.cjs");
38
38
  const { publishStateContract } = stateContract;
39
39
  // eslint-disable-next-line @typescript-eslint/no-require-imports
40
40
  const phaseIdMod = require("./phase-id.cjs");
41
- const { normalizePhaseName, matchPhaseDirs, isSentinelPhaseId, isSentinelPhaseDir, buildPhaseHeadingScanRegex, PHASE_HEADING_BASELINE } = phaseIdMod;
41
+ const { isSentinelPhaseId, isSentinelPhaseDir, buildPhaseHeadingScanRegex, PHASE_HEADING_BASELINE } = phaseIdMod;
42
42
  const pattern_cjs_1 = require("./pattern.cjs");
43
43
  const milestone_release_map_cjs_1 = require("./milestone-release-map.cjs");
44
44
  // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -55,7 +55,7 @@ const planScanMod = require("./plan-scan.cjs");
55
55
  const { scanPhasePlans } = planScanMod;
56
56
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-locator.cjs is an export= CommonJS module
57
57
  const phaseLocatorMod = require("./phase-locator.cjs");
58
- const { listMilestonePhaseDirs } = phaseLocatorMod;
58
+ const { phaseDirectoryOptions, listMilestonePhaseDirs, resolvePhaseDirectoryLookup, matchPhaseDirsForLookup } = phaseLocatorMod;
59
59
  const { planningPaths, resolvePhaseIdConvention } = planningWorkspace;
60
60
  const { extractFrontmatter } = frontmatterMod;
61
61
  // ADR-3408 §8.3 / #3469: `writeStateMd` gets sync and NO preservation — the
@@ -73,43 +73,6 @@ const { syncAndPreserveStateMd, withStateLock, readModifyWriteStateMd, assertVer
73
73
  // the caller-supplied `--archive-version` override and the STATE.md-derived
74
74
  // live-read value, so a crafted value cannot escape `.planning/milestones/`.
75
75
  const ARCHIVE_VERSION_LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
76
- /**
77
- * Scope an `updateTableCell` call to the `## Traceability` (or
78
- * `## Traceability Status`) heading's own section — up to the next H1/H2
79
- * heading — instead of handing it the WHOLE REQUIREMENTS.md content.
80
- *
81
- * F1 (#2245 review, BLOCKER): `updateTableCell` binds to the FIRST GFM table
82
- * found in whatever text it is given. The shipped requirements template
83
- * (gsd-core/templates/requirements.md) puts an `## Out of Scope` table
84
- * (`| Feature | Reason |`, no `Status` column) BEFORE `## Traceability` — so
85
- * an unscoped whole-file call targets the Out-of-Scope table instead, fails
86
- * with `{ok:false, reason:'unknown column: Status'}`, and the real
87
- * Traceability row is never flipped, while the checkbox surface still flips
88
- * and the command reports success (the #2140 silent-divergence class one
89
- * level deeper). Mirrors phase.cts's `editProgressHeadingSlice` scoping of
90
- * `## Progress` writes to that heading's own slice.
91
- *
92
- * Falls back to running `updateTableCell` against the whole `text` when no
93
- * `## Traceability` heading exists — matching the previous (unscoped)
94
- * behaviour for a REQUIREMENTS.md whose traceability table sits under some
95
- * other heading, or with no heading at all (never worse than before this fix).
96
- */
97
- function updateTraceabilityCell(text, match, column, newValue) {
98
- const headingMatch = text.match(/^##[ \t]+Traceability(?:[ \t]+Status)?\b/im);
99
- if (!headingMatch || headingMatch.index === undefined) {
100
- return (0, markdown_table_cjs_1.updateTableCell)(text, match, column, newValue);
101
- }
102
- const headingOffset = headingMatch.index;
103
- const before = text.slice(0, headingOffset);
104
- const fromHeading = text.slice(headingOffset);
105
- const nextHeadingOffset = fromHeading.search(/\n#{1,2}[ \t]/);
106
- const scoped = nextHeadingOffset >= 0 ? fromHeading.slice(0, nextHeadingOffset) : fromHeading;
107
- const after = nextHeadingOffset >= 0 ? fromHeading.slice(nextHeadingOffset) : '';
108
- const result = (0, markdown_table_cjs_1.updateTableCell)(scoped, match, column, newValue);
109
- if (!result.ok)
110
- return result;
111
- return { ok: true, value: before + result.value + after };
112
- }
113
76
  function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
114
77
  if (!reqIdsRaw || reqIdsRaw.length === 0) {
115
78
  error('requirement IDs required. Usage: requirements mark-complete REQ-01,REQ-02 or REQ-01 REQ-02');
@@ -187,7 +150,7 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
187
150
  // Complete" gate is folded into the newValue callback so one
188
151
  // updateTableCell call both probes the current value and writes.
189
152
  let tableHit = false;
190
- const tableUpdate = updateTraceabilityCell(reqContent, rowMatch, 'Status', (current) => {
153
+ const tableUpdate = (0, markdown_table_cjs_1.updateTraceabilityCell)(reqContent, rowMatch, 'Status', (current) => {
191
154
  // #2788: accept `Gaps Found` as a forward input too — `revert-phase` (the
192
155
  // documented gaps_found response) leaves a row stranded at Gaps Found with
193
156
  // no inverse; a genuinely-satisfied requirement must be able to reach
@@ -230,7 +193,7 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
230
193
  // that genuinely exists. Probe via a no-op updateTableCell write (its own
231
194
  // tolerant scan) instead of findTableWithColumns (whole-table parse gate).
232
195
  let currentStatusCell = '';
233
- const statusProbe = updateTraceabilityCell(reqContent, rowMatch, 'Status', (current) => {
196
+ const statusProbe = (0, markdown_table_cjs_1.updateTraceabilityCell)(reqContent, rowMatch, 'Status', (current) => {
234
197
  currentStatusCell = current;
235
198
  return current;
236
199
  });
@@ -434,7 +397,7 @@ function cmdRequirementsRevertPhase(cwd, reqIdsRaw, raw) {
434
397
  // "only flip Pending -> Complete" gate, in reverse).
435
398
  const rowMatch = (row) => (Object.values(row)[0] ?? '').trim().toLowerCase() === reqId.toLowerCase();
436
399
  let tableHit = false;
437
- const tableUpdate = updateTraceabilityCell(reqContent, rowMatch, 'Status', (current) => {
400
+ const tableUpdate = (0, markdown_table_cjs_1.updateTraceabilityCell)(reqContent, rowMatch, 'Status', (current) => {
438
401
  if (/^complete$/i.test(current.trim())) {
439
402
  tableHit = true;
440
403
  return ' Gaps Found ';
@@ -585,7 +548,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
585
548
  // `platformEnsureDir(archiveDir)` below — a refused run (scope not COMPLETE,
586
549
  // no --force) must be a true no-op on disk, and creating the archive
587
550
  // directory first left an empty directory behind even on refusal.
588
- const isDirInMilestone = getMilestonePhaseFilter(cwd, version);
551
+ const isDirInMilestone = getMilestonePhaseFilter(cwd, version, undefined, undefined, phaseDirectoryOptions(cwd));
589
552
  if (isDirInMilestone.missingExplicitVersion) {
590
553
  error(`no phases found for milestone ${version} in ROADMAP.md`);
591
554
  }
@@ -610,6 +573,10 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
610
573
  `entries, even though the ROADMAP has phase entries elsewhere), so phase scoping ` +
611
574
  `cannot be trusted for this destructive operation. Re-run with --force to override.`);
612
575
  }
576
+ // #2761/#4304: resolve once for every phase-directory consumer in this
577
+ // command. The matcher opts in only for bracket; the milestone enumerator
578
+ // retains the full resolved value it already accepted.
579
+ const phaseConvention = resolvePhaseIdConvention(cwd);
613
580
  // Guard: prevent marking complete when ROADMAP still lists phases that have
614
581
  // no directory on disk (disk_status: no_directory). This catches the case
615
582
  // where the active milestone was erroneously marked complete before phases
@@ -691,8 +658,8 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
691
658
  // properly-scaffolded bracket phase reads as disk_status: 'no_directory'
692
659
  // — turning a real bracket project's own phases into false "ROADMAP
693
660
  // lists N unstarted phase(s)" failures on `milestone complete`.
694
- const convention = resolvePhaseIdConvention(cwd);
695
- const { regex: phasePattern, phaseNumGroup } = buildPhaseHeadingScanRegex(PHASE_HEADING_BASELINE.ANY_BRACKET, convention);
661
+ const directoryContext = phaseDirectoryOptions(cwd, undefined, scopedContent);
662
+ const { regex: phasePattern, phaseNumGroup } = buildPhaseHeadingScanRegex(PHASE_HEADING_BASELINE.ANY_BRACKET, phaseConvention, directoryContext);
696
663
  const noDirectoryPhases = [];
697
664
  let pm;
698
665
  const phaseDirEntries = (() => {
@@ -716,12 +683,14 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
716
683
  // #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this local check already covered both 0 and 999; now delegates to the single canonical owner.
717
684
  if (isSentinelPhaseId(phaseNum))
718
685
  continue;
719
- const normalized = normalizePhaseName(phaseNum);
686
+ const lookup = resolvePhaseDirectoryLookup(cwd, phaseNum, undefined, directoryContext);
720
687
  // A phase has disk_status: 'no_directory' when no phase directory
721
688
  // with a matching token exists on disk. Use the same matchPhaseDirs
722
689
  // owner that roadmap.analyze uses to avoid false positives on decimal
723
- // (2.1) and letter-suffix (12A) phase IDs. (#2528)
724
- const hasDirectory = matchPhaseDirs(phaseDirEntries, normalized, convention).matches.length > 0;
690
+ // (2.1) and letter-suffix (12A) phase IDs. (#2528) Under #4304 the
691
+ // current checkout's bracket convention and migration-window legacy
692
+ // fallback are both owned by the shared lookup adapter.
693
+ const hasDirectory = matchPhaseDirsForLookup(phaseDirEntries, lookup, undefined, directoryContext).matches.length > 0;
725
694
  if (!hasDirectory) {
726
695
  noDirectoryPhases.push(phaseNum);
727
696
  }
@@ -751,7 +720,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
751
720
  let totalPlans = 0;
752
721
  let totalTasks = 0;
753
722
  const accomplishments = [];
754
- // #2761 (round-11 BLOCKER): resolved ONCE, ambiently (no explicit `ws` —
723
+ // #2761: resolved ONCE above, ambiently (no explicit `ws` —
755
724
  // this function has none of its own and already resolves everything else
756
725
  // off `cwd` via planningPaths(cwd), matching the ambient-workstream
757
726
  // contract `resolvePhaseIdConvention`/`planningDir` share), and threaded
@@ -766,7 +735,6 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
766
735
  // the silent-inherit path without changing behavior for `null` /
767
736
  // `milestone-prefixed` projects, whose resolved convention is the same
768
737
  // value `undefined` would have lazily produced anyway.
769
- const phaseConvention = resolvePhaseIdConvention(cwd);
770
738
  // #3597 (ADR-3180 Decision 2): SINGLE resolution of "which phase
771
739
  // directories belong to the current milestone" AND the SCOPE discriminator
772
740
  // that resolution came from — shared verbatim by the read-only stats loop
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ /**
3
+ * Phase-entry grammar — the one definition of a ROADMAP phase-list entry
4
+ * (`- [ ] **Phase 1: Name**`): bullet marker, checkbox, bold token and line
5
+ * anchoring (#5763, Phase 3 of epic #5692; census in #5703).
6
+ *
7
+ * Before this module every reader owned a private pattern, and the patterns
8
+ * disagreed on each axis (`-` only vs `[-*]`; required, optional or `\*{0,2}`
9
+ * bold; `[ x]` vs `[ xX]` vs `(x| )`; `^`-anchored vs the unanchored forms
10
+ * behind #4965 and #5545). Readers now compose their pattern from the prefix
11
+ * built here and keep only their own tail (label, id token, separator).
12
+ *
13
+ * The accepted grammar:
14
+ * entry := indent marker mgap box gap bold? gap <caller tail>
15
+ * indent := [ \t]* — an entry starts its line
16
+ * marker := '-' | '*'
17
+ * mgap := [ \t]{1,4} — a list item: `-[ ]` is not one (#5764)
18
+ * box := '[' (' ' | 'x' | 'X') ']'
19
+ * bold := '**' | '__' — a reader may require it
20
+ * gap := [ \t]* — horizontal whitespace only
21
+ * A reader that needs plain `- **Phase N**` bullets may make the box optional.
22
+ *
23
+ * Writers (flip, delete, renumber, id rewrite) compose their patterns here too
24
+ * (#5764, Phase 4): `phaseEntryRegExp` for a whole-line match and
25
+ * `phaseEntrySplitRegExp` when the writer rewrites the box and needs the text on
26
+ * either side of it.
27
+ */
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.PHASE_ENTRY_BOLD_CLOSE_SRC = exports.PHASE_ENTRY_BOLD_TOKEN_SRC = void 0;
30
+ exports.phaseEntryPrefixSrc = phaseEntryPrefixSrc;
31
+ exports.phaseEntryRegExp = phaseEntryRegExp;
32
+ exports.phaseEntrySplitRegExp = phaseEntrySplitRegExp;
33
+ exports.isPhaseEntryChecked = isPhaseEntryChecked;
34
+ exports.isBoxCharChecked = isBoxCharChecked;
35
+ const MARKER_SRC = '[-*]';
36
+ // A list item has 1 to 4 spaces or tabs after its marker (CommonMark: none is not a list item,
37
+ // five or more makes the rest an indented code block), so `-[ ] **Phase 1**` is not an entry.
38
+ const MARKER_GAP_SRC = '[ \\t]{1,4}';
39
+ const BOLD_TOKEN_SRC = '(?:\\*\\*|__)';
40
+ /** Regex source of one bold token, for a reader that must also consume the closing wrapper. */
41
+ exports.PHASE_ENTRY_BOLD_TOKEN_SRC = BOLD_TOKEN_SRC;
42
+ /**
43
+ * Regex source of a closing bold wrapper for a reader that captures the entry's
44
+ * name up to it. `**` closes anywhere; `__` closes only at the end of the line,
45
+ * so a name that contains `__init__` or `__tests__` is not cut short.
46
+ */
47
+ exports.PHASE_ENTRY_BOLD_CLOSE_SRC = '(?:\\*\\*|__(?=[ \\t]*\\r?$))';
48
+ /**
49
+ * Line start, indent, marker and the marker's gap, up to where the box opens.
50
+ * A leading BOM can only precede the first line; it is not part of the grammar's indent.
51
+ */
52
+ const LINE_OPEN_SRC = `^\\uFEFF?[ \\t]*${MARKER_SRC}${MARKER_GAP_SRC}`;
53
+ /**
54
+ * Source for the bold token and its trailing gap. `(?:token[ \t]*)?` rather than
55
+ * `[ \t]*` + token + `[ \t]*`, which backtracks quadratically on a long blank run.
56
+ */
57
+ function boldSrcFor(bold) {
58
+ return bold === 'required' ? `${BOLD_TOKEN_SRC}[ \\t]*` : `(?:${BOLD_TOKEN_SRC}[ \\t]*)?`;
59
+ }
60
+ /** Source for the box interior, honouring the requested state filter. */
61
+ function stateSrc(state) {
62
+ if (state === 'unchecked')
63
+ return ' ';
64
+ if (state === 'checked')
65
+ return '[xX]';
66
+ if (state === 'captured')
67
+ return '([ xX])';
68
+ return '[ xX]';
69
+ }
70
+ /**
71
+ * Regex source of everything before the caller's `Phase`/label tail: line start, indent,
72
+ * marker, box, bold token and the horizontal gaps between them. A `captured`
73
+ * state adds exactly one capture group (the box character) at the front.
74
+ */
75
+ function phaseEntryPrefixSrc(options = {}) {
76
+ const { bold = 'optional', box = 'required', state = 'any', decorated = false } = options;
77
+ // The decoration may not contain the word `Phase`, so the entry's own label is the first one
78
+ // on the line and `- [ ] **Phase 5**: needs Phase 2` is not an entry of phase 2. No gap
79
+ // before the run: `[ \t]*` then a lazy run overlap and backtrack quadratically.
80
+ if (decorated)
81
+ return `${LINE_OPEN_SRC}\\[${stateSrc(state)}\\](?:(?!Phase)[^\\r\\n])*?`;
82
+ const boxSrc = `\\[${stateSrc(state)}\\][ \\t]*`;
83
+ const body = box === 'optional' ? `(?:${boxSrc})?` : boxSrc;
84
+ return `${LINE_OPEN_SRC}${body}${boldSrcFor(bold)}`;
85
+ }
86
+ /**
87
+ * A RegExp for a phase entry: the grammar prefix followed by the caller's
88
+ * `tailSrc`. The `m` flag is always set, so `^` is a line start.
89
+ */
90
+ function phaseEntryRegExp(tailSrc, flags = '', options = {}) {
91
+ const withM = flags.includes('m') ? flags : `${flags}m`;
92
+ return new RegExp(`${phaseEntryPrefixSrc(options)}${tailSrc}`, withM);
93
+ }
94
+ /**
95
+ * A RegExp for a phase entry a writer rewrites at the box: capture group 1 is
96
+ * everything through the opening `[` (indent, marker, gap) and capture group 2
97
+ * is everything from the closing `]` through the end of `tailSrc` (gap, bold
98
+ * token, label). A flip is `line.replace(re, '$1x$2')`; the box state itself is
99
+ * matched, not captured, so the writer chooses the new state. The `m` flag is
100
+ * always set.
101
+ */
102
+ function phaseEntrySplitRegExp(tailSrc, flags = '', options = {}) {
103
+ const { bold = 'optional', state = 'any' } = options;
104
+ const withM = flags.includes('m') ? flags : `${flags}m`;
105
+ return new RegExp(`(${LINE_OPEN_SRC}\\[)${stateSrc(state)}(\\][ \\t]*${boldSrcFor(bold)}${tailSrc})`, withM);
106
+ }
107
+ const ENTRY_BOX_RE = new RegExp(`^\\uFEFF?[ \\t]*${MARKER_SRC}${MARKER_GAP_SRC}\\[([ xX])\\]`);
108
+ /**
109
+ * Whether a phase entry line (or a regex match of one, which starts at the
110
+ * line) is ticked. The single place the box character is interpreted.
111
+ */
112
+ function isPhaseEntryChecked(entryText) {
113
+ const m = ENTRY_BOX_RE.exec(entryText);
114
+ return m !== null && m[1].toLowerCase() === 'x';
115
+ }
116
+ /** Whether a captured box character (`captured` state option) is a tick. */
117
+ function isBoxCharChecked(boxChar) {
118
+ return boxChar.toLowerCase() === 'x';
119
+ }
@@ -75,4 +75,6 @@ function renderBracketMilestoneDisplay(milestone, projectCode) {
75
75
  module.exports = {
76
76
  renderBracketPhaseDisplay,
77
77
  renderBracketMilestoneDisplay,
78
+ milestoneToken,
79
+ phaseToken,
78
80
  };