@opengsd/gsd-core 1.19.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-code-fixer.compact.md +5 -5
- package/agents/gsd-code-fixer.md +9 -4
- package/agents/gsd-code-reviewer.compact.md +49 -8
- package/agents/gsd-code-reviewer.md +53 -14
- package/agents/gsd-executor.md +8 -4
- package/commands/gsd/update.md +5 -2
- package/gsd-core/bin/gsd-tools.cjs +73 -23
- package/gsd-core/bin/lib/capability-lifecycle.cjs +1 -12
- package/gsd-core/bin/lib/capability-registry.cjs +284 -93
- package/gsd-core/bin/lib/capability-validator.cjs +15 -0
- package/gsd-core/bin/lib/commands.cjs +27 -17
- package/gsd-core/bin/lib/config-loader.cjs +3 -3
- package/gsd-core/bin/lib/core-utils.cjs +11 -0
- package/gsd-core/bin/lib/frontmatter.cjs +21 -3
- package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +2 -2
- package/gsd-core/bin/lib/gate-context-drift.cjs +1 -1
- package/gsd-core/bin/lib/gate-phase-context.cjs +7 -8
- package/gsd-core/bin/lib/gate-schema-drift.cjs +1 -1
- package/gsd-core/bin/lib/git-info-exclude.cjs +1 -6
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +13 -9
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +9 -7
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +3 -3
- package/gsd-core/bin/lib/init.cjs +180 -67
- package/gsd-core/bin/lib/io.cjs +6 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +34 -1
- package/gsd-core/bin/lib/markdown-table.cjs +23 -0
- package/gsd-core/bin/lib/mcp-server.cjs +8 -5
- package/gsd-core/bin/lib/milestone.cjs +18 -50
- package/gsd-core/bin/lib/phase-entry-grammar.cjs +119 -0
- package/gsd-core/bin/lib/phase-id-display.cjs +2 -0
- package/gsd-core/bin/lib/phase-id.cjs +609 -35
- package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -74
- package/gsd-core/bin/lib/phase-locator.cjs +328 -19
- package/gsd-core/bin/lib/phase.cjs +2990 -666
- package/gsd-core/bin/lib/planning-document.cjs +26 -5
- package/gsd-core/bin/lib/planning-inspect.cjs +27 -35
- package/gsd-core/bin/lib/planning-snapshot.cjs +12 -7
- package/gsd-core/bin/lib/planning-workspace.cjs +3 -3
- package/gsd-core/bin/lib/read-only-git-env.cjs +23 -2
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +2 -2
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +22 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +125 -18
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +11 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +730 -144
- package/gsd-core/bin/lib/roadmap-progress.cjs +136 -0
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +8 -5
- package/gsd-core/bin/lib/roadmap.cjs +358 -106
- package/gsd-core/bin/lib/shell-command-projection.cjs +36 -9
- package/gsd-core/bin/lib/smart-entry.cjs +22 -11
- package/gsd-core/bin/lib/state-contract.cjs +11 -7
- package/gsd-core/bin/lib/state-document.cjs +332 -23
- package/gsd-core/bin/lib/state-transition.cjs +39 -10
- package/gsd-core/bin/lib/state.cjs +498 -179
- package/gsd-core/bin/lib/template.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +3 -2
- package/gsd-core/bin/lib/vendor/re2js.cjs +863 -577
- package/gsd-core/bin/lib/vendor/re2js.d.cts +284 -219
- package/gsd-core/bin/lib/verify.cjs +2 -2
- package/gsd-core/bin/lib/workstream-inventory.cjs +26 -20
- package/gsd-core/bin/verify-reapply-patches.cjs +411 -10
- package/gsd-core/references/cicd-review-patterns.md +45 -0
- package/gsd-core/references/necessity-review.md +48 -0
- package/gsd-core/workflows/code-review-fix.md +46 -5
- package/gsd-core/workflows/code-review.md +3 -0
- package/gsd-core/workflows/execute-phase.md +1 -1
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +13 -9
- package/gsd-core/workflows/plan-phase.md +1 -1
- package/gsd-core/workflows/pr-branch.md +2 -0
- package/gsd-core/workflows/reapply-patches.md +104 -0
- package/gsd-core/workflows/review.md +54 -141
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +57 -9
- package/gsd-core/workflows/ship.md +12 -0
- package/hooks/dist/gsd-graphify-update.sh +6 -0
- package/hooks/dist/gsd-statusline.js +3 -2
- package/hooks/dist/gsd-windsurf-pre-write.js +4 -4
- package/hooks/dist/gsd-workflow-guard.js +2 -1
- package/hooks/dist/gsd-worktree-path-guard.js +4 -4
- package/hooks/dist/lib/git-probe.js +16 -0
- package/hooks/gsd-graphify-update.sh +6 -0
- package/hooks/gsd-statusline.js +3 -2
- package/hooks/gsd-windsurf-pre-write.js +4 -4
- package/hooks/gsd-workflow-guard.js +2 -1
- package/hooks/gsd-worktree-path-guard.js +4 -4
- package/hooks/lib/git-probe.js +16 -0
- package/package.json +3 -3
- package/pi/package-agents/gsd-code-fixer.md +9 -4
- package/pi/package-agents/gsd-code-reviewer.md +53 -14
- package/pi/package-agents/gsd-executor.md +8 -4
- package/pi/package-agents/manifest.json +3 -3
- package/pi/package-skills/gsd-update/SKILL.md +4 -1
- package/scripts/ci-next-health.cjs +246 -23
- package/scripts/docs-guard-registry.cjs +2 -0
- package/scripts/gen-features.cjs +14 -2
- package/scripts/lib/drift-scan.cjs +27 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +6 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +11 -0
- package/scripts/lint-phase-enumeration-drift.cjs +8 -4
- package/scripts/lint-roadmap-progress-drift.cjs +421 -0
- package/scripts/mutation-matrix.cjs +23 -1
- package/scripts/sync-next-version.cjs +38 -6
- package/skills/gsd-update/SKILL.md +5 -2
- package/vscode/package.json +1 -1
|
@@ -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 {
|
|
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
|
|
695
|
-
const { regex: phasePattern, phaseNumGroup } = buildPhaseHeadingScanRegex(PHASE_HEADING_BASELINE.ANY_BRACKET,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|