@opengsd/gsd-core 1.20.0 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +33 -2
- package/agents/gsd-executor.md +1 -1
- package/agents/gsd-plan-checker.md +1 -1
- package/agents/gsd-planner.md +3 -3
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +265 -161
- package/commands/gsd/autonomous.md +1 -1
- package/commands/gsd/new-milestone.md +2 -2
- package/commands/gsd/new-project.md +2 -2
- package/gsd-core/bin/gsd-tools.cjs +554 -823
- package/gsd-core/bin/lib/capability-activation.cjs +8 -2
- package/gsd-core/bin/lib/capability-command-router.cjs +6 -2
- package/gsd-core/bin/lib/capability-ledger.cjs +1 -1
- package/gsd-core/bin/lib/capability-lock.cjs +19 -27
- package/gsd-core/bin/lib/capability-registry.cjs +74 -71
- package/gsd-core/bin/lib/capability-validator.cjs +1 -0
- package/gsd-core/bin/lib/capability-writer.cjs +7 -4
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +142 -62
- package/gsd-core/bin/lib/claude-orchestration.cjs +214 -122
- package/gsd-core/bin/lib/cli-surface.cjs +545 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +356 -103
- package/gsd-core/bin/lib/commands.cjs +99 -2
- package/gsd-core/bin/lib/config-file-read.cjs +85 -0
- package/gsd-core/bin/lib/config-loader.cjs +23 -15
- package/gsd-core/bin/lib/config.cjs +139 -101
- package/gsd-core/bin/lib/configuration.cjs +41 -2
- package/gsd-core/bin/lib/criterion-baseline.cjs +346 -0
- package/gsd-core/bin/lib/criterion-replay.cjs +380 -0
- package/gsd-core/bin/lib/estimate-cli.cjs +5 -1
- package/gsd-core/bin/lib/gap-checker.cjs +12 -1
- package/gsd-core/bin/lib/gate-evaluation-scope.cjs +593 -27
- package/gsd-core/bin/lib/gate-phase-context.cjs +16 -4
- package/gsd-core/bin/lib/graphify.cjs +19 -2
- package/gsd-core/bin/lib/gsd-tool-dir.cjs +134 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +5 -1
- package/gsd-core/bin/lib/init.cjs +20 -18
- package/gsd-core/bin/lib/install-effort-resolver.cjs +24 -4
- package/gsd-core/bin/lib/install-engine.cjs +20 -28
- package/gsd-core/bin/lib/install-fs-adapter.cjs +5 -1
- package/gsd-core/bin/lib/install-manifest-record.cjs +86 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -3
- package/gsd-core/bin/lib/install-profiles.cjs +4 -2
- package/gsd-core/bin/lib/io.cjs +6 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +62 -31
- package/gsd-core/bin/lib/markdown-table.cjs +39 -1
- package/gsd-core/bin/lib/milestone.cjs +8 -6
- package/gsd-core/bin/lib/model-resolver.cjs +8 -4
- package/gsd-core/bin/lib/phase-entry-grammar.cjs +14 -0
- package/gsd-core/bin/lib/phase-id.cjs +26 -9
- package/gsd-core/bin/lib/phase-locator.cjs +8 -3
- package/gsd-core/bin/lib/phase.cjs +170 -45
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +46 -23
- package/gsd-core/bin/lib/planning-snapshot.cjs +10 -6
- package/gsd-core/bin/lib/planning-workspace.cjs +20 -10
- package/gsd-core/bin/lib/profile-output.cjs +9 -6
- package/gsd-core/bin/lib/project-context.cjs +439 -0
- package/gsd-core/bin/lib/project-root.cjs +9 -230
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +1 -1
- package/gsd-core/bin/lib/report-parser.cjs +16 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +357 -50
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +24 -5
- package/gsd-core/bin/lib/roadmap.cjs +2 -2
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +317 -134
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +81 -3
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +6 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +279 -8
- package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +1010 -10
- package/gsd-core/bin/lib/stale-bake-guard.cjs +4 -1
- package/gsd-core/bin/lib/state-transition.cjs +103 -46
- package/gsd-core/bin/lib/state.cjs +448 -63
- package/gsd-core/bin/lib/surface.cjs +4 -7
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +10 -3
- package/gsd-core/bin/lib/verify-command-router.cjs +6 -1
- package/gsd-core/bin/lib/verify.cjs +49 -6
- package/gsd-core/bin/lib/workstream-inventory.cjs +4 -4
- package/gsd-core/bin/lib/worktree-base-ref.cjs +32 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +8 -111
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/autonomous-smart-discuss.md +99 -18
- package/gsd-core/references/checkpoints.md +1 -1
- package/gsd-core/references/planner-guidance.md +11 -0
- package/gsd-core/references/planner-human-verify-mode.md +1 -1
- package/gsd-core/references/planner-reversibility.md +2 -0
- package/gsd-core/references/planner-verify-command-grounding.md +22 -6
- package/gsd-core/workflows/autonomous.md +4 -4
- package/gsd-core/workflows/code-review-fix.md +5 -7
- package/gsd-core/workflows/execute-phase.md +21 -7
- package/gsd-core/workflows/plan-phase/steps/plan-pre-dispatch.md +101 -0
- package/gsd-core/workflows/plan-phase.md +18 -19
- package/hooks/dist/gsd-agent-isolation-guard.js +4 -3
- package/hooks/dist/gsd-config-reload.js +2 -2
- package/hooks/dist/gsd-context-monitor.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +9 -6
- package/hooks/dist/gsd-read-injection-scanner.js +2 -2
- package/hooks/dist/gsd-secret-read-guard.js +141 -6
- package/hooks/dist/gsd-statusline.js +3 -2
- package/hooks/dist/gsd-workflow-guard.js +2 -1
- package/hooks/dist/lib/config-read.js +49 -0
- package/hooks/dist/lib/git-cmd.js +3 -1
- package/hooks/dist/lib/git-head-tree.js +474 -0
- package/hooks/dist/lib/isolation-sentinel.js +168 -13
- package/hooks/dist/lib/secret-read-exemptions.js +161 -0
- package/hooks/gsd-agent-isolation-guard.js +4 -3
- package/hooks/gsd-config-reload.js +2 -2
- package/hooks/gsd-context-monitor.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +9 -6
- package/hooks/gsd-read-injection-scanner.js +2 -2
- package/hooks/gsd-secret-read-guard.js +141 -6
- package/hooks/gsd-statusline.js +3 -2
- package/hooks/gsd-workflow-guard.js +2 -1
- package/hooks/lib/config-read.js +49 -0
- package/hooks/lib/git-cmd.js +3 -1
- package/hooks/lib/git-head-tree.js +474 -0
- package/hooks/lib/isolation-sentinel.js +168 -13
- package/hooks/lib/secret-read-exemptions.js +161 -0
- package/package.json +7 -2
- package/pi/package-agents/gsd-executor.md +1 -1
- package/pi/package-agents/gsd-plan-checker.md +1 -1
- package/pi/package-agents/gsd-planner.md +3 -3
- package/pi/package-agents/gsd-verifier.md +2 -2
- package/pi/package-agents/manifest.json +4 -4
- package/pi/package-skills/gsd-autonomous/SKILL.md +1 -1
- package/pi/package-skills/gsd-new-milestone/SKILL.md +1 -1
- package/pi/package-skills/gsd-new-project/SKILL.md +1 -1
- package/scripts/changeset/cli.cjs +2 -2
- package/scripts/changeset/lint.cjs +28 -3
- package/scripts/changeset/serialize.cjs +12 -2
- package/scripts/ci-compact-v8-coverage.cjs +204 -0
- package/scripts/ci-fix-next-rerun.cjs +149 -0
- package/scripts/ci-next-health.cjs +52 -9
- package/scripts/ci-test-scope.cjs +77 -71
- package/scripts/command-contract-helpers.cjs +56 -7
- package/scripts/docs-guard-registry.cjs +3 -0
- package/scripts/gen-cli-surface-manifest.cjs +287 -0
- package/scripts/gen-platform-conformance-tier.cjs +258 -19
- package/scripts/lib/macos-conformance-tier.generated.cjs +59 -47
- package/scripts/lib/platform-conformance-tier.generated.cjs +23 -64
- package/scripts/lib/shellcheck-fetch.cjs +71 -4
- package/scripts/lint-removed-but-needed.cjs +562 -50
- package/scripts/merge-commit-pr.cjs +186 -0
- package/scripts/release-ancestry.cjs +86 -0
- package/scripts/release-notes/changelog-release-notes.cjs +345 -0
- package/scripts/release-notes/discord-release-summary.cjs +82 -1
- package/scripts/release-pipeline-pr.cjs +92 -0
- package/scripts/require-issue-link-policy.cjs +18 -9
- package/scripts/sync-next-version.cjs +11 -2
- package/scripts/timeout-evidence-from-ci.cjs +964 -0
- package/skills/gsd-autonomous/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-new-project/SKILL.md +1 -1
- package/vscode/package.json +1 -1
|
@@ -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,9 +35,33 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
35
35
|
const node_path_1 = __importDefault(require("node:path"));
|
|
36
36
|
const node_os_1 = __importDefault(require("node:os"));
|
|
37
37
|
const runtime_homes_cjs_1 = require("./runtime-homes.cjs");
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- config-file-read.cjs is an export= CommonJS module
|
|
39
|
+
const configFileReadMod = require("./config-file-read.cjs");
|
|
40
|
+
const { unboundedConfigFileReason, printablePath, readConfigFileBounded } = configFileReadMod;
|
|
41
|
+
/**
|
|
42
|
+
* Text of an install-time config file, or null (with a printable-path warning) when it is not a
|
|
43
|
+
* regular file within the cap or cannot be read. The real fs uses the descriptor-bound reader;
|
|
44
|
+
* an injected adapter gets the stat guard before its own read (#5759).
|
|
45
|
+
*/
|
|
46
|
+
function readInstallConfigText(absPath, label) {
|
|
47
|
+
try {
|
|
48
|
+
if (isRealInstallFs())
|
|
49
|
+
return readConfigFileBounded(absPath);
|
|
50
|
+
const unbounded = unboundedConfigFileReason(absPath);
|
|
51
|
+
if (unbounded !== null) {
|
|
52
|
+
process.stderr.write(`gsd: warning — could not read ${label} (${printablePath(absPath)}): ${unbounded}\n`);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return installFs().readFileSync(absPath, 'utf-8');
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
process.stderr.write(`gsd: warning — could not read ${label} (${printablePath(absPath)}): ${printablePath(err.message)}\n`);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
38
62
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- install-fs-adapter.cjs is an export= CommonJS module
|
|
39
63
|
const installFsAdapter = require("./install-fs-adapter.cjs");
|
|
40
|
-
const { installFs } = installFsAdapter;
|
|
64
|
+
const { installFs, isRealInstallFs } = installFsAdapter;
|
|
41
65
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- install-effort-resolver.cjs is an export= CommonJS module
|
|
42
66
|
const installEffortResolver = require("./install-effort-resolver.cjs");
|
|
43
67
|
const { _readGsdConfigFile, _findAncestorGsdConfigPath } = installEffortResolver;
|
|
@@ -62,7 +86,9 @@ function readGsdGlobalModelOverrides(options = {}) {
|
|
|
62
86
|
const defaultsPath = node_path_1.default.join(home, '.gsd', 'defaults.json');
|
|
63
87
|
if (!installFs().existsSync(defaultsPath))
|
|
64
88
|
return null;
|
|
65
|
-
const raw =
|
|
89
|
+
const raw = readInstallConfigText(defaultsPath, 'global defaults'); // #5759: never block on a planted FIFO
|
|
90
|
+
if (raw === null)
|
|
91
|
+
return null;
|
|
66
92
|
const parsed = JSON.parse(raw);
|
|
67
93
|
const overrides = parsed.model_overrides;
|
|
68
94
|
if (!overrides || typeof overrides !== 'object')
|
|
@@ -99,7 +125,11 @@ function readGsdEffectiveModelOverrides(targetDir = null, options = {}) {
|
|
|
99
125
|
const candidate = _findAncestorGsdConfigPath(targetDir);
|
|
100
126
|
if (candidate) {
|
|
101
127
|
try {
|
|
102
|
-
|
|
128
|
+
// #5759: refuse a FIFO/device/oversized project config before the adapter's blocking read.
|
|
129
|
+
const text = readInstallConfigText(candidate, 'project config');
|
|
130
|
+
if (text === null)
|
|
131
|
+
throw new Error('unusable project config');
|
|
132
|
+
const parsed = JSON.parse(text);
|
|
103
133
|
if (parsed && typeof parsed === 'object' && parsed.model_overrides && typeof parsed.model_overrides === 'object') {
|
|
104
134
|
projectOverrides = parsed.model_overrides;
|
|
105
135
|
}
|
|
@@ -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
|
@@ -364,6 +364,10 @@ const ERROR_REASON = Object.freeze({
|
|
|
364
364
|
COMMIT_DOCS_GUARD_NOT_A_REPO: 'commit_docs_guard_not_a_repo',
|
|
365
365
|
COMMIT_DOCS_GUARD_FOREIGN_HOOK: 'commit_docs_guard_foreign_hook',
|
|
366
366
|
COMMIT_DOCS_GUARD_HOOKS_PATH_SET: 'commit_docs_guard_hooks_path_set',
|
|
367
|
+
// commit verb (#6043, ADR-4908 §3): it commits in the worktree the process
|
|
368
|
+
// runs in, or refuses with one of these. Wire values are the typed `reason`.
|
|
369
|
+
COMMIT_NOT_GIT_REPO: 'not_git_repo',
|
|
370
|
+
COMMIT_OUTSIDE_WORKTREE: 'outside_worktree',
|
|
367
371
|
// security-scan
|
|
368
372
|
SECURITY_SCAN_FAILED: 'security_scan_failed',
|
|
369
373
|
// --pick (#3365 / #3358, ADR-3473 §8.4): an absent field or non-JSON
|
|
@@ -499,6 +503,8 @@ const REASON_TO_OUTCOME = Object.freeze({
|
|
|
499
503
|
[ERROR_REASON.COMMIT_DOCS_GUARD_NOT_A_REPO]: 'UNAVAILABLE',
|
|
500
504
|
[ERROR_REASON.COMMIT_DOCS_GUARD_FOREIGN_HOOK]: 'UNAVAILABLE',
|
|
501
505
|
[ERROR_REASON.COMMIT_DOCS_GUARD_HOOKS_PATH_SET]: 'UNAVAILABLE',
|
|
506
|
+
[ERROR_REASON.COMMIT_NOT_GIT_REPO]: 'UNAVAILABLE',
|
|
507
|
+
[ERROR_REASON.COMMIT_OUTSIDE_WORKTREE]: 'FAIL',
|
|
502
508
|
// Its own docstring: "an absent field or non-JSON command output is a
|
|
503
509
|
// failure, never a demotion to an empty answer" — the field/output was
|
|
504
510
|
// supposed to be there and was not; a prerequisite of the query failed.
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
14
|
exports.XML_DECISION_TAG_NAMES = void 0;
|
|
15
|
+
exports.matchFenceDelimiter = matchFenceDelimiter;
|
|
15
16
|
exports.stripFencedCode = stripFencedCode;
|
|
16
17
|
exports.stripInlineCode = stripInlineCode;
|
|
17
18
|
exports.scanInlineCodeSpans = scanInlineCodeSpans;
|
|
@@ -24,6 +25,7 @@ exports.iterateBullets = iterateBullets;
|
|
|
24
25
|
exports.updateBullet = updateBullet;
|
|
25
26
|
exports.updateHeading = updateHeading;
|
|
26
27
|
exports.extractTaggedBlocks = extractTaggedBlocks;
|
|
28
|
+
exports.extractTaggedBlockSpans = extractTaggedBlockSpans;
|
|
27
29
|
exports.stripTaggedBlocks = stripTaggedBlocks;
|
|
28
30
|
exports.editOutsideTaggedBlocks = editOutsideTaggedBlocks;
|
|
29
31
|
exports.maskFences = maskFences;
|
|
@@ -35,6 +37,30 @@ exports.editHeadingSlice = editHeadingSlice;
|
|
|
35
37
|
exports.deleteSection = deleteSection;
|
|
36
38
|
exports.extractXmlTagBodies = extractXmlTagBodies;
|
|
37
39
|
const pattern_cjs_1 = require("./pattern.cjs");
|
|
40
|
+
const FENCE_DELIMITER_RE = /^( {0,3})(`{3,}|~{3,})(.*)$/;
|
|
41
|
+
/**
|
|
42
|
+
* The single CommonMark fence-delimiter grammar: ≤3 spaces of indent, a run of
|
|
43
|
+
* ≥3 backticks or tildes, then trailing text. A trailing `\r` is stripped first
|
|
44
|
+
* (CRLF safety). Returns `null` when the line is not a delimiter line at all.
|
|
45
|
+
* `stripFencedCode`, `scanFencedBlocks`, `tokenizeHeadings` and the roadmap
|
|
46
|
+
* parser's comment/fence blanking all classify delimiter lines through this one
|
|
47
|
+
* matcher.
|
|
48
|
+
*/
|
|
49
|
+
function matchFenceDelimiter(line) {
|
|
50
|
+
const m = FENCE_DELIMITER_RE.exec(line.replace(/\r$/, ''));
|
|
51
|
+
if (m === null)
|
|
52
|
+
return null;
|
|
53
|
+
const char = m[2][0];
|
|
54
|
+
const len = m[2].length;
|
|
55
|
+
const trailing = m[3];
|
|
56
|
+
return {
|
|
57
|
+
char,
|
|
58
|
+
len,
|
|
59
|
+
trailing,
|
|
60
|
+
canOpen: !(char === '`' && trailing.includes('`')),
|
|
61
|
+
closes: (opener) => char === opener.char && len >= opener.len && /^\s*$/.test(trailing),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
38
64
|
// ─── stripFencedCode ──────────────────────────────────────────────────────────
|
|
39
65
|
/**
|
|
40
66
|
* CommonMark-correct fenced-code-block stripper.
|
|
@@ -62,27 +88,20 @@ function stripFencedCode(content) {
|
|
|
62
88
|
const lines = content.split('\n');
|
|
63
89
|
const kept = [];
|
|
64
90
|
let openFence = null;
|
|
65
|
-
// Matches: optional indent (≤3 spaces per CommonMark), fence run, optional info string
|
|
66
|
-
const delimRe = /^( {0,3})(`{3,}|~{3,})(.*)$/;
|
|
67
91
|
for (const rawLine of lines) {
|
|
68
|
-
|
|
69
|
-
const line = rawLine.replace(/\r$/, '');
|
|
70
|
-
const m = delimRe.exec(line);
|
|
92
|
+
const m = matchFenceDelimiter(rawLine);
|
|
71
93
|
if (m) {
|
|
72
|
-
const char = m[2][0];
|
|
73
|
-
const len = m[2].length;
|
|
74
|
-
const trailing = m[3];
|
|
75
94
|
if (openFence === null) {
|
|
76
95
|
// CommonMark §4.5: backtick fence info string must not contain a backtick.
|
|
77
96
|
// If it does, this line is NOT a valid fence opener (treat as ordinary content).
|
|
78
|
-
if (
|
|
97
|
+
if (!m.canOpen) {
|
|
79
98
|
kept.push(rawLine);
|
|
80
99
|
continue;
|
|
81
100
|
}
|
|
82
101
|
// Opening delimiter — record fence state, drop this line
|
|
83
|
-
openFence = { char, len };
|
|
102
|
+
openFence = { char: m.char, len: m.len };
|
|
84
103
|
}
|
|
85
|
-
else if (
|
|
104
|
+
else if (m.closes(openFence)) {
|
|
86
105
|
// Closing delimiter (same char, sufficient length, no trailing content) — close and drop
|
|
87
106
|
openFence = null;
|
|
88
107
|
}
|
|
@@ -213,9 +232,8 @@ function stripInlineCodeLine(line) {
|
|
|
213
232
|
* different-info-string fence, a fence nested/indented inside another fence,
|
|
214
233
|
* and a `~~~` fence are all classified exactly as `stripFencedCode` would.
|
|
215
234
|
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
* machine, pending a T-tier consolidation.
|
|
235
|
+
* Delimiter classification goes through `matchFenceDelimiter`, the one shared
|
|
236
|
+
* grammar (also used by `stripFencedCode` and `tokenizeHeadings`).
|
|
219
237
|
*
|
|
220
238
|
* Exported so `context-predicates.cts` can consume this seam directly for its
|
|
221
239
|
* line-preserving fenced-line skip detection, instead of carrying a third
|
|
@@ -223,24 +241,19 @@ function stripInlineCodeLine(line) {
|
|
|
223
241
|
* comment).
|
|
224
242
|
*/
|
|
225
243
|
function scanFencedBlocks(lines) {
|
|
226
|
-
const delimRe = /^( {0,3})(`{3,}|~{3,})(.*)$/;
|
|
227
244
|
const blocks = [];
|
|
228
245
|
let open = null;
|
|
229
246
|
for (let i = 0; i < lines.length; i++) {
|
|
230
|
-
const
|
|
231
|
-
const m = delimRe.exec(line);
|
|
247
|
+
const m = matchFenceDelimiter(lines[i]);
|
|
232
248
|
if (!m)
|
|
233
249
|
continue;
|
|
234
|
-
const char = m[2][0];
|
|
235
|
-
const len = m[2].length;
|
|
236
|
-
const trailing = m[3];
|
|
237
250
|
if (open === null) {
|
|
238
251
|
// CommonMark §4.5: backtick fence info string must not contain a backtick.
|
|
239
|
-
if (
|
|
252
|
+
if (!m.canOpen)
|
|
240
253
|
continue; // not a valid opener — ordinary content
|
|
241
|
-
open = { char, len, infoString: trailing.trim(), openLineIdx: i };
|
|
254
|
+
open = { char: m.char, len: m.len, infoString: m.trailing.trim(), openLineIdx: i };
|
|
242
255
|
}
|
|
243
|
-
else if (
|
|
256
|
+
else if (m.closes(open)) {
|
|
244
257
|
blocks.push({
|
|
245
258
|
char: open.char,
|
|
246
259
|
len: open.len,
|
|
@@ -319,30 +332,26 @@ function tokenizeHeadings(content) {
|
|
|
319
332
|
const tokens = [];
|
|
320
333
|
// We re-run the fence state machine to know which lines are "kept", so we
|
|
321
334
|
// can map line index in original to whether it survived.
|
|
322
|
-
const delimRe = /^( {0,3})(`{3,}|~{3,})(.*)$/;
|
|
323
335
|
let openFence = null;
|
|
324
336
|
// Accumulate character offset as we iterate lines
|
|
325
337
|
let charOffset = 0;
|
|
326
338
|
for (let i = 0; i < originalLines.length; i++) {
|
|
327
339
|
const rawLine = originalLines[i];
|
|
328
340
|
const line = rawLine.replace(/\r$/, '');
|
|
329
|
-
const dm =
|
|
341
|
+
const dm = matchFenceDelimiter(rawLine);
|
|
330
342
|
if (dm) {
|
|
331
|
-
const char = dm[2][0];
|
|
332
|
-
const len = dm[2].length;
|
|
333
|
-
const trailing = dm[3];
|
|
334
343
|
if (openFence === null) {
|
|
335
344
|
// CommonMark §4.5: backtick fence info string must not contain a backtick.
|
|
336
|
-
if (
|
|
345
|
+
if (!dm.canOpen) {
|
|
337
346
|
// Not a valid fence opener — check for heading on this line (will fall through)
|
|
338
347
|
}
|
|
339
348
|
else {
|
|
340
|
-
openFence = { char, len };
|
|
349
|
+
openFence = { char: dm.char, len: dm.len };
|
|
341
350
|
charOffset += rawLine.length + 1;
|
|
342
351
|
continue;
|
|
343
352
|
}
|
|
344
353
|
}
|
|
345
|
-
else if (
|
|
354
|
+
else if (dm.closes(openFence)) {
|
|
346
355
|
openFence = null;
|
|
347
356
|
charOffset += rawLine.length + 1;
|
|
348
357
|
continue;
|
|
@@ -818,6 +827,28 @@ function extractTaggedBlocks(content, tagName, allowAttributes = false) {
|
|
|
818
827
|
}
|
|
819
828
|
return results;
|
|
820
829
|
}
|
|
830
|
+
/**
|
|
831
|
+
* Offset spans of the bodies `extractTaggedBlocks` returns (#4309): same
|
|
832
|
+
* pattern, same blocks, but each body is reported as `{ start, end }` into
|
|
833
|
+
* `content` so a caller can test whether an arbitrary offset lies inside a
|
|
834
|
+
* block (and dedupe nested blocks) without re-harvesting the text.
|
|
835
|
+
* `content.slice(span.start, span.end)` equals the matching extracted body.
|
|
836
|
+
*/
|
|
837
|
+
function extractTaggedBlockSpans(content, tagName, allowAttributes = false) {
|
|
838
|
+
if (typeof content !== 'string' || content.length === 0)
|
|
839
|
+
return [];
|
|
840
|
+
if (typeof tagName !== 'string' || tagName.length === 0)
|
|
841
|
+
return [];
|
|
842
|
+
const closeLength = `</${tagName}>`.length;
|
|
843
|
+
const pattern = taggedBlockPattern(tagName, 'g', allowAttributes);
|
|
844
|
+
const spans = [];
|
|
845
|
+
let match;
|
|
846
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
847
|
+
const end = match.index + match[0].length - closeLength;
|
|
848
|
+
spans.push({ start: end - match[1].length, end });
|
|
849
|
+
}
|
|
850
|
+
return spans;
|
|
851
|
+
}
|
|
821
852
|
/**
|
|
822
853
|
* Build the single, ReDoS-safe `<tag>…</tag>` block regex shared by
|
|
823
854
|
* `extractTaggedBlocks` (extract bodies) and `stripTaggedBlocks` (remove blocks).
|
|
@@ -26,6 +26,7 @@ exports.appendQuickTaskRow = appendQuickTaskRow;
|
|
|
26
26
|
exports.migrateQuickTasksTable = migrateQuickTasksTable;
|
|
27
27
|
exports.resetQuickTaskRows = resetQuickTaskRows;
|
|
28
28
|
exports.updateTraceabilityCell = updateTraceabilityCell;
|
|
29
|
+
exports.traceabilityRowExists = traceabilityRowExists;
|
|
29
30
|
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
30
31
|
// ─── Schema registry ──────────────────────────────────────────────────────────
|
|
31
32
|
/**
|
|
@@ -979,6 +980,8 @@ function resetQuickTaskRows(stateContent) {
|
|
|
979
980
|
const content = (0, markdown_sectionizer_cjs_1.replaceSection)(stateContent, section, newBody);
|
|
980
981
|
return { ok: true, value: { content, cleared, variant: match.label } };
|
|
981
982
|
}
|
|
983
|
+
/** The one Traceability heading rule shared by the write and the existence read. */
|
|
984
|
+
const TRACEABILITY_HEADING_RE = /^Traceability(?:[ \t]+Status)?\b/i;
|
|
982
985
|
/**
|
|
983
986
|
* `updateTableCell` scoped to REQUIREMENTS.md's `## Traceability` (or
|
|
984
987
|
* `## Traceability Status`) section (#2245 F1, #5762): `updateTableCell` binds
|
|
@@ -991,7 +994,7 @@ function resetQuickTaskRows(stateContent) {
|
|
|
991
994
|
*/
|
|
992
995
|
function updateTraceabilityCell(text, match, column, newValue) {
|
|
993
996
|
const headings = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(text);
|
|
994
|
-
const index = headings.findIndex((h) => h.level === 2 &&
|
|
997
|
+
const index = headings.findIndex((h) => h.level === 2 && TRACEABILITY_HEADING_RE.test(h.text));
|
|
995
998
|
if (index === -1)
|
|
996
999
|
return updateTableCell(text, match, column, newValue);
|
|
997
1000
|
let result = { ok: false, reason: 'no matching row' };
|
|
@@ -1001,5 +1004,40 @@ function updateTraceabilityCell(text, match, column, newValue) {
|
|
|
1001
1004
|
});
|
|
1002
1005
|
return result.ok ? { ok: true, value: edited } : result;
|
|
1003
1006
|
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Row-existence READ for REQUIREMENTS.md's Traceability section (#5844): true
|
|
1009
|
+
* when any table row in the section has `id` (case-insensitive) as its first
|
|
1010
|
+
* cell, whatever the table's columns. `updateTraceabilityCell(..., 'Status', …)`
|
|
1011
|
+
* answers "is there a row" only for tables that HAVE a Status column; this
|
|
1012
|
+
* answers it for every table. Scoping mirrors `updateTraceabilityCell`: the
|
|
1013
|
+
* same heading rule, and only the FIRST table of the scoped text (with no
|
|
1014
|
+
* Traceability heading that is the first table in the file), so a table
|
|
1015
|
+
* elsewhere whose first cell equals the ID is not a Traceability row.
|
|
1016
|
+
*/
|
|
1017
|
+
function traceabilityRowExists(text, id) {
|
|
1018
|
+
const headings = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(text);
|
|
1019
|
+
const index = headings.findIndex((h) => h.level === 2 && TRACEABILITY_HEADING_RE.test(h.text));
|
|
1020
|
+
let scoped = text;
|
|
1021
|
+
if (index !== -1) {
|
|
1022
|
+
(0, markdown_sectionizer_cjs_1.editHeadingSlice)(text, (0, markdown_sectionizer_cjs_1.headingSliceRange)(text, headings, index, 2), (slice) => {
|
|
1023
|
+
scoped = slice;
|
|
1024
|
+
return slice;
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
const wanted = id.trim().toLowerCase();
|
|
1028
|
+
let inTable = false;
|
|
1029
|
+
for (const line of scoped.split(/\r?\n/)) {
|
|
1030
|
+
if (!line.trim().startsWith('|')) {
|
|
1031
|
+
if (inTable)
|
|
1032
|
+
return false;
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
inTable = true;
|
|
1036
|
+
const cells = splitTableRow(line);
|
|
1037
|
+
if (cells.length > 0 && cells[0].trim().toLowerCase() === wanted)
|
|
1038
|
+
return true;
|
|
1039
|
+
}
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
1004
1042
|
// Consumers: require('../gsd-core/bin/lib/markdown-table.cjs')
|
|
1005
1043
|
// Named CJS exports are the canonical surface (ADR-457 .cts → .cjs build-at-publish).
|
|
@@ -43,7 +43,7 @@ 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
|
|
45
45
|
const roadmapParserMod = require("./roadmap-parser.cjs");
|
|
46
|
-
const { getMilestonePhaseFilter, extractCurrentMilestone, getMilestoneInfo, sliceMilestoneWindow, } = roadmapParserMod;
|
|
46
|
+
const { getMilestonePhaseFilter, extractCurrentMilestone, getMilestoneInfo, sliceMilestoneWindow, classifyMilestoneScalar, readStateMilestoneScalar, sameMilestoneVersion, } = roadmapParserMod;
|
|
47
47
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
48
48
|
const planningScopeMod = require("./planning-scope.cjs");
|
|
49
49
|
const { SCOPE } = planningScopeMod;
|
|
@@ -225,7 +225,9 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
|
|
|
225
225
|
// Surface traceability drift: checkbox reconciled (this run or before) but the
|
|
226
226
|
// table has no row for this ID. This is what makes a partial reconcile
|
|
227
227
|
// distinguishable from a full one (#2140).
|
|
228
|
-
|
|
228
|
+
// #5844: a row in a table with no Status column is still a row; only the
|
|
229
|
+
// Status write is impossible there, so existence is read directly.
|
|
230
|
+
if (hasTable && doneCheckbox && !hasRow && !(0, markdown_table_cjs_1.traceabilityRowExists)(reqContent, reqId)) {
|
|
229
231
|
tableUnmatched.push(reqId);
|
|
230
232
|
}
|
|
231
233
|
}
|
|
@@ -603,15 +605,15 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
603
605
|
try {
|
|
604
606
|
const stateRaw = node_fs_1.default.existsSync(statePath) ? node_fs_1.default.readFileSync(statePath, 'utf-8') : null;
|
|
605
607
|
if (stateRaw) {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
608
|
+
// One owner for the STATE `milestone:` scalar (#5726): quoted, null and
|
|
609
|
+
// empty values and a body `milestone:` line are not drift.
|
|
610
|
+
stateVersion = classifyMilestoneScalar(readStateMilestoneScalar(stateRaw)).version;
|
|
609
611
|
}
|
|
610
612
|
}
|
|
611
613
|
catch {
|
|
612
614
|
/* skip — stateVersion stays null, scan still runs */
|
|
613
615
|
}
|
|
614
|
-
if (stateVersion !== null && stateVersion
|
|
616
|
+
if (stateVersion !== null && !sameMilestoneVersion(stateVersion, version)) {
|
|
615
617
|
// #2946: emit a WARNING so the suspicious STATE mismatch is visible
|
|
616
618
|
// rather than silently disarming the guard. Plain-text diagnostic on
|
|
617
619
|
// stderr, matching the existing [gsd-tools] WARNING convention
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* epic #1267; callers import resolvers from model-resolver.cjs directly.
|
|
12
12
|
*
|
|
13
13
|
* Dependencies (leaf modules only):
|
|
14
|
-
* - node:
|
|
14
|
+
* - node:path (project config path for the #2297 omit gate; read via config-loader's bounded reader, #5759)
|
|
15
15
|
* - ./runtime-name-policy.cjs (resolveRuntimeNameFromCandidates — canonicalize the active runtime)
|
|
16
16
|
* - ./planning-workspace.cjs (planningDir — workstream/project-aware project-config path)
|
|
17
17
|
* - ./config-loader.cjs (loadConfig)
|
|
@@ -25,14 +25,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
25
25
|
};
|
|
26
26
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
27
27
|
const configLoaderModule = require("./config-loader.cjs");
|
|
28
|
-
const { loadConfig } = configLoaderModule;
|
|
28
|
+
const { loadConfig, readConfigFileBounded } = configLoaderModule;
|
|
29
29
|
// ─── Configuration Module (for CANONICAL_CONFIG_DEFAULTS used by effort/fast_mode resolvers) ─
|
|
30
30
|
const configuration_cjs_1 = require("./configuration.cjs");
|
|
31
31
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
32
32
|
const modelProfiles = require("./model-profiles.cjs");
|
|
33
33
|
const { MODEL_PROFILES, AGENT_TO_PHASE_TYPE, AGENT_DEFAULT_TIERS, VALID_AGENT_TIERS, nextTier } = modelProfiles;
|
|
34
34
|
const model_catalog_cjs_1 = require("./model-catalog.cjs");
|
|
35
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
36
35
|
const node_path_1 = __importDefault(require("node:path"));
|
|
37
36
|
const runtime_name_policy_cjs_1 = require("./runtime-name-policy.cjs");
|
|
38
37
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
@@ -87,7 +86,12 @@ function projectExplicitlySetsOmit(cwd) {
|
|
|
87
86
|
const layers = wsDir === rootDir ? [rootDir] : [wsDir, rootDir]; // workstream > root
|
|
88
87
|
for (const dir of layers) {
|
|
89
88
|
try {
|
|
90
|
-
|
|
89
|
+
// Same bounded regular-file reader as the loader (#5759): a FIFO/device/oversized
|
|
90
|
+
// layer throws instead of blocking, and an absent layer returns null.
|
|
91
|
+
const raw = readConfigFileBounded(node_path_1.default.join(dir, 'config.json'));
|
|
92
|
+
if (raw === null)
|
|
93
|
+
continue;
|
|
94
|
+
const parsed = JSON.parse(raw);
|
|
91
95
|
const value = parsed?.['resolve_model_ids'];
|
|
92
96
|
// First layer that sets the key wins (matches loadConfig's deep-merge
|
|
93
97
|
// precedence). A layer that omits the key falls through to the next.
|
|
@@ -32,6 +32,7 @@ exports.phaseEntryRegExp = phaseEntryRegExp;
|
|
|
32
32
|
exports.phaseEntrySplitRegExp = phaseEntrySplitRegExp;
|
|
33
33
|
exports.isPhaseEntryChecked = isPhaseEntryChecked;
|
|
34
34
|
exports.isBoxCharChecked = isBoxCharChecked;
|
|
35
|
+
exports.stripInsertedMarker = stripInsertedMarker;
|
|
35
36
|
const MARKER_SRC = '[-*]';
|
|
36
37
|
// A list item has 1 to 4 spaces or tabs after its marker (CommonMark: none is not a list item,
|
|
37
38
|
// five or more makes the rest an indented code block), so `-[ ] **Phase 1**` is not an entry.
|
|
@@ -117,3 +118,16 @@ function isPhaseEntryChecked(entryText) {
|
|
|
117
118
|
function isBoxCharChecked(boxChar) {
|
|
118
119
|
return boxChar.toLowerCase() === 'x';
|
|
119
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* #5824: `phase insert` writes `(INSERTED)` into the heading text as
|
|
123
|
+
* bookkeeping; it is not part of the phase's name. The single place that
|
|
124
|
+
* removes it: only a TRAILING `(INSERTED)` (case-insensitive) is dropped, then
|
|
125
|
+
* a trim — `phase insert` always appends it last, and a `(INSERTED)` inside the
|
|
126
|
+
* user's own text is kept. A name that is only the marker is returned as
|
|
127
|
+
* written rather than reduced to an empty name. Every phase-name reader goes
|
|
128
|
+
* through here so none can drift from the others.
|
|
129
|
+
*/
|
|
130
|
+
function stripInsertedMarker(name) {
|
|
131
|
+
const stripped = name.replace(/\s*\(INSERTED\)\s*$/i, '').trim();
|
|
132
|
+
return stripped === '' ? name.trim() : stripped;
|
|
133
|
+
}
|
|
@@ -71,6 +71,8 @@ const OPTIONAL_PHASE_TAG_SOURCE = '(?:\\s*\\([^)\\n]{0,200}\\))?';
|
|
|
71
71
|
// (scripts/lint-phase-id-drift.cjs) fails CI if a literal re-derivation is
|
|
72
72
|
// introduced outside this module without a `// phase-id-owner:` justification.
|
|
73
73
|
const PHASE_NUMBER_TOKEN_SOURCE = '\\d+[A-Z]?(?:\\.\\d+)*';
|
|
74
|
+
// #5676: milestone-prefixed M-NN token grammar (one optional -NN segment, the #5626 grammar).
|
|
75
|
+
const PHASE_NUMBER_TOKEN_SOURCE_MILESTONE_PREFIXED = '\\d+(?:-\\d+)?[A-Z]?(?:\\.\\d+)*';
|
|
74
76
|
// #4764: a legacy phase REFERENCE in depends-on PROSE — the token in context, directly
|
|
75
77
|
// following "Phase"/"Phases", with bare-token list continuation ("Phases 1 and 2",
|
|
76
78
|
// "Phase 1, 2, and 3", "Phase 1-3"). Built HERE, beside the token grammar it is
|
|
@@ -381,7 +383,7 @@ function phaseHeadingPrefixSrcFor(baseline, convention, capturing = false) {
|
|
|
381
383
|
function parsePhaseChecklistLine(line, convention, context) {
|
|
382
384
|
const capturesBracketId = convention === 'bracket';
|
|
383
385
|
const intro = phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.LABEL_ONLY, convention, capturesBracketId);
|
|
384
|
-
const pattern = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`${intro}(${context ? phaseScanTokenSource(context) :
|
|
386
|
+
const pattern = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`${intro}(${context ? phaseScanTokenSource(context, convention) : phaseNumberTokenSource(convention)})`
|
|
385
387
|
+ `${OPTIONAL_PHASE_TAG_SOURCE}(?=[:\\s])`, 'i', { state: 'captured' });
|
|
386
388
|
const match = pattern.exec(line);
|
|
387
389
|
if (!match)
|
|
@@ -413,7 +415,7 @@ function buildPhaseHeadingScanRegex(baseline, convention, context) {
|
|
|
413
415
|
const bracketGroup = convention === 'bracket' ? 1 : null;
|
|
414
416
|
const phaseNumGroup = bracketGroup ? 2 : 1;
|
|
415
417
|
const phaseNameGroup = phaseNumGroup + 1;
|
|
416
|
-
const regex = new RegExp(`^ {0,3}#{2,4}\\s*${phaseHeadingPrefixSrcFor(baseline, convention, true)}(${context && convention !== 'bracket' ? phaseScanTokenSource(context) :
|
|
418
|
+
const regex = new RegExp(`^ {0,3}#{2,4}\\s*${phaseHeadingPrefixSrcFor(baseline, convention, true)}(${context && convention !== 'bracket' ? phaseScanTokenSource(context, convention) : phaseNumberTokenSource(convention)})${OPTIONAL_PHASE_TAG_SOURCE}:\\s*([^\\n]+)`, 'gim');
|
|
417
419
|
return { regex, bracketGroup, phaseNumGroup, phaseNameGroup };
|
|
418
420
|
}
|
|
419
421
|
/**
|
|
@@ -435,7 +437,7 @@ function buildPhaseDeclarationScans(convention, withBullets = true) {
|
|
|
435
437
|
// Under bracket a numbered-list item (`1. [ ] **[GSD.02] 05: n**`) is a checklist row too.
|
|
436
438
|
const bullet = bracket ? '^[ \\t]*(?:-|\\d+[.)])[ \\t]*' : '^[ \\t]*-[ \\t]*';
|
|
437
439
|
// Bracket ids may carry a letter suffix (`12A`), as the heading scan's phase token does.
|
|
438
|
-
const tail = bracket ? '(\\d+)(?=[A-Za-z]?(?:[:.\\s*]|$))' : '(\\d+)(?=[:.\\s*]|$)';
|
|
440
|
+
const tail = bracket ? '(\\d+)(?=[A-Za-z]?(?:[:.\\s*]|$))' : convention === 'milestone-prefixed' ? `(${phaseNumberTokenSource(convention)})(?=[:.\\s*]|$)` : '(\\d+)(?=[:.\\s*]|$)';
|
|
439
441
|
const checkbox = `\\[${bracket ? `(?!${BRACKET_ID_SRC}\\])` : ''}[^\\]]{0,200}\\][ \\t]*`;
|
|
440
442
|
if (!bracket) {
|
|
441
443
|
if (withBullets)
|
|
@@ -1582,9 +1584,17 @@ const CUSTOM_PHASE_ID_FILE = '.phase-id';
|
|
|
1582
1584
|
function phaseDirectoryContext(config, roadmap = '', knownIds = []) {
|
|
1583
1585
|
return { phaseNaming: typeof config.phase_naming === 'string' ? config.phase_naming.toLowerCase() : config.phase_naming, projectCode: config.project_code ?? null, roadmap, knownIds };
|
|
1584
1586
|
}
|
|
1585
|
-
/**
|
|
1586
|
-
function
|
|
1587
|
-
return
|
|
1587
|
+
/** #5676: the single owner of the numeric phase-token source per convention. */
|
|
1588
|
+
function phaseNumberTokenSource(convention) {
|
|
1589
|
+
return convention === 'milestone-prefixed' ? PHASE_NUMBER_TOKEN_SOURCE_MILESTONE_PREFIXED : PHASE_NUMBER_TOKEN_SOURCE;
|
|
1590
|
+
}
|
|
1591
|
+
/**
|
|
1592
|
+
* Custom scans reuse the numeric source verbatim outside configured custom projects.
|
|
1593
|
+
* #5676: under `milestone-prefixed` the numeric source admits the `M-NN` token
|
|
1594
|
+
* (`3-02`), so a `03-02-beta` directory is read as phase `3-02`, not phase `3`.
|
|
1595
|
+
*/
|
|
1596
|
+
function phaseScanTokenSource(context, convention) {
|
|
1597
|
+
return context?.phaseNaming === 'custom' ? '[^ \t:\n]+' : phaseNumberTokenSource(convention);
|
|
1588
1598
|
}
|
|
1589
1599
|
const customIdentityIndexes = new WeakMap();
|
|
1590
1600
|
function customIdentityIndex(options) {
|
|
@@ -1986,12 +1996,17 @@ function phaseKeyFromDir(dirName, convention, context) {
|
|
|
1986
1996
|
* `Phase:` value. Markdown emphasis is stripped first so a bolded cell is not
|
|
1987
1997
|
* mistaken for a non-phase. Returns null when the value does not BEGIN with a
|
|
1988
1998
|
* phase token (`parsePhaseFromProse` anchoring, #2111).
|
|
1999
|
+
*
|
|
2000
|
+
* #5675: `convention` is the project's `phase_id_convention`. Under `milestone-prefixed` the id is
|
|
2001
|
+
* the whole `M-NN` (`3-01`), so the key equals `phaseKeyFromDir` for the same phase; every other
|
|
2002
|
+
* convention (and an absent one) keeps the leading-integer read. It is the trailing parameter so the
|
|
2003
|
+
* existing `(value, context)` callers stay byte-identical.
|
|
1989
2004
|
*/
|
|
1990
|
-
function phaseKeyFromProse(value, context) {
|
|
2005
|
+
function phaseKeyFromProse(value, context, convention) {
|
|
1991
2006
|
if (value == null)
|
|
1992
2007
|
return null;
|
|
1993
2008
|
const clean = String(value).replace(/[*_`~]/g, '');
|
|
1994
|
-
const { phase } = parsePhaseFromProse(clean,
|
|
2009
|
+
const { phase } = parsePhaseFromProse(clean, convention, context);
|
|
1995
2010
|
return phase === null ? null : phaseKeyFromToken(phase, context);
|
|
1996
2011
|
}
|
|
1997
2012
|
/**
|
|
@@ -2031,7 +2046,7 @@ function isPlannedPhaseStatusPhrase(value) {
|
|
|
2031
2046
|
// (`3-01`), so the number capture takes one optional `-NN` segment; every other convention (and an absent
|
|
2032
2047
|
// one) keeps the dotted-only capture, where `3-01` reads as phase `3` exactly as before.
|
|
2033
2048
|
const PHASE_PROSE_RE = /^\s*(?:Phase\s+)?(?:[A-Z][A-Z0-9_]*-)?(\d+[A-Z]?(?:\.\d+)*)\b/i;
|
|
2034
|
-
const PHASE_PROSE_MILESTONE_PREFIXED_RE =
|
|
2049
|
+
const PHASE_PROSE_MILESTONE_PREFIXED_RE = new RegExp(`^\\s*(?:Phase\\s+)?(?:[A-Z][A-Z0-9_]*-)?(${PHASE_NUMBER_TOKEN_SOURCE_MILESTONE_PREFIXED})\\b`, 'i');
|
|
2035
2050
|
function parsePhaseFromProse(value, convention, context) {
|
|
2036
2051
|
if (!value)
|
|
2037
2052
|
return { phase: null, name: null };
|
|
@@ -2206,6 +2221,8 @@ const phaseIdExports = {
|
|
|
2206
2221
|
OPTIONAL_PROJECT_CODE_PREFIX_SOURCE,
|
|
2207
2222
|
OPTIONAL_PHASE_TAG_SOURCE,
|
|
2208
2223
|
PHASE_NUMBER_TOKEN_SOURCE,
|
|
2224
|
+
PHASE_NUMBER_TOKEN_SOURCE_MILESTONE_PREFIXED,
|
|
2225
|
+
phaseNumberTokenSource,
|
|
2209
2226
|
PHASE_DEP_REF_SOURCE,
|
|
2210
2227
|
tokenizePhaseDependencyReferences,
|
|
2211
2228
|
extractPhaseDependencyTokens,
|