@opengsd/gsd-core 1.10.0 → 1.11.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-debug-session-manager.md +11 -0
- package/agents/gsd-doc-synthesizer.md +2 -4
- package/agents/gsd-executor.md +5 -5
- package/agents/gsd-mempalace-curator.md +5 -2
- package/agents/gsd-phase-researcher.md +20 -1
- package/agents/gsd-plan-checker.md +37 -0
- package/agents/gsd-planner.md +44 -46
- package/agents/gsd-user-profiler.md +3 -0
- package/agents/gsd-verifier.md +12 -3
- package/bin/install.js +841 -971
- package/bin/lib/ui-safety-gate.cjs +2 -0
- package/commands/gsd/code-review.md +1 -1
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/map-codebase.md +1 -1
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/mempalace-recall.md +1 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/quick.md +1 -1
- package/commands/gsd/review-backlog.md +2 -1
- package/commands/gsd/verify-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +469 -88
- package/gsd-core/bin/lib/active-workstream-store.cjs +138 -22
- package/gsd-core/bin/lib/agent-install-check.cjs +230 -32
- package/gsd-core/bin/lib/api-coverage.cjs +3 -5
- package/gsd-core/bin/lib/artifacts.cjs +3 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
- package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
- package/gsd-core/bin/lib/audit.cjs +876 -240
- package/gsd-core/bin/lib/broken-windows.cjs +1 -1
- package/gsd-core/bin/lib/capability-consent.cjs +149 -15
- package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
- package/gsd-core/bin/lib/capability-registry.cjs +575 -101
- package/gsd-core/bin/lib/capability-source.cjs +92 -0
- package/gsd-core/bin/lib/capability-trust.cjs +444 -25
- package/gsd-core/bin/lib/capability-validator.cjs +495 -22
- package/gsd-core/bin/lib/capability-writer.cjs +3 -2
- package/gsd-core/bin/lib/check-command-router.cjs +71 -37
- package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
- package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
- package/gsd-core/bin/lib/command-aliases.cjs +22 -0
- package/gsd-core/bin/lib/command-roster.cjs +44 -1
- package/gsd-core/bin/lib/commands.cjs +651 -86
- package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
- package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
- package/gsd-core/bin/lib/config-loader.cjs +75 -0
- package/gsd-core/bin/lib/config.cjs +10 -1
- package/gsd-core/bin/lib/core-utils.cjs +127 -29
- package/gsd-core/bin/lib/decisions.cjs +23 -0
- package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
- package/gsd-core/bin/lib/frontmatter.cjs +155 -20
- package/gsd-core/bin/lib/gap-checker.cjs +68 -7
- package/gsd-core/bin/lib/git-base-branch.cjs +102 -0
- package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
- package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
- package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
- package/gsd-core/bin/lib/init.cjs +321 -129
- package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
- package/gsd-core/bin/lib/install-engine.cjs +745 -258
- package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
- package/gsd-core/bin/lib/install-profiles.cjs +134 -57
- package/gsd-core/bin/lib/install-scope.cjs +270 -0
- package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
- package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +138 -31
- package/gsd-core/bin/lib/io.cjs +10 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
- package/gsd-core/bin/lib/markdown-table.cjs +133 -20
- package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
- package/gsd-core/bin/lib/milestone.cjs +754 -70
- package/gsd-core/bin/lib/model-catalog.cjs +59 -1
- package/gsd-core/bin/lib/model-resolver.cjs +183 -40
- package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
- package/gsd-core/bin/lib/pattern.cjs +122 -0
- package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
- package/gsd-core/bin/lib/phase-id.cjs +444 -36
- package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
- package/gsd-core/bin/lib/phase-locator.cjs +125 -18
- package/gsd-core/bin/lib/phase.cjs +646 -143
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
- package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
- package/gsd-core/bin/lib/plan-scan.cjs +86 -2
- package/gsd-core/bin/lib/planning-scope.cjs +31 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +56 -6
- package/gsd-core/bin/lib/probe-core.cjs +1 -1
- package/gsd-core/bin/lib/profile-output.cjs +1 -1
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +13 -4
- package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
- package/gsd-core/bin/lib/roadmap-command-router.cjs +34 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +943 -184
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
- package/gsd-core/bin/lib/roadmap.cjs +385 -94
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +608 -46
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +426 -55
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
- package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +115 -3
- package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
- package/gsd-core/bin/lib/security.cjs +104 -5
- package/gsd-core/bin/lib/shell-command-projection.cjs +275 -3
- package/gsd-core/bin/lib/smart-entry.cjs +142 -22
- package/gsd-core/bin/lib/state-command-router.cjs +5 -1
- package/gsd-core/bin/lib/state-document.cjs +152 -8
- package/gsd-core/bin/lib/state-transition.cjs +371 -117
- package/gsd-core/bin/lib/state.cjs +1794 -357
- package/gsd-core/bin/lib/surface.cjs +23 -9
- package/gsd-core/bin/lib/text-lines.cjs +80 -0
- package/gsd-core/bin/lib/token-scanner.cjs +76 -0
- package/gsd-core/bin/lib/uat-predicate.cjs +9 -3
- package/gsd-core/bin/lib/uat.cjs +399 -56
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
- package/gsd-core/bin/lib/unusable-input.cjs +24 -0
- package/gsd-core/bin/lib/update-context.cjs +8 -2
- package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
- package/gsd-core/bin/lib/validate.cjs +20 -6
- package/gsd-core/bin/lib/vendor/README.md +37 -0
- package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
- package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +258 -8
- package/gsd-core/bin/lib/verify.cjs +368 -888
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
- package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
- package/gsd-core/bin/lib/workstream.cjs +2 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +176 -9
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +7 -1
- package/gsd-core/references/agent-contracts.md +43 -26
- package/gsd-core/references/checkpoints.md +2 -2
- package/gsd-core/references/context-budget.md +1 -1
- package/gsd-core/references/dispatch-isolation-gate.md +138 -0
- package/gsd-core/references/doc-conflict-engine.md +1 -1
- package/gsd-core/references/execute-mvp-tdd.md +3 -3
- package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
- package/gsd-core/references/execute-phase-context-guard.md +1 -1
- package/gsd-core/references/execute-phase-response-language.md +1 -1
- package/gsd-core/references/execute-phase-wave-guard.md +6 -2
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-planning-commit.md +2 -1
- package/gsd-core/references/loop-hook-dispatch.md +39 -2
- package/gsd-core/references/model-profiles.md +12 -4
- package/gsd-core/references/mvp-concepts.md +9 -9
- package/gsd-core/references/planner-guidance.md +3 -9
- package/gsd-core/references/planner-preconditions.md +1 -1
- package/gsd-core/references/planner-reviews.md +1 -1
- package/gsd-core/references/planning-config.md +8 -6
- package/gsd-core/references/revision-loop.md +1 -1
- package/gsd-core/references/specless-probe-fallback.md +1 -1
- package/gsd-core/references/universal-anti-patterns.md +3 -3
- package/gsd-core/references/verifier-phase-gates.md +192 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +22 -6
- package/gsd-core/templates/discussion-log.md +1 -1
- package/gsd-core/templates/phase-prompt.md +2 -4
- package/gsd-core/templates/state.md +4 -4
- package/gsd-core/templates/verification-report.md +9 -1
- package/gsd-core/workflows/ai-integration-phase.md +9 -11
- package/gsd-core/workflows/autonomous.md +1 -1
- package/gsd-core/workflows/cleanup.md +62 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +13 -3
- package/gsd-core/workflows/code-review-fix.md +37 -10
- package/gsd-core/workflows/code-review.md +38 -12
- package/gsd-core/workflows/complete-milestone.md +141 -18
- package/gsd-core/workflows/debug.md +7 -5
- package/gsd-core/workflows/diagnose-issues.md +35 -9
- package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
- package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +2 -1
- package/gsd-core/workflows/edit-phase.md +26 -1
- package/gsd-core/workflows/eval-review.md +3 -5
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +31 -6
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +2 -0
- package/gsd-core/workflows/execute-phase.md +38 -50
- package/gsd-core/workflows/execute-plan.md +36 -4
- package/gsd-core/workflows/explore.md +131 -4
- package/gsd-core/workflows/fast.md +10 -2
- package/gsd-core/workflows/health.md +73 -4
- package/gsd-core/workflows/import.md +4 -4
- package/gsd-core/workflows/ingest-docs.md +5 -5
- package/gsd-core/workflows/mvp-phase.md +6 -3
- package/gsd-core/workflows/new-milestone.md +14 -9
- package/gsd-core/workflows/new-project.md +14 -14
- package/gsd-core/workflows/next.md +12 -0
- package/gsd-core/workflows/plan-phase.md +41 -17
- package/gsd-core/workflows/plan-review-convergence.md +50 -2
- package/gsd-core/workflows/progress.md +34 -6
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -4
- package/gsd-core/workflows/quick/steps/quick-verification.md +27 -6
- package/gsd-core/workflows/quick/steps/research-phase.md +2 -2
- package/gsd-core/workflows/quick.md +35 -15
- package/gsd-core/workflows/review.md +26 -5
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/session-report.md +2 -1
- package/gsd-core/workflows/settings.md +66 -2
- package/gsd-core/workflows/ship.md +104 -44
- package/gsd-core/workflows/spec-phase.md +30 -12
- package/gsd-core/workflows/sync-skills.md +63 -8
- package/gsd-core/workflows/transition.md +46 -11
- package/gsd-core/workflows/ui-phase.md +5 -5
- package/gsd-core/workflows/ui-review.md +2 -2
- package/gsd-core/workflows/update.md +1 -1
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +9 -7
- package/hooks/dist/gsd-agent-isolation-guard.js +103 -14
- package/hooks/dist/gsd-check-update-worker.js +56 -13
- package/hooks/dist/gsd-check-update.js +19 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
- package/hooks/dist/gsd-cursor-subagent-start.js +77 -2
- package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/dist/gsd-prompt-guard.js +21 -20
- package/hooks/dist/gsd-read-injection-scanner.js +38 -24
- package/hooks/dist/gsd-statusline.js +18 -0
- package/hooks/dist/gsd-update-banner.js +22 -1
- package/hooks/dist/gsd-workflow-guard.js +134 -36
- package/hooks/dist/lib/git-cmd.js +92 -59
- package/hooks/dist/lib/injection-patterns.js +45 -0
- package/hooks/dist/lib/isolation-deny-reason.js +39 -0
- package/hooks/dist/lib/isolation-sentinel.js +9 -0
- package/hooks/gsd-agent-isolation-guard.js +103 -14
- package/hooks/gsd-check-update-worker.js +56 -13
- package/hooks/gsd-check-update.js +19 -1
- package/hooks/gsd-cursor-pre-tool.js +0 -3
- package/hooks/gsd-cursor-subagent-start.js +77 -2
- package/hooks/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/gsd-prompt-guard.js +21 -20
- package/hooks/gsd-read-injection-scanner.js +38 -24
- package/hooks/gsd-statusline.js +18 -0
- package/hooks/gsd-update-banner.js +22 -1
- package/hooks/gsd-workflow-guard.js +134 -36
- package/hooks/lib/git-cmd.js +92 -59
- package/hooks/lib/injection-patterns.js +45 -0
- package/hooks/lib/isolation-deny-reason.js +39 -0
- package/hooks/lib/isolation-sentinel.js +9 -0
- package/package.json +21 -9
- package/pi/gsd.cjs +19 -5
- package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
- package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
- package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
- package/scripts/changeset/lint.cjs +60 -5
- package/scripts/check-alias-drift.cjs +7 -43
- package/scripts/check-contract-drift.cjs +297 -0
- package/scripts/ci-test-scope.cjs +19 -2
- package/scripts/command-contract-helpers.cjs +903 -1
- package/scripts/gen-adr-index.cjs +728 -38
- package/scripts/gen-capability-registry.cjs +3 -15
- package/scripts/gen-context-index.cjs +2 -11
- package/scripts/gen-health-docs.cjs +390 -0
- package/scripts/gen-inventory-manifest.cjs +50 -4
- package/scripts/gen-loop-host-contract.cjs +4 -24
- package/scripts/gen-registry.cjs +3 -14
- package/scripts/lib/alias-drift-families.cjs +46 -0
- package/scripts/lib/drift-scan.cjs +278 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
- package/scripts/lint-canary-version-leak.cjs +73 -0
- package/scripts/lint-command-contract.cjs +96 -13
- package/scripts/lint-completion-predicate-drift.cjs +933 -0
- package/scripts/lint-completion-ratio-drift.cjs +214 -0
- package/scripts/lint-default-flip-documentation.cjs +193 -0
- package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
- package/scripts/lint-eslint-glob-coverage.cjs +340 -0
- package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
- package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
- package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
- package/scripts/lint-milestone-window-drift.cjs +468 -0
- package/scripts/lint-phase-enumeration-drift.cjs +479 -0
- package/scripts/lint-plan-count-drift.cjs +318 -0
- package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
- package/scripts/lint-planning-prompt-drift.cjs +434 -0
- package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
- package/scripts/lint-regression-test-names.cjs +15 -13
- package/scripts/lint-removed-but-needed.cjs +320 -0
- package/scripts/lint-state-field-drift.cjs +805 -0
- package/scripts/lint-state-write-path-drift.cjs +1045 -0
- package/scripts/lint-test-file-count.allowlist.json +21 -10
- package/scripts/lint-unreachable-guard-drift.cjs +843 -0
- package/scripts/lint-vendored-deps.cjs +124 -0
- package/scripts/pr-changed-files.cjs +63 -0
- package/scripts/pr-template-policy.cjs +14 -4
- package/scripts/prompt-injection-scan.sh +25 -0
- package/scripts/require-issue-link-policy.cjs +192 -0
- package/scripts/state-write-path-drift-baseline.json +19 -0
- package/scripts/sync-runtime-launcher.cjs +2 -4
- package/skills/gsd-autonomous/SKILL.md +0 -1
- package/skills/gsd-code-review/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -2
- package/skills/gsd-map-codebase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-mempalace-recall/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-next/SKILL.md +0 -1
- package/skills/gsd-plan-phase/SKILL.md +0 -1
- package/skills/gsd-progress/SKILL.md +0 -1
- package/skills/gsd-quick/SKILL.md +1 -1
- package/skills/gsd-review-backlog/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +0 -1
- package/skills/gsd-verify-work/SKILL.md +1 -1
- package/vscode/package.json +1 -1
- package/gsd-core/workflows/discovery-phase.md +0 -298
- package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
- package/gsd-core/workflows/verify-phase.md +0 -574
- package/scripts/affected-tests-lib.cjs +0 -554
- package/scripts/lint-allow-test-rule-refs.cjs +0 -162
- package/scripts/run-affected-tests.cjs +0 -7
- package/scripts/run-tests.cjs +0 -1051
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Refactor-trigger command router — CLI subcommand dispatcher for
|
|
7
|
+
* `gsd-tools refactor` (issue #1953).
|
|
8
|
+
*
|
|
9
|
+
* Follows `src/intel-command-router.cts` exactly: `routeHubCommandFamily`,
|
|
10
|
+
* `makeInvalidArgs` for validation failures, `output(value, raw)` for
|
|
11
|
+
* success, `_`-prefixed injection seams, lazy `require` of the heavy leaf
|
|
12
|
+
* module (`complexity-trigger.cjs`) and the adjacent modules
|
|
13
|
+
* (`git-base-branch.cjs`, `broken-windows.cjs`) inside the route function.
|
|
14
|
+
*
|
|
15
|
+
* Authoritative surfaces:
|
|
16
|
+
* .gsd/phase/feat-1953-complexity-triggered-refactor/42-router-contract.md
|
|
17
|
+
* .gsd/phase/feat-1953-complexity-triggered-refactor/41-api-contract.md
|
|
18
|
+
*
|
|
19
|
+
* `src/complexity-trigger.cts` stays a pure leaf (analyzer, evaluator,
|
|
20
|
+
* baseline persistence) — this module owns capability-activation gating,
|
|
21
|
+
* git invocation (via the `src/git-base-branch.cts` adapters), config
|
|
22
|
+
* reads, CLI arg parsing, phase-directory resolution, and the optional
|
|
23
|
+
* broken-windows ledger integration. It never duplicates the leaf's logic.
|
|
24
|
+
*
|
|
25
|
+
* Arg indexing:
|
|
26
|
+
* args[0] = 'refactor' (family — matched by dispatchCapabilityCommand)
|
|
27
|
+
* args[1] = subcommand (accept | decline | evaluate | status)
|
|
28
|
+
* args[2..] = flags (--phase, --since, --reason, --raw)
|
|
29
|
+
*
|
|
30
|
+
* Seams: `_complexity` (complexity-trigger.cjs), `_git` (git-base-branch.cjs),
|
|
31
|
+
* `_windows` (broken-windows.cjs — OPTIONAL; absent or a throwing `require`
|
|
32
|
+
* is the documented degrade path, never an error), `_core` (output capture).
|
|
33
|
+
* Production callers omit all four.
|
|
34
|
+
*/
|
|
35
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
36
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
37
|
+
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
39
|
+
const io = require("./io.cjs");
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
41
|
+
const commandRoutingHub = require("./command-routing-hub.cjs");
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
43
|
+
const cjsCommandRouterAdapter = require("./cjs-command-router-adapter.cjs");
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
45
|
+
const capabilityStateMod = require("./capability-state.cjs");
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
47
|
+
const capabilityActivationMod = require("./capability-activation.cjs");
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
49
|
+
const configLoaderMod = require("./config-loader.cjs");
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
51
|
+
const phaseLocatorMod = require("./phase-locator.cjs");
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
53
|
+
const planningWorkspaceMod = require("./planning-workspace.cjs");
|
|
54
|
+
const { output } = io;
|
|
55
|
+
const { makeInvalidArgs } = commandRoutingHub;
|
|
56
|
+
const { routeHubCommandFamily } = cjsCommandRouterAdapter;
|
|
57
|
+
const { isCapabilityActive } = capabilityStateMod;
|
|
58
|
+
const { resolveConfigKey } = capabilityActivationMod;
|
|
59
|
+
const { loadConfig } = configLoaderMod;
|
|
60
|
+
const { findPhaseInternal, listMilestonePhaseDirs } = phaseLocatorMod;
|
|
61
|
+
const { planningDir } = planningWorkspaceMod;
|
|
62
|
+
const CAPABILITY_ID = 'refactor-trigger';
|
|
63
|
+
// Default CoreModule implementation. `_core` seam overrides this entirely
|
|
64
|
+
// for test injection (captures output calls without writing to real stdout).
|
|
65
|
+
const _defaultCore = { output };
|
|
66
|
+
// ─── Disabled response ──────────────────────────────────────────────────────
|
|
67
|
+
function disabledResponse() {
|
|
68
|
+
return {
|
|
69
|
+
disabled: true,
|
|
70
|
+
message: 'refactor-trigger is not enabled. Enable with: gsd config-set refactor.trigger_enabled true',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// ─── Arg parsing ────────────────────────────────────────────────────────────
|
|
74
|
+
// Positive integer, optionally zero-padded, optionally with dotted decimal
|
|
75
|
+
// sub-phase segments (1, 01, 12, 3.1). Rejects 0, negative, letters, and
|
|
76
|
+
// whitespace — matches the router contract's "positive integer or decimal
|
|
77
|
+
// phase id" rule.
|
|
78
|
+
const PHASE_VALUE_RE = /^0*[1-9]\d*(?:\.\d+)*$/;
|
|
79
|
+
/**
|
|
80
|
+
* Reads a `--flag value` or `--flag=value` occurrence from `args`. Repeated
|
|
81
|
+
* occurrences are reported via `duplicated` (the last one wins in `value`,
|
|
82
|
+
* but callers should treat `duplicated` as invalid input). A token
|
|
83
|
+
* immediately following `--flag` that itself starts with `--` (or the flag
|
|
84
|
+
* being the last token) is NOT consumed as a value — `flagShaped` is set and
|
|
85
|
+
* `value` stays `''`, so `--phase --raw` never silently swallows `--raw`.
|
|
86
|
+
*/
|
|
87
|
+
function readFlag(args, flag) {
|
|
88
|
+
let present = false;
|
|
89
|
+
let value = '';
|
|
90
|
+
let count = 0;
|
|
91
|
+
let flagShaped = false;
|
|
92
|
+
const eqPrefix = `${flag}=`;
|
|
93
|
+
for (let i = 0; i < args.length; i++) {
|
|
94
|
+
const a = args[i];
|
|
95
|
+
if (a === flag) {
|
|
96
|
+
count++;
|
|
97
|
+
present = true;
|
|
98
|
+
const next = args[i + 1];
|
|
99
|
+
if (next === undefined) {
|
|
100
|
+
value = '';
|
|
101
|
+
}
|
|
102
|
+
else if (next.startsWith('--')) {
|
|
103
|
+
value = '';
|
|
104
|
+
flagShaped = true;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
value = next;
|
|
108
|
+
flagShaped = false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else if (a.startsWith(eqPrefix)) {
|
|
112
|
+
count++;
|
|
113
|
+
present = true;
|
|
114
|
+
value = a.slice(eqPrefix.length);
|
|
115
|
+
flagShaped = false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { present, value, duplicated: count > 1, flagShaped };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Validate `--phase`: required, and a positive integer or decimal phase id.
|
|
122
|
+
* Empty, whitespace-only, duplicated, `--phase=`, `--phase==N`, or a
|
|
123
|
+
* flag-shaped value all fail. Missing entirely -> REFACTOR_USAGE (no value
|
|
124
|
+
* was ever offered); present but invalid -> REFACTOR_INVALID_PHASE (a value
|
|
125
|
+
* was given but rejected). Never throws.
|
|
126
|
+
*/
|
|
127
|
+
function requirePhaseArg(args, complexity, usage) {
|
|
128
|
+
const flag = readFlag(args, '--phase');
|
|
129
|
+
if (!flag.present) {
|
|
130
|
+
return { ok: false, result: makeInvalidArgs('phase', usage, complexity.REASON.REFACTOR_USAGE) };
|
|
131
|
+
}
|
|
132
|
+
const trimmed = flag.value.trim();
|
|
133
|
+
if (flag.duplicated || flag.flagShaped || trimmed === '' || !PHASE_VALUE_RE.test(trimmed)) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
result: makeInvalidArgs('phase', `Invalid --phase value: ${JSON.stringify(flag.value)}. ${usage}`, complexity.REASON.REFACTOR_INVALID_PHASE),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return { ok: true, phase: trimmed };
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Resolve the on-disk phase directory for a validated `--phase` value. A
|
|
143
|
+
* phase that cannot be located on disk (never planned, wrong number, an
|
|
144
|
+
* archived milestone this call isn't scoped to) returns `null` — this is a
|
|
145
|
+
* distinct fact from a git failure (git is fine; the phase just doesn't
|
|
146
|
+
* exist), so callers report REFACTOR_INVALID_PHASE, never
|
|
147
|
+
* REFACTOR_GIT_UNAVAILABLE, for this case.
|
|
148
|
+
*/
|
|
149
|
+
function resolvePhaseDirForArg(cwd, phaseArg) {
|
|
150
|
+
const search = findPhaseInternal(cwd, phaseArg);
|
|
151
|
+
if (!search || search.found !== true)
|
|
152
|
+
return null;
|
|
153
|
+
return { phaseDir: search.directory, padded: search.phase_number };
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Resolve `relFile` (a repo-relative path, typically from `git diff
|
|
157
|
+
* --name-only`) against `cwd` and refuse a path that escapes the project
|
|
158
|
+
* root. Returns the absolute path, or `null` when it escapes.
|
|
159
|
+
*
|
|
160
|
+
* Two layers: the string-level `startsWith` check is a cheap first gate but
|
|
161
|
+
* confines only the SYMLINK's own path, not its target — a symlink
|
|
162
|
+
* committed in the repo (e.g. `src/evil.cts -> /etc/passwd`) has an
|
|
163
|
+
* in-tree path that passes the string check, and `fs.readFileSync` on it
|
|
164
|
+
* would follow the link and read outside the root. `lstatSync` (which does
|
|
165
|
+
* NOT follow symlinks, unlike `statSync`) is the second gate: anything that
|
|
166
|
+
* is not a regular file — a symlink most of all — is refused outright.
|
|
167
|
+
* Refusing rather than resolving-and-re-confining is deliberate: it is
|
|
168
|
+
* simpler and strictly stricter (a symlink whose target legitimately lives
|
|
169
|
+
* inside the root is still refused, which is an acceptable false positive
|
|
170
|
+
* for this analyzer). An `lstatSync` failure (ENOENT on a broken symlink,
|
|
171
|
+
* EACCES, a race) is treated the same as "not a regular file" — never
|
|
172
|
+
* propagated — so callers can keep using their existing null-means-skip
|
|
173
|
+
* convention (`REASON.REFACTOR_FILE_UNREADABLE`) uniformly.
|
|
174
|
+
*/
|
|
175
|
+
function resolveConfinedPath(cwd, relFile) {
|
|
176
|
+
const root = node_path_1.default.resolve(cwd);
|
|
177
|
+
const resolved = node_path_1.default.resolve(root, relFile);
|
|
178
|
+
if (resolved !== root && !resolved.startsWith(root + node_path_1.default.sep))
|
|
179
|
+
return null;
|
|
180
|
+
try {
|
|
181
|
+
if (!node_fs_1.default.lstatSync(resolved).isFile())
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
return resolved;
|
|
188
|
+
}
|
|
189
|
+
function artifactFileName(padded, complexity) {
|
|
190
|
+
return `${padded}${complexity.PROPOSAL_SUFFIX}`;
|
|
191
|
+
}
|
|
192
|
+
function artifactAbsPath(cwd, phaseDir, padded, complexity) {
|
|
193
|
+
return node_path_1.default.join(cwd, phaseDir, artifactFileName(padded, complexity));
|
|
194
|
+
}
|
|
195
|
+
function candidateKey(file, name) {
|
|
196
|
+
return `${file}::${name}`;
|
|
197
|
+
}
|
|
198
|
+
// ─── Atomic write (proposal + ledger) ───────────────────────────────────────
|
|
199
|
+
/**
|
|
200
|
+
* Atomic publish: write a sibling `.tmp.<pid>` file, then rename over the
|
|
201
|
+
* target via `retryRenameSync` (transient-Windows-lock-tolerant — matches
|
|
202
|
+
* `local/require-fs-op-fallback`, ADR-1703 Phase 6). On any failure, best-
|
|
203
|
+
* effort unlinks the temp file and rethrows.
|
|
204
|
+
*/
|
|
205
|
+
function writeTextAtomic(filePath, content) {
|
|
206
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
|
|
207
|
+
const tmp = `${filePath}.${process.pid}.tmp`;
|
|
208
|
+
node_fs_1.default.writeFileSync(tmp, content, 'utf8');
|
|
209
|
+
try {
|
|
210
|
+
(0, shell_command_projection_cjs_1.retryRenameSync)(tmp, filePath);
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
try {
|
|
214
|
+
node_fs_1.default.unlinkSync(tmp);
|
|
215
|
+
}
|
|
216
|
+
catch { /* best-effort cleanup */ }
|
|
217
|
+
throw err;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// ─── Config reads ───────────────────────────────────────────────────────────
|
|
221
|
+
/**
|
|
222
|
+
* Read `refactor.complexity_threshold` / `refactor.complexity_jump_delta` /
|
|
223
|
+
* `refactor.trigger_strict` via the same four-level precedence walk
|
|
224
|
+
* (`resolveConfigKey`) the capability-activation gate uses: loadConfig
|
|
225
|
+
* result -> workstream config.json -> root config.json -> registry
|
|
226
|
+
* `configSchema` default. Reading the frozen first-party registry directly
|
|
227
|
+
* is deliberate (not `capability-loader.cjs`'s `loadRegistry`): with
|
|
228
|
+
* `includeInstalled` omitted, `loadRegistry()` returns the exact same frozen
|
|
229
|
+
* object — refactor-trigger is first-party, so the overlay/consent machinery
|
|
230
|
+
* has nothing to add here and this avoids the extra indirection.
|
|
231
|
+
*/
|
|
232
|
+
function readEvalConfig(cwd) {
|
|
233
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
234
|
+
const registry = require('./capability-registry.cjs');
|
|
235
|
+
const config = loadConfig(cwd);
|
|
236
|
+
const threshold = resolveConfigKey('refactor.complexity_threshold', { config, cwd, registry }).value;
|
|
237
|
+
const jumpDelta = resolveConfigKey('refactor.complexity_jump_delta', { config, cwd, registry }).value;
|
|
238
|
+
const strict = resolveConfigKey('refactor.trigger_strict', { config, cwd, registry }).value;
|
|
239
|
+
// Read via the SAME four-level precedence walk as the `refactor.*` keys
|
|
240
|
+
// above — reusing `resolveConfigKey`/`loadConfig`/the frozen registry
|
|
241
|
+
// rather than a second config reader. A missing key resolves to the
|
|
242
|
+
// registry default (false), matching "treat a missing key as falsy".
|
|
243
|
+
const windowsEnforce = resolveConfigKey('workflow.windows_enforce', { config, cwd, registry }).value;
|
|
244
|
+
return { threshold, jumpDelta, strict: Boolean(strict), windowsEnforce: Boolean(windowsEnforce) };
|
|
245
|
+
}
|
|
246
|
+
// ─── Strict-mode enforcement-gap warning (#1953) ────────────────────────────
|
|
247
|
+
const WINDOWS_ENFORCE_REMEDIATION = 'gsd config-set workflow.windows_enforce true';
|
|
248
|
+
/**
|
|
249
|
+
* `refactor.trigger_strict` only ever APPENDS a deviation window to the
|
|
250
|
+
* broken-windows ledger — a ship only actually stops when the separate
|
|
251
|
+
* `workflow.windows_enforce` toggle is also on. A user who enables only
|
|
252
|
+
* `refactor.trigger_strict` gets tracking with no enforcement and, absent
|
|
253
|
+
* this warning, no signal that this is the case. Fires strictly on TRIGGERED
|
|
254
|
+
* evaluates with strict mode on, mirroring the existing ledger-recording
|
|
255
|
+
* gate: either the broken-windows capability could not record the window at
|
|
256
|
+
* all (`ledgerRecorded === false` — the existing degrade path), or it did,
|
|
257
|
+
* but `workflow.windows_enforce` is falsy. Never fires with strict off.
|
|
258
|
+
*/
|
|
259
|
+
function strictNotEnforcingWarning(complexity, ledgerRecorded, windowsEnforce) {
|
|
260
|
+
if (!ledgerRecorded) {
|
|
261
|
+
return {
|
|
262
|
+
reason: complexity.REASON.REFACTOR_STRICT_NOT_ENFORCING,
|
|
263
|
+
message: 'refactor.trigger_strict is on, but the broken-windows capability is unavailable, so ship will not '
|
|
264
|
+
+ `actually be blocked. Install the broken-windows capability, then run: ${WINDOWS_ENFORCE_REMEDIATION}`,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
if (!windowsEnforce) {
|
|
268
|
+
return {
|
|
269
|
+
reason: complexity.REASON.REFACTOR_STRICT_NOT_ENFORCING,
|
|
270
|
+
message: 'refactor.trigger_strict is on, but workflow.windows_enforce is off, so ship will not actually be '
|
|
271
|
+
+ `blocked. Run: ${WINDOWS_ENFORCE_REMEDIATION}`,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
// ─── Broken-windows integration (strict mode; OPTIONAL) ─────────────────────
|
|
277
|
+
function windowsLedgerPath(cwd, windows) {
|
|
278
|
+
return node_path_1.default.join(cwd, '.planning', windows.LEDGER_FILE_NAME);
|
|
279
|
+
}
|
|
280
|
+
function readLedgerOrEmpty(cwd, windows) {
|
|
281
|
+
const ledgerPath = windowsLedgerPath(cwd, windows);
|
|
282
|
+
try {
|
|
283
|
+
const raw = node_fs_1.default.readFileSync(ledgerPath, 'utf8');
|
|
284
|
+
return windows.parseLedger(raw);
|
|
285
|
+
}
|
|
286
|
+
catch (e) {
|
|
287
|
+
const code = (e && typeof e === 'object' && 'code' in e) ? String(e.code) : '';
|
|
288
|
+
if (code === 'ENOENT')
|
|
289
|
+
return windows.emptyLedger(new Date().toISOString());
|
|
290
|
+
return null; // unreadable/malformed — degrade rather than throw
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Structural identity match (#1953 defect 2): a deviation window's identity
|
|
295
|
+
* is the typed `phase`/`file`/`line` triple `appendWindow` already persists
|
|
296
|
+
* — never the human-readable `description` prose. A reworded description or
|
|
297
|
+
* a user editing `WINDOWS.md` prose can never break dedup.
|
|
298
|
+
*/
|
|
299
|
+
function findOpenDeviationEntry(ledger, phase, file, line) {
|
|
300
|
+
return ledger.entries.find((e) => e.status === 'open' && e.kind === 'deviation' && e.phase === phase && e.file === file && e.line === line) ?? null;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Shared require-or-degrade + ledger-read boilerplate for
|
|
304
|
+
* `recordStrictWindow` / `resolveLedgerWindow` (#1953 defect 5a). `_windows`
|
|
305
|
+
* unavailable (module absent, or its `require` throws) or the ledger
|
|
306
|
+
* unreadable both degrade to a `{ ok: false, note }` result — never an
|
|
307
|
+
* error, never throws.
|
|
308
|
+
*/
|
|
309
|
+
function loadWindowsOrDegrade(cwd, windowsOverride) {
|
|
310
|
+
let windows;
|
|
311
|
+
try {
|
|
312
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
313
|
+
windows = windowsOverride ?? require('./broken-windows.cjs');
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return { ok: false, note: 'broken-windows capability unavailable — proposal recorded locally only' };
|
|
317
|
+
}
|
|
318
|
+
const ledger = readLedgerOrEmpty(cwd, windows);
|
|
319
|
+
if (ledger === null) {
|
|
320
|
+
return { ok: false, note: 'broken-windows ledger unreadable — proposal recorded locally only' };
|
|
321
|
+
}
|
|
322
|
+
return { ok: true, windows, ledger };
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Strict-mode window append (step 9 of `evaluate`). Degrades to
|
|
326
|
+
* `{ recorded: false, note }` per `loadWindowsOrDegrade` — never an error,
|
|
327
|
+
* never throws. Idempotent: re-evaluating the same still-untriaged phase
|
|
328
|
+
* finds the existing OPEN entry (matched structurally on phase + target
|
|
329
|
+
* file/line) and does not append a second one.
|
|
330
|
+
*/
|
|
331
|
+
function recordStrictWindow(cwd, padded, target, windowsOverride) {
|
|
332
|
+
const loaded = loadWindowsOrDegrade(cwd, windowsOverride);
|
|
333
|
+
if (!loaded.ok)
|
|
334
|
+
return { recorded: false, note: loaded.note };
|
|
335
|
+
const { windows, ledger } = loaded;
|
|
336
|
+
if (findOpenDeviationEntry(ledger, padded, target.file, target.startLine)) {
|
|
337
|
+
return { recorded: true, note: 'already recorded for this phase (idempotent)' };
|
|
338
|
+
}
|
|
339
|
+
const key = candidateKey(target.file, target.name);
|
|
340
|
+
const description = `${key} — complexity ${target.score}` +
|
|
341
|
+
(target.baseline !== null ? ` (baseline ${target.baseline}, delta ${target.delta})` : '');
|
|
342
|
+
const now = new Date().toISOString();
|
|
343
|
+
try {
|
|
344
|
+
const result = windows.appendWindow(ledger, { kind: 'deviation', phase: padded, file: target.file, line: target.startLine, description }, { now });
|
|
345
|
+
writeTextAtomic(windowsLedgerPath(cwd, windows), windows.renderLedger(result.ledger));
|
|
346
|
+
return { recorded: true };
|
|
347
|
+
}
|
|
348
|
+
catch (e) {
|
|
349
|
+
return { recorded: false, note: `failed to record broken-windows entry: ${e instanceof Error ? e.message : String(e)}` };
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Resolve (mark fixed/waived) the ledger window matching phase + target
|
|
354
|
+
* file/line, if any. Never throws. Mirrors `recordStrictWindow`'s
|
|
355
|
+
* `{ recorded, note }` degrade shape (#1953 defect 2): every
|
|
356
|
+
* `resolved: false` path carries a `note` naming the real reason, so
|
|
357
|
+
* `accept`/`decline` never tell a user "it failed" without saying why.
|
|
358
|
+
*/
|
|
359
|
+
function resolveLedgerWindow(cwd, padded, file, line, kind, reasonText, windowsOverride) {
|
|
360
|
+
const loaded = loadWindowsOrDegrade(cwd, windowsOverride);
|
|
361
|
+
if (!loaded.ok)
|
|
362
|
+
return { resolved: false, note: loaded.note };
|
|
363
|
+
const { windows, ledger } = loaded;
|
|
364
|
+
const entry = findOpenDeviationEntry(ledger, padded, file, line);
|
|
365
|
+
if (!entry) {
|
|
366
|
+
return { resolved: false, note: 'no open broken-windows entry found for this phase/target — nothing to resolve' };
|
|
367
|
+
}
|
|
368
|
+
const now = new Date().toISOString();
|
|
369
|
+
try {
|
|
370
|
+
const updated = kind === 'accept'
|
|
371
|
+
? windows.markFixed(ledger, entry.id, { now })
|
|
372
|
+
: windows.markWaived(ledger, entry.id, reasonText || 'declined', { now });
|
|
373
|
+
writeTextAtomic(windowsLedgerPath(cwd, windows), windows.renderLedger(updated));
|
|
374
|
+
return { resolved: true };
|
|
375
|
+
}
|
|
376
|
+
catch (e) {
|
|
377
|
+
return { resolved: false, note: `failed to resolve broken-windows entry: ${e instanceof Error ? e.message : String(e)}` };
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// ─── evaluate ────────────────────────────────────────────────────────────────
|
|
381
|
+
const EVALUATE_USAGE = 'Usage: gsd-tools refactor evaluate --phase <N> [--since <ref>] [--raw]';
|
|
382
|
+
function handleEvaluate(args, cwd, raw, c, complexity, git, windowsOverride) {
|
|
383
|
+
const rest = args.slice(2);
|
|
384
|
+
const phaseCheck = requirePhaseArg(rest, complexity, EVALUATE_USAGE);
|
|
385
|
+
if (!phaseCheck.ok)
|
|
386
|
+
return phaseCheck.result;
|
|
387
|
+
// Step 3: resolve PHASE_DIR + anchor (phaseStartCommit, or --since override).
|
|
388
|
+
const resolved = resolvePhaseDirForArg(cwd, phaseCheck.phase);
|
|
389
|
+
if (resolved === null) {
|
|
390
|
+
c.output({ verdict: complexity.VERDICT.SKIPPED, reason: complexity.REASON.REFACTOR_INVALID_PHASE, phase: phaseCheck.phase }, raw);
|
|
391
|
+
return undefined;
|
|
392
|
+
}
|
|
393
|
+
const { phaseDir, padded } = resolved;
|
|
394
|
+
const sinceFlag = readFlag(rest, '--since');
|
|
395
|
+
const sinceOverride = sinceFlag.present && !sinceFlag.flagShaped ? sinceFlag.value.trim() : '';
|
|
396
|
+
const sinceRef = sinceOverride !== '' ? sinceOverride : git.phaseStartCommit(cwd, phaseDir);
|
|
397
|
+
if (sinceRef === null) {
|
|
398
|
+
c.output({ verdict: complexity.VERDICT.SKIPPED, reason: complexity.REASON.REFACTOR_GIT_UNAVAILABLE, phase: padded }, raw);
|
|
399
|
+
return undefined;
|
|
400
|
+
}
|
|
401
|
+
const touched = git.changedFilesSince(cwd, sinceRef);
|
|
402
|
+
if (touched === null) {
|
|
403
|
+
c.output({ verdict: complexity.VERDICT.SKIPPED, reason: complexity.REASON.REFACTOR_GIT_UNAVAILABLE, phase: padded }, raw);
|
|
404
|
+
return undefined;
|
|
405
|
+
}
|
|
406
|
+
// Step 4: empty touched set.
|
|
407
|
+
if (touched.length === 0) {
|
|
408
|
+
c.output({ verdict: complexity.VERDICT.BELOW_THRESHOLD, reason: complexity.REASON.REFACTOR_NO_TOUCHED_FILES, phase: padded }, raw);
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
// Step 5: filter with isAnalyzablePath; read each survivor. A read failure
|
|
412
|
+
// (or a path that escapes the project root) skips that one file with a
|
|
413
|
+
// reason and the run continues — never aborts the evaluation.
|
|
414
|
+
const analyzed = [];
|
|
415
|
+
// Only files that were SUCCESSFULLY analyzed feed nextBaseline's prune set
|
|
416
|
+
// — an unreadable file must never wipe that file's baseline history via
|
|
417
|
+
// the "file in analyzedFiles but function missing" prune rule.
|
|
418
|
+
const successfullyAnalyzedFiles = [];
|
|
419
|
+
for (const relFile of touched) {
|
|
420
|
+
if (!complexity.isAnalyzablePath(relFile))
|
|
421
|
+
continue;
|
|
422
|
+
const confined = resolveConfinedPath(cwd, relFile);
|
|
423
|
+
if (confined === null) {
|
|
424
|
+
analyzed.push({ file: relFile, ok: false, reason: complexity.REASON.REFACTOR_FILE_UNREADABLE });
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
let source;
|
|
428
|
+
try {
|
|
429
|
+
source = node_fs_1.default.readFileSync(confined, 'utf8');
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
analyzed.push({ file: relFile, ok: false, reason: complexity.REASON.REFACTOR_FILE_UNREADABLE });
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const result = complexity.analyzeSource(source);
|
|
436
|
+
if (!result.ok) {
|
|
437
|
+
analyzed.push({ file: relFile, ok: false, reason: result.reason });
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
analyzed.push({ file: relFile, ok: true, method: result.method, functions: result.functions });
|
|
441
|
+
successfullyAnalyzedFiles.push(relFile);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
// Step 6.
|
|
445
|
+
const planningDirPath = planningDir(cwd);
|
|
446
|
+
const baselineRead = complexity.readBaseline(planningDirPath);
|
|
447
|
+
const evalConfig = readEvalConfig(cwd);
|
|
448
|
+
const evaluation = complexity.evaluateCandidates({
|
|
449
|
+
analyzed,
|
|
450
|
+
baseline: baselineRead.baseline,
|
|
451
|
+
threshold: evalConfig.threshold,
|
|
452
|
+
jumpDelta: evalConfig.jumpDelta,
|
|
453
|
+
});
|
|
454
|
+
// Step 7.
|
|
455
|
+
let artifactWritten = false;
|
|
456
|
+
let artifactPath = null;
|
|
457
|
+
let artifactWriteError = null;
|
|
458
|
+
if (evaluation.verdict === complexity.VERDICT.TRIGGERED && evaluation.target) {
|
|
459
|
+
const target = evaluation.target;
|
|
460
|
+
const proposal = {
|
|
461
|
+
schema_version: complexity.SCHEMA_VERSION,
|
|
462
|
+
status: 'proposed',
|
|
463
|
+
phase: padded,
|
|
464
|
+
target_file: target.file,
|
|
465
|
+
target_function: target.name,
|
|
466
|
+
score: target.score,
|
|
467
|
+
baseline: target.baseline,
|
|
468
|
+
delta: target.delta,
|
|
469
|
+
metric: 'decision-points',
|
|
470
|
+
recorded_at: new Date().toISOString(),
|
|
471
|
+
resolved_at: null,
|
|
472
|
+
reason: target.reasons.join(','),
|
|
473
|
+
candidates: evaluation.candidates,
|
|
474
|
+
};
|
|
475
|
+
artifactPath = artifactAbsPath(cwd, phaseDir, padded, complexity);
|
|
476
|
+
try {
|
|
477
|
+
writeTextAtomic(artifactPath, complexity.renderProposal(proposal));
|
|
478
|
+
artifactWritten = true;
|
|
479
|
+
}
|
|
480
|
+
catch (e) {
|
|
481
|
+
artifactWriteError = e instanceof Error ? e.message : String(e);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// Step 8: baseline write failure is reported but does not fail the command.
|
|
485
|
+
const nextBaselineValue = complexity.nextBaseline(baselineRead.baseline, analyzed, { analyzedFiles: successfullyAnalyzedFiles, phase: padded });
|
|
486
|
+
const baselineWrite = complexity.writeBaseline(planningDirPath, nextBaselineValue);
|
|
487
|
+
// Step 9: strict mode, TRIGGERED only.
|
|
488
|
+
let ledgerRecorded;
|
|
489
|
+
let ledgerNote;
|
|
490
|
+
const warnings = [];
|
|
491
|
+
if (evalConfig.strict && evaluation.verdict === complexity.VERDICT.TRIGGERED && evaluation.target) {
|
|
492
|
+
const strictResult = recordStrictWindow(cwd, padded, evaluation.target, windowsOverride);
|
|
493
|
+
ledgerRecorded = strictResult.recorded;
|
|
494
|
+
ledgerNote = strictResult.note;
|
|
495
|
+
const warning = strictNotEnforcingWarning(complexity, ledgerRecorded, evalConfig.windowsEnforce);
|
|
496
|
+
if (warning)
|
|
497
|
+
warnings.push(warning);
|
|
498
|
+
}
|
|
499
|
+
const result = {
|
|
500
|
+
verdict: evaluation.verdict,
|
|
501
|
+
phase: padded,
|
|
502
|
+
candidates: evaluation.candidates,
|
|
503
|
+
target: evaluation.target,
|
|
504
|
+
skipped: evaluation.skipped,
|
|
505
|
+
threshold_used: evaluation.thresholdUsed,
|
|
506
|
+
jump_delta_used: evaluation.jumpDeltaUsed,
|
|
507
|
+
artifact_written: artifactWritten,
|
|
508
|
+
artifact_path: artifactPath,
|
|
509
|
+
baseline_write: baselineWrite.ok,
|
|
510
|
+
};
|
|
511
|
+
if (artifactWriteError !== null)
|
|
512
|
+
result.artifact_write_error = artifactWriteError;
|
|
513
|
+
if (!baselineWrite.ok && baselineWrite.reason)
|
|
514
|
+
result.baseline_write_reason = baselineWrite.reason;
|
|
515
|
+
if (ledgerRecorded !== undefined)
|
|
516
|
+
result.ledger_recorded = ledgerRecorded;
|
|
517
|
+
if (ledgerNote !== undefined)
|
|
518
|
+
result.ledger_note = ledgerNote;
|
|
519
|
+
if (warnings.length > 0)
|
|
520
|
+
result.warnings = warnings;
|
|
521
|
+
c.output(result, raw);
|
|
522
|
+
return undefined;
|
|
523
|
+
}
|
|
524
|
+
// ─── status ──────────────────────────────────────────────────────────────────
|
|
525
|
+
const STATUS_USAGE = 'Usage: gsd-tools refactor status [--phase <N>] [--raw]';
|
|
526
|
+
function scanProposals(cwd, complexity) {
|
|
527
|
+
const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
528
|
+
const results = [];
|
|
529
|
+
// #1953 (phase-enumeration-drift guard): phase-directory enumeration has one
|
|
530
|
+
// owner (`listMilestonePhaseDirs`, src/phase-locator.cts). Called unscoped
|
|
531
|
+
// (no `cwd` in opts) to preserve this scan's prior all-phases-directory
|
|
532
|
+
// reach — the only behavior change is that sentinel directories (backlog
|
|
533
|
+
// `0-*` / icebox `999-*`, per the canonical `isSentinelPhaseId`) are now
|
|
534
|
+
// excluded, and the enumeration order is `comparePhaseNum`-sorted rather
|
|
535
|
+
// than raw filesystem order.
|
|
536
|
+
const { value: dirs } = listMilestonePhaseDirs(phasesDir);
|
|
537
|
+
for (const dirName of dirs) {
|
|
538
|
+
const full = node_path_1.default.join(phasesDir, dirName);
|
|
539
|
+
let files;
|
|
540
|
+
try {
|
|
541
|
+
files = node_fs_1.default.readdirSync(full);
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
for (const fileName of files) {
|
|
547
|
+
if (!fileName.endsWith(complexity.PROPOSAL_SUFFIX))
|
|
548
|
+
continue;
|
|
549
|
+
try {
|
|
550
|
+
const text = node_fs_1.default.readFileSync(node_path_1.default.join(full, fileName), 'utf8');
|
|
551
|
+
const proposal = complexity.parseProposal(text);
|
|
552
|
+
if (proposal) {
|
|
553
|
+
results.push({
|
|
554
|
+
phase: proposal.phase,
|
|
555
|
+
target_file: proposal.target_file,
|
|
556
|
+
target_function: proposal.target_function,
|
|
557
|
+
status: proposal.status,
|
|
558
|
+
score: proposal.score,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return results;
|
|
568
|
+
}
|
|
569
|
+
function handleStatus(args, cwd, raw, c, complexity) {
|
|
570
|
+
const rest = args.slice(2);
|
|
571
|
+
const phaseFlag = readFlag(rest, '--phase');
|
|
572
|
+
if (!phaseFlag.present) {
|
|
573
|
+
c.output({ proposals: scanProposals(cwd, complexity) }, raw);
|
|
574
|
+
return undefined;
|
|
575
|
+
}
|
|
576
|
+
const trimmed = phaseFlag.value.trim();
|
|
577
|
+
if (phaseFlag.duplicated || phaseFlag.flagShaped || trimmed === '' || !PHASE_VALUE_RE.test(trimmed)) {
|
|
578
|
+
return makeInvalidArgs('phase', `Invalid --phase value: ${JSON.stringify(phaseFlag.value)}. ${STATUS_USAGE}`, complexity.REASON.REFACTOR_INVALID_PHASE);
|
|
579
|
+
}
|
|
580
|
+
const resolved = resolvePhaseDirForArg(cwd, trimmed);
|
|
581
|
+
if (resolved === null) {
|
|
582
|
+
c.output({ found: false, phase: trimmed }, raw);
|
|
583
|
+
return undefined;
|
|
584
|
+
}
|
|
585
|
+
const artifactPath = artifactAbsPath(cwd, resolved.phaseDir, resolved.padded, complexity);
|
|
586
|
+
let text;
|
|
587
|
+
try {
|
|
588
|
+
text = node_fs_1.default.readFileSync(artifactPath, 'utf8');
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
c.output({ found: false, phase: resolved.padded }, raw);
|
|
592
|
+
return undefined;
|
|
593
|
+
}
|
|
594
|
+
const proposal = complexity.parseProposal(text);
|
|
595
|
+
if (proposal === null) {
|
|
596
|
+
c.output({ found: false, phase: resolved.padded }, raw);
|
|
597
|
+
return undefined;
|
|
598
|
+
}
|
|
599
|
+
c.output({ found: true, phase: resolved.padded, proposal }, raw);
|
|
600
|
+
return undefined;
|
|
601
|
+
}
|
|
602
|
+
// ─── accept / decline ──────────────────────────────────────────────────────
|
|
603
|
+
/**
|
|
604
|
+
* Re-measure the target function's CURRENT complexity score by re-reading
|
|
605
|
+
* and re-analyzing `proposal.target_file` — `accept` re-anchors to the
|
|
606
|
+
* post-refactor score and `decline` to the current (unchanged) score, and
|
|
607
|
+
* both need the LIVE value, not the frozen `proposal.score` snapshot from
|
|
608
|
+
* evaluate-time. Falls back to `proposal.score` when the file is gone,
|
|
609
|
+
* unreadable, unparseable, or the function can no longer be found (e.g.
|
|
610
|
+
* renamed) — reanchorBaseline always needs *a* number, and the proposal's
|
|
611
|
+
* last-known score is the least-surprising fallback.
|
|
612
|
+
*/
|
|
613
|
+
function measureCurrentScore(cwd, proposal, complexity) {
|
|
614
|
+
const confined = resolveConfinedPath(cwd, proposal.target_file);
|
|
615
|
+
if (confined === null)
|
|
616
|
+
return proposal.score;
|
|
617
|
+
let source;
|
|
618
|
+
try {
|
|
619
|
+
source = node_fs_1.default.readFileSync(confined, 'utf8');
|
|
620
|
+
}
|
|
621
|
+
catch {
|
|
622
|
+
return proposal.score;
|
|
623
|
+
}
|
|
624
|
+
const result = complexity.analyzeSource(source);
|
|
625
|
+
if (!result.ok)
|
|
626
|
+
return proposal.score;
|
|
627
|
+
const fn = result.functions.find((f) => f.name === proposal.target_function);
|
|
628
|
+
return fn ? fn.score : proposal.score;
|
|
629
|
+
}
|
|
630
|
+
function dispositionUsage(kind) {
|
|
631
|
+
return kind === 'accept'
|
|
632
|
+
? 'Usage: gsd-tools refactor accept --phase <N> [--raw]'
|
|
633
|
+
: 'Usage: gsd-tools refactor decline --phase <N> --reason "<text>" [--raw]';
|
|
634
|
+
}
|
|
635
|
+
function handleDisposition(kind, args, cwd, raw, c, complexity, windowsOverride) {
|
|
636
|
+
const rest = args.slice(2);
|
|
637
|
+
const usage = dispositionUsage(kind);
|
|
638
|
+
const phaseCheck = requirePhaseArg(rest, complexity, usage);
|
|
639
|
+
if (!phaseCheck.ok)
|
|
640
|
+
return phaseCheck.result;
|
|
641
|
+
let reasonText = '';
|
|
642
|
+
if (kind === 'decline') {
|
|
643
|
+
const reasonFlag = readFlag(rest, '--reason');
|
|
644
|
+
const trimmedReason = reasonFlag.value.trim();
|
|
645
|
+
if (!reasonFlag.present || reasonFlag.flagShaped || trimmedReason === '') {
|
|
646
|
+
return makeInvalidArgs('reason', usage, complexity.REASON.REFACTOR_DECLINE_REASON_EMPTY);
|
|
647
|
+
}
|
|
648
|
+
reasonText = reasonFlag.value;
|
|
649
|
+
}
|
|
650
|
+
const resolved = resolvePhaseDirForArg(cwd, phaseCheck.phase);
|
|
651
|
+
if (resolved === null) {
|
|
652
|
+
return makeInvalidArgs('phase', `No refactor proposal found for phase ${phaseCheck.phase}.`, complexity.REASON.REFACTOR_ARTIFACT_NOT_FOUND);
|
|
653
|
+
}
|
|
654
|
+
const artifactPath = artifactAbsPath(cwd, resolved.phaseDir, resolved.padded, complexity);
|
|
655
|
+
let text;
|
|
656
|
+
try {
|
|
657
|
+
text = node_fs_1.default.readFileSync(artifactPath, 'utf8');
|
|
658
|
+
}
|
|
659
|
+
catch {
|
|
660
|
+
return makeInvalidArgs('phase', `No refactor proposal found for phase ${resolved.padded}.`, complexity.REASON.REFACTOR_ARTIFACT_NOT_FOUND);
|
|
661
|
+
}
|
|
662
|
+
const proposal = complexity.parseProposal(text);
|
|
663
|
+
if (proposal === null) {
|
|
664
|
+
return makeInvalidArgs('phase', `Refactor proposal for phase ${resolved.padded} is unreadable.`, complexity.REASON.REFACTOR_ARTIFACT_NOT_FOUND);
|
|
665
|
+
}
|
|
666
|
+
if (proposal.status !== 'proposed') {
|
|
667
|
+
return makeInvalidArgs('phase', `Refactor proposal for phase ${resolved.padded} is already ${proposal.status}.`, complexity.REASON.REFACTOR_ALREADY_DISPOSITIONED);
|
|
668
|
+
}
|
|
669
|
+
const key = candidateKey(proposal.target_file, proposal.target_function);
|
|
670
|
+
const now = new Date().toISOString();
|
|
671
|
+
const liveScore = measureCurrentScore(cwd, proposal, complexity);
|
|
672
|
+
const targetCandidate = proposal.candidates.find((cand) => cand.file === proposal.target_file && cand.name === proposal.target_function) ?? proposal.candidates[0] ?? null;
|
|
673
|
+
const updatedProposal = {
|
|
674
|
+
...proposal,
|
|
675
|
+
status: kind === 'accept' ? 'accepted' : 'declined',
|
|
676
|
+
resolved_at: now,
|
|
677
|
+
reason: kind === 'accept' ? proposal.reason : reasonText,
|
|
678
|
+
};
|
|
679
|
+
writeTextAtomic(artifactPath, complexity.renderProposal(updatedProposal));
|
|
680
|
+
const planningDirPath = planningDir(cwd);
|
|
681
|
+
const baselineRead = complexity.readBaseline(planningDirPath);
|
|
682
|
+
const nextBaselineValue = complexity.reanchorBaseline(baselineRead.baseline, key, liveScore, { phase: resolved.padded });
|
|
683
|
+
const baselineWrite = complexity.writeBaseline(planningDirPath, nextBaselineValue);
|
|
684
|
+
const ledgerResult = resolveLedgerWindow(cwd, resolved.padded, proposal.target_file, targetCandidate ? targetCandidate.startLine : -1, kind, reasonText, windowsOverride);
|
|
685
|
+
const dispositionResult = {
|
|
686
|
+
status: updatedProposal.status,
|
|
687
|
+
phase: resolved.padded,
|
|
688
|
+
target: key,
|
|
689
|
+
reanchored_to: liveScore,
|
|
690
|
+
baseline_write: baselineWrite.ok,
|
|
691
|
+
ledger_resolved: ledgerResult.resolved,
|
|
692
|
+
};
|
|
693
|
+
if (ledgerResult.note !== undefined)
|
|
694
|
+
dispositionResult.ledger_note = ledgerResult.note;
|
|
695
|
+
c.output(dispositionResult, raw);
|
|
696
|
+
return undefined;
|
|
697
|
+
}
|
|
698
|
+
// ─── Dispatch ────────────────────────────────────────────────────────────────
|
|
699
|
+
/**
|
|
700
|
+
* Shared capability-active gate + lazy `complexity-trigger.cjs` require
|
|
701
|
+
* (#1953 defect 5b) — identical across all four subcommand handlers. Emits
|
|
702
|
+
* the disabled response and returns `undefined` without invoking `run` when
|
|
703
|
+
* the capability is off; otherwise resolves the module (respecting the
|
|
704
|
+
* `_complexity` test seam) and hands it to `run`.
|
|
705
|
+
*/
|
|
706
|
+
function withActiveComplexity(cwd, raw, c, complexityOverride, run) {
|
|
707
|
+
if (!isCapabilityActive(CAPABILITY_ID, cwd)) {
|
|
708
|
+
c.output(disabledResponse(), raw);
|
|
709
|
+
return undefined;
|
|
710
|
+
}
|
|
711
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
712
|
+
const complexity = complexityOverride ?? require('./complexity-trigger.cjs');
|
|
713
|
+
return run(complexity);
|
|
714
|
+
}
|
|
715
|
+
function routeRefactorTriggerCommand({ args, cwd, raw, error, _complexity, _git, _windows, _core }) {
|
|
716
|
+
const c = _core ?? _defaultCore;
|
|
717
|
+
routeHubCommandFamily({
|
|
718
|
+
family: 'refactor',
|
|
719
|
+
args,
|
|
720
|
+
// Alphabetical for a stable unknown-subcommand message, matching intel/graphify.
|
|
721
|
+
subcommands: ['accept', 'decline', 'evaluate', 'status'],
|
|
722
|
+
handlers: {
|
|
723
|
+
evaluate: () => withActiveComplexity(cwd, raw, c, _complexity, (complexity) => {
|
|
724
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
725
|
+
const git = _git ?? require('./git-base-branch.cjs');
|
|
726
|
+
return handleEvaluate(args, cwd, raw, c, complexity, git, _windows);
|
|
727
|
+
}),
|
|
728
|
+
status: () => withActiveComplexity(cwd, raw, c, _complexity, (complexity) => handleStatus(args, cwd, raw, c, complexity)),
|
|
729
|
+
accept: () => withActiveComplexity(cwd, raw, c, _complexity, (complexity) => handleDisposition('accept', args, cwd, raw, c, complexity, _windows)),
|
|
730
|
+
decline: () => withActiveComplexity(cwd, raw, c, _complexity, (complexity) => handleDisposition('decline', args, cwd, raw, c, complexity, _windows)),
|
|
731
|
+
},
|
|
732
|
+
unknownMessage: (subcommand, available) => `Unknown refactor subcommand. Available: ${available.join(', ')}`,
|
|
733
|
+
error,
|
|
734
|
+
cwd,
|
|
735
|
+
raw,
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
module.exports = {
|
|
739
|
+
routeRefactorTriggerCommand,
|
|
740
|
+
};
|