@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
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Install manifest per-file hash ownership (#5683).
|
|
4
|
+
*
|
|
5
|
+
* `gsd-file-manifest.json` records `sha256(file bytes)` per installed file,
|
|
6
|
+
* keyed by path relative to the runtime config dir. `effort sync --apply`
|
|
7
|
+
* rewrites installed agents, so it re-records exactly those files here.
|
|
8
|
+
* `bin/install.js` fileHash delegates to this module: one hash function.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.fileHash = fileHash;
|
|
12
|
+
exports.tryFileHash = tryFileHash;
|
|
13
|
+
exports.recordManifestFileHashes = recordManifestFileHashes;
|
|
14
|
+
const node_fs_1 = require("node:fs");
|
|
15
|
+
const node_path_1 = require("node:path");
|
|
16
|
+
const node_crypto_1 = require("node:crypto");
|
|
17
|
+
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
18
|
+
const MANIFEST_NAME = 'gsd-file-manifest.json';
|
|
19
|
+
/** SHA256 (lowercase hex) of a file's bytes — the manifest's per-file hash. */
|
|
20
|
+
function fileHash(filePath) {
|
|
21
|
+
return (0, node_crypto_1.createHash)('sha256').update((0, node_fs_1.readFileSync)(filePath)).digest('hex');
|
|
22
|
+
}
|
|
23
|
+
/** fileHash that returns null instead of throwing — for capturing a pre-rewrite hash. */
|
|
24
|
+
function tryFileHash(filePath) {
|
|
25
|
+
try {
|
|
26
|
+
return fileHash(filePath);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Update ONLY the manifest entries for `rewritten` files (under `configDir`),
|
|
34
|
+
* and only those that were GSD-owned before the rewrite: `preHash` equal to the
|
|
35
|
+
* recorded manifest entry. A file the user had already edited keeps its stale
|
|
36
|
+
* entry so the next install still detects it and backs it up.
|
|
37
|
+
* Skips silently (returns false) when the manifest is absent, unparseable or
|
|
38
|
+
* not the documented shape; publishes via tmp + rename.
|
|
39
|
+
*/
|
|
40
|
+
function recordManifestFileHashes(configDir, rewritten) {
|
|
41
|
+
if (rewritten.length === 0)
|
|
42
|
+
return false;
|
|
43
|
+
const manifestPath = (0, node_path_1.join)(configDir, MANIFEST_NAME);
|
|
44
|
+
let manifest;
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse((0, node_fs_1.readFileSync)(manifestPath, 'utf8'));
|
|
47
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
48
|
+
return false;
|
|
49
|
+
manifest = parsed;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const files = manifest['files'];
|
|
55
|
+
if (!files || typeof files !== 'object' || Array.isArray(files))
|
|
56
|
+
return false;
|
|
57
|
+
const entries = files;
|
|
58
|
+
let changed = false;
|
|
59
|
+
for (const { path: absPath, preHash } of rewritten) {
|
|
60
|
+
const rel = (0, node_path_1.relative)(configDir, absPath).replace(/\\/g, '/');
|
|
61
|
+
if (rel === '' || rel.startsWith('..') || (0, node_path_1.isAbsolute)(rel))
|
|
62
|
+
continue;
|
|
63
|
+
if (preHash === null || entries[rel] !== preHash)
|
|
64
|
+
continue;
|
|
65
|
+
try {
|
|
66
|
+
entries[rel] = fileHash(absPath);
|
|
67
|
+
changed = true;
|
|
68
|
+
}
|
|
69
|
+
catch { /* unreadable file: leave its entry as recorded */ }
|
|
70
|
+
}
|
|
71
|
+
if (!changed)
|
|
72
|
+
return false;
|
|
73
|
+
const tmpPath = `${manifestPath}.tmp.${process.pid}`;
|
|
74
|
+
try {
|
|
75
|
+
(0, node_fs_1.writeFileSync)(tmpPath, JSON.stringify(manifest, null, 2));
|
|
76
|
+
(0, shell_command_projection_cjs_1.retryRenameSync)(tmpPath, manifestPath);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
try {
|
|
81
|
+
(0, node_fs_1.unlinkSync)(tmpPath);
|
|
82
|
+
}
|
|
83
|
+
catch { /* already gone or never created */ }
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -35,7 +35,7 @@ const external_descriptor_trust_cjs_1 = require("./external-descriptor-trust.cjs
|
|
|
35
35
|
const capabilityLedger = require("./capability-ledger.cjs");
|
|
36
36
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
37
37
|
const conversionModule = require("./runtime-artifact-conversion.cjs");
|
|
38
|
-
const { applyAgentPathRewrites: _applyAgentPathRewrites, processAttribution: _processAttribution, normalizeAgentBodyForRuntime: _normalizeAgentBodyForRuntime, readGsdCommandNames: _readGsdCommandNames, deriveAgentName: _deriveAgentName, applyAgentFrontmatterExtensions: _applyAgentFrontmatterExtensions, appendAgentTools: _appendAgentTools, } = conversionModule;
|
|
38
|
+
const { applyAgentPathRewrites: _applyAgentPathRewrites, processAttribution: _processAttribution, normalizeAgentBodyForRuntime: _normalizeAgentBodyForRuntime, readGsdCommandNames: _readGsdCommandNames, deriveAgentName: _deriveAgentName, applyAgentFrontmatterExtensions: _applyAgentFrontmatterExtensions, appendAgentTools: _appendAgentTools, _shieldAgentConverterPrefix, } = conversionModule;
|
|
39
39
|
// #2995 (epic #1671 Phase 6.4): agent bodies join the fragment model. Markers are
|
|
40
40
|
// stripped at emit BEFORE any path rewrite or converter runs, so a `.claude/` ->
|
|
41
41
|
// `.windsurf/` regex can never reach inside a marker attribute and corrupt it —
|
|
@@ -1011,7 +1011,9 @@ function stageAgentsForRuntimeWithConverter(srcAgentsDir, resolvedProfile, conve
|
|
|
1011
1011
|
// Step 3: validated canonical grants, before host conversion.
|
|
1012
1012
|
content = _appendAgentTools(content, grants);
|
|
1013
1013
|
// Step 4: converter (runtime-specific frontmatter/body transform)
|
|
1014
|
-
|
|
1014
|
+
// #5715: the converter runs after the prefix pass above; shield the emitted prefix
|
|
1015
|
+
// from it, except where the runtime declares no path rewrite (nothing was emitted).
|
|
1016
|
+
content = _shieldAgentConverterPrefix(content, agentCtx.runtime, agentCtx.pathPrefix, (shielded) => converter(shielded, isGlobal, { agentName, pathPrefix: agentCtx.pathPrefix }));
|
|
1015
1017
|
// Step 5: frontmatter extensions (effort/disallowedTools; no-op unless
|
|
1016
1018
|
// the runtime declares hostBehaviors.agentFrontmatterExtensions)
|
|
1017
1019
|
content = _applyAgentFrontmatterExtensions(content, { runtime: agentCtx.runtime, agentName, targetDir: agentCtx.targetDir });
|
package/gsd-core/bin/lib/io.cjs
CHANGED
|
@@ -339,6 +339,9 @@ const ERROR_REASON = Object.freeze({
|
|
|
339
339
|
SDK_MISSING_ARG: 'sdk_missing_arg',
|
|
340
340
|
// workflow / phase
|
|
341
341
|
PHASE_NOT_FOUND: 'phase_not_found',
|
|
342
|
+
PHASE_ID_COLLISION: 'phase_id_collision',
|
|
343
|
+
AMBIGUOUS_PHASE_OWNER: 'ambiguous_phase_owner',
|
|
344
|
+
PHASE_NAMING_NOT_CUSTOM: 'phase_naming_not_custom',
|
|
342
345
|
PHASE_VERIFICATION_INCOMPLETE: 'phase_verification_incomplete',
|
|
343
346
|
PHASE_PLAN_COVERAGE_INCOMPLETE: 'phase_plan_coverage_incomplete',
|
|
344
347
|
SUMMARY_NO_PLANNING: 'summary_no_planning',
|
|
@@ -471,6 +474,9 @@ const REASON_TO_OUTCOME = Object.freeze({
|
|
|
471
474
|
[ERROR_REASON.SDK_MISSING_ARG]: 'USAGE',
|
|
472
475
|
[ERROR_REASON.GRAPHIFY_INVALID_QUERY]: 'USAGE',
|
|
473
476
|
[ERROR_REASON.USAGE]: 'USAGE',
|
|
477
|
+
[ERROR_REASON.PHASE_ID_COLLISION]: 'USAGE',
|
|
478
|
+
[ERROR_REASON.AMBIGUOUS_PHASE_OWNER]: 'USAGE',
|
|
479
|
+
[ERROR_REASON.PHASE_NAMING_NOT_CUSTOM]: 'USAGE',
|
|
474
480
|
// A specific, named thing does not exist / nothing was there to find —
|
|
475
481
|
// genuine, known emptiness rather than a broken prerequisite.
|
|
476
482
|
[ERROR_REASON.CONFIG_KEY_NOT_FOUND]: 'NO_INPUT',
|
|
@@ -30,6 +30,8 @@ exports.maskFences = maskFences;
|
|
|
30
30
|
exports.editOutsideFences = editOutsideFences;
|
|
31
31
|
exports.replaceSection = replaceSection;
|
|
32
32
|
exports.withSection = withSection;
|
|
33
|
+
exports.headingSliceRange = headingSliceRange;
|
|
34
|
+
exports.editHeadingSlice = editHeadingSlice;
|
|
33
35
|
exports.deleteSection = deleteSection;
|
|
34
36
|
exports.extractXmlTagBodies = extractXmlTagBodies;
|
|
35
37
|
const pattern_cjs_1 = require("./pattern.cjs");
|
|
@@ -1035,6 +1037,29 @@ function withSection(content, target, edit, opts = {}) {
|
|
|
1035
1037
|
return content;
|
|
1036
1038
|
return replaceSection(content, section, newBody);
|
|
1037
1039
|
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Range of the slice that starts at `headings[index]` and runs to the next
|
|
1042
|
+
* heading of level <= `maxLevel` (fence-aware: `headings` comes from
|
|
1043
|
+
* `tokenizeHeadings`). Unlike `collectSection` it keeps the heading line and
|
|
1044
|
+
* every trailing byte, so a caller that edits the slice can splice it back
|
|
1045
|
+
* byte-exact (#5762).
|
|
1046
|
+
*/
|
|
1047
|
+
function headingSliceRange(content, headings, index, maxLevel = 2) {
|
|
1048
|
+
const next = headings.slice(index + 1).find((h) => h.level <= maxLevel);
|
|
1049
|
+
return { start: headings[index].offset, end: next ? next.offset : content.length };
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Run `edit` on `content.slice(range.start, end)` and splice the result back,
|
|
1053
|
+
* where `end` excludes the single newline that precedes the next heading — the
|
|
1054
|
+
* slice the pre-owner writers cut with `search(/\n#{1,2}[ \t]/)` — so a row
|
|
1055
|
+
* inserted at the end of a table keeps the blank line before that heading.
|
|
1056
|
+
*/
|
|
1057
|
+
function editHeadingSlice(content, range, edit) {
|
|
1058
|
+
const end = range.end < content.length && range.end > range.start && content[range.end - 1] === '\n'
|
|
1059
|
+
? range.end - 1
|
|
1060
|
+
: range.end;
|
|
1061
|
+
return content.slice(0, range.start) + edit(content.slice(range.start, end)) + content.slice(end);
|
|
1062
|
+
}
|
|
1038
1063
|
// ─── deleteSection ────────────────────────────────────────────────────────────
|
|
1039
1064
|
/**
|
|
1040
1065
|
* Delete an entire section — the matching heading line ITSELF plus its body —
|
|
@@ -1065,6 +1090,8 @@ function withSection(content, target, edit, opts = {}) {
|
|
|
1065
1090
|
* double-blank gap accumulates where the section used to sit. Content
|
|
1066
1091
|
* elsewhere in the document is never touched.
|
|
1067
1092
|
*
|
|
1093
|
+
* `endOffset` adds an optional caller-owned maximum boundary. It never widens
|
|
1094
|
+
* the heading-level range, and callers that omit it retain the legacy bytes.
|
|
1068
1095
|
* Returns `content` unchanged when no heading matches `headingPredicate`
|
|
1069
1096
|
* (bounded no-op, mirroring `withSection`'s miss behaviour).
|
|
1070
1097
|
*/
|
|
@@ -1103,7 +1130,13 @@ function deleteSection(content, headingPredicate, opts = {}) {
|
|
|
1103
1130
|
}
|
|
1104
1131
|
const eofOffset = acc;
|
|
1105
1132
|
const sectionStart = lineOffsets[target.line - 1]; // start of the target heading LINE itself
|
|
1106
|
-
const
|
|
1133
|
+
const headingBoundedEnd = stopLine <= lines.length ? lineOffsets[stopLine - 1] : eofOffset;
|
|
1134
|
+
const explicitEnd = typeof opts.endOffset === 'number' && Number.isFinite(opts.endOffset)
|
|
1135
|
+
? Math.max(0, Math.min(content.length, opts.endOffset))
|
|
1136
|
+
: headingBoundedEnd;
|
|
1137
|
+
const sectionEnd = Math.min(headingBoundedEnd, explicitEnd);
|
|
1138
|
+
if (sectionEnd <= sectionStart)
|
|
1139
|
+
return content;
|
|
1107
1140
|
const before = content.slice(0, sectionStart);
|
|
1108
1141
|
const after = content.slice(sectionEnd);
|
|
1109
1142
|
// Collapse a resulting blank-line seam to at most one blank line (2 newlines).
|
|
@@ -25,6 +25,7 @@ exports.escapeCell = escapeCell;
|
|
|
25
25
|
exports.appendQuickTaskRow = appendQuickTaskRow;
|
|
26
26
|
exports.migrateQuickTasksTable = migrateQuickTasksTable;
|
|
27
27
|
exports.resetQuickTaskRows = resetQuickTaskRows;
|
|
28
|
+
exports.updateTraceabilityCell = updateTraceabilityCell;
|
|
28
29
|
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
29
30
|
// ─── Schema registry ──────────────────────────────────────────────────────────
|
|
30
31
|
/**
|
|
@@ -978,5 +979,27 @@ function resetQuickTaskRows(stateContent) {
|
|
|
978
979
|
const content = (0, markdown_sectionizer_cjs_1.replaceSection)(stateContent, section, newBody);
|
|
979
980
|
return { ok: true, value: { content, cleared, variant: match.label } };
|
|
980
981
|
}
|
|
982
|
+
/**
|
|
983
|
+
* `updateTableCell` scoped to REQUIREMENTS.md's `## Traceability` (or
|
|
984
|
+
* `## Traceability Status`) section (#2245 F1, #5762): `updateTableCell` binds
|
|
985
|
+
* to the first table it finds, and the shipped template puts an `## Out of
|
|
986
|
+
* Scope` table (`Feature | Reason`, no `Status` column) before Traceability, so
|
|
987
|
+
* an unscoped call never reaches the real rows. The ONE copy of this slice
|
|
988
|
+
* (it used to live in both milestone.cts and phase.cts); the heading rule is
|
|
989
|
+
* the Progress owner's — first level-2 heading outside fenced code, running to
|
|
990
|
+
* the next level-1/2 heading. No such heading: the whole text, as before.
|
|
991
|
+
*/
|
|
992
|
+
function updateTraceabilityCell(text, match, column, newValue) {
|
|
993
|
+
const headings = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(text);
|
|
994
|
+
const index = headings.findIndex((h) => h.level === 2 && /^Traceability(?:[ \t]+Status)?\b/i.test(h.text));
|
|
995
|
+
if (index === -1)
|
|
996
|
+
return updateTableCell(text, match, column, newValue);
|
|
997
|
+
let result = { ok: false, reason: 'no matching row' };
|
|
998
|
+
const edited = (0, markdown_sectionizer_cjs_1.editHeadingSlice)(text, (0, markdown_sectionizer_cjs_1.headingSliceRange)(text, headings, index, 2), (scoped) => {
|
|
999
|
+
result = updateTableCell(scoped, match, column, newValue);
|
|
1000
|
+
return result.ok ? result.value : scoped;
|
|
1001
|
+
});
|
|
1002
|
+
return result.ok ? { ok: true, value: edited } : result;
|
|
1003
|
+
}
|
|
981
1004
|
// Consumers: require('../gsd-core/bin/lib/markdown-table.cjs')
|
|
982
1005
|
// Named CJS exports are the canonical surface (ADR-457 .cts → .cjs build-at-publish).
|
|
@@ -109,10 +109,10 @@ const TOOLS = [
|
|
|
109
109
|
type: 'object',
|
|
110
110
|
properties: {
|
|
111
111
|
family: { type: 'string', description: 'Command family (e.g. "query", "state", "phase").' },
|
|
112
|
-
subcommand: { type: 'string', description: 'Subcommand name.' },
|
|
112
|
+
subcommand: { type: 'string', description: 'Subcommand name, if the command takes one. Omit or leave empty for family-only commands.' },
|
|
113
113
|
args: { type: 'array', items: {}, description: 'Positional args.' },
|
|
114
114
|
},
|
|
115
|
-
required: ['family'
|
|
115
|
+
required: ['family'],
|
|
116
116
|
},
|
|
117
117
|
},
|
|
118
118
|
{
|
|
@@ -182,10 +182,13 @@ function callTool(name, args, ctx) {
|
|
|
182
182
|
if (name === 'gsd_invoke_command') {
|
|
183
183
|
const family = asString(a.family);
|
|
184
184
|
const subcommand = asString(a.subcommand);
|
|
185
|
-
if (!family
|
|
186
|
-
return { isError: true, content: [{ type: 'text', text: 'gsd_invoke_command requires string "family"
|
|
185
|
+
if (!family) {
|
|
186
|
+
return { isError: true, content: [{ type: 'text', text: 'gsd_invoke_command requires a non-empty string "family".' }] };
|
|
187
187
|
}
|
|
188
|
-
|
|
188
|
+
if (a.subcommand !== undefined && subcommand === null) {
|
|
189
|
+
return { isError: true, content: [{ type: 'text', text: 'gsd_invoke_command requires string "subcommand" when provided.' }] };
|
|
190
|
+
}
|
|
191
|
+
const res = dispatchGsdCommand({ family, subcommand: subcommand ?? undefined, args: Array.isArray(a.args) ? a.args : [], cwd });
|
|
189
192
|
if (!res.ok) {
|
|
190
193
|
return { isError: true, content: [{ type: 'text', text: res.stderr || res.stdout || `dispatch failed (exit ${res.code})` }] };
|
|
191
194
|
}
|
|
@@ -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
|
+
}
|