@opengsd/gsd-core 1.10.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-debug-session-manager.md +12 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-doc-synthesizer.md +2 -4
- package/agents/gsd-dom-verifier.md +169 -0
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +22 -14
- package/agents/gsd-framework-selector.md +1 -3
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +5 -3
- package/agents/gsd-pattern-mapper.md +11 -0
- package/agents/gsd-phase-researcher.md +23 -2
- package/agents/gsd-plan-checker.md +50 -53
- package/agents/gsd-planner.md +50 -50
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.md +2 -2
- package/agents/gsd-roadmapper.md +15 -11
- package/agents/gsd-ui-checker.md +63 -4
- package/agents/gsd-ui-researcher.md +41 -3
- package/agents/gsd-user-profiler.md +3 -0
- package/agents/gsd-verifier.md +13 -4
- package/bin/install.js +1448 -1103
- package/commands/gsd/code-review.md +1 -1
- package/commands/gsd/discuss-phase.md +1 -1
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/import.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 +9 -5
- package/commands/gsd/review-backlog.md +2 -1
- package/commands/gsd/verify-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +1035 -138
- package/gsd-core/bin/lib/active-workstream-store.cjs +146 -22
- package/gsd-core/bin/lib/adr-parser.cjs +13 -7
- package/gsd-core/bin/lib/agent-install-check.cjs +392 -32
- package/gsd-core/bin/lib/api-coverage.cjs +33 -14
- package/gsd-core/bin/lib/artifacts.cjs +5 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +32 -15
- package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
- package/gsd-core/bin/lib/audit.cjs +1026 -268
- package/gsd-core/bin/lib/broken-windows.cjs +306 -28
- 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-lock.cjs +10 -4
- package/gsd-core/bin/lib/capability-registry.cjs +845 -130
- package/gsd-core/bin/lib/capability-source.cjs +92 -0
- package/gsd-core/bin/lib/capability-state.cjs +18 -3
- package/gsd-core/bin/lib/capability-trust.cjs +444 -25
- package/gsd-core/bin/lib/capability-validator.cjs +700 -40
- package/gsd-core/bin/lib/capability-writer.cjs +3 -2
- package/gsd-core/bin/lib/check-command-router.cjs +216 -42
- package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
- package/gsd-core/bin/lib/cli-exit.cjs +496 -10
- package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
- package/gsd-core/bin/lib/codex-agent-toml.cjs +735 -0
- package/gsd-core/bin/lib/command-aliases.cjs +22 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
- package/gsd-core/bin/lib/command-roster.cjs +44 -1
- package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
- package/gsd-core/bin/lib/commands.cjs +1172 -108
- package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
- package/gsd-core/bin/lib/complexity-trigger.cjs +1192 -0
- package/gsd-core/bin/lib/config-loader.cjs +187 -23
- package/gsd-core/bin/lib/config.cjs +102 -3
- package/gsd-core/bin/lib/configuration.cjs +129 -37
- package/gsd-core/bin/lib/core-utils.cjs +208 -33
- package/gsd-core/bin/lib/decisions.cjs +23 -0
- package/gsd-core/bin/lib/edge-probe.cjs +9 -1
- package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
- package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
- package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
- package/gsd-core/bin/lib/frontmatter.cjs +899 -229
- package/gsd-core/bin/lib/gap-checker.cjs +95 -10
- package/gsd-core/bin/lib/git-base-branch.cjs +276 -39
- 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 +149 -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 +268 -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 +187 -0
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +451 -0
- package/gsd-core/bin/lib/host-integration.cjs +39 -6
- package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
- package/gsd-core/bin/lib/init-command-router.cjs +118 -21
- package/gsd-core/bin/lib/init.cjs +439 -168
- package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
- package/gsd-core/bin/lib/install-engine.cjs +811 -259
- package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +235 -0
- package/gsd-core/bin/lib/install-profiles.cjs +212 -61
- 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-migration-report.cjs +3 -0
- package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +148 -38
- package/gsd-core/bin/lib/intel.cjs +101 -26
- package/gsd-core/bin/lib/io.cjs +170 -15
- package/gsd-core/bin/lib/learnings.cjs +85 -14
- package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
- package/gsd-core/bin/lib/markdown-table.cjs +183 -22
- package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
- package/gsd-core/bin/lib/milestone.cjs +842 -73
- package/gsd-core/bin/lib/model-catalog.cjs +232 -16
- package/gsd-core/bin/lib/model-resolver.cjs +193 -68
- package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
- package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
- package/gsd-core/bin/lib/pattern.cjs +122 -0
- package/gsd-core/bin/lib/phase-estimation.cjs +18 -9
- package/gsd-core/bin/lib/phase-id.cjs +514 -40
- package/gsd-core/bin/lib/phase-lifecycle.cjs +52 -19
- package/gsd-core/bin/lib/phase-locator.cjs +262 -34
- package/gsd-core/bin/lib/phase.cjs +1038 -214
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
- package/gsd-core/bin/lib/plan-document.cjs +263 -0
- package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
- package/gsd-core/bin/lib/plan-scan.cjs +98 -3
- package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
- package/gsd-core/bin/lib/planning-scope.cjs +31 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +894 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +112 -6
- package/gsd-core/bin/lib/probe-core.cjs +5 -2
- package/gsd-core/bin/lib/profile-output.cjs +1 -1
- package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
- package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
- package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +766 -0
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +22 -13
- 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 +59 -11
- package/gsd-core/bin/lib/roadmap-parser.cjs +1006 -184
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
- package/gsd-core/bin/lib/roadmap.cjs +442 -96
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +702 -52
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +459 -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 +402 -58
- package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +96 -8
- package/gsd-core/bin/lib/security.cjs +104 -5
- package/gsd-core/bin/lib/shell-command-projection.cjs +342 -7
- package/gsd-core/bin/lib/smart-entry.cjs +133 -23
- package/gsd-core/bin/lib/spec-section.cjs +12 -7
- package/gsd-core/bin/lib/state-command-router.cjs +52 -19
- package/gsd-core/bin/lib/state-contract.cjs +359 -0
- package/gsd-core/bin/lib/state-document.cjs +338 -8
- package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
- package/gsd-core/bin/lib/state-transition.cjs +846 -176
- package/gsd-core/bin/lib/state.cjs +2589 -369
- package/gsd-core/bin/lib/surface.cjs +33 -11
- package/gsd-core/bin/lib/task-command-router.cjs +111 -1
- package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
- package/gsd-core/bin/lib/teams-status.cjs +4 -1
- 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 +67 -23
- package/gsd-core/bin/lib/uat.cjs +1761 -167
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +51 -12
- package/gsd-core/bin/lib/unusable-input.cjs +37 -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-command-router.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +20 -6
- package/gsd-core/bin/lib/vendor/README.md +75 -0
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -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 +272 -9
- package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
- package/gsd-core/bin/lib/verify.cjs +453 -918
- 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-name-policy.cjs +25 -4
- package/gsd-core/bin/lib/workstream.cjs +2 -2
- package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
- package/gsd-core/bin/lib/worktree-safety.cjs +341 -18
- package/gsd-core/bin/shared/config-defaults.manifest.json +8 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +12 -1
- package/gsd-core/bin/shared/exit-codes.json +8 -0
- package/gsd-core/bin/shared/exit-codes.sh +20 -0
- package/gsd-core/bin/shared/model-catalog.json +8 -1
- package/gsd-core/references/agent-contracts.md +44 -26
- package/gsd-core/references/api-coverage.md +24 -2
- package/gsd-core/references/autonomous-smart-discuss.md +3 -3
- package/gsd-core/references/checkpoints.md +39 -21
- package/gsd-core/references/context-budget.md +1 -1
- package/gsd-core/references/decimal-phase-calculation.md +5 -5
- package/gsd-core/references/dispatch-isolation-gate.md +138 -0
- package/gsd-core/references/doc-conflict-engine.md +1 -1
- package/gsd-core/references/edge-probe.md +8 -0
- package/gsd-core/references/execute-mvp-tdd.md +4 -6
- package/gsd-core/references/execute-phase-between-wave-reset.md +15 -14
- 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 +17 -11
- package/gsd-core/references/failing-direction.md +78 -0
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-integration.md +5 -5
- package/gsd-core/references/git-planning-commit.md +5 -4
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +61 -2
- package/gsd-core/references/model-profiles.md +12 -4
- package/gsd-core/references/mvp-concepts.md +9 -9
- package/gsd-core/references/nyquist-compliance.md +74 -0
- package/gsd-core/references/offer-next.md +3 -5
- package/gsd-core/references/phase-argument-parsing.md +3 -3
- package/gsd-core/references/planner-failing-direction.md +53 -0
- package/gsd-core/references/planner-guidance.md +3 -9
- package/gsd-core/references/planner-human-verify-mode.md +15 -1
- package/gsd-core/references/planner-preconditions.md +1 -1
- package/gsd-core/references/planner-reviews.md +1 -1
- package/gsd-core/references/planner-revision.md +1 -1
- package/gsd-core/references/planner-verify-command-grounding.md +17 -0
- package/gsd-core/references/planning-config.md +44 -13
- package/gsd-core/references/reviewer-instances.md +31 -0
- package/gsd-core/references/revision-loop.md +1 -1
- package/gsd-core/references/runtime-aware-dispatch.md +1 -1
- package/gsd-core/references/specless-probe-fallback.md +1 -1
- package/gsd-core/references/tdd.md +1 -3
- package/gsd-core/references/ui-brand.md +65 -21
- package/gsd-core/references/ui-consideration-probe.md +1 -1
- package/gsd-core/references/universal-anti-patterns.md +5 -5
- package/gsd-core/references/verifier-phase-gates.md +192 -0
- package/gsd-core/references/verify-command-path-resolvability.md +42 -0
- package/gsd-core/references/verify-mvp-mode.md +2 -2
- package/gsd-core/references/workstream-flag.md +33 -17
- package/gsd-core/templates/README.md +1 -1
- package/gsd-core/templates/SECURITY.md +3 -3
- package/gsd-core/templates/UI-SPEC.md +25 -3
- package/gsd-core/templates/VALIDATION.md +3 -3
- package/gsd-core/templates/discussion-log.md +1 -1
- package/gsd-core/templates/phase-prompt.md +5 -4
- package/gsd-core/templates/state.md +11 -4
- package/gsd-core/templates/verification-report.md +9 -1
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +3 -3
- package/gsd-core/workflows/add-tests.md +3 -8
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +13 -20
- package/gsd-core/workflows/audit-fix.md +12 -3
- package/gsd-core/workflows/audit-milestone.md +9 -9
- package/gsd-core/workflows/audit-uat.md +17 -2
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
- package/gsd-core/workflows/autonomous.md +11 -27
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +64 -5
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +14 -4
- package/gsd-core/workflows/code-review-fix.md +38 -11
- package/gsd-core/workflows/code-review.md +159 -52
- package/gsd-core/workflows/complete-milestone.md +151 -23
- package/gsd-core/workflows/debug.md +12 -8
- package/gsd-core/workflows/diagnose-issues.md +47 -15
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -8
- package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/text.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
- package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +3 -6
- package/gsd-core/workflows/docs-update.md +5 -4
- package/gsd-core/workflows/edit-phase.md +27 -2
- package/gsd-core/workflows/eval-review.md +7 -14
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +142 -15
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
- 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 +24 -4
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
- package/gsd-core/workflows/execute-phase.md +72 -100
- package/gsd-core/workflows/execute-plan.md +52 -15
- package/gsd-core/workflows/explore.md +131 -4
- package/gsd-core/workflows/extract-learnings.md +1 -1
- package/gsd-core/workflows/fast.md +10 -2
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +5 -5
- package/gsd-core/workflows/health.md +76 -10
- package/gsd-core/workflows/import.md +18 -15
- package/gsd-core/workflows/inbox.md +4 -5
- package/gsd-core/workflows/ingest-docs.md +49 -16
- package/gsd-core/workflows/insert-phase.md +5 -5
- package/gsd-core/workflows/list-seeds.md +5 -3
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +12 -23
- package/gsd-core/workflows/map-codebase.md +1 -1
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +8 -5
- package/gsd-core/workflows/new-milestone.md +22 -29
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
- package/gsd-core/workflows/new-project.md +26 -40
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +14 -2
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -4
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +3 -3
- package/gsd-core/workflows/plan-phase.md +162 -59
- package/gsd-core/workflows/plan-review-convergence.md +96 -11
- package/gsd-core/workflows/plant-seed.md +2 -2
- package/gsd-core/workflows/pr-branch.md +187 -51
- package/gsd-core/workflows/profile-user.md +16 -14
- package/gsd-core/workflows/progress.md +61 -18
- package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +5 -7
- package/gsd-core/workflows/quick/steps/quick-verification.md +28 -9
- package/gsd-core/workflows/quick/steps/research-phase.md +4 -6
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
- package/gsd-core/workflows/quick.md +55 -44
- package/gsd-core/workflows/remove-phase.md +4 -4
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +8 -12
- package/gsd-core/workflows/review.md +219 -20
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +3 -3
- package/gsd-core/workflows/session-report.md +2 -1
- package/gsd-core/workflows/settings-advanced.md +7 -9
- package/gsd-core/workflows/settings-integrations.md +64 -31
- package/gsd-core/workflows/settings.md +69 -7
- package/gsd-core/workflows/ship.md +116 -50
- package/gsd-core/workflows/sketch-wrap-up.md +11 -17
- package/gsd-core/workflows/sketch.md +12 -18
- package/gsd-core/workflows/smart-entry.md +3 -5
- package/gsd-core/workflows/spec-phase.md +53 -13
- package/gsd-core/workflows/spike-wrap-up.md +7 -11
- package/gsd-core/workflows/spike.md +20 -31
- package/gsd-core/workflows/stats.md +2 -2
- package/gsd-core/workflows/sync-skills.md +64 -9
- package/gsd-core/workflows/thread.md +11 -7
- package/gsd-core/workflows/transition.md +49 -14
- package/gsd-core/workflows/ui-phase.md +15 -21
- package/gsd-core/workflows/ui-review.md +8 -12
- package/gsd-core/workflows/ultraplan-phase.md +5 -13
- package/gsd-core/workflows/undo.md +8 -16
- package/gsd-core/workflows/update.md +7 -11
- package/gsd-core/workflows/validate-phase.md +3 -3
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
- package/gsd-core/workflows/verify-work.md +66 -25
- package/hooks/dist/gsd-agent-isolation-guard.js +158 -30
- package/hooks/dist/gsd-check-update-worker.js +56 -13
- package/hooks/dist/gsd-check-update.js +19 -1
- package/hooks/dist/gsd-config-reload.js +18 -12
- package/hooks/dist/gsd-context-monitor.js +19 -10
- package/hooks/dist/gsd-cursor-post-tool.js +3 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +2 -3
- package/hooks/dist/gsd-cursor-session-start.js +2 -1
- package/hooks/dist/gsd-cursor-stop.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +83 -3
- package/hooks/dist/gsd-cursor-subagent-stop.js +6 -3
- package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
- package/hooks/dist/gsd-graphify-update.sh +22 -18
- package/hooks/dist/gsd-node-runner.sh +76 -0
- package/hooks/dist/gsd-phase-boundary.sh +1 -0
- package/hooks/dist/gsd-prompt-guard.js +37 -27
- package/hooks/dist/gsd-read-guard.js +16 -7
- package/hooks/dist/gsd-read-injection-scanner.js +55 -32
- package/hooks/dist/gsd-session-state.sh +1 -0
- package/hooks/dist/gsd-statusline.js +231 -24
- package/hooks/dist/gsd-update-banner.js +22 -1
- package/hooks/dist/gsd-validate-commit.sh +80 -6
- package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
- package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
- package/hooks/dist/gsd-workflow-guard.js +162 -46
- package/hooks/dist/gsd-worktree-path-guard.js +36 -21
- package/hooks/dist/gsd-write-guard.js +35 -25
- package/hooks/dist/lib/cli-exit.js +560 -0
- package/hooks/dist/lib/exit-code-registry.js +98 -0
- package/hooks/dist/lib/git-cmd.js +92 -59
- package/hooks/dist/lib/git-probe.js +84 -0
- package/hooks/dist/lib/hook-exit.js +81 -0
- 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/dist/managed-hooks-registry.cjs +3 -0
- package/hooks/gsd-agent-isolation-guard.js +158 -30
- package/hooks/gsd-check-update-worker.js +56 -13
- package/hooks/gsd-check-update.js +19 -1
- package/hooks/gsd-config-reload.js +18 -12
- package/hooks/gsd-context-monitor.js +19 -10
- package/hooks/gsd-cursor-post-tool.js +3 -1
- package/hooks/gsd-cursor-pre-tool.js +2 -3
- package/hooks/gsd-cursor-session-start.js +2 -1
- package/hooks/gsd-cursor-stop.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +83 -3
- package/hooks/gsd-cursor-subagent-stop.js +6 -3
- package/hooks/gsd-ensure-canonical-path.js +2 -1
- package/hooks/gsd-graphify-update.sh +22 -18
- package/hooks/gsd-node-runner.sh +76 -0
- package/hooks/gsd-phase-boundary.sh +1 -0
- package/hooks/gsd-prompt-guard.js +37 -27
- package/hooks/gsd-read-guard.js +16 -7
- package/hooks/gsd-read-injection-scanner.js +55 -32
- package/hooks/gsd-session-state.sh +1 -0
- package/hooks/gsd-statusline.js +231 -24
- package/hooks/gsd-update-banner.js +22 -1
- package/hooks/gsd-validate-commit.sh +80 -6
- package/hooks/gsd-windsurf-pre-command.js +16 -11
- package/hooks/gsd-windsurf-pre-write.js +22 -13
- package/hooks/gsd-workflow-guard.js +162 -46
- package/hooks/gsd-worktree-path-guard.js +36 -21
- package/hooks/gsd-write-guard.js +35 -25
- package/hooks/lib/cli-exit.js +560 -0
- package/hooks/lib/exit-code-registry.js +98 -0
- package/hooks/lib/git-cmd.js +92 -59
- package/hooks/lib/git-probe.js +84 -0
- package/hooks/lib/hook-exit.js +81 -0
- 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/hooks/managed-hooks-registry.cjs +3 -0
- package/package.json +28 -11
- package/pi/gsd.cjs +19 -5
- package/scripts/base64-scan.sh +74 -12
- 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/build-hooks.js +5 -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/check-glossary-refs.cjs +77 -15
- package/scripts/check-mutation-score-ratchet.cjs +156 -0
- package/scripts/ci-check-job-near-cap.cjs +49 -0
- package/scripts/ci-pr-mergeability.cjs +262 -0
- package/scripts/ci-test-scope.cjs +64 -14
- package/scripts/ci-timeout-report.cjs +230 -0
- package/scripts/command-contract-helpers.cjs +903 -1
- package/scripts/docs-guard-registry.cjs +396 -0
- package/scripts/gen-adr-index.cjs +728 -38
- package/scripts/gen-capability-registry.cjs +11 -21
- package/scripts/gen-context-index.cjs +2 -11
- package/scripts/gen-exit-code-docs.cjs +318 -0
- package/scripts/gen-exit-code-registry.cjs +891 -0
- package/scripts/gen-features.cjs +836 -0
- package/scripts/gen-health-docs.cjs +390 -0
- package/scripts/gen-hooks-cli-exit.cjs +239 -0
- package/scripts/gen-install-tree-fixtures.cjs +2 -2
- package/scripts/gen-inventory-manifest.cjs +50 -4
- package/scripts/gen-loop-host-contract.cjs +138 -25
- package/scripts/gen-registry.cjs +3 -14
- package/scripts/gen-scripts-cli-exit.cjs +185 -0
- package/scripts/gen-state-md-docs.cjs +727 -0
- package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
- package/scripts/lib/alias-drift-families.cjs +46 -0
- package/scripts/lib/ci-job-timing.cjs +72 -0
- package/scripts/lib/cli-exit.cjs +546 -44
- package/scripts/lib/drift-scan.cjs +308 -0
- package/scripts/lib/exit-code-registry.cjs +98 -0
- package/scripts/lib/ndjson-reporter.cjs +119 -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-docs-guard-registration.cjs +495 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +193 -0
- package/scripts/lint-eslint-glob-coverage.allowlist.json +38 -0
- package/scripts/lint-eslint-glob-coverage.cjs +340 -0
- package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
- package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
- package/scripts/lint-health-diagnostic-rule-table.cjs +461 -0
- package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
- package/scripts/lint-milestone-window-drift.cjs +468 -0
- package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
- package/scripts/lint-phase-enumeration-drift.cjs +492 -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 +471 -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 +488 -0
- package/scripts/lint-seam-enforcement.cjs +182 -0
- package/scripts/lint-slug-derivation-drift.cjs +921 -0
- package/scripts/lint-source-test-name-collision.cjs +241 -0
- package/scripts/lint-state-field-drift.cjs +805 -0
- package/scripts/lint-state-write-path-drift.cjs +950 -0
- package/scripts/lint-test-file-count.allowlist.json +137 -8
- package/scripts/lint-test-file-count.cjs +25 -3
- package/scripts/lint-unreachable-guard-drift.cjs +830 -0
- package/scripts/lint-vendored-deps.cjs +297 -0
- package/scripts/mutation-matrix.cjs +599 -50
- package/scripts/pr-changed-files.cjs +63 -0
- package/scripts/pr-template-policy.cjs +14 -4
- package/scripts/prompt-injection-scan.sh +100 -14
- package/scripts/require-issue-link-policy.cjs +192 -0
- package/scripts/secret-scan.sh +75 -13
- package/scripts/select-docs-guards.cjs +56 -0
- package/scripts/sync-runtime-launcher.cjs +24 -7
- package/skills/gsd-autonomous/SKILL.md +0 -1
- package/skills/gsd-code-review/SKILL.md +1 -1
- package/skills/gsd-discuss-phase/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -2
- package/skills/gsd-import/SKILL.md +1 -1
- 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 +9 -5
- 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/bin/lib/ui-safety-gate.cjs +0 -107
- 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/lint-emitted-drift-ack.cjs +0 -344
- package/scripts/run-affected-tests.cjs +0 -7
- package/scripts/run-tests.cjs +0 -1051
|
@@ -0,0 +1,1192 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Complexity-triggered refactor extension point — analyzer, evaluator, and
|
|
4
|
+
* baseline persistence (issue #1953).
|
|
5
|
+
*
|
|
6
|
+
* LEAF MODULE — imports ONLY: node:fs, node:path. No other src/ imports.
|
|
7
|
+
*
|
|
8
|
+
* Pipeline: analyzeSource (decision-point complexity per function, via
|
|
9
|
+
* stripLiterals to blank comments/string/regex content while preserving
|
|
10
|
+
* length and line numbers) -> evaluateCandidates (threshold/jump-delta
|
|
11
|
+
* scoring against a stable per-function baseline anchor) -> nextBaseline /
|
|
12
|
+
* reanchorBaseline (anchor bookkeeping; the anchor never advances on a
|
|
13
|
+
* plain evaluate — only an explicit disposition moves it, via
|
|
14
|
+
* reanchorBaseline) -> renderProposal / parseProposal (the on-disk
|
|
15
|
+
* `-REFACTOR.md` artifact) -> readBaseline / writeBaseline (I/O, atomic
|
|
16
|
+
* temp-then-rename, never throws).
|
|
17
|
+
*
|
|
18
|
+
* Authoritative surface: .gsd/phase/feat-1953-complexity-triggered-refactor/41-api-contract.md
|
|
19
|
+
* Executable spec: tests/complexity-trigger.test.cjs
|
|
20
|
+
*
|
|
21
|
+
* Out of scope for this module: git invocation, config reads,
|
|
22
|
+
* capability-activation checks, CLI arg parsing, and the ship-gate query —
|
|
23
|
+
* those live in their own modules (command router / ship gate, not this
|
|
24
|
+
* leaf module).
|
|
25
|
+
*
|
|
26
|
+
* Exports:
|
|
27
|
+
* Constants: BASELINE_FILE_NAME, PROPOSAL_SUFFIX, SCHEMA_VERSION,
|
|
28
|
+
* DEFAULTS, ANALYZABLE_EXTENSIONS, VERDICT, REASON (includes
|
|
29
|
+
* REFACTOR_FILE_UNREADABLE for an unreadable/confined-escape file)
|
|
30
|
+
* Pure: stripLiterals, analyzeSource, isAnalyzablePath,
|
|
31
|
+
* evaluateCandidates, nextBaseline, reanchorBaseline,
|
|
32
|
+
* renderProposal, parseProposal
|
|
33
|
+
* I/O: readBaseline, writeBaseline
|
|
34
|
+
*/
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.REASON = exports.VERDICT = exports.ANALYZABLE_EXTENSIONS = exports.DEFAULTS = exports.SCHEMA_VERSION = exports.PROPOSAL_SUFFIX = exports.BASELINE_FILE_NAME = void 0;
|
|
40
|
+
exports.stripLiterals = stripLiterals;
|
|
41
|
+
exports.analyzeSource = analyzeSource;
|
|
42
|
+
exports.isAnalyzablePath = isAnalyzablePath;
|
|
43
|
+
exports.evaluateCandidates = evaluateCandidates;
|
|
44
|
+
exports.nextBaseline = nextBaseline;
|
|
45
|
+
exports.reanchorBaseline = reanchorBaseline;
|
|
46
|
+
exports.renderProposal = renderProposal;
|
|
47
|
+
exports.parseProposal = parseProposal;
|
|
48
|
+
exports.readBaseline = readBaseline;
|
|
49
|
+
exports.writeBaseline = writeBaseline;
|
|
50
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
51
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
52
|
+
// ─── Constants ─────────────────────────────────────────────────────────────
|
|
53
|
+
exports.BASELINE_FILE_NAME = 'complexity-baseline.json'; // under .planning/
|
|
54
|
+
exports.PROPOSAL_SUFFIX = '-REFACTOR.md'; // ${PHASE_DIR}/${PADDED}-REFACTOR.md
|
|
55
|
+
exports.SCHEMA_VERSION = 1;
|
|
56
|
+
exports.DEFAULTS = Object.freeze({ threshold: 15, jumpDelta: 5 });
|
|
57
|
+
exports.ANALYZABLE_EXTENSIONS = Object.freeze(['.js', '.cjs', '.mjs', '.ts', '.cts', '.mts']);
|
|
58
|
+
exports.VERDICT = Object.freeze({
|
|
59
|
+
TRIGGERED: 'triggered',
|
|
60
|
+
BELOW_THRESHOLD: 'below_threshold',
|
|
61
|
+
SKIPPED: 'skipped',
|
|
62
|
+
});
|
|
63
|
+
/**
|
|
64
|
+
* Frozen reason enum. Adding a code = three coordinated changes: this enum,
|
|
65
|
+
* the emitting call site, and the Object.keys(REASON).sort() lock test.
|
|
66
|
+
*/
|
|
67
|
+
exports.REASON = Object.freeze({
|
|
68
|
+
REFACTOR_OK: 'refactor_ok',
|
|
69
|
+
REFACTOR_DISABLED: 'refactor_disabled',
|
|
70
|
+
REFACTOR_NO_TOUCHED_FILES: 'refactor_no_touched_files',
|
|
71
|
+
REFACTOR_GIT_UNAVAILABLE: 'refactor_git_unavailable',
|
|
72
|
+
REFACTOR_ANALYZER_UNSUPPORTED: 'refactor_analyzer_unsupported',
|
|
73
|
+
REFACTOR_ANALYZER_UNPARSEABLE: 'refactor_analyzer_unparseable',
|
|
74
|
+
REFACTOR_FILE_UNREADABLE: 'refactor_file_unreadable',
|
|
75
|
+
REFACTOR_BASELINE_MALFORMED: 'refactor_baseline_malformed',
|
|
76
|
+
REFACTOR_BASELINE_WRITE_FAILED: 'refactor_baseline_write_failed',
|
|
77
|
+
REFACTOR_STRICT_NOT_ENFORCING: 'refactor_strict_not_enforcing',
|
|
78
|
+
REFACTOR_ARTIFACT_NOT_FOUND: 'refactor_artifact_not_found',
|
|
79
|
+
REFACTOR_ALREADY_DISPOSITIONED: 'refactor_already_dispositioned',
|
|
80
|
+
REFACTOR_DECLINE_REASON_EMPTY: 'refactor_decline_reason_empty',
|
|
81
|
+
REFACTOR_INVALID_PHASE: 'refactor_invalid_phase',
|
|
82
|
+
REFACTOR_USAGE: 'refactor_usage',
|
|
83
|
+
});
|
|
84
|
+
// ─── stripLiterals ──────────────────────────────────────────────────────────
|
|
85
|
+
/**
|
|
86
|
+
* Words after which a following `/` must be interpreted as the start of an
|
|
87
|
+
* expression (so a `/` there is a regex literal, never division).
|
|
88
|
+
*/
|
|
89
|
+
const REGEX_KEYWORD_CONTEXT_WORDS = new Set([
|
|
90
|
+
'return', 'typeof', 'instanceof', 'in', 'of', 'new', 'delete', 'void',
|
|
91
|
+
'throw', 'case', 'do', 'else', 'yield', 'await', 'default',
|
|
92
|
+
]);
|
|
93
|
+
function blankChar(ch) {
|
|
94
|
+
return ch === '\n' ? '\n' : ' ';
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Replaces comment and literal *content* with spaces, preserving length and
|
|
98
|
+
* every newline so line numbers survive. Handles `//`, `/* *\/`, `'`, `"`,
|
|
99
|
+
* backtick templates (`${...}` interpolations are kept as code), regex
|
|
100
|
+
* literals (disambiguated from division by the preceding significant
|
|
101
|
+
* token), and backslash escapes. Unterminated string / template / block
|
|
102
|
+
* comment => { ok:false, reason: REFACTOR_ANALYZER_UNPARSEABLE }.
|
|
103
|
+
*/
|
|
104
|
+
function stripLiterals(source) {
|
|
105
|
+
const len = source.length;
|
|
106
|
+
const out = new Array(len);
|
|
107
|
+
let i = 0;
|
|
108
|
+
let lastTokenIsValue = false;
|
|
109
|
+
const templateStack = [];
|
|
110
|
+
while (i < len) {
|
|
111
|
+
const top = templateStack[templateStack.length - 1];
|
|
112
|
+
if (top && top.state === 'literal') {
|
|
113
|
+
const ch = source[i];
|
|
114
|
+
if (ch === '\\') {
|
|
115
|
+
out[i] = blankChar(ch);
|
|
116
|
+
i++;
|
|
117
|
+
if (i < len) {
|
|
118
|
+
out[i] = blankChar(source[i]);
|
|
119
|
+
i++;
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (ch === '`') {
|
|
124
|
+
out[i] = ' ';
|
|
125
|
+
templateStack.pop();
|
|
126
|
+
i++;
|
|
127
|
+
lastTokenIsValue = true;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (ch === '$' && source[i + 1] === '{') {
|
|
131
|
+
out[i] = '$';
|
|
132
|
+
out[i + 1] = '{';
|
|
133
|
+
top.state = 'interp';
|
|
134
|
+
top.braceDepth = 0;
|
|
135
|
+
i += 2;
|
|
136
|
+
lastTokenIsValue = false;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
out[i] = blankChar(ch);
|
|
140
|
+
i++;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const ch = source[i];
|
|
144
|
+
// Line comment.
|
|
145
|
+
if (ch === '/' && source[i + 1] === '/') {
|
|
146
|
+
while (i < len && source[i] !== '\n') {
|
|
147
|
+
out[i] = ' ';
|
|
148
|
+
i++;
|
|
149
|
+
}
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
// Block comment.
|
|
153
|
+
if (ch === '/' && source[i + 1] === '*') {
|
|
154
|
+
out[i] = ' ';
|
|
155
|
+
out[i + 1] = ' ';
|
|
156
|
+
i += 2;
|
|
157
|
+
let closed = false;
|
|
158
|
+
while (i < len) {
|
|
159
|
+
if (source[i] === '*' && source[i + 1] === '/') {
|
|
160
|
+
out[i] = ' ';
|
|
161
|
+
out[i + 1] = ' ';
|
|
162
|
+
i += 2;
|
|
163
|
+
closed = true;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
out[i] = blankChar(source[i]);
|
|
167
|
+
i++;
|
|
168
|
+
}
|
|
169
|
+
if (!closed)
|
|
170
|
+
return { ok: false, reason: exports.REASON.REFACTOR_ANALYZER_UNPARSEABLE };
|
|
171
|
+
lastTokenIsValue = false;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
// Single/double-quoted string. A raw (unescaped) newline before the
|
|
175
|
+
// closing quote is treated as unterminated (matches real JS syntax).
|
|
176
|
+
if (ch === '\'' || ch === '"') {
|
|
177
|
+
const quote = ch;
|
|
178
|
+
out[i] = ' ';
|
|
179
|
+
i++;
|
|
180
|
+
let closed = false;
|
|
181
|
+
while (i < len) {
|
|
182
|
+
const c = source[i];
|
|
183
|
+
if (c === '\n')
|
|
184
|
+
break;
|
|
185
|
+
if (c === '\\') {
|
|
186
|
+
out[i] = ' ';
|
|
187
|
+
i++;
|
|
188
|
+
if (i < len && source[i] !== '\n') {
|
|
189
|
+
out[i] = ' ';
|
|
190
|
+
i++;
|
|
191
|
+
}
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (c === quote) {
|
|
195
|
+
out[i] = ' ';
|
|
196
|
+
i++;
|
|
197
|
+
closed = true;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
out[i] = ' ';
|
|
201
|
+
i++;
|
|
202
|
+
}
|
|
203
|
+
if (!closed)
|
|
204
|
+
return { ok: false, reason: exports.REASON.REFACTOR_ANALYZER_UNPARSEABLE };
|
|
205
|
+
lastTokenIsValue = true;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
// Template literal open.
|
|
209
|
+
if (ch === '`') {
|
|
210
|
+
out[i] = ' ';
|
|
211
|
+
templateStack.push({ state: 'literal', braceDepth: 0 });
|
|
212
|
+
i++;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
// Regex literal candidate — only when the preceding significant token
|
|
216
|
+
// indicates an expression context (never after a value-producing token).
|
|
217
|
+
if (ch === '/' && !lastTokenIsValue) {
|
|
218
|
+
const start = i;
|
|
219
|
+
out[i] = ' ';
|
|
220
|
+
i++;
|
|
221
|
+
let inClass = false;
|
|
222
|
+
let closed = false;
|
|
223
|
+
while (i < len) {
|
|
224
|
+
const c = source[i];
|
|
225
|
+
if (c === '\n')
|
|
226
|
+
break;
|
|
227
|
+
if (c === '\\') {
|
|
228
|
+
out[i] = ' ';
|
|
229
|
+
i++;
|
|
230
|
+
if (i < len && source[i] !== '\n') {
|
|
231
|
+
out[i] = ' ';
|
|
232
|
+
i++;
|
|
233
|
+
}
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (c === '[') {
|
|
237
|
+
inClass = true;
|
|
238
|
+
out[i] = ' ';
|
|
239
|
+
i++;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (c === ']') {
|
|
243
|
+
inClass = false;
|
|
244
|
+
out[i] = ' ';
|
|
245
|
+
i++;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (c === '/' && !inClass) {
|
|
249
|
+
out[i] = ' ';
|
|
250
|
+
i++;
|
|
251
|
+
closed = true;
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
out[i] = ' ';
|
|
255
|
+
i++;
|
|
256
|
+
}
|
|
257
|
+
if (closed) {
|
|
258
|
+
while (i < len && /[a-zA-Z]/.test(source[i])) {
|
|
259
|
+
out[i] = ' ';
|
|
260
|
+
i++;
|
|
261
|
+
}
|
|
262
|
+
lastTokenIsValue = true;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
// Not actually a regex literal (never closed) — fall back to
|
|
266
|
+
// treating the '/' as division and re-scan normally from there.
|
|
267
|
+
i = start;
|
|
268
|
+
out[i] = source[i];
|
|
269
|
+
i++;
|
|
270
|
+
lastTokenIsValue = false;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
// Template interpolation brace tracking (real code inside ${...}).
|
|
274
|
+
if (top && top.state === 'interp') {
|
|
275
|
+
if (ch === '{') {
|
|
276
|
+
top.braceDepth++;
|
|
277
|
+
out[i] = ch;
|
|
278
|
+
i++;
|
|
279
|
+
lastTokenIsValue = false;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (ch === '}') {
|
|
283
|
+
if (top.braceDepth === 0) {
|
|
284
|
+
out[i] = '}';
|
|
285
|
+
top.state = 'literal';
|
|
286
|
+
i++;
|
|
287
|
+
lastTokenIsValue = false;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
top.braceDepth--;
|
|
291
|
+
out[i] = ch;
|
|
292
|
+
i++;
|
|
293
|
+
lastTokenIsValue = true;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Identifier / keyword.
|
|
298
|
+
if (/[A-Za-z_$]/.test(ch)) {
|
|
299
|
+
let j = i;
|
|
300
|
+
while (j < len && /[A-Za-z0-9_$]/.test(source[j]))
|
|
301
|
+
j++;
|
|
302
|
+
const word = source.slice(i, j);
|
|
303
|
+
for (let k = i; k < j; k++)
|
|
304
|
+
out[k] = source[k];
|
|
305
|
+
lastTokenIsValue = !REGEX_KEYWORD_CONTEXT_WORDS.has(word);
|
|
306
|
+
i = j;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
// Numeric literal.
|
|
310
|
+
if (/[0-9]/.test(ch)) {
|
|
311
|
+
let j = i;
|
|
312
|
+
while (j < len && /[0-9a-fA-FxXoObB.]/.test(source[j]))
|
|
313
|
+
j++;
|
|
314
|
+
for (let k = i; k < j; k++)
|
|
315
|
+
out[k] = source[k];
|
|
316
|
+
lastTokenIsValue = true;
|
|
317
|
+
i = j;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (ch === ')' || ch === ']') {
|
|
321
|
+
out[i] = ch;
|
|
322
|
+
i++;
|
|
323
|
+
lastTokenIsValue = true;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (ch === '\n') {
|
|
327
|
+
out[i] = '\n';
|
|
328
|
+
i++;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
out[i] = ch;
|
|
332
|
+
i++;
|
|
333
|
+
if (!/\s/.test(ch))
|
|
334
|
+
lastTokenIsValue = false;
|
|
335
|
+
}
|
|
336
|
+
if (templateStack.length > 0) {
|
|
337
|
+
return { ok: false, reason: exports.REASON.REFACTOR_ANALYZER_UNPARSEABLE };
|
|
338
|
+
}
|
|
339
|
+
return { ok: true, stripped: out.join('') };
|
|
340
|
+
}
|
|
341
|
+
// ─── analyzeSource ──────────────────────────────────────────────────────────
|
|
342
|
+
const DECISION_KEYWORDS = new Set(['if', 'for', 'while', 'do', 'case', 'catch']);
|
|
343
|
+
// Words that can textually precede `(...) {` without being a method/function
|
|
344
|
+
// definition — must be excluded from the method-shorthand heuristic so
|
|
345
|
+
// `if (x) {` / `switch (x) {` etc. are never mistaken for a function.
|
|
346
|
+
const NON_METHOD_WORDS = new Set([...DECISION_KEYWORDS, 'switch', 'with']);
|
|
347
|
+
function findMatchingParen(s, openIdx) {
|
|
348
|
+
let depth = 0;
|
|
349
|
+
for (let k = openIdx; k < s.length; k++) {
|
|
350
|
+
if (s[k] === '(')
|
|
351
|
+
depth++;
|
|
352
|
+
else if (s[k] === ')') {
|
|
353
|
+
depth--;
|
|
354
|
+
if (depth === 0)
|
|
355
|
+
return k;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return -1;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Attempts to skip a generic type-parameter list starting at `s[idx] === '<'`
|
|
362
|
+
* (e.g. `<T>`, `<T extends X>`, `<T = Y>`, `<T, U>`, nested `<T extends
|
|
363
|
+
* Record<string, X>>`). Returns the index just past the matching top-level
|
|
364
|
+
* `>`, or -1 when the content is not shaped like one — either the brackets
|
|
365
|
+
* never balance, or an operator token that never appears inside a type
|
|
366
|
+
* parameter list (`&&`, `||`, `==`, `<=`, `>=`, arithmetic, a bare `?`)
|
|
367
|
+
* shows up first. This — plus the caller only ever committing to the skip
|
|
368
|
+
* when a `(` immediately follows the close — is what keeps a `<` used as a
|
|
369
|
+
* less-than comparison from being mistaken for generics: a real comparison
|
|
370
|
+
* either fails to balance before an unrelated bracket closes, contains one
|
|
371
|
+
* of the disallowed operator tokens, or is not immediately followed by a
|
|
372
|
+
* parameter list.
|
|
373
|
+
*/
|
|
374
|
+
function skipGenericParamList(s, idx) {
|
|
375
|
+
const len = s.length;
|
|
376
|
+
let i = idx + 1;
|
|
377
|
+
const stack = ['>'];
|
|
378
|
+
while (i < len) {
|
|
379
|
+
const ch = s[i];
|
|
380
|
+
const next = s[i + 1];
|
|
381
|
+
if (ch === '=' && next === '>') {
|
|
382
|
+
i += 2;
|
|
383
|
+
continue;
|
|
384
|
+
} // nested function-type arrow (e.g. a default's `(a:X)=>Y`)
|
|
385
|
+
if ((ch === '=' && next === '=')
|
|
386
|
+
|| (ch === '<' && next === '=')
|
|
387
|
+
|| (ch === '>' && next === '=')
|
|
388
|
+
|| (ch === '&' && next === '&')
|
|
389
|
+
|| (ch === '|' && next === '|')
|
|
390
|
+
|| ch === '+' || ch === '*' || ch === '%' || ch === '!'
|
|
391
|
+
|| ch === '?' || ch === ';' || ch === '/' || ch === '-')
|
|
392
|
+
return -1;
|
|
393
|
+
if (ch === '<') {
|
|
394
|
+
stack.push('>');
|
|
395
|
+
i++;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (ch === '(') {
|
|
399
|
+
stack.push(')');
|
|
400
|
+
i++;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (ch === '[') {
|
|
404
|
+
stack.push(']');
|
|
405
|
+
i++;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (ch === '{') {
|
|
409
|
+
stack.push('}');
|
|
410
|
+
i++;
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (ch === '>' || ch === ')' || ch === ']' || ch === '}') {
|
|
414
|
+
if (stack.length === 0 || stack[stack.length - 1] !== ch)
|
|
415
|
+
return -1;
|
|
416
|
+
stack.pop();
|
|
417
|
+
i++;
|
|
418
|
+
if (stack.length === 0)
|
|
419
|
+
return i;
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
i++;
|
|
423
|
+
}
|
|
424
|
+
return -1;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Attempts to skip a return-type / type-predicate annotation starting at
|
|
428
|
+
* `s[idx] === ':'` (e.g. `: Promise<void>`, `: Record<string, X>`, `: A |
|
|
429
|
+
* B`, `: { a: number }`, `: string[]`, `: x is Foo`). Returns the index of
|
|
430
|
+
* the terminator that follows — a function-body `{`, an arrow `=>`, or a
|
|
431
|
+
* statement-ending `;` — never consuming the terminator itself, or -1 when
|
|
432
|
+
* the content is not shaped like a type. `expectStart` tracks whether a
|
|
433
|
+
* fresh type token may begin here so an object-type literal's own `{`/`}`
|
|
434
|
+
* (`: { a: number }`) is never mistaken for the function body, and an
|
|
435
|
+
* unexpected/unmatched closing bracket (e.g. the real end of an enclosing
|
|
436
|
+
* `switch`/ternary this `:` was actually a member of, not a return type)
|
|
437
|
+
* aborts rather than guesses.
|
|
438
|
+
*/
|
|
439
|
+
function skipTypeExpr(s, idx) {
|
|
440
|
+
const len = s.length;
|
|
441
|
+
let i = idx + 1;
|
|
442
|
+
const stack = [];
|
|
443
|
+
let expectStart = true;
|
|
444
|
+
while (i < len) {
|
|
445
|
+
const ch = s[i];
|
|
446
|
+
if (/\s/.test(ch)) {
|
|
447
|
+
i++;
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const next = s[i + 1];
|
|
451
|
+
if (stack.length === 0) {
|
|
452
|
+
if (ch === ';')
|
|
453
|
+
return i;
|
|
454
|
+
if (ch === '=' && next === '>')
|
|
455
|
+
return i;
|
|
456
|
+
if (ch === '{' && !expectStart)
|
|
457
|
+
return i;
|
|
458
|
+
if (ch === ',' && !expectStart)
|
|
459
|
+
return i;
|
|
460
|
+
if (ch === '?')
|
|
461
|
+
return -1; // a bare top-level '?' means this was a ternary, not a return type
|
|
462
|
+
}
|
|
463
|
+
if (ch === '=' && next === '>') {
|
|
464
|
+
i += 2;
|
|
465
|
+
expectStart = true;
|
|
466
|
+
continue;
|
|
467
|
+
} // nested function-type arrow
|
|
468
|
+
if ((ch === '=' && next === '=')
|
|
469
|
+
|| (ch === '<' && next === '=')
|
|
470
|
+
|| (ch === '>' && next === '=')
|
|
471
|
+
|| (ch === '&' && next === '&')
|
|
472
|
+
|| (ch === '|' && next === '|')
|
|
473
|
+
|| ch === '+' || ch === '*' || ch === '%' || ch === '!')
|
|
474
|
+
return -1;
|
|
475
|
+
if (ch === '<') {
|
|
476
|
+
stack.push('>');
|
|
477
|
+
i++;
|
|
478
|
+
expectStart = true;
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
if (ch === '(') {
|
|
482
|
+
stack.push(')');
|
|
483
|
+
i++;
|
|
484
|
+
expectStart = true;
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (ch === '[') {
|
|
488
|
+
stack.push(']');
|
|
489
|
+
i++;
|
|
490
|
+
expectStart = true;
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
if (ch === '{') {
|
|
494
|
+
stack.push('}');
|
|
495
|
+
i++;
|
|
496
|
+
expectStart = true;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (ch === '>' || ch === ')' || ch === ']' || ch === '}') {
|
|
500
|
+
if (stack.length === 0 || stack[stack.length - 1] !== ch)
|
|
501
|
+
return -1;
|
|
502
|
+
stack.pop();
|
|
503
|
+
i++;
|
|
504
|
+
expectStart = false;
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
if (ch === '|' || ch === '&' || ch === ',' || ch === '.' || ch === ':') {
|
|
508
|
+
i++;
|
|
509
|
+
expectStart = true;
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
if (/[A-Za-z0-9_$]/.test(ch)) {
|
|
513
|
+
let j = i;
|
|
514
|
+
while (j < len && /[A-Za-z0-9_$]/.test(s[j]))
|
|
515
|
+
j++;
|
|
516
|
+
i = j;
|
|
517
|
+
expectStart = false;
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
i++;
|
|
521
|
+
}
|
|
522
|
+
return -1;
|
|
523
|
+
}
|
|
524
|
+
function newlinesBetween(s, from, to) {
|
|
525
|
+
let n = 0;
|
|
526
|
+
for (let k = from; k < to; k++)
|
|
527
|
+
if (s[k] === '\n')
|
|
528
|
+
n++;
|
|
529
|
+
return n;
|
|
530
|
+
}
|
|
531
|
+
/** Best-effort name inference for an arrow function from `NAME = ` or `NAME: ` immediately preceding it. */
|
|
532
|
+
function inferArrowName(s, pos) {
|
|
533
|
+
let j = pos - 1;
|
|
534
|
+
while (j >= 0 && /\s/.test(s[j]))
|
|
535
|
+
j--;
|
|
536
|
+
const isPlainEquals = j >= 0 && s[j] === '='
|
|
537
|
+
&& s[j - 1] !== '=' && s[j - 1] !== '!' && s[j - 1] !== '<' && s[j - 1] !== '>';
|
|
538
|
+
const isPropColon = j >= 0 && s[j] === ':';
|
|
539
|
+
if (!isPlainEquals && !isPropColon)
|
|
540
|
+
return '';
|
|
541
|
+
j--;
|
|
542
|
+
while (j >= 0 && /\s/.test(s[j]))
|
|
543
|
+
j--;
|
|
544
|
+
const end = j + 1;
|
|
545
|
+
while (j >= 0 && /[A-Za-z0-9_$]/.test(s[j]))
|
|
546
|
+
j--;
|
|
547
|
+
return s.slice(j + 1, end);
|
|
548
|
+
}
|
|
549
|
+
function scanFunctions(stripped) {
|
|
550
|
+
const len = stripped.length;
|
|
551
|
+
let i = 0;
|
|
552
|
+
let line = 1;
|
|
553
|
+
let depth = 0;
|
|
554
|
+
const stack = [];
|
|
555
|
+
const results = [];
|
|
556
|
+
function currentFunctionFrame() {
|
|
557
|
+
for (let k = stack.length - 1; k >= 0; k--) {
|
|
558
|
+
const f = stack[k];
|
|
559
|
+
if (f.kind === 'function')
|
|
560
|
+
return f;
|
|
561
|
+
}
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
function bump() {
|
|
565
|
+
const f = currentFunctionFrame();
|
|
566
|
+
if (f)
|
|
567
|
+
f.score += 1;
|
|
568
|
+
}
|
|
569
|
+
function closeExprFramesIfNeeded(atDepth) {
|
|
570
|
+
for (;;) {
|
|
571
|
+
const top = stack[stack.length - 1];
|
|
572
|
+
if (top && top.kind === 'function' && top.mode === 'expr' && top.entryDepth === atDepth) {
|
|
573
|
+
stack.pop();
|
|
574
|
+
results.push({ name: top.name, startLine: top.startLine, endLine: line, score: top.score });
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
while (i < len) {
|
|
582
|
+
const ch = stripped[i];
|
|
583
|
+
if (ch === '\n') {
|
|
584
|
+
line++;
|
|
585
|
+
i++;
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
if (/\s/.test(ch)) {
|
|
589
|
+
i++;
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
if (ch === '&' && stripped[i + 1] === '&') {
|
|
593
|
+
bump();
|
|
594
|
+
i += 2;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
if (ch === '|' && stripped[i + 1] === '|') {
|
|
598
|
+
bump();
|
|
599
|
+
i += 2;
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (ch === '?') {
|
|
603
|
+
if (stripped[i + 1] === '.') {
|
|
604
|
+
i += 2;
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
if (stripped[i + 1] === '?') {
|
|
608
|
+
i += 2;
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
bump();
|
|
612
|
+
i += 1;
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
if (ch === '(') {
|
|
616
|
+
const closeIdx = findMatchingParen(stripped, i);
|
|
617
|
+
if (closeIdx !== -1) {
|
|
618
|
+
let k = closeIdx + 1;
|
|
619
|
+
while (k < len && /\s/.test(stripped[k]))
|
|
620
|
+
k++;
|
|
621
|
+
if (stripped[k] === ':') {
|
|
622
|
+
const afterType = skipTypeExpr(stripped, k);
|
|
623
|
+
if (afterType !== -1) {
|
|
624
|
+
k = afterType;
|
|
625
|
+
while (k < len && /\s/.test(stripped[k]))
|
|
626
|
+
k++;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
if (stripped[k] === '=' && stripped[k + 1] === '>') {
|
|
630
|
+
const name = inferArrowName(stripped, i);
|
|
631
|
+
const startLine = line;
|
|
632
|
+
let bodyStart = k + 2;
|
|
633
|
+
while (bodyStart < len && /\s/.test(stripped[bodyStart]))
|
|
634
|
+
bodyStart++;
|
|
635
|
+
const nl = newlinesBetween(stripped, i, bodyStart);
|
|
636
|
+
if (stripped[bodyStart] === '{') {
|
|
637
|
+
line += nl;
|
|
638
|
+
stack.push({ kind: 'function', name, startLine, score: 1, mode: 'brace' });
|
|
639
|
+
depth++;
|
|
640
|
+
i = bodyStart + 1;
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
line += nl;
|
|
644
|
+
stack.push({ kind: 'function', name, startLine, score: 1, mode: 'expr', entryDepth: depth });
|
|
645
|
+
i = bodyStart;
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
depth++;
|
|
650
|
+
i++;
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
if (ch === '[') {
|
|
654
|
+
depth++;
|
|
655
|
+
i++;
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
if (ch === ')' || ch === ']') {
|
|
659
|
+
closeExprFramesIfNeeded(depth);
|
|
660
|
+
depth--;
|
|
661
|
+
i++;
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
if (ch === '{') {
|
|
665
|
+
stack.push({ kind: 'block' });
|
|
666
|
+
depth++;
|
|
667
|
+
i++;
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
if (ch === '}') {
|
|
671
|
+
closeExprFramesIfNeeded(depth);
|
|
672
|
+
depth--;
|
|
673
|
+
const top = stack.pop();
|
|
674
|
+
if (top && top.kind === 'function' && top.mode === 'brace') {
|
|
675
|
+
results.push({ name: top.name, startLine: top.startLine, endLine: line, score: top.score });
|
|
676
|
+
}
|
|
677
|
+
i++;
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
if (ch === ';' || ch === ',') {
|
|
681
|
+
closeExprFramesIfNeeded(depth);
|
|
682
|
+
i++;
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
if (/[A-Za-z_$]/.test(ch)) {
|
|
686
|
+
let j = i;
|
|
687
|
+
while (j < len && /[A-Za-z0-9_$]/.test(stripped[j]))
|
|
688
|
+
j++;
|
|
689
|
+
const word = stripped.slice(i, j);
|
|
690
|
+
const wordLine = line;
|
|
691
|
+
if (word === 'function') {
|
|
692
|
+
let k = j;
|
|
693
|
+
while (k < len && /\s/.test(stripped[k]))
|
|
694
|
+
k++;
|
|
695
|
+
if (stripped[k] === '*') {
|
|
696
|
+
k++;
|
|
697
|
+
while (k < len && /\s/.test(stripped[k]))
|
|
698
|
+
k++;
|
|
699
|
+
}
|
|
700
|
+
let name = '';
|
|
701
|
+
if (k < len && /[A-Za-z_$]/.test(stripped[k])) {
|
|
702
|
+
let m = k;
|
|
703
|
+
while (m < len && /[A-Za-z0-9_$]/.test(stripped[m]))
|
|
704
|
+
m++;
|
|
705
|
+
name = stripped.slice(k, m);
|
|
706
|
+
k = m;
|
|
707
|
+
while (k < len && /\s/.test(stripped[k]))
|
|
708
|
+
k++;
|
|
709
|
+
}
|
|
710
|
+
if (stripped[k] === '<') {
|
|
711
|
+
const afterGenerics = skipGenericParamList(stripped, k);
|
|
712
|
+
if (afterGenerics !== -1) {
|
|
713
|
+
let kk = afterGenerics;
|
|
714
|
+
while (kk < len && /\s/.test(stripped[kk]))
|
|
715
|
+
kk++;
|
|
716
|
+
if (stripped[kk] === '(')
|
|
717
|
+
k = kk; // only commit if genuinely followed by params
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (stripped[k] === '(') {
|
|
721
|
+
const closeIdx = findMatchingParen(stripped, k);
|
|
722
|
+
if (closeIdx !== -1) {
|
|
723
|
+
let p = closeIdx + 1;
|
|
724
|
+
while (p < len && /\s/.test(stripped[p]))
|
|
725
|
+
p++;
|
|
726
|
+
if (stripped[p] === ':') {
|
|
727
|
+
const afterType = skipTypeExpr(stripped, p);
|
|
728
|
+
if (afterType !== -1) {
|
|
729
|
+
p = afterType;
|
|
730
|
+
while (p < len && /\s/.test(stripped[p]))
|
|
731
|
+
p++;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (stripped[p] === '{') {
|
|
735
|
+
line += newlinesBetween(stripped, i, p);
|
|
736
|
+
stack.push({ kind: 'function', name, startLine: wordLine, score: 1, mode: 'brace' });
|
|
737
|
+
depth++;
|
|
738
|
+
i = p + 1;
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
i = j;
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
if (NON_METHOD_WORDS.has(word)) {
|
|
747
|
+
if (DECISION_KEYWORDS.has(word))
|
|
748
|
+
bump();
|
|
749
|
+
i = j;
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
{
|
|
753
|
+
let k = j;
|
|
754
|
+
while (k < len && /\s/.test(stripped[k]))
|
|
755
|
+
k++;
|
|
756
|
+
if (stripped[k] === '<') {
|
|
757
|
+
const afterGenerics = skipGenericParamList(stripped, k);
|
|
758
|
+
if (afterGenerics !== -1) {
|
|
759
|
+
let kk = afterGenerics;
|
|
760
|
+
while (kk < len && /\s/.test(stripped[kk]))
|
|
761
|
+
kk++;
|
|
762
|
+
if (stripped[kk] === '(')
|
|
763
|
+
k = kk; // only commit if genuinely followed by params
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (stripped[k] === '(') {
|
|
767
|
+
const closeIdx = findMatchingParen(stripped, k);
|
|
768
|
+
if (closeIdx !== -1) {
|
|
769
|
+
let p = closeIdx + 1;
|
|
770
|
+
while (p < len && /\s/.test(stripped[p]))
|
|
771
|
+
p++;
|
|
772
|
+
if (stripped[p] === ':') {
|
|
773
|
+
const afterType = skipTypeExpr(stripped, p);
|
|
774
|
+
if (afterType !== -1) {
|
|
775
|
+
p = afterType;
|
|
776
|
+
while (p < len && /\s/.test(stripped[p]))
|
|
777
|
+
p++;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
if (stripped[p] === '{') {
|
|
781
|
+
line += newlinesBetween(stripped, i, p);
|
|
782
|
+
stack.push({ kind: 'function', name: word, startLine: wordLine, score: 1, mode: 'brace' });
|
|
783
|
+
depth++;
|
|
784
|
+
i = p + 1;
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
else if (stripped[k] === '=' && stripped[k + 1] === '>') {
|
|
790
|
+
const name = inferArrowName(stripped, i);
|
|
791
|
+
let bodyStart = k + 2;
|
|
792
|
+
while (bodyStart < len && /\s/.test(stripped[bodyStart]))
|
|
793
|
+
bodyStart++;
|
|
794
|
+
const nl = newlinesBetween(stripped, i, bodyStart);
|
|
795
|
+
if (stripped[bodyStart] === '{') {
|
|
796
|
+
line += nl;
|
|
797
|
+
stack.push({ kind: 'function', name, startLine: wordLine, score: 1, mode: 'brace' });
|
|
798
|
+
depth++;
|
|
799
|
+
i = bodyStart + 1;
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
line += nl;
|
|
803
|
+
stack.push({ kind: 'function', name, startLine: wordLine, score: 1, mode: 'expr', entryDepth: depth });
|
|
804
|
+
i = bodyStart;
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
i = j;
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
i++;
|
|
812
|
+
}
|
|
813
|
+
closeExprFramesIfNeeded(depth);
|
|
814
|
+
return results;
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Normalizes CRLF -> LF *before* analysis so scores and line numbers are
|
|
818
|
+
* identical either way. Base score 1 per function, +1 for each of: if,
|
|
819
|
+
* else-if (the if, not the else), for, for..of, for..in, while, do, case
|
|
820
|
+
* (not default), catch, &&, ||, ?:. Word-boundary matched. Not counted:
|
|
821
|
+
* ?., ??, bare else, default:. Decision points are attributed to the
|
|
822
|
+
* innermost enclosing function; top-level code belongs to no function.
|
|
823
|
+
*/
|
|
824
|
+
function analyzeSource(source) {
|
|
825
|
+
const normalized = source.replace(/\r\n/g, '\n');
|
|
826
|
+
const stripped = stripLiterals(normalized);
|
|
827
|
+
if (!stripped.ok)
|
|
828
|
+
return { ok: false, reason: stripped.reason };
|
|
829
|
+
return { ok: true, method: 'decision-points', functions: scanFunctions(stripped.stripped) };
|
|
830
|
+
}
|
|
831
|
+
// ─── isAnalyzablePath ───────────────────────────────────────────────────────
|
|
832
|
+
/**
|
|
833
|
+
* Normalizes separators unconditionally (never via path.sep). True iff the
|
|
834
|
+
* extension is analyzable and the path is not under tests/ or
|
|
835
|
+
* gsd-core/bin/lib/.
|
|
836
|
+
*/
|
|
837
|
+
function isAnalyzablePath(relPath) {
|
|
838
|
+
const normalized = relPath.replace(/\\/g, '/');
|
|
839
|
+
const ext = node_path_1.default.posix.extname(normalized);
|
|
840
|
+
if (!exports.ANALYZABLE_EXTENSIONS.includes(ext))
|
|
841
|
+
return false;
|
|
842
|
+
if (normalized === 'tests' || normalized.startsWith('tests/'))
|
|
843
|
+
return false;
|
|
844
|
+
if (normalized === 'gsd-core/bin/lib' || normalized.startsWith('gsd-core/bin/lib/'))
|
|
845
|
+
return false;
|
|
846
|
+
return true;
|
|
847
|
+
}
|
|
848
|
+
// ─── evaluateCandidates ─────────────────────────────────────────────────────
|
|
849
|
+
function coercePositiveNumber(v, fallback) {
|
|
850
|
+
const n = typeof v === 'number' ? v : Number(v);
|
|
851
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
852
|
+
}
|
|
853
|
+
function evaluateCandidates(input) {
|
|
854
|
+
const thresholdUsed = coercePositiveNumber(input.threshold, exports.DEFAULTS.threshold);
|
|
855
|
+
const jumpDeltaUsed = coercePositiveNumber(input.jumpDelta, exports.DEFAULTS.jumpDelta);
|
|
856
|
+
const candidates = [];
|
|
857
|
+
const skipped = [];
|
|
858
|
+
for (const entry of input.analyzed) {
|
|
859
|
+
if (!entry.ok) {
|
|
860
|
+
skipped.push({ file: entry.file, reason: entry.reason });
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
for (const fn of entry.functions) {
|
|
864
|
+
const key = `${entry.file}::${fn.name}`;
|
|
865
|
+
const baselineEntry = input.baseline[key];
|
|
866
|
+
const baselineScore = baselineEntry ? baselineEntry.score : null;
|
|
867
|
+
const reasons = [];
|
|
868
|
+
if (fn.score > thresholdUsed)
|
|
869
|
+
reasons.push('threshold');
|
|
870
|
+
let delta = null;
|
|
871
|
+
if (baselineScore !== null) {
|
|
872
|
+
delta = fn.score - baselineScore;
|
|
873
|
+
if (delta > jumpDeltaUsed)
|
|
874
|
+
reasons.push('jump');
|
|
875
|
+
}
|
|
876
|
+
if (reasons.length > 0) {
|
|
877
|
+
candidates.push({
|
|
878
|
+
file: entry.file,
|
|
879
|
+
name: fn.name,
|
|
880
|
+
startLine: fn.startLine,
|
|
881
|
+
score: fn.score,
|
|
882
|
+
baseline: baselineScore,
|
|
883
|
+
delta,
|
|
884
|
+
reasons,
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
candidates.sort((a, b) => {
|
|
890
|
+
if (b.score !== a.score)
|
|
891
|
+
return b.score - a.score;
|
|
892
|
+
const ad = a.delta === null ? -Infinity : a.delta;
|
|
893
|
+
const bd = b.delta === null ? -Infinity : b.delta;
|
|
894
|
+
if (bd !== ad)
|
|
895
|
+
return bd - ad;
|
|
896
|
+
if (a.file !== b.file)
|
|
897
|
+
return a.file < b.file ? -1 : 1;
|
|
898
|
+
if (a.name !== b.name)
|
|
899
|
+
return a.name < b.name ? -1 : 1;
|
|
900
|
+
return a.startLine - b.startLine;
|
|
901
|
+
});
|
|
902
|
+
return {
|
|
903
|
+
verdict: candidates.length > 0 ? exports.VERDICT.TRIGGERED : exports.VERDICT.BELOW_THRESHOLD,
|
|
904
|
+
candidates,
|
|
905
|
+
target: candidates[0] ?? null,
|
|
906
|
+
skipped,
|
|
907
|
+
thresholdUsed,
|
|
908
|
+
jumpDeltaUsed,
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
// ─── Baseline anchor bookkeeping ────────────────────────────────────────────
|
|
912
|
+
/**
|
|
913
|
+
* Anchor semantics (see 41-api-contract.md "Anchor semantics" — corrected
|
|
914
|
+
* 2026-08-09): for each successfully analyzed function, no prior entry
|
|
915
|
+
* inserts an anchor at the current score (first observation); a prior
|
|
916
|
+
* entry carries forward UNCHANGED. The anchor never advances on a plain
|
|
917
|
+
* evaluate, whether or not the function triggered — only reanchorBaseline
|
|
918
|
+
* (called by an explicit disposition) moves it.
|
|
919
|
+
*/
|
|
920
|
+
function nextBaseline(prev, analyzed, opts = {}) {
|
|
921
|
+
const next = {};
|
|
922
|
+
const seenKeys = new Set();
|
|
923
|
+
for (const entry of analyzed) {
|
|
924
|
+
if (!entry.ok)
|
|
925
|
+
continue;
|
|
926
|
+
for (const fn of entry.functions) {
|
|
927
|
+
const key = `${entry.file}::${fn.name}`;
|
|
928
|
+
seenKeys.add(key);
|
|
929
|
+
const existing = prev[key];
|
|
930
|
+
if (existing) {
|
|
931
|
+
next[key] = existing;
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
next[key] = opts.phase ? { score: fn.score, phase: opts.phase } : { score: fn.score };
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
const analyzedFileSet = new Set(opts.analyzedFiles ?? []);
|
|
939
|
+
for (const [key, entry] of Object.entries(prev)) {
|
|
940
|
+
if (seenKeys.has(key))
|
|
941
|
+
continue;
|
|
942
|
+
const file = key.slice(0, key.lastIndexOf('::'));
|
|
943
|
+
if (analyzedFileSet.has(file))
|
|
944
|
+
continue; // function no longer exists -> prune
|
|
945
|
+
next[key] = entry; // file untouched this run -> never prune
|
|
946
|
+
}
|
|
947
|
+
return next;
|
|
948
|
+
}
|
|
949
|
+
/** Moves the anchor for `key` to `score` — accept re-anchors to the post-refactor score, decline re-anchors to the current (higher) score. */
|
|
950
|
+
function reanchorBaseline(prev, key, score, opts = {}) {
|
|
951
|
+
return {
|
|
952
|
+
...prev,
|
|
953
|
+
[key]: opts.phase ? { score, phase: opts.phase } : { score },
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
// ─── Proposal render/parse ──────────────────────────────────────────────────
|
|
957
|
+
const PROPOSAL_JSON_FENCE_OPEN = '````json';
|
|
958
|
+
const PROPOSAL_JSON_FENCE_CLOSE = '````';
|
|
959
|
+
// Reader-side fence tolerance (#3657 — same defect class as the WINDOWS.md
|
|
960
|
+
// ledger): CommonMark formatters (Prettier et al.) narrow the written
|
|
961
|
+
// 4-backtick fence to the shortest legal width (3) whenever the body holds no
|
|
962
|
+
// backtick run, and a canonical-JSON candidates array never does. Locate the
|
|
963
|
+
// block by a line-anchored 3+ fence and close on a run at least as wide
|
|
964
|
+
// (CommonMark: a shorter run does not close). The writer above is unchanged.
|
|
965
|
+
// This module is a leaf (CONTEXT.md — imports only node:fs/node:path), so the
|
|
966
|
+
// span logic is local rather than imported from broken-windows.
|
|
967
|
+
const PROPOSAL_FENCE_OPEN_RE = /^(`{3,})json[ \t]*\r?$/m;
|
|
968
|
+
function locateProposalJsonBlock(text) {
|
|
969
|
+
const open = text.match(PROPOSAL_FENCE_OPEN_RE);
|
|
970
|
+
if (!open || open.index === undefined)
|
|
971
|
+
return null;
|
|
972
|
+
const width = open[1].length;
|
|
973
|
+
const bodyStart = open.index + open[0].length;
|
|
974
|
+
for (const close of text.slice(bodyStart).matchAll(/^(`{3,})[ \t]*\r?$/gm)) {
|
|
975
|
+
if (close[1].length < width)
|
|
976
|
+
continue;
|
|
977
|
+
const bodyEnd = close.index ?? 0;
|
|
978
|
+
return { jsonText: text.slice(bodyStart, bodyStart + bodyEnd).trim() };
|
|
979
|
+
}
|
|
980
|
+
return null;
|
|
981
|
+
}
|
|
982
|
+
function renderProposal(p) {
|
|
983
|
+
const fm = [
|
|
984
|
+
'---',
|
|
985
|
+
`schema_version: ${p.schema_version}`,
|
|
986
|
+
`status: ${p.status}`,
|
|
987
|
+
`phase: ${p.phase}`,
|
|
988
|
+
`target_file: ${p.target_file}`,
|
|
989
|
+
`target_function: ${p.target_function}`,
|
|
990
|
+
`score: ${p.score}`,
|
|
991
|
+
`baseline: ${p.baseline === null ? 'null' : p.baseline}`,
|
|
992
|
+
`delta: ${p.delta === null ? 'null' : p.delta}`,
|
|
993
|
+
`metric: ${p.metric}`,
|
|
994
|
+
`recorded_at: ${p.recorded_at}`,
|
|
995
|
+
`resolved_at: ${p.resolved_at === null ? 'null' : p.resolved_at}`,
|
|
996
|
+
`reason: ${JSON.stringify(p.reason)}`,
|
|
997
|
+
'---',
|
|
998
|
+
'',
|
|
999
|
+
].join('\n');
|
|
1000
|
+
const header = [
|
|
1001
|
+
`# Refactor proposal: ${p.target_file}::${p.target_function}`,
|
|
1002
|
+
'',
|
|
1003
|
+
`Score ${p.score}${p.baseline !== null ? ` (baseline ${p.baseline}, delta ${p.delta})` : ''} — status: ${p.status}.`,
|
|
1004
|
+
'',
|
|
1005
|
+
].join('\n');
|
|
1006
|
+
const jsonBlock = [
|
|
1007
|
+
PROPOSAL_JSON_FENCE_OPEN,
|
|
1008
|
+
JSON.stringify(p.candidates, null, 2),
|
|
1009
|
+
PROPOSAL_JSON_FENCE_CLOSE,
|
|
1010
|
+
'',
|
|
1011
|
+
].join('\n');
|
|
1012
|
+
return [fm, header, jsonBlock].join('\n');
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Parses a rendered proposal back into its IR. Frontmatter is the fast
|
|
1016
|
+
* scalar path; the JSON block is authoritative for candidates. Fails
|
|
1017
|
+
* closed (returns null) on any structural drift rather than guessing.
|
|
1018
|
+
*/
|
|
1019
|
+
function parseProposal(text) {
|
|
1020
|
+
try {
|
|
1021
|
+
if (!text.startsWith('---\n') && !text.startsWith('---\r\n'))
|
|
1022
|
+
return null;
|
|
1023
|
+
const headerEnd = text.startsWith('---\r\n') ? 5 : 4;
|
|
1024
|
+
const closeIdx = text.indexOf('\n---', headerEnd);
|
|
1025
|
+
if (closeIdx === -1)
|
|
1026
|
+
return null;
|
|
1027
|
+
const yamlBody = text.slice(headerEnd, closeIdx);
|
|
1028
|
+
const fm = {};
|
|
1029
|
+
for (const rawLine of yamlBody.split(/\r?\n/)) {
|
|
1030
|
+
const line = rawLine.replace(/\r$/, '');
|
|
1031
|
+
if (line.trim() === '')
|
|
1032
|
+
continue;
|
|
1033
|
+
const m = line.match(/^([a-zA-Z0-9_]+):\s*(.*)$/);
|
|
1034
|
+
if (!m)
|
|
1035
|
+
return null;
|
|
1036
|
+
fm[m[1]] = m[2].trim();
|
|
1037
|
+
}
|
|
1038
|
+
const span = locateProposalJsonBlock(text);
|
|
1039
|
+
if (span === null)
|
|
1040
|
+
return null;
|
|
1041
|
+
const jsonText = span.jsonText;
|
|
1042
|
+
let candidates;
|
|
1043
|
+
try {
|
|
1044
|
+
candidates = JSON.parse(jsonText);
|
|
1045
|
+
}
|
|
1046
|
+
catch {
|
|
1047
|
+
return null;
|
|
1048
|
+
}
|
|
1049
|
+
if (!Array.isArray(candidates))
|
|
1050
|
+
return null;
|
|
1051
|
+
const status = fm.status;
|
|
1052
|
+
if (status !== 'proposed' && status !== 'accepted' && status !== 'declined')
|
|
1053
|
+
return null;
|
|
1054
|
+
const schemaVersion = Number(fm.schema_version);
|
|
1055
|
+
if (!Number.isInteger(schemaVersion))
|
|
1056
|
+
return null;
|
|
1057
|
+
const score = Number(fm.score);
|
|
1058
|
+
if (!Number.isFinite(score))
|
|
1059
|
+
return null;
|
|
1060
|
+
const baseline = fm.baseline === 'null' ? null : Number(fm.baseline);
|
|
1061
|
+
if (baseline !== null && !Number.isFinite(baseline))
|
|
1062
|
+
return null;
|
|
1063
|
+
const delta = fm.delta === 'null' ? null : Number(fm.delta);
|
|
1064
|
+
if (delta !== null && !Number.isFinite(delta))
|
|
1065
|
+
return null;
|
|
1066
|
+
const resolvedAt = fm.resolved_at === 'null' || fm.resolved_at === undefined ? null : fm.resolved_at;
|
|
1067
|
+
let reason = '';
|
|
1068
|
+
try {
|
|
1069
|
+
const parsedReason = JSON.parse(fm.reason ?? '""');
|
|
1070
|
+
reason = typeof parsedReason === 'string' ? parsedReason : (fm.reason ?? '');
|
|
1071
|
+
}
|
|
1072
|
+
catch {
|
|
1073
|
+
reason = fm.reason ?? '';
|
|
1074
|
+
}
|
|
1075
|
+
return {
|
|
1076
|
+
schema_version: schemaVersion,
|
|
1077
|
+
status,
|
|
1078
|
+
phase: fm.phase ?? '',
|
|
1079
|
+
target_file: fm.target_file ?? '',
|
|
1080
|
+
target_function: fm.target_function ?? '',
|
|
1081
|
+
score,
|
|
1082
|
+
baseline,
|
|
1083
|
+
delta,
|
|
1084
|
+
metric: 'decision-points',
|
|
1085
|
+
recorded_at: fm.recorded_at ?? '',
|
|
1086
|
+
resolved_at: resolvedAt,
|
|
1087
|
+
reason,
|
|
1088
|
+
candidates: candidates,
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
catch {
|
|
1092
|
+
return null;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
function baselinePath(planningDir) {
|
|
1096
|
+
return node_path_1.default.join(planningDir, exports.BASELINE_FILE_NAME);
|
|
1097
|
+
}
|
|
1098
|
+
const RENAME_RETRY_ERRNOS = new Set(['EPERM', 'EBUSY', 'EACCES']);
|
|
1099
|
+
const RENAME_MAX_ATTEMPTS = 5;
|
|
1100
|
+
const RENAME_BACKOFF_MS = 25;
|
|
1101
|
+
function renameWithRetry(fsImpl, tmp, target) {
|
|
1102
|
+
let lastErr;
|
|
1103
|
+
for (let attempt = 0; attempt < RENAME_MAX_ATTEMPTS; attempt++) {
|
|
1104
|
+
try {
|
|
1105
|
+
fsImpl.renameSync(tmp, target);
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
catch (err) {
|
|
1109
|
+
lastErr = err;
|
|
1110
|
+
const code = (err && typeof err === 'object' && 'code' in err)
|
|
1111
|
+
? String(err.code)
|
|
1112
|
+
: '';
|
|
1113
|
+
if (code && RENAME_RETRY_ERRNOS.has(code) && attempt < RENAME_MAX_ATTEMPTS - 1) {
|
|
1114
|
+
const delay = RENAME_BACKOFF_MS * Math.pow(2, attempt);
|
|
1115
|
+
const start = Date.now();
|
|
1116
|
+
while (Date.now() - start < delay) {
|
|
1117
|
+
// Busy-wait a very short time — transient locks usually clear quickly.
|
|
1118
|
+
}
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
throw err;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
throw lastErr;
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Degrades to { ok:false, baseline:{}, reason: REFACTOR_BASELINE_MALFORMED }
|
|
1128
|
+
* on absent-but-unreadable, bad JSON, or a non-plain-object root — and
|
|
1129
|
+
* never rewrites the file on a failed read. Missing file is
|
|
1130
|
+
* { ok:true, baseline:{} } with no reason. Never throws.
|
|
1131
|
+
*/
|
|
1132
|
+
function readBaseline(planningDir, deps = {}) {
|
|
1133
|
+
const fsImpl = deps.fs ?? node_fs_1.default;
|
|
1134
|
+
const p = baselinePath(planningDir);
|
|
1135
|
+
let raw;
|
|
1136
|
+
try {
|
|
1137
|
+
raw = fsImpl.readFileSync(p, 'utf8');
|
|
1138
|
+
}
|
|
1139
|
+
catch (e) {
|
|
1140
|
+
const code = (e && typeof e === 'object' && 'code' in e)
|
|
1141
|
+
? String(e.code)
|
|
1142
|
+
: '';
|
|
1143
|
+
if (code === 'ENOENT')
|
|
1144
|
+
return { ok: true, baseline: {} };
|
|
1145
|
+
return { ok: false, baseline: {}, reason: exports.REASON.REFACTOR_BASELINE_MALFORMED };
|
|
1146
|
+
}
|
|
1147
|
+
let parsed;
|
|
1148
|
+
try {
|
|
1149
|
+
parsed = JSON.parse(raw);
|
|
1150
|
+
}
|
|
1151
|
+
catch {
|
|
1152
|
+
return { ok: false, baseline: {}, reason: exports.REASON.REFACTOR_BASELINE_MALFORMED };
|
|
1153
|
+
}
|
|
1154
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
1155
|
+
return { ok: false, baseline: {}, reason: exports.REASON.REFACTOR_BASELINE_MALFORMED };
|
|
1156
|
+
}
|
|
1157
|
+
return { ok: true, baseline: parsed };
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Writes `<name>.<pid>.tmp` then renames; on any failure unlinks the temp
|
|
1161
|
+
* file and returns { ok:false, reason: REFACTOR_BASELINE_WRITE_FAILED }.
|
|
1162
|
+
* Never throws.
|
|
1163
|
+
*/
|
|
1164
|
+
function writeBaseline(planningDir, baseline, deps = {}) {
|
|
1165
|
+
const fsImpl = deps.fs ?? node_fs_1.default;
|
|
1166
|
+
const p = baselinePath(planningDir);
|
|
1167
|
+
const tmp = `${p}.${process.pid}.tmp`;
|
|
1168
|
+
try {
|
|
1169
|
+
if (!fsImpl.existsSync(planningDir)) {
|
|
1170
|
+
fsImpl.mkdirSync(planningDir, { recursive: true });
|
|
1171
|
+
}
|
|
1172
|
+
fsImpl.writeFileSync(tmp, JSON.stringify(baseline, null, 2), 'utf8');
|
|
1173
|
+
}
|
|
1174
|
+
catch {
|
|
1175
|
+
try {
|
|
1176
|
+
fsImpl.unlinkSync(tmp);
|
|
1177
|
+
}
|
|
1178
|
+
catch { /* best-effort cleanup */ }
|
|
1179
|
+
return { ok: false, reason: exports.REASON.REFACTOR_BASELINE_WRITE_FAILED };
|
|
1180
|
+
}
|
|
1181
|
+
try {
|
|
1182
|
+
renameWithRetry(fsImpl, tmp, p);
|
|
1183
|
+
}
|
|
1184
|
+
catch {
|
|
1185
|
+
try {
|
|
1186
|
+
fsImpl.unlinkSync(tmp);
|
|
1187
|
+
}
|
|
1188
|
+
catch { /* best-effort cleanup */ }
|
|
1189
|
+
return { ok: false, reason: exports.REASON.REFACTOR_BASELINE_WRITE_FAILED };
|
|
1190
|
+
}
|
|
1191
|
+
return { ok: true };
|
|
1192
|
+
}
|