@opengsd/gsd-core 1.13.0 → 1.14.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-advisor-researcher.compact.md +85 -0
- package/agents/gsd-ai-researcher.compact.md +96 -0
- package/agents/gsd-assumptions-analyzer.compact.md +81 -0
- package/agents/gsd-code-fixer.compact.md +458 -0
- package/agents/gsd-code-fixer.md +5 -5
- package/agents/gsd-code-reviewer.compact.md +269 -0
- package/agents/gsd-code-reviewer.md +15 -3
- package/agents/gsd-codebase-mapper.compact.md +760 -0
- package/agents/gsd-debug-session-manager.compact.md +345 -0
- package/agents/gsd-doc-classifier.compact.md +192 -0
- package/agents/gsd-doc-synthesizer.compact.md +200 -0
- package/agents/gsd-doc-verifier.compact.md +143 -0
- package/agents/gsd-doc-writer.compact.md +440 -0
- package/agents/gsd-dom-verifier.compact.md +138 -0
- package/agents/gsd-domain-researcher.compact.md +141 -0
- package/agents/gsd-eval-auditor.compact.md +160 -0
- package/agents/gsd-eval-planner.compact.md +137 -0
- package/agents/gsd-framework-selector.compact.md +82 -0
- package/agents/gsd-integration-checker.compact.md +245 -0
- package/agents/gsd-intel-updater.compact.md +226 -0
- package/agents/gsd-mempalace-curator.compact.md +45 -0
- package/agents/gsd-nyquist-auditor.compact.md +179 -0
- package/agents/gsd-pattern-mapper.compact.md +275 -0
- package/agents/gsd-project-researcher.compact.md +587 -0
- package/agents/gsd-research-synthesizer.compact.md +212 -0
- package/agents/gsd-roadmapper.compact.md +454 -0
- package/agents/gsd-roadmapper.md +13 -0
- package/agents/gsd-security-auditor.compact.md +162 -0
- package/agents/gsd-ui-auditor.compact.md +404 -0
- package/agents/gsd-ui-checker.compact.md +277 -0
- package/agents/gsd-ui-researcher.compact.md +282 -0
- package/agents/gsd-user-profiler.compact.md +108 -0
- package/bin/install.js +206 -68
- package/commands/gsd/cleanup.md +1 -0
- package/commands/gsd/code-review.md +2 -1
- package/commands/gsd/complete-milestone.md +1 -0
- package/commands/gsd/config.md +1 -0
- package/commands/gsd/debug.md +1 -0
- package/commands/gsd/graphify.md +1 -0
- package/commands/gsd/health.md +1 -0
- package/commands/gsd/mempalace-capture.md +1 -0
- package/commands/gsd/mempalace-recall.md +1 -0
- package/commands/gsd/new-milestone.md +1 -0
- package/commands/gsd/new-project.md +1 -0
- package/commands/gsd/next.md +1 -0
- package/commands/gsd/pause-work.md +1 -0
- package/commands/gsd/phase.md +1 -0
- package/commands/gsd/pr-branch.md +1 -0
- package/commands/gsd/resume-work.md +1 -0
- package/commands/gsd/review-backlog.md +1 -0
- package/commands/gsd/settings.md +2 -1
- package/commands/gsd/stats.md +1 -0
- package/commands/gsd/thread.md +1 -0
- package/commands/gsd/workspace.md +1 -0
- package/commands/gsd/workstreams.md +1 -0
- package/gsd-core/bin/check-latest-version.cjs +8 -3
- package/gsd-core/bin/gsd-tools.cjs +338 -125
- package/gsd-core/bin/lib/adr-parser.cjs +1 -1
- package/gsd-core/bin/lib/artifacts.cjs +2 -1
- package/gsd-core/bin/lib/audit.cjs +39 -22
- package/gsd-core/bin/lib/broken-windows.cjs +168 -49
- package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
- package/gsd-core/bin/lib/capability-loader.cjs +135 -1
- package/gsd-core/bin/lib/capability-registry.cjs +79 -67
- package/gsd-core/bin/lib/capability-source.cjs +19 -2
- package/gsd-core/bin/lib/capability-validator.cjs +14 -1
- package/gsd-core/bin/lib/check-command-router.cjs +113 -36
- package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
- package/gsd-core/bin/lib/commands.cjs +650 -72
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +153 -38
- package/gsd-core/bin/lib/coverage.cjs +1 -1
- package/gsd-core/bin/lib/decisions.cjs +137 -34
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
- package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +12 -1
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
- package/gsd-core/bin/lib/init.cjs +409 -47
- package/gsd-core/bin/lib/install-engine.cjs +16 -3
- package/gsd-core/bin/lib/install-profiles.cjs +14 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +33 -4
- package/gsd-core/bin/lib/loop-resolver.cjs +50 -31
- package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
- package/gsd-core/bin/lib/milestone.cjs +19 -8
- package/gsd-core/bin/lib/model-resolver.cjs +101 -10
- package/gsd-core/bin/lib/phase-command-router.cjs +7 -1
- package/gsd-core/bin/lib/phase-id.cjs +161 -22
- package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
- package/gsd-core/bin/lib/phase.cjs +167 -63
- package/gsd-core/bin/lib/planning-inspect.cjs +34 -18
- package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
- package/gsd-core/bin/lib/planning-workspace.cjs +50 -1
- package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
- package/gsd-core/bin/lib/quick-batch.cjs +1 -1
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
- package/gsd-core/bin/lib/research-store.cjs +11 -12
- package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +56 -15
- package/gsd-core/bin/lib/roadmap.cjs +108 -14
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +27 -10
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +12 -3
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +193 -4
- package/gsd-core/bin/lib/security.cjs +126 -7
- package/gsd-core/bin/lib/state-document.cjs +130 -28
- package/gsd-core/bin/lib/state-md-schema.cjs +21 -14
- package/gsd-core/bin/lib/state-transition.cjs +142 -28
- package/gsd-core/bin/lib/state.cjs +223 -27
- package/gsd-core/bin/lib/surface.cjs +60 -2
- package/gsd-core/bin/lib/task-command-router.cjs +12 -6
- package/gsd-core/bin/lib/uat.cjs +1 -1
- package/gsd-core/bin/lib/update-context.cjs +30 -24
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
- package/gsd-core/bin/lib/verification.cjs +47 -15
- package/gsd-core/bin/lib/verify-command-grounding.cjs +1 -1
- package/gsd-core/bin/lib/verify.cjs +188 -23
- package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
- package/gsd-core/bin/lib/worktree-safety.cjs +13 -7
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
- package/gsd-core/references/compact-content-gate.md +66 -0
- package/gsd-core/references/loop-hook-dispatch.md +18 -0
- package/gsd-core/references/model-profiles.md +12 -3
- package/gsd-core/references/planning-config.md +3 -0
- package/gsd-core/references/tdd.md +5 -2
- package/gsd-core/references/thinking-models-planning.md +18 -2
- package/gsd-core/references/verification-patterns.md +17 -4
- package/gsd-core/references/worktree-path-safety.md +112 -2
- package/gsd-core/templates/README.md +7 -1
- package/gsd-core/templates/state.md +6 -3
- package/gsd-core/templates/summary.compact.md +212 -0
- package/gsd-core/templates/user-setup.compact.md +199 -0
- package/gsd-core/templates/user-setup.md +0 -9
- package/gsd-core/workflows/add-todo.md +3 -2
- package/gsd-core/workflows/autonomous.md +13 -10
- package/gsd-core/workflows/check-todos.md +4 -2
- package/gsd-core/workflows/cleanup.md +3 -1
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +7 -0
- package/gsd-core/workflows/code-review-fix.md +3 -3
- package/gsd-core/workflows/code-review.md +156 -30
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
- package/gsd-core/workflows/complete-milestone.md +39 -262
- package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
- package/gsd-core/workflows/docs-update.md +14 -155
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +18 -3
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +7 -2
- package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
- package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
- package/gsd-core/workflows/execute-phase.md +53 -152
- package/gsd-core/workflows/execute-plan.md +20 -7
- package/gsd-core/workflows/help/modes/full.compact.md +398 -0
- package/gsd-core/workflows/help.md +1 -1
- package/gsd-core/workflows/map-codebase.md +50 -3
- package/gsd-core/workflows/new-milestone.md +54 -12
- package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
- package/gsd-core/workflows/new-project.md +32 -202
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
- package/gsd-core/workflows/plan-phase.md +22 -181
- package/gsd-core/workflows/pr-branch.md +19 -7
- package/gsd-core/workflows/quick.md +8 -1
- package/gsd-core/workflows/reapply-patches.md +77 -3
- package/gsd-core/workflows/settings.md +18 -5
- package/gsd-core/workflows/update.md +7 -5
- package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
- package/gsd-core/workflows/verify-work.md +20 -180
- package/hooks/dist/gsd-agent-isolation-guard.js +42 -16
- package/hooks/dist/gsd-context-monitor.js +88 -15
- package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
- package/hooks/dist/gsd-secret-read-guard.js +44 -18
- package/hooks/dist/gsd-statusline.js +11 -7
- package/hooks/dist/gsd-validate-commit.sh +34 -4
- package/hooks/dist/gsd-worktree-path-guard.js +25 -14
- package/hooks/dist/gsd-write-guard.js +46 -1
- package/hooks/dist/lib/dispatch-identity.js +187 -0
- package/hooks/dist/lib/filename-classification.js +64 -0
- package/hooks/dist/lib/isolation-deny-reason.js +53 -1
- package/hooks/dist/lib/isolation-sentinel.js +58 -19
- package/hooks/gsd-agent-isolation-guard.js +42 -16
- package/hooks/gsd-context-monitor.js +88 -15
- package/hooks/gsd-cursor-subagent-start.js +34 -14
- package/hooks/gsd-secret-read-guard.js +44 -18
- package/hooks/gsd-statusline.js +11 -7
- package/hooks/gsd-validate-commit.sh +34 -4
- package/hooks/gsd-worktree-path-guard.js +25 -14
- package/hooks/gsd-write-guard.js +46 -1
- package/hooks/lib/dispatch-identity.js +187 -0
- package/hooks/lib/filename-classification.js +64 -0
- package/hooks/lib/isolation-deny-reason.js +53 -1
- package/hooks/lib/isolation-sentinel.js +58 -19
- package/package.json +10 -6
- package/scripts/benchmark-compact-content-variants.cjs +298 -0
- package/scripts/benchmark-compact-content.cjs +368 -0
- package/scripts/check-contract-drift.cjs +4 -1
- package/scripts/check-env.cjs +36 -8
- package/scripts/check-glossary-refs.cjs +25 -21
- package/scripts/ci-next-health.cjs +271 -0
- package/scripts/ci-prepare-test-scope.cjs +7 -7
- package/scripts/ci-test-scope.cjs +126 -20
- package/scripts/ci-timeout-report.cjs +1 -1
- package/scripts/diff-touches-shipped-paths.cjs +1 -1
- package/scripts/docs-guard-registry.cjs +7 -2
- package/scripts/gen-adr-index.cjs +8 -2
- package/scripts/gen-inventory-manifest.cjs +12 -0
- package/scripts/gen-platform-conformance-tier.cjs +557 -0
- package/scripts/lib/drift-scan.cjs +1 -1
- package/scripts/lib/macos-conformance-tier.generated.cjs +210 -0
- package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +276 -0
- package/scripts/lib/suite-detection.cjs +32 -0
- package/scripts/lint-allowed-tools-parity.cjs +221 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +19 -2
- package/scripts/lint-phase-id-drift.cjs +338 -13
- package/scripts/lint-response-language-coverage.cjs +9 -3
- package/scripts/lint-source-test-name-collision.cjs +1 -1
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/lint-vendored-deps.cjs +128 -17
- package/scripts/lint-workflow-shellcheck-baseline.json +85 -0
- package/scripts/prompt-injection-scan.sh +14 -0
- package/scripts/workflow-size.cjs +139 -0
- package/skills/gsd-cleanup/SKILL.md +1 -0
- package/skills/gsd-code-review/SKILL.md +2 -1
- package/skills/gsd-complete-milestone/SKILL.md +1 -0
- package/skills/gsd-config/SKILL.md +1 -0
- package/skills/gsd-debug/SKILL.md +1 -0
- package/skills/gsd-graphify/SKILL.md +1 -0
- package/skills/gsd-health/SKILL.md +1 -0
- package/skills/gsd-mempalace-capture/SKILL.md +1 -0
- package/skills/gsd-mempalace-recall/SKILL.md +1 -0
- package/skills/gsd-new-milestone/SKILL.md +1 -0
- package/skills/gsd-new-project/SKILL.md +1 -0
- package/skills/gsd-next/SKILL.md +1 -0
- package/skills/gsd-pause-work/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -0
- package/skills/gsd-pr-branch/SKILL.md +1 -0
- package/skills/gsd-resume-work/SKILL.md +1 -0
- package/skills/gsd-review-backlog/SKILL.md +1 -0
- package/skills/gsd-settings/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +1 -0
- package/skills/gsd-thread/SKILL.md +1 -0
- package/skills/gsd-workspace/SKILL.md +1 -0
- package/skills/gsd-workstreams/SKILL.md +1 -0
- package/vscode/package.json +1 -1
- package/gsd-core/templates/claude-md.md +0 -145
- package/gsd-core/templates/codebase/concerns.md +0 -310
- package/gsd-core/templates/codebase/conventions.md +0 -307
- package/gsd-core/templates/codebase/integrations.md +0 -280
- package/gsd-core/templates/codebase/structure.md +0 -285
- package/gsd-core/templates/codebase/testing.md +0 -480
- package/gsd-core/templates/debug-subagent-prompt.md +0 -91
- package/gsd-core/templates/discovery.md +0 -146
|
@@ -135,6 +135,7 @@ const CONFIG_DEFAULTS = {
|
|
|
135
135
|
firecrawl: _getConfigDefault('firecrawl'),
|
|
136
136
|
exa_search: _getConfigDefault('exa_search'),
|
|
137
137
|
text_mode: _getNestedConfigDefault('workflow', 'text_mode'),
|
|
138
|
+
compact_content: _getNestedConfigDefault('workflow', 'compact_content'),
|
|
138
139
|
sub_repos: _getNestedConfigDefault('planning', 'sub_repos'),
|
|
139
140
|
pr_strict: _getNestedConfigDefault('planning', 'pr_strict'),
|
|
140
141
|
resolve_model_ids: _getConfigDefault('resolve_model_ids'),
|
|
@@ -81,6 +81,11 @@ const SCHEMA_DEFAULTS = {
|
|
|
81
81
|
// #1689: per-plan agent_hint executor routing — default-on. A no-op for plans
|
|
82
82
|
// without an agent_hint field, so existing dispatch is byte-identical.
|
|
83
83
|
'workflow.agent_hint_routing': true,
|
|
84
|
+
// #4401: Compact Content mode gate — derived from the defaults manifest via
|
|
85
|
+
// CONFIG_DEFAULTS (added in config-loader.cts) so the manifest stays the
|
|
86
|
+
// single source of truth, matching workflow.smart_zone_tokens /
|
|
87
|
+
// planning.pr_strict / workflow.inline_plan_threshold below.
|
|
88
|
+
'workflow.compact_content': CONFIG_DEFAULTS.compact_content,
|
|
84
89
|
// Derived from the defaults manifest rather than restated, so the manifest
|
|
85
90
|
// stays the single source of truth for the smart-zone budget (#2630).
|
|
86
91
|
'workflow.smart_zone_tokens': CONFIG_DEFAULTS.smart_zone_tokens,
|
|
@@ -93,6 +98,27 @@ const SCHEMA_DEFAULTS = {
|
|
|
93
98
|
// effective default existed only as the workflow's shell fallback and the
|
|
94
99
|
// docs disagreed (settings-advanced said 3). Manifest stays the one owner.
|
|
95
100
|
'workflow.inline_plan_threshold': CONFIG_DEFAULTS.inline_plan_threshold,
|
|
101
|
+
// #4285 review: an absent threshold resolved to "Key not found" while the
|
|
102
|
+
// hook silently used 35/25 — the query surface disagreeing with the reader.
|
|
103
|
+
//
|
|
104
|
+
// Restated here rather than derived: `CONFIG_DEFAULTS` is re-exported with a
|
|
105
|
+
// FLATTENED shape that drops the manifest's nested blocks, so
|
|
106
|
+
// `CONFIG_DEFAULTS.hooks` is undefined at runtime and the manifest cannot
|
|
107
|
+
// feed these two rows the way `workflow.smart_zone_tokens` above is fed.
|
|
108
|
+
//
|
|
109
|
+
// Not added to `buildNewProjectConfig` either, and that one is deliberate
|
|
110
|
+
// rather than incidental: it writes a `hooks` object into every NEW project's
|
|
111
|
+
// config.json, which would freeze today's fire-points as an explicit
|
|
112
|
+
// per-project override everywhere — the opposite of this PR's premise that an
|
|
113
|
+
// absent key tracks the shipped default. (The manifest alone would NOT have
|
|
114
|
+
// that effect; `buildNewProjectConfig` builds its own literal. Correcting an
|
|
115
|
+
// earlier version of this comment that ran the two together.)
|
|
116
|
+
//
|
|
117
|
+
// That leaves ONE copy of 35/25 outside the hook — these two rows — and
|
|
118
|
+
// `tests/config.test.cjs` pins them against the hook's exported
|
|
119
|
+
// WARNING_THRESHOLD/CRITICAL_THRESHOLD so the copies cannot drift.
|
|
120
|
+
'hooks.context_warning_threshold': 35,
|
|
121
|
+
'hooks.context_critical_threshold': 25,
|
|
96
122
|
};
|
|
97
123
|
/**
|
|
98
124
|
* Resolve a schema-level default for an absent key (#2256). Checks the legacy
|
|
@@ -308,6 +334,7 @@ function buildNewProjectConfig(userChoices) {
|
|
|
308
334
|
human_verify_mode: 'end-of-phase',
|
|
309
335
|
context_guard_mode: 'warn',
|
|
310
336
|
text_mode: false,
|
|
337
|
+
compact_content: false,
|
|
311
338
|
research_before_questions: false,
|
|
312
339
|
discuss_mode: 'discuss',
|
|
313
340
|
skip_discuss: false,
|
|
@@ -564,19 +591,35 @@ function _unsetNestedValue(config, keyPath) {
|
|
|
564
591
|
* Does not call `output()`, so can be used as one step in a command without triggering `exit(0)` in
|
|
565
592
|
* the happy path. But note that `error()` will still `exit(1)` out of the process.
|
|
566
593
|
*/
|
|
594
|
+
/**
|
|
595
|
+
* Loads `.planning/config.json` as a plain object, or `{}` if the file does
|
|
596
|
+
* not exist. A parse failure calls `error()` (process-exiting) rather than
|
|
597
|
+
* throwing, matching every caller's existing behavior.
|
|
598
|
+
*
|
|
599
|
+
* Single source for this load+parse step — `setConfigValue`,
|
|
600
|
+
* `unsetConfigValue`, `setConfigValues`, `previewConfigValue`, and
|
|
601
|
+
* `previewUnsetConfigValue` all delegate here instead of each repeating the
|
|
602
|
+
* same try/catch (CLAUDE.md's "Generative Fix Divergence" known-defect
|
|
603
|
+
* pattern: independently-guessed copies of the same logic can silently
|
|
604
|
+
* drift apart).
|
|
605
|
+
*/
|
|
606
|
+
function loadConfigJson(cwd) {
|
|
607
|
+
const configPath = node_path_1.default.join(planningDir(cwd), 'config.json');
|
|
608
|
+
let config = {};
|
|
609
|
+
try {
|
|
610
|
+
if (node_fs_1.default.existsSync(configPath)) {
|
|
611
|
+
config = JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
catch (err) {
|
|
615
|
+
error('Failed to read config.json: ' + err.message, ERROR_REASON.CONFIG_PARSE_FAILED);
|
|
616
|
+
}
|
|
617
|
+
return config;
|
|
618
|
+
}
|
|
567
619
|
function unsetConfigValue(cwd, keyPath) {
|
|
568
620
|
const configPath = node_path_1.default.join(planningDir(cwd), 'config.json');
|
|
569
621
|
return withPlanningLock(cwd, () => {
|
|
570
|
-
|
|
571
|
-
let config = {};
|
|
572
|
-
try {
|
|
573
|
-
if (node_fs_1.default.existsSync(configPath)) {
|
|
574
|
-
config = JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
catch (err) {
|
|
578
|
-
error('Failed to read config.json: ' + err.message, ERROR_REASON.CONFIG_PARSE_FAILED);
|
|
579
|
-
}
|
|
622
|
+
const config = loadConfigJson(cwd);
|
|
580
623
|
const { previousValue, existed } = _unsetNestedValue(config, keyPath);
|
|
581
624
|
// Write back
|
|
582
625
|
try {
|
|
@@ -598,16 +641,7 @@ function unsetConfigValue(cwd, keyPath) {
|
|
|
598
641
|
function setConfigValue(cwd, keyPath, parsedValue) {
|
|
599
642
|
const configPath = node_path_1.default.join(planningDir(cwd), 'config.json');
|
|
600
643
|
return withPlanningLock(cwd, () => {
|
|
601
|
-
|
|
602
|
-
let config = {};
|
|
603
|
-
try {
|
|
604
|
-
if (node_fs_1.default.existsSync(configPath)) {
|
|
605
|
-
config = JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
catch (err) {
|
|
609
|
-
error('Failed to read config.json: ' + err.message, ERROR_REASON.CONFIG_PARSE_FAILED);
|
|
610
|
-
}
|
|
644
|
+
const config = loadConfigJson(cwd);
|
|
611
645
|
const previousValue = _setNestedValue(config, keyPath, parsedValue);
|
|
612
646
|
// Write back
|
|
613
647
|
try {
|
|
@@ -619,6 +653,29 @@ function setConfigValue(cwd, keyPath, parsedValue) {
|
|
|
619
653
|
}
|
|
620
654
|
});
|
|
621
655
|
}
|
|
656
|
+
/**
|
|
657
|
+
* #4444: read-only preview counterpart to `setConfigValue` — loads config
|
|
658
|
+
* exactly like the real setter and reuses `_setNestedValue` (the SAME
|
|
659
|
+
* traversal/creation logic, including its prototype-pollution guards) on a
|
|
660
|
+
* throwaway in-memory copy that is NEVER written back to disk. This is what
|
|
661
|
+
* makes the dry-run preview provably identical to what the real write would
|
|
662
|
+
* compute, rather than a second, hand-maintained traversal that could drift
|
|
663
|
+
* from the real one.
|
|
664
|
+
*/
|
|
665
|
+
function previewConfigValue(cwd, keyPath, parsedValue) {
|
|
666
|
+
const config = loadConfigJson(cwd);
|
|
667
|
+
const previousValue = _setNestedValue(config, keyPath, parsedValue);
|
|
668
|
+
return { key: keyPath, value: parsedValue, previousValue };
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* #4444: read-only preview counterpart to `unsetConfigValue` — same pattern
|
|
672
|
+
* as `previewConfigValue`, reusing `_unsetNestedValue` on a throwaway copy.
|
|
673
|
+
*/
|
|
674
|
+
function previewUnsetConfigValue(cwd, keyPath) {
|
|
675
|
+
const config = loadConfigJson(cwd);
|
|
676
|
+
const { previousValue, existed } = _unsetNestedValue(config, keyPath);
|
|
677
|
+
return { key: keyPath, value: null, previousValue, existed };
|
|
678
|
+
}
|
|
622
679
|
/**
|
|
623
680
|
* Batched sibling of setConfigValue: apply multiple key-path writes in a
|
|
624
681
|
* single load → set-all → write cycle inside ONE withPlanningLock call.
|
|
@@ -635,16 +692,7 @@ function setConfigValues(cwd, entries) {
|
|
|
635
692
|
}
|
|
636
693
|
const configPath = node_path_1.default.join(planningDir(cwd), 'config.json');
|
|
637
694
|
return withPlanningLock(cwd, () => {
|
|
638
|
-
|
|
639
|
-
let config = {};
|
|
640
|
-
try {
|
|
641
|
-
if (node_fs_1.default.existsSync(configPath)) {
|
|
642
|
-
config = JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
catch (err) {
|
|
646
|
-
error('Failed to read config.json: ' + err.message, ERROR_REASON.CONFIG_PARSE_FAILED);
|
|
647
|
-
}
|
|
695
|
+
const config = loadConfigJson(cwd);
|
|
648
696
|
const results = [];
|
|
649
697
|
for (const entry of entries) {
|
|
650
698
|
const previousValue = _setNestedValue(config, entry.keyPath, entry.value);
|
|
@@ -676,14 +724,8 @@ function assertEnumValue(parsedValue, rawVal, allowed, label) {
|
|
|
676
724
|
error(`Invalid ${label} '${rawVal}'. Valid values: ${allowed.join(', ')}`);
|
|
677
725
|
}
|
|
678
726
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
* "workflow.research").
|
|
682
|
-
*
|
|
683
|
-
* Note that this exits the process (via `output()`) even in the happy path; use `setConfigValue()`
|
|
684
|
-
* directly if you need to avoid this.
|
|
685
|
-
*/
|
|
686
|
-
function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
727
|
+
function cmdConfigSet(cwd, keyPath, value, raw, options = {}) {
|
|
728
|
+
const dryRun = options.dryRun === true;
|
|
687
729
|
if (!keyPath) {
|
|
688
730
|
error('Usage: config-set <key.path> <value>', ERROR_REASON.USAGE);
|
|
689
731
|
}
|
|
@@ -733,6 +775,18 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
|
733
775
|
// present, truthy-adjacent value that consumers must special-case — worst for
|
|
734
776
|
// secret keys where a leftover value can be passed as a real credential.
|
|
735
777
|
if (parsedValue === null) {
|
|
778
|
+
if (dryRun) {
|
|
779
|
+
const preview = previewUnsetConfigValue(cwd, kp);
|
|
780
|
+
if ((0, secrets_cjs_1.isSecretKey)(kp)) {
|
|
781
|
+
const maskedPrev = preview.previousValue === undefined
|
|
782
|
+
? undefined
|
|
783
|
+
: (0, secrets_cjs_1.maskSecret)(preview.previousValue);
|
|
784
|
+
output({ dry_run: true, would_unset: true, key: kp, value: null, previousValue: maskedPrev, masked: true }, raw, `${kp} unset (dry run)`);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
output({ dry_run: true, would_unset: true, key: kp, value: null, previousValue: preview.previousValue }, raw, `${kp} unset (dry run)`);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
736
790
|
const unsetResult = unsetConfigValue(cwd, kp);
|
|
737
791
|
if ((0, secrets_cjs_1.isSecretKey)(kp)) {
|
|
738
792
|
const maskedPrev = unsetResult.previousValue === undefined
|
|
@@ -787,6 +841,12 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
|
787
841
|
error(`Invalid workflow.post_planning_gaps '${val}'. Must be a boolean (true or false).`);
|
|
788
842
|
}
|
|
789
843
|
}
|
|
844
|
+
// Compact Content mode gate (#4139)
|
|
845
|
+
if (kp === 'workflow.compact_content') {
|
|
846
|
+
if (typeof parsedValue !== 'boolean') {
|
|
847
|
+
error(`Invalid workflow.compact_content '${val}'. Must be a boolean (true or false).`);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
790
850
|
// Per-plan executor routing via agent_hint frontmatter (#1689)
|
|
791
851
|
if (kp === 'workflow.agent_hint_routing') {
|
|
792
852
|
if (typeof parsedValue !== 'boolean') {
|
|
@@ -835,6 +895,48 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
|
835
895
|
error(`Invalid statusline.show_git '${val}'. Must be a boolean (true or false).`);
|
|
836
896
|
}
|
|
837
897
|
}
|
|
898
|
+
// Context-monitor fire-points (#4285) — a percentage of the context window
|
|
899
|
+
// REMAINING, so the domain is 0-100 and the hook compares them against
|
|
900
|
+
// `remaining_percentage`. Rejecting an out-of-domain value here keeps accept
|
|
901
|
+
// and honour in agreement ON THE DOMAIN: the hook falls back to its default
|
|
902
|
+
// for a value outside it, so reporting success would be a lie. That agreement
|
|
903
|
+
// is per-key and no wider — a value accepted here can still be superseded at
|
|
904
|
+
// read time by the hook's pair check, and a scoped write (GSD_PROJECT /
|
|
905
|
+
// GSD_WORKSTREAM) lands in a config the hook does not read at all. The PAIR
|
|
906
|
+
// (critical < warning) is deliberately NOT enforced here: config-set writes
|
|
907
|
+
// one key per call, so a two-step retune can be transiently inconsistent on
|
|
908
|
+
// disk and a check here would reject that intermediate write.
|
|
909
|
+
if (kp === 'hooks.context_warning_threshold' || kp === 'hooks.context_critical_threshold') {
|
|
910
|
+
if (typeof parsedValue !== 'number' || !Number.isFinite(parsedValue) || parsedValue < 0 || parsedValue > 100) {
|
|
911
|
+
error(`Invalid ${kp} '${val}'. Must be a number between 0 and 100 (percent of context window remaining).`);
|
|
912
|
+
}
|
|
913
|
+
// The two ENDPOINTS that are in range but can never form a valid pair are
|
|
914
|
+
// refused here rather than stored (#4285 review). `critical < warning` must
|
|
915
|
+
// hold at read time and BOTH sides are clamped to 0-100, so `warning: 0`
|
|
916
|
+
// has no legal partner (nothing is below 0) and `critical: 100` has none
|
|
917
|
+
// either (nothing above 100). Either one is silently discarded by the hook
|
|
918
|
+
// for EVERY value of the other key — verified: both resolve to the 35/25
|
|
919
|
+
// defaults against a present, absent, or extreme partner, while 0.001 and
|
|
920
|
+
// 99.999 are honoured.
|
|
921
|
+
//
|
|
922
|
+
// Storing a value the reader can never honour is exactly the
|
|
923
|
+
// accept-then-discard shape this codebase refuses elsewhere, so this fails
|
|
924
|
+
// at write time where the operator can see it. The pair itself is still NOT
|
|
925
|
+
// checked here — config-set writes one key per call, so a two-step retune
|
|
926
|
+
// is legitimately inconsistent on disk in between.
|
|
927
|
+
if (kp === 'hooks.context_warning_threshold' && parsedValue === 0) {
|
|
928
|
+
error(`Invalid ${kp} '${val}'. 0 is in range but unusable: the monitor requires `
|
|
929
|
+
+ `hooks.context_critical_threshold < hooks.context_warning_threshold, and no valid `
|
|
930
|
+
+ `critical value is below 0, so a warning of 0 would always fall back to the 35/25 `
|
|
931
|
+
+ `defaults. Use a value above 0.`);
|
|
932
|
+
}
|
|
933
|
+
if (kp === 'hooks.context_critical_threshold' && parsedValue === 100) {
|
|
934
|
+
error(`Invalid ${kp} '${val}'. 100 is in range but unusable: the monitor requires `
|
|
935
|
+
+ `hooks.context_critical_threshold < hooks.context_warning_threshold, and no valid `
|
|
936
|
+
+ `warning value is above 100, so a critical of 100 would always fall back to the `
|
|
937
|
+
+ `35/25 defaults. Use a value below 100.`);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
838
940
|
// Fallow scope + profile enum validation (#3424)
|
|
839
941
|
const VALID_FALLOW_SCOPES = ['phase', 'repo'];
|
|
840
942
|
if (kp === 'code_quality.fallow.scope')
|
|
@@ -921,6 +1023,19 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
|
921
1023
|
}
|
|
922
1024
|
}
|
|
923
1025
|
}
|
|
1026
|
+
if (dryRun) {
|
|
1027
|
+
const preview = previewConfigValue(cwd, kp, parsedValue);
|
|
1028
|
+
if ((0, secrets_cjs_1.isSecretKey)(kp)) {
|
|
1029
|
+
const masked = (0, secrets_cjs_1.maskSecret)(parsedValue);
|
|
1030
|
+
const maskedPrev = preview.previousValue === undefined
|
|
1031
|
+
? undefined
|
|
1032
|
+
: (0, secrets_cjs_1.maskSecret)(preview.previousValue);
|
|
1033
|
+
output({ dry_run: true, would_update: true, key: kp, value: masked, previousValue: maskedPrev, masked: true }, raw, `${kp}=${masked} (dry run)`);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
output({ dry_run: true, would_update: true, key: kp, value: parsedValue, previousValue: preview.previousValue }, raw, `${kp}=${String(parsedValue)} (dry run)`);
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
924
1039
|
const setConfigValueResult = setConfigValue(cwd, kp, parsedValue);
|
|
925
1040
|
// Mask secrets in both JSON and text output. The plaintext is written
|
|
926
1041
|
// to config.json (that's where secrets live on disk); the CLI output
|
|
@@ -435,7 +435,7 @@ function cmdClassify(cwd, options = {}, raw) {
|
|
|
435
435
|
}
|
|
436
436
|
let resolvedPath;
|
|
437
437
|
try {
|
|
438
|
-
resolvedPath = (0, security_cjs_1.requireSafePath)(filePath, cwd, 'SUMMARY file',
|
|
438
|
+
resolvedPath = (0, security_cjs_1.requireSafePath)(filePath, cwd, 'SUMMARY file', security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
439
439
|
}
|
|
440
440
|
catch (e) {
|
|
441
441
|
// Emit a structured command error instead of leaking a raw stack trace.
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* truth). Behaviour is preserved byte-for-behaviour from the prior hand-written
|
|
6
6
|
* .cjs; only types are added.
|
|
7
7
|
*
|
|
8
|
-
* Accepts
|
|
8
|
+
* Accepts numeric (D-42), alphanumeric (D-INFRA-01), and phase-prefixed
|
|
9
|
+
* (D4-01 — an optional digit-run between the leading letter and the hyphen,
|
|
10
|
+
* #4130) IDs.
|
|
9
11
|
* Returns {id, text, category, tags, trackable} per decision.
|
|
10
12
|
* CJS callers that only use {id, text} safely ignore the extra fields.
|
|
11
13
|
*
|
|
@@ -16,6 +18,11 @@
|
|
|
16
18
|
* - Outer bullet loop → seam's `iterateBullets` (for the header-fallback path)
|
|
17
19
|
*
|
|
18
20
|
* Resolves #1364 (markdown-header + em-dash recall) and #1365 (fail-loud gate).
|
|
21
|
+
*
|
|
22
|
+
* #4130 follow-up (hardening): the three bullet grammars below consume the
|
|
23
|
+
* decision ID atomically and narrow the em-dash first separator, eliminating
|
|
24
|
+
* the quadratic-backtracking cliff on pathological single bullets. Output is
|
|
25
|
+
* byte-identical on all legal inputs — see the notes at DECISION_ID_SOURCE.
|
|
19
26
|
*/
|
|
20
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
28
|
exports.extractDecisions = extractDecisions;
|
|
@@ -30,21 +37,84 @@ const DISCRETION_HEADINGS = new Set([
|
|
|
30
37
|
const NON_TRACKABLE_TAGS = new Set(['informational', 'folded', 'deferred']);
|
|
31
38
|
// ─── Bullet parsers (decisions-specific grammar) ─────────────────────────────
|
|
32
39
|
/**
|
|
33
|
-
*
|
|
40
|
+
* #4130: the ID grammar every extractor regex below shares, as ONE source.
|
|
41
|
+
* `D`, an OPTIONAL digit-run phase prefix, a hyphen, then the pre-existing
|
|
42
|
+
* alphanumeric tail — so `D-01` (bare), `D4-01`/`D12-01` (phase-prefixed,
|
|
43
|
+
* the reporter's multi-phase convention where bare D-01 collides across
|
|
44
|
+
* phases), and `D-INFRA-01` (alnum tail) are all the same grammar now.
|
|
45
|
+
* #2347 had already taught the shape DETECTOR to call `D4-01` decision-shaped
|
|
46
|
+
* while the EXTRACTOR still anchored on the literal `**D-` — the disagreement
|
|
47
|
+
* that made a whole phase-prefixed CONTEXT.md report could-not-parse. Deriving
|
|
48
|
+
* the three grammars (and the token evidence below) from this one constant is
|
|
49
|
+
* the parity pin: the extractor's ID universe cannot drift from the declared
|
|
50
|
+
* grammar again without editing this line, which the #4130 property tests
|
|
51
|
+
* watch from the other side.
|
|
52
|
+
*/
|
|
53
|
+
const DECISION_ID_SOURCE = 'D[0-9]*-[A-Za-z0-9][A-Za-z0-9_-]*';
|
|
54
|
+
/**
|
|
55
|
+
* #4130 follow-up (hardening): how the three grammars below CONSUME the ID —
|
|
56
|
+
* atomically, via the `(?=(X))\1` lookahead emulation (lookarounds are atomic
|
|
57
|
+
* in ECMAScript; the backreference must replay exactly what the lookahead
|
|
58
|
+
* captured, so the engine can never give the ID tail back one character at a
|
|
59
|
+
* time). That give-back was quadratic driver #1: the tail class
|
|
60
|
+
* `[A-Za-z0-9_-]*` overlaps the pre-separator class `[^:*]*` (every id char
|
|
61
|
+
* is also `[^:*]`), so on a FAILING bullet the base regex re-split the tail
|
|
62
|
+
* O(n) times with an O(n) scan after each — measured ~1.1s @ 40k chars on
|
|
63
|
+
* `- **D-` + `a-`×20k (the #4357 review's deferred cliff).
|
|
64
|
+
*
|
|
65
|
+
* Byte-identical on all legal inputs: a successful match always consumes the
|
|
66
|
+
* MAXIMAL id run (the lookahead's own match is exactly that maximal run), and
|
|
67
|
+
* the continuation's success depends only on the position of the first
|
|
68
|
+
* `:`/`*` (or `*` for the em-dash form) after the id boundary — id chars
|
|
69
|
+
* contain neither, so moving the boundary inside the run cannot change
|
|
70
|
+
* success or any capture. Group 1 stays the full id (the lookahead's capture
|
|
71
|
+
* IS group 1), so handlers keep reading match[1]/[2]/[3] untouched. Pinned by
|
|
72
|
+
* the differential property test against a frozen copy of the pre-hardening
|
|
73
|
+
* grammars and by the regex-lattice test in tests/decisions.test.cjs.
|
|
74
|
+
*/
|
|
75
|
+
/**
|
|
76
|
+
* #4130: the bold lead-in that ATTEMPTS the ID grammar above — used by the
|
|
77
|
+
* parse-miss guard and the #3939 join regexes, where recognising MORE shapes
|
|
78
|
+
* is the conservative direction (an over-broad match can only make a
|
|
79
|
+
* malformed bullet fail loud). The prefix run is either empty (bare `D-`) or
|
|
80
|
+
* DIGIT-INITIAL (`4`, `4x` — a phase prefix with a typo still counts as an
|
|
81
|
+
* attempted ID, so `D4x-01` reaches the guard and fails loud instead of
|
|
82
|
+
* vanishing), but never letter-initial: `D` + letters + `-` (`Deferred-until`)
|
|
83
|
+
* is a prose word, and prose must stay `none-present` (#2347's law).
|
|
84
|
+
*/
|
|
85
|
+
const ID_ATTEMPT_SOURCE = 'D(?:[0-9][A-Za-z0-9]*)?-';
|
|
86
|
+
/**
|
|
87
|
+
* Colon form: `- **D[phase]-NN[ [tags]]:** text`
|
|
34
88
|
* (#1343: `[^:*]*` subsumes any pre-colon prose, stops at `:**`)
|
|
89
|
+
* Group 1 captures the FULL id including any phase prefix (#4130).
|
|
90
|
+
* The ID is consumed atomically `(?=(…))\1` — see the hardening note above
|
|
91
|
+
* the constants (#4130 follow-up); with the tail unable to give back, the
|
|
92
|
+
* remaining `[^:*]*:` scan has a single viable split and the whole match is
|
|
93
|
+
* linear in line length.
|
|
35
94
|
*/
|
|
36
|
-
const bulletColonRe =
|
|
95
|
+
const bulletColonRe = new RegExp(`^\\s*-\\s+\\*\\*(?=(${DECISION_ID_SOURCE}))\\1(?:\\s*\\[([^\\]]+)\\])?[^:*]*:\\*\\*\\s*(.*)$`);
|
|
37
96
|
/**
|
|
38
|
-
* Em-dash form: `- **D-NN[ [tags]] — title** body`
|
|
97
|
+
* Em-dash form: `- **D[phase]-NN[ [tags]] — title** body`
|
|
39
98
|
* The em-dash (U+2014) or its lookalike separates the ID+tags group from a title
|
|
40
99
|
* that lives inside the bold markers; the body (which may be empty) follows
|
|
41
100
|
* outside the closing `**`. This form was not handled pre-T1 (bug #1364).
|
|
42
101
|
*
|
|
43
102
|
* Accepts both U+2014 em-dash (—) and U+2013 en-dash (–) for robustness.
|
|
103
|
+
*
|
|
104
|
+
* #4130 follow-up (hardening), quadratic driver #2: the first separator was
|
|
105
|
+
* `[^*]*[—–]`, whose leading class ALSO accepts the dash — on a failing
|
|
106
|
+
* dash-laden title the engine retried the separator at every dash position
|
|
107
|
+
* with an O(n) scan after each (~1.7s @ 40k). Narrowed to `[^*—–]*[—–]`:
|
|
108
|
+
* the leading class now excludes the dash, so the separator is the FIRST
|
|
109
|
+
* dash — one viable split, single pass. Behavior-preserving because every
|
|
110
|
+
* candidate dash lies before the first `*` (the leading class cannot cross
|
|
111
|
+
* a star), so the trailing `[^*]*` reaches that same first star from any
|
|
112
|
+
* candidate and `**` succeeds or fails identically; no capture involves the
|
|
113
|
+
* dash position. The ID is atomic like the other forms (driver #1).
|
|
44
114
|
*/
|
|
45
|
-
const bulletEmDashRe =
|
|
115
|
+
const bulletEmDashRe = new RegExp(`^\\s*-\\s+\\*\\*(?=(${DECISION_ID_SOURCE}))\\1(?:\\s*\\[([^\\]]+)\\])?[^*—–]*[—–][^*]*\\*\\*\\s*(.*)$`);
|
|
46
116
|
/**
|
|
47
|
-
* Titled-colon form: `- **D-NN[ [tags]]: Title.** body`
|
|
117
|
+
* Titled-colon form: `- **D[phase]-NN[ [tags]]: Title.** body`
|
|
48
118
|
* A title sits between the colon and the closing `**` (so the `:**` anchor of
|
|
49
119
|
* bulletColonRe fails, and there is no em-dash for bulletEmDashRe). This is a strict
|
|
50
120
|
* superset of the colon-immediate form, so it MUST be checked AFTER bulletColonRe and
|
|
@@ -53,16 +123,34 @@ const bulletEmDashRe = /^\s*-\s+\*\*D-([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s*\[([^\]]+
|
|
|
53
123
|
* (e.g. `D-07 ratio 3:1:**`) still fails the anchor and falls through to the parse-miss
|
|
54
124
|
* guard — matching bulletColonRe's `[^:*]*` discipline that the separator colon is the
|
|
55
125
|
* only colon permitted before `**`. (#1639)
|
|
126
|
+
*
|
|
127
|
+
* The ID is consumed atomically `(?=(…))\1` like the other forms — the
|
|
128
|
+
* hardening note above the constants explains why (#4130 follow-up).
|
|
129
|
+
*/
|
|
130
|
+
const bulletTitledColonRe = new RegExp(`^\\s*-\\s+\\*\\*(?=(${DECISION_ID_SOURCE}))\\1(?:\\s*\\[([^\\]]+)\\])?[^:*]*:[^:*]*\\*\\*\\s*(.*)$`);
|
|
131
|
+
/**
|
|
132
|
+
* #4130: the parse-miss guard's probe — a line whose bold lead-in ATTEMPTS the
|
|
133
|
+
* ID grammar (see `ID_ATTEMPT_SOURCE`) but failed all three bullet patterns
|
|
134
|
+
* above. Bare `D-` attempts behave exactly as before #4130; a digit-initial
|
|
135
|
+
* prefix run (`D4-`… including a typo'd `D4x-`) is new evidence of an attempt,
|
|
136
|
+
* so the malformed-prefixed bullet fails loud instead of silently vanishing.
|
|
56
137
|
*/
|
|
57
|
-
const
|
|
138
|
+
const parseMissGuardRe = new RegExp(`^\\s*-\\s+\\*\\*${ID_ATTEMPT_SOURCE}`);
|
|
139
|
+
/**
|
|
140
|
+
* #4130: bare-token evidence of decision-shaped content — a `D…-<alnum>` token
|
|
141
|
+
* in running text. `D-01` matched before; the digit-run phase prefix (`D4-01`)
|
|
142
|
+
* is added so token evidence agrees with the extractor's ID grammar
|
|
143
|
+
* (DECISION_ID_SOURCE) instead of silently ignoring prefixed mentions.
|
|
144
|
+
*/
|
|
145
|
+
const decisionTokenRe = new RegExp(`\\bD[0-9]*-[A-Za-z0-9]`, 'm');
|
|
58
146
|
/**
|
|
59
147
|
* #2347: format-agnostic evidence that a block/section holds real decision
|
|
60
148
|
* ENTRIES the parser could not read — a bullet whose bold lead-in is an
|
|
61
149
|
* ID-SHAPED token (uppercase prefix, optional digits, hyphen, alnum), whatever
|
|
62
|
-
* the exact ID grammar.
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
150
|
+
* the exact ID grammar. #1365's fail-loud guard originally reused the parser's
|
|
151
|
+
* own `\bD-` test as its "is this decision-shaped?" evidence, so any prefix the
|
|
152
|
+
* parser could not read (e.g. `D5-01` then, `DEC-01` now) was invisible to
|
|
153
|
+
* BOTH parser and guard, collapsing `could-not-parse` into a clean
|
|
66
154
|
* `none-present` pass.
|
|
67
155
|
*
|
|
68
156
|
* The ID-shape requirement (not "any bold bullet") is deliberate: a decisions
|
|
@@ -70,25 +158,35 @@ const bulletTitledColonRe = /^\s*-\s+\*\*D-([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s*\[([
|
|
|
70
158
|
* bullets with bold labels (`- **Scope:** …`, `- **Why:** …`, `- **Note:** …`).
|
|
71
159
|
* Those are NOT decision entries and must stay `none-present` — a false
|
|
72
160
|
* `could-not-parse` hard-blocks the plan gate. `[A-Z]+[0-9]*-[A-Za-z0-9]` matches
|
|
73
|
-
* `D-01` / `
|
|
161
|
+
* `D-01` / `D4-01` / `DEC-01` but not `Scope:` / `Why:` / `Follow-up:` (mixed
|
|
74
162
|
* case) / `TODO:` (no `-<alnum>` id) — mirroring the parser's own `D-<alnum>`
|
|
75
163
|
* shape without hardcoding the `D`.
|
|
164
|
+
*
|
|
165
|
+
* #4130 parity note: for the D-prefixed universe this detector's grammar
|
|
166
|
+
* (`D` + digit-run + `-` + alnum) is exactly `DECISION_ID_SOURCE` above, so a
|
|
167
|
+
* well-formed bullet the detector calls decision-shaped is now always one the
|
|
168
|
+
* extractor can read. The detector stays WIDER on purpose (`DEC-01` is still
|
|
169
|
+
* evidence): an ID grammar outside the parser's universe must keep failing
|
|
170
|
+
* loud, never silently passing. The #4130 property tests pin both directions.
|
|
76
171
|
*/
|
|
77
172
|
const boldLeadInBulletRe = /^\s*-\s+\*\*[A-Z]+[0-9]*-[A-Za-z0-9]/m;
|
|
78
173
|
/**
|
|
79
|
-
* #3939: a decision bullet's DECLARATION line — the `- **D-NN … **` bold
|
|
80
|
-
* the three grammars above anchor on — may wrap across a line break.
|
|
81
|
-
* line breaks inside a bullet are markdown-insignificant, and GSD's own
|
|
174
|
+
* #3939: a decision bullet's DECLARATION line — the `- **D[phase]-NN … **` bold
|
|
175
|
+
* lead-in the three grammars above anchor on — may wrap across a line break.
|
|
176
|
+
* Physical line breaks inside a bullet are markdown-insignificant, and GSD's own
|
|
82
177
|
* discuss-phase writer emits the wrapped shape whenever a decision title runs
|
|
83
178
|
* past the wrap column. All three grammars require the closing `**` in the same
|
|
84
|
-
* string as the `- **D
|
|
179
|
+
* string as the `- **D…-` anchor, so a wrapped declaration matched none of them
|
|
85
180
|
* and fell to the #1365 parse-miss guard, forcing `could-not-parse` (which
|
|
86
181
|
* hard-blocks `check.decision-coverage-plan`) on a well-formed CONTEXT.md.
|
|
87
182
|
*
|
|
88
183
|
* The repair is confined to how the LOGICAL bullet is assembled — the grammars
|
|
89
184
|
* themselves are untouched, so every single-line form parses exactly as before.
|
|
185
|
+
* #4130: the anchor uses `ID_ATTEMPT_SOURCE` (digit-run phase prefixes join
|
|
186
|
+
* like bare ones; recognising more start shapes only reassembles the logical
|
|
187
|
+
* bullet, which then parses or fails loud as itself).
|
|
90
188
|
*/
|
|
91
|
-
const decisionBulletStartRe =
|
|
189
|
+
const decisionBulletStartRe = new RegExp(`^\\s*-\\s+\\*\\*${ID_ATTEMPT_SOURCE}`);
|
|
92
190
|
/**
|
|
93
191
|
* A line that opens a new BLOCK-LEVEL construct, and therefore terminates the
|
|
94
192
|
* bullet above it: a list marker of any family (`-`, `*`, `+`, `1.`, `1)`), an
|
|
@@ -136,7 +234,8 @@ const blockConstructRe = /^(?:[-*+]\s|\d+[.)]\s|#{1,6}\s|>\s|\|)/;
|
|
|
136
234
|
* to watch for a splice.
|
|
137
235
|
*
|
|
138
236
|
* The id character class is deliberately looser than the grammars' (it admits
|
|
139
|
-
* an empty id, so a bare `- **D-` still counts as unsettled
|
|
237
|
+
* an empty id, so a bare `- **D-` still counts as unsettled, and — #4130 — a
|
|
238
|
+
* digit-run phase prefix between the `D` and the first hyphen). This regex only
|
|
140
239
|
* answers "may an id-adjacent bracket still open here?", where recognising MORE
|
|
141
240
|
* shapes is the conservative direction: an over-broad match can only make a
|
|
142
241
|
* malformed bullet fail loud, while a missed one silently re-classifies.
|
|
@@ -145,7 +244,7 @@ const blockConstructRe = /^(?:[-*+]\s|\d+[.)]\s|#{1,6}\s|>\s|\|)/;
|
|
|
145
244
|
* into `tags` (and therefore into `trackable`). A `[` further along the title is
|
|
146
245
|
* ordinary text and does not restrict the join.
|
|
147
246
|
*/
|
|
148
|
-
const tagRegionRe =
|
|
247
|
+
const tagRegionRe = new RegExp(`^\\s*-\\s+\\*\\*D[0-9]*-[A-Za-z0-9_-]*\\s*(?:\\[([^\\]]*))?$`);
|
|
149
248
|
/**
|
|
150
249
|
* #3939 (review): would folding `next` onto a lead-in whose `[tags]` bracket is
|
|
151
250
|
* still open splice the inserted space INTO a tag token?
|
|
@@ -347,11 +446,11 @@ function parseDecisionLines(block) {
|
|
|
347
446
|
current.text += ' ' + trimmed;
|
|
348
447
|
continue;
|
|
349
448
|
}
|
|
350
|
-
// Colon form: `- **D-NN[ [tags]]:** text`
|
|
449
|
+
// Colon form: `- **D[phase]-NN[ [tags]]:** text`
|
|
351
450
|
const colonMatch = line.match(bulletColonRe);
|
|
352
451
|
if (colonMatch) {
|
|
353
452
|
flush();
|
|
354
|
-
const id =
|
|
453
|
+
const id = colonMatch[1];
|
|
355
454
|
const tags = colonMatch[2]
|
|
356
455
|
? colonMatch[2].split(',').map((t) => t.trim().toLowerCase()).filter(Boolean)
|
|
357
456
|
: [];
|
|
@@ -360,11 +459,11 @@ function parseDecisionLines(block) {
|
|
|
360
459
|
openIndent = (0, token_scanner_cjs_1.indentWidth)(line);
|
|
361
460
|
continue;
|
|
362
461
|
}
|
|
363
|
-
// Em-dash form: `- **D-NN[ [tags]] — title** body`
|
|
462
|
+
// Em-dash form: `- **D[phase]-NN[ [tags]] — title** body`
|
|
364
463
|
const emDashMatch = line.match(bulletEmDashRe);
|
|
365
464
|
if (emDashMatch) {
|
|
366
465
|
flush();
|
|
367
|
-
const id =
|
|
466
|
+
const id = emDashMatch[1];
|
|
368
467
|
const tags = emDashMatch[2]
|
|
369
468
|
? emDashMatch[2].split(',').map((t) => t.trim().toLowerCase()).filter(Boolean)
|
|
370
469
|
: [];
|
|
@@ -376,14 +475,14 @@ function parseDecisionLines(block) {
|
|
|
376
475
|
openIndent = (0, token_scanner_cjs_1.indentWidth)(line);
|
|
377
476
|
continue;
|
|
378
477
|
}
|
|
379
|
-
// Titled-colon form: `- **D-NN[ [tags]]: Title.** body` (#1639). Checked LAST — it is
|
|
478
|
+
// Titled-colon form: `- **D[phase]-NN[ [tags]]: Title.** body` (#1639). Checked LAST — it is
|
|
380
479
|
// a strict superset of bulletColonRe, so it only catches bullets the colon-immediate
|
|
381
480
|
// and em-dash forms missed (minimal blast radius). id + [tags] trackability honored;
|
|
382
481
|
// the body after the closing bold run is reported as text.
|
|
383
482
|
const titledColonMatch = line.match(bulletTitledColonRe);
|
|
384
483
|
if (titledColonMatch) {
|
|
385
484
|
flush();
|
|
386
|
-
const id =
|
|
485
|
+
const id = titledColonMatch[1];
|
|
387
486
|
const tags = titledColonMatch[2]
|
|
388
487
|
? titledColonMatch[2].split(',').map((t) => t.trim().toLowerCase()).filter(Boolean)
|
|
389
488
|
: [];
|
|
@@ -392,10 +491,14 @@ function parseDecisionLines(block) {
|
|
|
392
491
|
openIndent = (0, token_scanner_cjs_1.indentWidth)(line);
|
|
393
492
|
continue;
|
|
394
493
|
}
|
|
395
|
-
// Parse-miss guard (FIX B + #1343): a line
|
|
396
|
-
//
|
|
494
|
+
// Parse-miss guard (FIX B + #1343, grammar widened #4130): a line whose bold
|
|
495
|
+
// lead-in ATTEMPTS the ID grammar but failed all three patterns — flush,
|
|
496
|
+
// warn, and record the miss. `ID_ATTEMPT_SOURCE` accepts the bare `D-` form
|
|
497
|
+
// (as before) plus a digit-initial prefix run, so a typo'd phase prefix
|
|
498
|
+
// (`D4x-01`) fails loud instead of silently vanishing, while a letter-initial
|
|
499
|
+
// run (`Deferred-until`) stays prose and stays invisible.
|
|
397
500
|
// parseMisses > 0 forces could-not-parse even when other decisions parsed.
|
|
398
|
-
if (
|
|
501
|
+
if (parseMissGuardRe.test(line)) {
|
|
399
502
|
flush();
|
|
400
503
|
parseMisses += 1;
|
|
401
504
|
console.warn(`parseDecisions: ignored unparseable decision bullet: ${trimmed}`);
|
|
@@ -449,10 +552,10 @@ function extractDecisions(content) {
|
|
|
449
552
|
// FIX A: Block present but 0 extracted and no parse-misses.
|
|
450
553
|
// Only report could-not-parse when there is genuine evidence of real decisions
|
|
451
554
|
// that failed to parse: a bold-lead-in bullet (`- **…**`, any ID grammar — #2347),
|
|
452
|
-
// a
|
|
555
|
+
// a bare `D[phase]-<alnum>` token (#4130) in the block text, or an unterminated fence. An empty scaffold
|
|
453
556
|
// (<decisions></decisions>) or an all-prose block has no such evidence — treat
|
|
454
557
|
// as none-present so the gate passes cleanly.
|
|
455
|
-
const hasDecisionTokenInBlock =
|
|
558
|
+
const hasDecisionTokenInBlock = decisionTokenRe.test(combined);
|
|
456
559
|
const hasBoldLeadInBullet = boldLeadInBulletRe.test(combined);
|
|
457
560
|
if (hasDecisionTokenInBlock || hasBoldLeadInBullet || unterminatedFence) {
|
|
458
561
|
return { decisions: [], outcome: 'could-not-parse' };
|
|
@@ -475,10 +578,10 @@ function extractDecisions(content) {
|
|
|
475
578
|
}
|
|
476
579
|
// FIX A: Heading found but 0 extracted and no parse-misses.
|
|
477
580
|
// Report could-not-parse when the section body holds a decision-entry-shaped
|
|
478
|
-
// bold-lead-in bullet (`- **…**`, any ID grammar — #2347) or a D
|
|
581
|
+
// bold-lead-in bullet (`- **…**`, any ID grammar — #2347) or a `D[phase]-<alnum>` token (#4130). A
|
|
479
582
|
// heading with only prose, sub-headings, or all-discretion content (no such
|
|
480
583
|
// evidence) is a legitimate empty/discretion section → none-present.
|
|
481
|
-
const hasDecisionTokenInSection =
|
|
584
|
+
const hasDecisionTokenInSection = decisionTokenRe.test(section.body);
|
|
482
585
|
const hasBoldLeadInBulletInSection = boldLeadInBulletRe.test(section.body);
|
|
483
586
|
if (hasDecisionTokenInSection || hasBoldLeadInBulletInSection) {
|
|
484
587
|
return { decisions: [], outcome: 'could-not-parse' };
|
|
@@ -487,8 +590,8 @@ function extractDecisions(content) {
|
|
|
487
590
|
}
|
|
488
591
|
// ── Path 3: no blocks, no heading ────────────────────────────────────────────
|
|
489
592
|
// Apply shape heuristics to distinguish none-present from could-not-parse.
|
|
490
|
-
// We re-use the already-computed unterminatedFence and check for
|
|
491
|
-
const hasDecisionToken =
|
|
593
|
+
// We re-use the already-computed unterminatedFence and check for decision tokens.
|
|
594
|
+
const hasDecisionToken = decisionTokenRe.test(stripped);
|
|
492
595
|
if (unterminatedFence || hasDecisionToken) {
|
|
493
596
|
return { decisions: [], outcome: 'could-not-parse' };
|
|
494
597
|
}
|