tumwater 0.4.2 → 0.4.4
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/README.md +6 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +6 -6
- package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
- package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
- package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
- package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
- package/dist/src/agent/backend-traits.js +2 -2
- package/dist/src/agent/claude/claude-account.js +6 -27
- package/dist/src/agent/claude/claude-backend.js +3 -3
- package/dist/src/agent/claude/claude-env.js +27 -31
- package/dist/src/agent/claude/claude-stream.js +111 -54
- package/dist/src/agent/codex/codex-account.js +8 -0
- package/dist/src/agent/codex/codex-args.js +5 -5
- package/dist/src/agent/codex/codex-backend.js +2 -2
- package/dist/src/agent/codex/codex-env.js +28 -15
- package/dist/src/agent/codex/codex-hook-trust.js +39 -20
- package/dist/src/agent/codex/codex-rollout.js +93 -56
- package/dist/src/agent/codex/codex-stream.js +1 -1
- package/dist/src/agent/env-scrub.js +18 -0
- package/dist/src/agent/image-input.js +68 -11
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +26 -24
- package/dist/src/agent/opencode/opencode-catalog.js +85 -36
- package/dist/src/agent/opencode/opencode-config.js +32 -14
- package/dist/src/agent/opencode/opencode-env.js +12 -13
- package/dist/src/agent/opencode/opencode-stream.js +47 -25
- package/dist/src/agent/session-ledger.js +23 -27
- package/dist/src/agent/subscription-account.js +2 -83
- package/dist/src/backlog/backlog-archive.js +3 -4
- package/dist/src/backlog/backlog-cli-view.js +81 -41
- package/dist/src/backlog/backlog-cli.js +57 -26
- package/dist/src/backlog/backlog-eligibility.js +56 -122
- package/dist/src/backlog/backlog-md.js +5 -5
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
- package/dist/src/backlog/backlog-release.js +12 -12
- package/dist/src/backlog/backlog-structure.js +114 -61
- package/dist/src/backlog/backlog.js +3 -2
- package/dist/src/baseline/main-baseline.js +148 -84
- package/dist/src/baseline/main-guardrails.js +2 -2
- package/dist/src/baseline/main-watch.js +84 -50
- package/dist/src/brief/brief.js +26 -13
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +29 -65
- package/dist/src/budget/budget.js +4 -5
- package/dist/src/budget/fallback-breaker.js +2 -2
- package/dist/src/build/build-check-detect.js +43 -14
- package/dist/src/build/build-check-events.js +2 -2
- package/dist/src/build/build-check-report.js +10 -0
- package/dist/src/build/build-check-scoped.js +40 -18
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/build/build-stage.js +6 -4
- package/dist/src/change/change-data.js +1 -4
- package/dist/src/cli/cli-command-args.js +89 -52
- package/dist/src/cli/cli-completion-command.js +75 -0
- package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
- package/dist/src/cli/cli-doctor-command.js +103 -0
- package/dist/src/cli/cli-flag-specs.js +42 -22
- package/dist/src/cli/cli-init.js +36 -14
- package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
- package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
- package/dist/src/cli/cli-query-commands.js +2 -2
- package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
- package/dist/src/cli/cli-repo-gate.js +2 -2
- package/dist/src/cli/cli-role-command.js +2 -41
- package/dist/src/cli/cli-start-summary.js +4 -3
- package/dist/src/cli/cli-start-supervisor.js +190 -0
- package/dist/src/cli/cli-start.js +94 -244
- package/dist/src/cli/completion.js +85 -49
- package/dist/src/cli/follow-view.js +20 -2
- package/dist/src/cli/help.js +6 -13
- package/dist/src/cli.js +70 -176
- package/dist/src/collections.js +39 -27
- package/dist/src/concurrency/check-slots.js +70 -0
- package/dist/src/concurrency/lock.js +23 -1
- package/dist/src/concurrency/run-permit.js +49 -0
- package/dist/src/concurrency/semaphore.js +46 -250
- package/dist/src/config/config-editable-keys.js +4 -7
- package/dist/src/config/config-fallbacks.js +81 -101
- package/dist/src/config/config-field-checks.js +66 -37
- package/dist/src/config/config-keys.js +66 -40
- package/dist/src/config/config-live.js +4 -28
- package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
- package/dist/src/config/config-request.js +163 -102
- package/dist/src/config/config-schema.js +10 -12
- package/dist/src/config/config-selector-checks.js +1 -1
- package/dist/src/config/config-validation-agents.js +2 -2
- package/dist/src/config/config-validation-roles.js +8 -72
- package/dist/src/config/config-validation.js +4 -39
- package/dist/src/config/config-views.js +26 -50
- package/dist/src/config/config-write.js +128 -102
- package/dist/src/config/config.js +5 -14
- package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
- package/dist/src/doctor/doctor-backlog-plans.js +53 -120
- package/dist/src/doctor/doctor-backlog.js +16 -14
- package/dist/src/doctor/doctor-check-names.js +3 -6
- package/dist/src/doctor/doctor-checks.js +8 -8
- package/dist/src/doctor/doctor-claude-checks.js +70 -36
- package/dist/src/doctor/doctor-codex-checks.js +6 -6
- package/dist/src/doctor/doctor-contention-checks.js +57 -117
- package/dist/src/doctor/doctor-fallback-checks.js +48 -26
- package/dist/src/doctor/doctor-holds.js +8 -47
- package/dist/src/doctor/doctor-install.js +1 -1
- package/dist/src/doctor/doctor-launch-services.js +1 -1
- package/dist/src/doctor/doctor-model-checks.js +178 -94
- package/dist/src/doctor/doctor-notify.js +2 -2
- package/dist/src/doctor/doctor-opencode-checks.js +5 -4
- package/dist/src/doctor/doctor-orphans.js +122 -21
- package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
- package/dist/src/doctor/doctor-pi-checks.js +2 -2
- package/dist/src/doctor/doctor-principles.js +3 -3
- package/dist/src/doctor/doctor-project-checks.js +24 -11
- package/dist/src/doctor/doctor-role-notes.js +1 -1
- package/dist/src/doctor/doctor.js +8 -11
- package/dist/src/events/event-format.js +44 -64
- package/dist/src/events/event-labels.js +1 -5
- package/dist/src/events/event-legacy.js +3 -5
- package/dist/src/events/{notify.js → event-notify.js} +6 -6
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +8 -0
- package/dist/src/failure/failure-cluster.js +9 -18
- package/dist/src/failure/failure-data.js +12 -8
- package/dist/src/failure/failure-render.js +46 -36
- package/dist/src/failure/failure-state-change.js +54 -120
- package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
- package/dist/src/files/files.js +30 -0
- package/dist/src/files/json-object.js +8 -8
- package/dist/src/files/stat-cache.js +12 -8
- package/dist/src/gates/budget-gates.js +85 -84
- package/dist/src/gates/gate-polls.js +32 -74
- package/dist/src/gates/pause-gates.js +16 -2
- package/dist/src/gates/readiness.js +1 -1
- package/dist/src/gates/role-cap-gates.js +44 -35
- package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- package/dist/src/git/git-diff.js +6 -2
- package/dist/src/git/git.js +75 -45
- package/dist/src/git/worktree-pool.js +3 -6
- package/dist/src/gui/gui-admin-endpoints.js +3 -3
- package/dist/src/gui/gui-args.js +12 -0
- package/dist/src/gui/gui-backlog-commands.js +4 -4
- package/dist/src/gui/gui-backlog-endpoints.js +61 -41
- package/dist/src/gui/gui-command.js +43 -24
- package/dist/src/gui/gui-config-commands.js +2 -64
- package/dist/src/gui/gui-endpoint-commands.js +1 -1
- package/dist/src/gui/gui-endpoint-common.js +19 -1
- package/dist/src/gui/gui-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +43 -48
- package/dist/src/gui/gui-prompt-commands.js +25 -25
- package/dist/src/gui/gui-report-endpoints.js +1 -1
- package/dist/src/gui/gui-server.js +1 -2
- package/dist/src/gui/gui-team-commands.js +2 -1
- package/dist/src/history/history-data.js +71 -44
- package/dist/src/history/history-queued.js +49 -27
- package/dist/src/history/history.js +54 -36
- package/dist/src/inbox/prompt-commands.js +59 -42
- package/dist/src/init/init.js +88 -40
- package/dist/src/merge/merge-backlog-conflicts.js +1 -1
- package/dist/src/merge/merge-check-failures.js +12 -9
- package/dist/src/merge/merge-core.js +7 -32
- package/dist/src/merge/merge-drain.js +11 -13
- package/dist/src/merge/merge-pipeline.js +6 -7
- package/dist/src/merge/merge-post-review-drain.js +11 -14
- package/dist/src/merge/merge-queue-render.js +3 -3
- package/dist/src/merge/merge-requeue.js +1 -1
- package/dist/src/merge/merge-slot.js +0 -8
- package/dist/src/merge/merge-stack.js +17 -18
- package/dist/src/merge/merge-to-main.js +13 -58
- package/dist/src/merge/merge-verify.js +12 -21
- package/dist/src/notes-cli.js +52 -42
- package/dist/src/operator/operator-commands.js +80 -47
- package/dist/src/operator/operator-intent.js +3 -3
- package/dist/src/orchestrator/orchestrator-launch.js +31 -63
- package/dist/src/orchestrator/orchestrator-poll.js +44 -69
- package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
- package/dist/src/orchestrator/orchestrator.js +8 -21
- package/dist/src/orchestrator/scheduling-admit.js +101 -68
- package/dist/src/orchestrator/scheduling-scope.js +18 -27
- package/dist/src/orchestrator/scheduling-skip.js +10 -14
- package/dist/src/orchestrator/startup-migrations.js +1 -1
- package/dist/src/paths.js +10 -9
- package/dist/src/pi/pi-args.js +5 -2
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/pi/pi-bin.js +3 -3
- package/dist/src/pi/pi-event-line.js +28 -1
- package/dist/src/pi/pi-listed-models.js +13 -0
- package/dist/src/pi/pi-models.js +43 -29
- package/dist/src/pi/pi-settings.js +43 -20
- package/dist/src/pi/pi-stream.js +41 -8
- package/dist/src/pi/pi-watchdogs.js +43 -15
- package/dist/src/pi/pi.js +2 -2
- package/dist/src/pi-extension/context-shake.js +32 -20
- package/dist/src/pi-extension/kill-guard.js +69 -0
- package/dist/src/process/process-group.js +51 -26
- package/dist/src/process/process.js +20 -12
- package/dist/src/process/run-marker.js +33 -15
- package/dist/src/process/supervisor.js +40 -28
- package/dist/src/prompt/dialect.js +1 -1
- package/dist/src/prompt/gate-notes.js +2 -2
- package/dist/src/prompt/gate-prompts.js +1 -44
- package/dist/src/prompt/notes-backlog.js +66 -37
- package/dist/src/prompt/principles.js +6 -4
- package/dist/src/prompt/prompt-followup.js +12 -4
- package/dist/src/prompt/prompt.js +88 -38
- package/dist/src/redeploy/redeployer.js +21 -1
- package/dist/src/report/report-data.js +53 -37
- package/dist/src/report/report-fold.js +16 -9
- package/dist/src/report/report-render.js +21 -194
- package/dist/src/report/report-since-data.js +4 -3
- package/dist/src/report/report-since-render.js +173 -0
- package/dist/src/report/report-usage-fold.js +8 -8
- package/dist/src/report/report.js +46 -22
- package/dist/src/review/diff-rules.js +35 -21
- package/dist/src/review/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +48 -41
- package/dist/src/review/review-precheck.js +14 -4
- package/dist/src/review/review.js +48 -170
- package/dist/src/review/secret-leaks.js +26 -16
- package/dist/src/roles/legacy-role-migration.js +6 -3
- package/dist/src/roles/role-catalog.js +11 -3
- package/dist/src/roles/role-guidance.js +16 -12
- package/dist/src/roles/role-render.js +7 -46
- package/dist/src/roles/role-view.js +10 -33
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/ci-parity.js +19 -8
- package/dist/src/run/run-apply.js +12 -15
- package/dist/src/run/run-detail-data.js +121 -68
- package/dist/src/run/run-detail-render.js +92 -35
- package/dist/src/run/run-detail.js +72 -41
- package/dist/src/run/run-prompt.js +63 -50
- package/dist/src/run/run-resume.js +76 -29
- package/dist/src/run/run-stage.js +141 -90
- package/dist/src/run/run-timing.js +8 -2
- package/dist/src/run/run-usage.js +31 -1
- package/dist/src/run/run-verdict.js +128 -50
- package/dist/src/run/stage-check.js +60 -50
- package/dist/src/run/stage-checks-shared.js +28 -0
- package/dist/src/run/stage-name-checks.js +111 -92
- package/dist/src/run/stage-path-checks.js +69 -42
- package/dist/src/run/stage-prose-checks.js +55 -40
- package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
- package/dist/src/runner/runner-outcome.js +7 -2
- package/dist/src/runner/runner-pi.js +9 -22
- package/dist/src/runner/runner-recovery.js +5 -13
- package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
- package/dist/src/runner/runner-run.js +103 -50
- package/dist/src/runner/runner-state-load.js +18 -28
- package/dist/src/runner/runner-state.js +0 -14
- package/dist/src/runner/runner.js +41 -25
- package/dist/src/scheduling/backoff.js +8 -3
- package/dist/src/scheduling/claim-entries.js +2 -1
- package/dist/src/scheduling/claims.js +0 -4
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-budget.js +4 -43
- package/dist/src/status/status-data.js +35 -39
- package/dist/src/status/status-role-notes.js +2 -2
- package/dist/src/status/status-roles.js +9 -18
- package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
- package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
- package/dist/src/team/team-hold.js +105 -70
- package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
- package/dist/src/team/team-polls.js +53 -201
- package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
- package/dist/src/team/team-state.js +4 -13
- package/dist/src/text/datetime.js +16 -7
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +29 -15
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +6 -5
- package/dist/src/ui/alert-feeds.js +46 -111
- package/dist/src/ui/badges.js +17 -88
- package/dist/src/ui/gui/gui-client-boot.js +2 -8
- package/dist/src/ui/gui/gui-client-composer.js +1 -1
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-feed.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +28 -14
- package/dist/src/ui/gui/gui-client-model.js +0 -12
- package/dist/src/ui/gui/gui-client-operator.js +7 -5
- package/dist/src/ui/gui/gui-client-pending.js +7 -6
- package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
- package/dist/src/ui/gui/gui-client-report.js +1 -1
- package/dist/src/ui/gui/gui-client-roles.js +0 -1
- package/dist/src/ui/gui/gui-client-settings.js +43 -84
- package/dist/src/ui/gui/gui-client-team.js +12 -6
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/progress-data.js +107 -44
- package/dist/src/ui/run-progress-model.js +20 -4
- package/dist/src/ui/status-model.js +2 -24
- package/dist/src/ui/status-payload.js +11 -30
- package/dist/src/ui/status-render.js +13 -24
- package/dist/src/ui/team-alert-backlog.js +14 -204
- package/dist/src/ui/team-alert-shared.js +6 -29
- package/dist/src/ui/team-alert-status.js +9 -170
- package/dist/src/ui/team-alert-tail.js +19 -215
- package/dist/src/ui/team-alerts.js +8 -15
- package/dist/src/ui/transcript-run.js +27 -16
- package/dist/src/ui/tui/tui-backlog.js +2 -2
- package/dist/src/ui/tui/tui-editor-answer.js +12 -10
- package/dist/src/ui/tui/tui-editor-attach.js +4 -3
- package/dist/src/ui/tui/tui-editor-backends.js +4 -3
- package/dist/src/ui/tui/tui-editor-brief.js +6 -9
- package/dist/src/ui/tui/tui-editor-budget.js +4 -3
- package/dist/src/ui/tui/tui-editor-config.js +80 -68
- package/dist/src/ui/tui/tui-editor-entry.js +12 -20
- package/dist/src/ui/tui/tui-editor-pause.js +51 -57
- package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
- package/dist/src/ui/tui/tui-editor-queued.js +11 -14
- package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
- package/dist/src/ui/tui/tui-editor-release.js +4 -3
- package/dist/src/ui/tui/tui-editor-retire.js +7 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
- package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
- package/dist/src/ui/tui/tui-editor-search.js +4 -3
- package/dist/src/ui/tui/tui-editor-session.js +17 -4
- package/dist/src/ui/tui/tui-editors.js +5 -10
- package/dist/src/ui/tui/tui-frame.js +182 -134
- package/dist/src/ui/tui/tui-guard.js +20 -0
- package/dist/src/ui/tui/tui-input.js +167 -98
- package/dist/src/ui/tui/tui-keymap.js +51 -29
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +5 -4
- package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +252 -230
- package/dist/src/ui/tui/tui-pane-history.js +67 -74
- package/dist/src/ui/tui/tui-pane-pending.js +288 -247
- package/dist/src/ui/tui/tui-pane-queued.js +25 -27
- package/dist/src/ui/tui/tui-pane-report.js +2 -1
- package/dist/src/ui/tui/tui-pane-session.js +15 -10
- package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
- package/dist/src/ui/tui/tui-pane.js +14 -11
- package/dist/src/ui/tui/tui-panes.js +38 -33
- package/dist/src/ui/tui/tui.js +4 -7
- package/dist/src/verdict/fix-claim.js +107 -56
- package/dist/src/verdict/refusal.js +1 -1
- package/dist/src/verdict/reply-contract.js +3 -4
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- package/dist/src/review/review-followup.js +0 -87
- package/dist/src/review/review-run.js +0 -395
- package/dist/src/runner/model-fallback.js +0 -76
- package/dist/src/runner/runner-fallback.js +0 -66
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
- package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
- /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
- /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
- /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
- /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
- /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* not run any of them. */
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { gitTry } from "../git/git-run.js";
|
|
11
|
-
import { JS_FILE, MAX_STALE_HITS, MAX_STALE_PATHS, cappedList, gitGrepHits, isRecordProse, standalonePathIndexes, } from "./stage-checks-shared.js";
|
|
11
|
+
import { JS_FILE, MAX_STALE_HITS, MAX_STALE_PATHS, cappedList, diffRemovedAdded, gitGrepHits, isRecordProse, standalonePathIndexes, } from "./stage-checks-shared.js";
|
|
12
12
|
/** How many removed identifiers one stale-name check looks up. */
|
|
13
13
|
const MAX_STALE_NAMES = 40;
|
|
14
14
|
/** The most characters between a moved name and its old file's name for a line to count as
|
|
@@ -59,86 +59,114 @@ function insideStringLiteral(line, index) {
|
|
|
59
59
|
/** (d) Compound identifiers the change removed from JS/TS source that a comment or doc still
|
|
60
60
|
* names. Candidates come from removed `-U0` lines; one `git grep -w --cached` covers them all. */
|
|
61
61
|
export async function staleNameFindings(wt, diff) {
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
let jsFile = false;
|
|
65
|
-
for (const line of diff.split("\n")) {
|
|
66
|
-
const header = /^diff --git a\/.* b\/(.*)$/.exec(line);
|
|
67
|
-
if (header) {
|
|
68
|
-
jsFile = JS_FILE.test(header[1]);
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
if (!jsFile)
|
|
72
|
-
continue;
|
|
73
|
-
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
74
|
-
removed.push(...declaredNames(line.slice(1)));
|
|
75
|
-
}
|
|
76
|
-
else if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
77
|
-
for (const n of declaredNames(line.slice(1)))
|
|
78
|
-
added.add(n);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
const candidates = [...new Set(removed)]
|
|
82
|
-
.filter((n) => isCompoundName(n) && !added.has(n))
|
|
83
|
-
.slice(0, MAX_STALE_NAMES);
|
|
62
|
+
const { removedIn, addedIn } = declaredNameDeltas(diff);
|
|
63
|
+
const candidates = staleNameCandidates(removedIn, addedIn);
|
|
84
64
|
if (candidates.length === 0)
|
|
85
65
|
return [];
|
|
86
|
-
const
|
|
87
|
-
if (!out)
|
|
88
|
-
return [];
|
|
89
|
-
const lines = gitGrepHits(out);
|
|
66
|
+
const lines = await wholeWordGrep(wt, candidates);
|
|
90
67
|
const findings = [];
|
|
91
68
|
for (const name of candidates) {
|
|
92
69
|
if (findings.length >= MAX_STALE_PATHS)
|
|
93
70
|
break;
|
|
94
|
-
const word =
|
|
95
|
-
const mine =
|
|
71
|
+
const word = staleNameWord(name);
|
|
72
|
+
const mine = staleNameMatches(word, lines);
|
|
96
73
|
// Another module may still own the name.
|
|
97
|
-
if (
|
|
74
|
+
if (ownedElsewhere(name, mine))
|
|
98
75
|
continue;
|
|
99
|
-
const hits =
|
|
100
|
-
for (const l of mine) {
|
|
101
|
-
if (isRecordProse(l.file) || l.text.includes("retired-name-ok"))
|
|
102
|
-
continue;
|
|
103
|
-
if ([...l.text.matchAll(word)].some((m) => !insideStringLiteral(l.text, m.index))) {
|
|
104
|
-
hits.push(`${l.file}:${l.no}`);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
76
|
+
const hits = staleNameHits(word, mine);
|
|
107
77
|
if (hits.length === 0)
|
|
108
78
|
continue;
|
|
109
79
|
findings.push(`\`${name}\` was removed by this change but is still named at: ${cappedList(hits, MAX_STALE_HITS)}`);
|
|
110
80
|
}
|
|
111
81
|
return findings;
|
|
112
82
|
}
|
|
83
|
+
/** The candidates this check looks up: compound names the change removed from JS/TS source that
|
|
84
|
+
* were not also added, capped. (`declaredNameDeltas` keeps only compound names, so every key is
|
|
85
|
+
* one.) */
|
|
86
|
+
function staleNameCandidates(removedIn, addedIn) {
|
|
87
|
+
return [...removedIn.keys()].filter((n) => !addedIn.has(n)).slice(0, MAX_STALE_NAMES);
|
|
88
|
+
}
|
|
89
|
+
/** A whole-word, case-sensitive matcher for `name`. */
|
|
90
|
+
function staleNameWord(name) {
|
|
91
|
+
return new RegExp(`(?<![\\w$])${name.replace(/\$/g, "\\$")}(?![\\w$])`, "g");
|
|
92
|
+
}
|
|
93
|
+
/** `git grep -w --cached` for every name in `names`, returning its `file:line` hit records;
|
|
94
|
+
* `null` (gitTry maps a clean no-match and a real failure the same way) becomes `[]`. Shared by
|
|
95
|
+
* the stale-name and moved-name sweeps, which both grep the staged tree for a batch of compound
|
|
96
|
+
* names exactly the same way before each filters its own subset of the hits. */
|
|
97
|
+
async function wholeWordGrep(wt, names) {
|
|
98
|
+
const out = await gitTry(wt, "grep", "-n", "-w", "-I", "--cached", ...names.flatMap((n) => ["-e", n]));
|
|
99
|
+
if (!out)
|
|
100
|
+
return [];
|
|
101
|
+
return gitGrepHits(out);
|
|
102
|
+
}
|
|
103
|
+
/** The hits whose text has at least one whole-word occurrence of `word`. */
|
|
104
|
+
function staleNameMatches(word, lines) {
|
|
105
|
+
return lines.filter((l) => [...l.text.matchAll(word)].length > 0);
|
|
106
|
+
}
|
|
107
|
+
/** True when a JS/TS hit still declares `name` — another module may still own it. */
|
|
108
|
+
function ownedElsewhere(name, mine) {
|
|
109
|
+
return mine.some((l) => JS_FILE.test(l.file) && declaredNames(l.text).includes(name));
|
|
110
|
+
}
|
|
111
|
+
/** The `file:line` hits that still name `word` outside a string literal, skipping record prose
|
|
112
|
+
* and retired-name-ok lines. */
|
|
113
|
+
function staleNameHits(word, mine) {
|
|
114
|
+
const hits = [];
|
|
115
|
+
for (const l of mine) {
|
|
116
|
+
if (isRecordProse(l.file) || l.text.includes("retired-name-ok"))
|
|
117
|
+
continue;
|
|
118
|
+
if ([...l.text.matchAll(word)].some((m) => !insideStringLiteral(l.text, m.index))) {
|
|
119
|
+
hits.push(`${l.file}:${l.no}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return hits;
|
|
123
|
+
}
|
|
113
124
|
/** (g) A compound name the change moved out of one JS/TS file into another that a comment or doc
|
|
114
125
|
* still places in the old file: the line has the name within MOVED_NAME_GAP_MAX characters of the
|
|
115
126
|
* old file's name (full path or basename), with no `,` or `;` between them. An old file that
|
|
116
127
|
* still names the moved name (a re-export or import) and record prose are skipped. */
|
|
117
128
|
export async function movedNameFindings(wt, diff) {
|
|
129
|
+
const { removedIn, addedIn } = declaredNameDeltas(diff);
|
|
130
|
+
const picked = movedNames(removedIn, addedIn);
|
|
131
|
+
if (picked.length === 0)
|
|
132
|
+
return [];
|
|
133
|
+
const lines = await wholeWordGrep(wt, picked.map((m) => m.name));
|
|
134
|
+
const findings = [];
|
|
135
|
+
for (const move of picked) {
|
|
136
|
+
if (findings.length >= MAX_STALE_PATHS)
|
|
137
|
+
break;
|
|
138
|
+
const hits = movedNameHits(move, lines);
|
|
139
|
+
if (hits.length === 0)
|
|
140
|
+
continue;
|
|
141
|
+
findings.push(`\`${move.name}\` moved from ${move.from} to ${cappedList(move.to, MAX_STALE_PATHS)}, but ${hits.join(", ")} ` +
|
|
142
|
+
`still place it in ${path.basename(move.from)} — point each at its new file`);
|
|
143
|
+
}
|
|
144
|
+
return findings;
|
|
145
|
+
}
|
|
146
|
+
/** Record that `file` declares `name` into the per-name file set of `map`. */
|
|
147
|
+
function noteDeclaredName(map, name, f) {
|
|
148
|
+
const set = map.get(name) ?? new Set();
|
|
149
|
+
set.add(f);
|
|
150
|
+
map.set(name, set);
|
|
151
|
+
}
|
|
152
|
+
/** Split a `-U0` diff into the compound names each JS/TS file removed and added. */
|
|
153
|
+
function declaredNameDeltas(diff) {
|
|
118
154
|
const removedIn = new Map();
|
|
119
155
|
const addedIn = new Map();
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const set = map.get(name) ?? new Set();
|
|
123
|
-
set.add(f);
|
|
124
|
-
map.set(name, set);
|
|
125
|
-
};
|
|
126
|
-
for (const line of diff.split("\n")) {
|
|
127
|
-
const header = /^diff --git a\/.* b\/(.*)$/.exec(line);
|
|
128
|
-
if (header) {
|
|
129
|
-
file = JS_FILE.test(header[1]) ? header[1] : null;
|
|
156
|
+
for (const f of diffRemovedAdded(diff)) {
|
|
157
|
+
if (!JS_FILE.test(f.path))
|
|
130
158
|
continue;
|
|
159
|
+
for (const n of f.removed.flatMap((l) => declaredNames(l)).filter(isCompoundName)) {
|
|
160
|
+
noteDeclaredName(removedIn, n, f.path);
|
|
131
161
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const isRemoved = line.startsWith("-") && !line.startsWith("---");
|
|
135
|
-
const isAdded = line.startsWith("+") && !line.startsWith("+++");
|
|
136
|
-
if (!isRemoved && !isAdded)
|
|
137
|
-
continue;
|
|
138
|
-
for (const n of declaredNames(line.slice(1)).filter(isCompoundName)) {
|
|
139
|
-
note(isRemoved ? removedIn : addedIn, n, file);
|
|
162
|
+
for (const n of f.added.flatMap((l) => declaredNames(l)).filter(isCompoundName)) {
|
|
163
|
+
noteDeclaredName(addedIn, n, f.path);
|
|
140
164
|
}
|
|
141
165
|
}
|
|
166
|
+
return { removedIn, addedIn };
|
|
167
|
+
}
|
|
168
|
+
/** The names removed from one JS/TS file that the change moved into another, capped. */
|
|
169
|
+
function movedNames(removedIn, addedIn) {
|
|
142
170
|
const moves = [];
|
|
143
171
|
for (const [name, froms] of removedIn) {
|
|
144
172
|
const added = addedIn.get(name);
|
|
@@ -151,44 +179,35 @@ export async function movedNameFindings(wt, diff) {
|
|
|
151
179
|
break;
|
|
152
180
|
}
|
|
153
181
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
182
|
+
return moves.slice(0, MAX_STALE_NAMES);
|
|
183
|
+
}
|
|
184
|
+
/** True when `text` sits near a standalone occurrence of the old file's basename `base`. */
|
|
185
|
+
function placesMovedNameInOldFile(text, word, name, base) {
|
|
186
|
+
const names = [...text.matchAll(word)].map((m) => m.index);
|
|
187
|
+
const bases = standalonePathIndexes(text, base);
|
|
188
|
+
return names.some((i) => bases.some((j) => {
|
|
189
|
+
const gap = j >= i + name.length ? text.slice(i + name.length, j) : text.slice(j + base.length, i);
|
|
190
|
+
const overlaps = j < i + name.length && i < j + base.length;
|
|
191
|
+
return !overlaps && gap.length <= MOVED_NAME_GAP_MAX && !/[,;]/.test(gap);
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
194
|
+
/** The staged-index hits that still place `move.name` in its old file, capped. */
|
|
195
|
+
function movedNameHits(move, lines) {
|
|
196
|
+
const word = staleNameWord(move.name);
|
|
197
|
+
const mine = lines.filter((l) => l.text.search(word) !== -1);
|
|
198
|
+
if (mine.some((l) => l.file === move.from))
|
|
159
199
|
return [];
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
for (const
|
|
163
|
-
if (
|
|
164
|
-
break;
|
|
165
|
-
const word = new RegExp(`(?<![\\w$])${name.replace(/\$/g, "\\$")}(?![\\w$])`, "g");
|
|
166
|
-
const mine = lines.filter((l) => l.text.search(word) !== -1);
|
|
167
|
-
if (mine.some((l) => l.file === from))
|
|
200
|
+
const base = path.basename(move.from);
|
|
201
|
+
const hits = [];
|
|
202
|
+
for (const l of mine) {
|
|
203
|
+
if (isRecordProse(l.file))
|
|
168
204
|
continue;
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
for (const l of mine) {
|
|
172
|
-
if (isRecordProse(l.file))
|
|
173
|
-
continue;
|
|
174
|
-
const text = l.text.split(from).join(base);
|
|
175
|
-
const names = [...text.matchAll(word)].map((m) => m.index);
|
|
176
|
-
const bases = standalonePathIndexes(text, base);
|
|
177
|
-
const near = names.some((i) => bases.some((j) => {
|
|
178
|
-
const gap = j >= i + name.length ? text.slice(i + name.length, j) : text.slice(j + base.length, i);
|
|
179
|
-
const overlaps = j < i + name.length && i < j + base.length;
|
|
180
|
-
return !overlaps && gap.length <= MOVED_NAME_GAP_MAX && !/[,;]/.test(gap);
|
|
181
|
-
}));
|
|
182
|
-
if (!near)
|
|
183
|
-
continue;
|
|
184
|
-
hits.push(`${l.file}:${l.no}`);
|
|
185
|
-
if (hits.length >= MAX_STALE_HITS)
|
|
186
|
-
break;
|
|
187
|
-
}
|
|
188
|
-
if (hits.length === 0)
|
|
205
|
+
const text = l.text.split(move.from).join(base);
|
|
206
|
+
if (!placesMovedNameInOldFile(text, word, move.name, base))
|
|
189
207
|
continue;
|
|
190
|
-
|
|
191
|
-
|
|
208
|
+
hits.push(`${l.file}:${l.no}`);
|
|
209
|
+
if (hits.length >= MAX_STALE_HITS)
|
|
210
|
+
break;
|
|
192
211
|
}
|
|
193
|
-
return
|
|
212
|
+
return hits;
|
|
194
213
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { changeBaseRev } from "../git/git.js";
|
|
13
13
|
import { gitTry } from "../git/git-run.js";
|
|
14
14
|
import { pathTokens } from "../text/text.js";
|
|
15
|
-
import { MAX_STALE_HITS, MAX_STALE_PATHS, cappedList, diffPlusPath, gitGrepHits, hasStandalonePathOccurrence, isRecordProse, } from "./stage-checks-shared.js";
|
|
15
|
+
import { MAX_STALE_HITS, MAX_STALE_PATHS, cappedList, diffPlusPath, diffRemovedAdded, gitGrepHits, hasStandalonePathOccurrence, isRecordProse, } from "./stage-checks-shared.js";
|
|
16
16
|
import { movedNameFindings, staleNameFindings } from "./stage-name-checks.js";
|
|
17
17
|
import { detachedDocFindings, garbledWordingFindings, newlineFindings } from "./stage-prose-checks.js";
|
|
18
18
|
/** How many nonexistent added paths one finding names before it stops. */
|
|
@@ -94,6 +94,34 @@ async function pathGrepHits(wt, needle, excludeFile) {
|
|
|
94
94
|
}
|
|
95
95
|
return hits;
|
|
96
96
|
}
|
|
97
|
+
/** The moved-line delta per changed file, from `diffRemovedAdded(diff)`: each file's removed
|
|
98
|
+
* lines and added set, trimmed and filtered at MOVED_LINE_MIN_CHARS. The single place the
|
|
99
|
+
* walk and its threshold live, so a reader asking what counts as a moved line finds it here. */
|
|
100
|
+
function movedLineDeltas(diff) {
|
|
101
|
+
const files = new Map();
|
|
102
|
+
for (const f of diffRemovedAdded(diff)) {
|
|
103
|
+
const removed = f.removed.map((l) => l.trim()).filter((l) => l.length >= MOVED_LINE_MIN_CHARS);
|
|
104
|
+
const added = new Set(f.added.map((l) => l.trim()).filter((l) => l.length >= MOVED_LINE_MIN_CHARS));
|
|
105
|
+
files.set(f.path, { removed, added });
|
|
106
|
+
}
|
|
107
|
+
return files;
|
|
108
|
+
}
|
|
109
|
+
/** Which files count as receivers of a move source: the other files whose added set contains
|
|
110
|
+
* at least one of the source's removed lines, in diff order. */
|
|
111
|
+
function movedLineReceivers(src, others) {
|
|
112
|
+
return others.filter(([, v]) => src.removed.some((l) => v.added.has(l))).map(([f]) => f);
|
|
113
|
+
}
|
|
114
|
+
/** How many of the source's removed lines another file added verbatim. */
|
|
115
|
+
function movedLineCount(src, others) {
|
|
116
|
+
return src.removed.filter((l) => others.some(([, v]) => v.added.has(l))).length;
|
|
117
|
+
}
|
|
118
|
+
/** The moved-source finding wording: the source path, its moved-line count, the receiving
|
|
119
|
+
* files (capped at MAX_STALE_PATHS) and the still-matching `file:line` hits to re-check. */
|
|
120
|
+
function movedSourceFinding(path, moved, receivers, hits) {
|
|
121
|
+
return (`${path} had ${moved} lines moved to ${cappedList(receivers, MAX_STALE_PATHS)}, but it is ` +
|
|
122
|
+
`still named at: ${hits.join(", ")} — check each still describes what stays in ${path}, ` +
|
|
123
|
+
`and point any that describes the moved code at its new file`);
|
|
124
|
+
}
|
|
97
125
|
/** (f) References to a file the change moved code out of. A modified (`M`) file is a source
|
|
98
126
|
* when at least MOVED_LINES_MIN of its removed `-U0` lines (trimmed, at least MOVED_LINE_MIN_CHARS
|
|
99
127
|
* long) were added verbatim to another file. Each hit of the source's path outside record prose
|
|
@@ -104,29 +132,7 @@ async function pathGrepHits(wt, needle, excludeFile) {
|
|
|
104
132
|
* and revision runs that rightly changed nothing were rejected until the review chain ran out
|
|
105
133
|
* (6 of 7 exhausted chains on 2026-10-20 and -21). */
|
|
106
134
|
async function movedSourceFindings(wt, diff, entries) {
|
|
107
|
-
const files =
|
|
108
|
-
let cur;
|
|
109
|
-
for (const line of diff.split("\n")) {
|
|
110
|
-
const header = /^diff --git a\/.* b\/(.*)$/.exec(line);
|
|
111
|
-
if (header) {
|
|
112
|
-
cur = { removed: [], added: new Set() };
|
|
113
|
-
files.set(header[1], cur);
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
if (!cur)
|
|
117
|
-
continue;
|
|
118
|
-
const isRemoved = line.startsWith("-") && !line.startsWith("---");
|
|
119
|
-
const isAdded = line.startsWith("+") && !line.startsWith("+++");
|
|
120
|
-
if (!isRemoved && !isAdded)
|
|
121
|
-
continue;
|
|
122
|
-
const text = line.slice(1).trim();
|
|
123
|
-
if (text.length < MOVED_LINE_MIN_CHARS)
|
|
124
|
-
continue;
|
|
125
|
-
if (isRemoved)
|
|
126
|
-
cur.removed.push(text);
|
|
127
|
-
else
|
|
128
|
-
cur.added.add(text);
|
|
129
|
-
}
|
|
135
|
+
const files = movedLineDeltas(diff);
|
|
130
136
|
const findings = [];
|
|
131
137
|
for (const e of entries) {
|
|
132
138
|
if (e.status !== "M" || isRecordProse(e.path))
|
|
@@ -137,18 +143,13 @@ async function movedSourceFindings(wt, diff, entries) {
|
|
|
137
143
|
if (!src)
|
|
138
144
|
continue;
|
|
139
145
|
const others = [...files].filter(([f]) => f !== e.path);
|
|
140
|
-
const
|
|
141
|
-
.filter(([, v]) => src.removed.some((l) => v.added.has(l)))
|
|
142
|
-
.map(([f]) => f);
|
|
143
|
-
const moved = src.removed.filter((l) => others.some(([, v]) => v.added.has(l))).length;
|
|
146
|
+
const moved = movedLineCount(src, others);
|
|
144
147
|
if (moved < MOVED_LINES_MIN)
|
|
145
148
|
continue;
|
|
146
149
|
const hits = await pathGrepHits(wt, e.path, e.path);
|
|
147
150
|
if (hits.length === 0)
|
|
148
151
|
continue;
|
|
149
|
-
findings.push(
|
|
150
|
-
`still named at: ${hits.join(", ")} — check each still describes what stays in ${e.path}, ` +
|
|
151
|
-
`and point any that describes the moved code at its new file`);
|
|
152
|
+
findings.push(movedSourceFinding(e.path, moved, movedLineReceivers(src, others), hits));
|
|
152
153
|
}
|
|
153
154
|
return findings;
|
|
154
155
|
}
|
|
@@ -174,10 +175,9 @@ async function staleReferenceFindings(wt, entries) {
|
|
|
174
175
|
}
|
|
175
176
|
return findings;
|
|
176
177
|
}
|
|
177
|
-
/**
|
|
178
|
-
*
|
|
179
|
-
|
|
180
|
-
async function missingPathFindings(wt, diff, entries) {
|
|
178
|
+
/** The staged tree as the added-path check needs it: the set of every tracked path and the set
|
|
179
|
+
* of its top-level directories, from `git ls-files`. */
|
|
180
|
+
async function readStagedIndex(wt) {
|
|
181
181
|
const ls = await gitTry(wt, "ls-files");
|
|
182
182
|
const staged = new Set();
|
|
183
183
|
const topDirs = new Set();
|
|
@@ -189,6 +189,11 @@ async function missingPathFindings(wt, diff, entries) {
|
|
|
189
189
|
if (slash > 0)
|
|
190
190
|
topDirs.add(p.slice(0, slash));
|
|
191
191
|
}
|
|
192
|
+
return { staged, topDirs };
|
|
193
|
+
}
|
|
194
|
+
/** The paths a change moved code away from and must not flag: every rename origin and every
|
|
195
|
+
* deleted path in `entries`, which "moved from X" prose names legitimately. */
|
|
196
|
+
function renameOriginPaths(entries) {
|
|
192
197
|
const oldPaths = new Set();
|
|
193
198
|
for (const e of entries) {
|
|
194
199
|
if (e.oldPath)
|
|
@@ -196,6 +201,25 @@ async function missingPathFindings(wt, diff, entries) {
|
|
|
196
201
|
if (e.status === "D")
|
|
197
202
|
oldPaths.add(e.path);
|
|
198
203
|
}
|
|
204
|
+
return oldPaths;
|
|
205
|
+
}
|
|
206
|
+
/** The path-shaped tokens an added line names that are absent from the staged tree: a token whose
|
|
207
|
+
* first segment is a tracked top-level directory, not a rename origin or deleted path, not
|
|
208
|
+
* already staged, and not already seen. Each returned token is added to `seen` as it goes. */
|
|
209
|
+
function missingPathTokens(line, staged, topDirs, oldPaths, seen) {
|
|
210
|
+
const found = [];
|
|
211
|
+
for (const p of pathTokens(line.slice(1))) {
|
|
212
|
+
const top = p.slice(0, p.indexOf("/"));
|
|
213
|
+
if (!topDirs.has(top) || oldPaths.has(p) || staged.has(p) || seen.has(p))
|
|
214
|
+
continue;
|
|
215
|
+
seen.add(p);
|
|
216
|
+
found.push(p);
|
|
217
|
+
}
|
|
218
|
+
return found;
|
|
219
|
+
}
|
|
220
|
+
/** Every added diff line's missing-path tokens, tracking the current file to spare added lines in
|
|
221
|
+
* test/ and in record prose (isRecordProse). */
|
|
222
|
+
function missingPathsInDiff(diff, staged, topDirs, oldPaths) {
|
|
199
223
|
const seen = new Set();
|
|
200
224
|
const missing = [];
|
|
201
225
|
let file = null;
|
|
@@ -212,14 +236,17 @@ async function missingPathFindings(wt, diff, entries) {
|
|
|
212
236
|
// test/ and in record prose (isRecordProse).
|
|
213
237
|
if (file !== null && (file.startsWith("test/") || isRecordProse(file)))
|
|
214
238
|
continue;
|
|
215
|
-
|
|
216
|
-
const top = p.slice(0, p.indexOf("/"));
|
|
217
|
-
if (!topDirs.has(top) || oldPaths.has(p) || staged.has(p) || seen.has(p))
|
|
218
|
-
continue;
|
|
219
|
-
seen.add(p);
|
|
220
|
-
missing.push(p);
|
|
221
|
-
}
|
|
239
|
+
missing.push(...missingPathTokens(line, staged, topDirs, oldPaths, seen));
|
|
222
240
|
}
|
|
241
|
+
return missing;
|
|
242
|
+
}
|
|
243
|
+
/** (b) Nonexistent paths: every path-shaped token on an added diff line whose first segment is
|
|
244
|
+
* a tracked top-level directory but which is absent from the staged tree. Rename origins and
|
|
245
|
+
* deleted paths are excluded — "moved from X" prose names them legitimately. */
|
|
246
|
+
async function missingPathFindings(wt, diff, entries) {
|
|
247
|
+
const { staged, topDirs } = await readStagedIndex(wt);
|
|
248
|
+
const oldPaths = renameOriginPaths(entries);
|
|
249
|
+
const missing = missingPathsInDiff(diff, staged, topDirs, oldPaths);
|
|
223
250
|
if (missing.length === 0)
|
|
224
251
|
return [];
|
|
225
252
|
return [
|
|
@@ -196,64 +196,79 @@ function spanningGarbled(joined, joinAt, patterns) {
|
|
|
196
196
|
}
|
|
197
197
|
return null;
|
|
198
198
|
}
|
|
199
|
-
/**
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
export function garbledWordingFindings(diff) {
|
|
203
|
-
const hits = [];
|
|
199
|
+
/** The added lines of a diff, in order, each with the added line that precedes it. */
|
|
200
|
+
function addedDiffLines(diff) {
|
|
201
|
+
const out = [];
|
|
204
202
|
let file = null;
|
|
205
203
|
let lineNo = 0;
|
|
206
|
-
let
|
|
204
|
+
let prev = null;
|
|
207
205
|
for (const line of diff.split("\n")) {
|
|
208
206
|
const header = /^diff --git a\/.* b\/(.*)$/.exec(line);
|
|
209
207
|
if (header) {
|
|
210
208
|
file = header[1];
|
|
211
|
-
|
|
209
|
+
prev = null;
|
|
212
210
|
continue;
|
|
213
211
|
}
|
|
214
212
|
const hunk = /^@@ .*\+(\d+)(?:,\d+)? @@/.exec(line);
|
|
215
213
|
if (hunk) {
|
|
216
214
|
lineNo = Number(hunk[1]);
|
|
217
|
-
|
|
215
|
+
prev = null;
|
|
218
216
|
continue;
|
|
219
217
|
}
|
|
220
218
|
if (!line.startsWith("+") || line.startsWith("+++ ")) {
|
|
221
|
-
|
|
219
|
+
prev = null;
|
|
222
220
|
continue;
|
|
223
221
|
}
|
|
224
|
-
const
|
|
225
|
-
if (file !== null
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const m = re.exec(cur);
|
|
229
|
-
if (m) {
|
|
230
|
-
hits.push(`${file}:${lineNo} "${m[0]}"`);
|
|
231
|
-
hit = true;
|
|
232
|
-
break;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
const prose = proseOf(file, cur);
|
|
236
|
-
if (!hit && prose !== null) {
|
|
237
|
-
const m = DOUBLED_WORD.exec(prose);
|
|
238
|
-
if (m) {
|
|
239
|
-
hits.push(`${file}:${lineNo} "${m[0]}"`);
|
|
240
|
-
hit = true;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
if (!hit && prevAdded !== null) {
|
|
244
|
-
const head = prevAdded.trimEnd();
|
|
245
|
-
const joined = head + " " + cur.replace(/^\s*(?:\/\/+|\*|#)\s*/, "");
|
|
246
|
-
let split = spanningGarbled(joined, head.length, GARBLED_WORDING);
|
|
247
|
-
if (!split && prose !== null && proseOf(file, prevAdded) !== null) {
|
|
248
|
-
split = spanningGarbled(joined, head.length, [DOUBLED_WORD]);
|
|
249
|
-
}
|
|
250
|
-
if (split)
|
|
251
|
-
hits.push(`${file}:${lineNo - 1}-${lineNo} "${split}"`);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
prevAdded = cur;
|
|
222
|
+
const text = line.slice(1);
|
|
223
|
+
if (file !== null)
|
|
224
|
+
out.push({ file, lineNo, text, prev });
|
|
225
|
+
prev = text;
|
|
255
226
|
lineNo++;
|
|
256
227
|
}
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
/** The first GARBLED_WORDING hit on a single added line, or the DOUBLED_WORD hit in its prose. */
|
|
231
|
+
function singleLineGarbledHit(file, lineNo, cur) {
|
|
232
|
+
for (const re of GARBLED_WORDING) {
|
|
233
|
+
const m = re.exec(cur);
|
|
234
|
+
if (m)
|
|
235
|
+
return `${file}:${lineNo} "${m[0]}"`;
|
|
236
|
+
}
|
|
237
|
+
const prose = proseOf(file, cur);
|
|
238
|
+
if (prose !== null) {
|
|
239
|
+
const m = DOUBLED_WORD.exec(prose);
|
|
240
|
+
if (m)
|
|
241
|
+
return `${file}:${lineNo} "${m[0]}"`;
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
/** A garbled or doubled phrase split across two consecutive added lines, joined at their seam. */
|
|
246
|
+
function spanningGarbledHit(file, lineNo, cur, prevAdded) {
|
|
247
|
+
if (prevAdded === null)
|
|
248
|
+
return null;
|
|
249
|
+
const head = prevAdded.trimEnd();
|
|
250
|
+
const joined = head + " " + cur.replace(/^\s*(?:\/\/+|\*|#)\s*/, "");
|
|
251
|
+
let split = spanningGarbled(joined, head.length, GARBLED_WORDING);
|
|
252
|
+
const prose = proseOf(file, cur);
|
|
253
|
+
if (!split && prose !== null && proseOf(file, prevAdded) !== null) {
|
|
254
|
+
split = spanningGarbled(joined, head.length, [DOUBLED_WORD]);
|
|
255
|
+
}
|
|
256
|
+
if (split)
|
|
257
|
+
return `${file}:${lineNo - 1}-${lineNo} "${split}"`;
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
/** (e) Added lines, outside record prose, that match GARBLED_WORDING: at most one hit per line,
|
|
261
|
+
* reported as `file:line "matched text"` in a single finding. A phrase split across two
|
|
262
|
+
* consecutive added lines is reported as `file:N-M`. */
|
|
263
|
+
export function garbledWordingFindings(diff) {
|
|
264
|
+
const hits = [];
|
|
265
|
+
for (const { file, lineNo, text, prev } of addedDiffLines(diff)) {
|
|
266
|
+
if (isRecordProse(file))
|
|
267
|
+
continue;
|
|
268
|
+
const hit = singleLineGarbledHit(file, lineNo, text) ?? spanningGarbledHit(file, lineNo, text, prev);
|
|
269
|
+
if (hit !== null)
|
|
270
|
+
hits.push(hit);
|
|
271
|
+
}
|
|
257
272
|
if (hits.length === 0)
|
|
258
273
|
return [];
|
|
259
274
|
const lead = 'added lines carry garbled rename wording (reword them; a rote word swap such as "tick" → "run" reads wrong here or leaves a word doubled): '; // vocabulary-ok: quotes the retired word
|
|
@@ -16,8 +16,8 @@ import { shortSha } from "../text/format.js";
|
|
|
16
16
|
* holds whether or not the pin survived. Split out of runner.ts — which keeps the run lifecycle
|
|
17
17
|
* around it — because this is a self-contained concern with its own entry condition and git
|
|
18
18
|
* surface; the only things it borrows from the runner are identity, the run number, the
|
|
19
|
-
* worktree (for the no-pin fallback), and
|
|
20
|
-
*
|
|
19
|
+
* worktree (for the no-pin fallback), and two pieces of the role's merge state: its conflict
|
|
20
|
+
* streak and outstanding hand-back. */
|
|
21
21
|
/** Consecutive merges of one pinned sha that may end `merge_conflict` before recovery stops
|
|
22
22
|
* re-queuing it (RunnerState.mergeConflicts). Each attempt costs a conflict-resolution run, and
|
|
23
23
|
* a role holding a pin never authors: without a cap an unmergeable change would re-queue
|
|
@@ -43,19 +43,14 @@ async function dropDanglingPin(root, role, ref, sha) {
|
|
|
43
43
|
warnEventBestEffort(root, role, `dropped merge pin ${shortSha(sha)}: its commit is missing from the repository, so it can never merge`);
|
|
44
44
|
return true;
|
|
45
45
|
}
|
|
46
|
-
/** The permanent-reviewer-config hold when it names `sha`, else undefined. */
|
|
47
|
-
function reviewHold(held, sha) {
|
|
48
|
-
return held?.sha === sha ? held : undefined;
|
|
49
|
-
}
|
|
50
46
|
/** Consecutive conflict-ended merges of `sha` (0 when the streak names another sha). */
|
|
51
47
|
function conflictAttempts(streak, sha) {
|
|
52
48
|
return streak?.sha === sha ? streak.count : 0;
|
|
53
49
|
}
|
|
54
50
|
/** The sha of a role's kept pin when the merge pipeline may re-queue it itself: the pin exists,
|
|
55
|
-
* main does not already contain it (a stale pin is deleted, as recovery does),
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* config change, and both stay with recoverLeftover. */
|
|
51
|
+
* main does not already contain it (a stale pin is deleted, as recovery does), and its conflict
|
|
52
|
+
* streak is under MERGE_CONFLICT_LIMIT. Null otherwise — a pin at the limit is the author's
|
|
53
|
+
* conflict hand-back, and it stays with recoverLeftover. */
|
|
59
54
|
export async function pinnedRetrySha(root, role, mainBranch, state) {
|
|
60
55
|
const ref = mergeRefName(role);
|
|
61
56
|
const sha = await refSha(root, ref).catch(() => null);
|
|
@@ -65,8 +60,6 @@ export async function pinnedRetrySha(root, role, mainBranch, state) {
|
|
|
65
60
|
return null;
|
|
66
61
|
if (await dropStalePin(root, ref, sha, mainBranch))
|
|
67
62
|
return null;
|
|
68
|
-
if (reviewHold(state.mergeReviewError, sha))
|
|
69
|
-
return null;
|
|
70
63
|
if (conflictAttempts(state.mergeConflicts, sha) >= MERGE_CONFLICT_LIMIT)
|
|
71
64
|
return null;
|
|
72
65
|
return sha;
|
|
@@ -130,13 +123,10 @@ export async function recoverLeftover(ctx) {
|
|
|
130
123
|
sha = await headOf(ctx.wt, "HEAD").catch(() => null);
|
|
131
124
|
if (!sha)
|
|
132
125
|
return null;
|
|
133
|
-
// Hold checks BEFORE adopting: when a
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
const heldTip = reviewHold(ctx.mergeReviewError, sha);
|
|
138
|
-
if (heldTip)
|
|
139
|
-
return { kind: "held", sha, message: heldTip.message };
|
|
126
|
+
// Hold checks BEFORE adopting: when a main_red retry names this tip, adopting it into the
|
|
127
|
+
// merge ref would create/repoint the pin and lose the kept sha the wait exists to preserve.
|
|
128
|
+
// Return the wait path without touching the ref (BUGS.md 2026-10-06, the main_red wait),
|
|
129
|
+
// leaving the commit on the branch.
|
|
140
130
|
if (ctx.mainRedRetry) {
|
|
141
131
|
return mainRedWait(sha, ctx.mainRedRetry.head);
|
|
142
132
|
}
|
|
@@ -150,13 +140,6 @@ export async function recoverLeftover(ctx) {
|
|
|
150
140
|
}
|
|
151
141
|
}
|
|
152
142
|
const meta = await recoveredMetadata(ctx.root, sha);
|
|
153
|
-
// A permanent reviewer configuration error holds the pin: re-queuing it would pay another
|
|
154
|
-
// full gate check for a request that can never succeed until the model config changes. The
|
|
155
|
-
// caller clears the hold (and re-queues) once the resolved reviewer selector differs, so a
|
|
156
|
-
// fixed config recovers automatically (BUGS.md 2026-10-06).
|
|
157
|
-
const held = reviewHold(ctx.mergeReviewError, sha);
|
|
158
|
-
if (held)
|
|
159
|
-
return { kind: "held", sha, message: held.message };
|
|
160
143
|
// `requeueMainRedPin` owns the re-queue of a main_red pin, once main's head moves. Queuing it
|
|
161
144
|
// here would pay a full gate check against the same red head, whose result is already known.
|
|
162
145
|
if (ctx.mainRedRetry) {
|
|
@@ -10,7 +10,7 @@ import { mergeToMain } from "../merge/merge-to-main.js";
|
|
|
10
10
|
import { MERGE_LOCK_WAIT_MS } from "../paths.js";
|
|
11
11
|
import { resolveRunVerdict } from "../run/run-verdict.js";
|
|
12
12
|
import { modelTrailerLine } from "../git/commit-message.js";
|
|
13
|
-
import { seamAgent } from "../config/model-selector.js";
|
|
13
|
+
import { seamAgent } from "../config/config-model-selector.js";
|
|
14
14
|
import { stageRunMerge } from "../run/run-stage.js";
|
|
15
15
|
import { stageCheckFindings } from "../run/stage-check.js";
|
|
16
16
|
/** Merge the worktree branch on main (see src/merge/merge-to-main.ts for the
|
|
@@ -62,6 +62,11 @@ export async function handlePiResultPhase(ctx, pi, userPrompt, revisionRound, wt
|
|
|
62
62
|
flow,
|
|
63
63
|
filed,
|
|
64
64
|
ci,
|
|
65
|
+
// The image-reroute arm resolves its target from the live config and the current
|
|
66
|
+
// regime, and names the model this run ran on as the handoff's `from`.
|
|
67
|
+
config: ctx.config,
|
|
68
|
+
rerouteRegime: ctx.rerouteRegime(),
|
|
69
|
+
runModel: cfg.model,
|
|
65
70
|
warn: (message) => ctx.warn(message),
|
|
66
71
|
merge: (w, sum) => mergePhase(ctx, w, sum),
|
|
67
72
|
finishAbortedRun: () => ctx.finishAbortedRun(userPrompt, wt),
|
|
@@ -96,7 +101,7 @@ export async function handlePiResultPhase(ctx, pi, userPrompt, revisionRound, wt
|
|
|
96
101
|
piStartedAt,
|
|
97
102
|
warn: (message) => ctx.warn(message),
|
|
98
103
|
requestSummary: (w, n) => ctx.pi.requestSummary(w, cfg, n),
|
|
99
|
-
stageCheck: (w) => stageCheckFindings(w, ctx.mainBranch, ctx.
|
|
104
|
+
stageCheck: (w) => stageCheckFindings(w, ctx.mainBranch, ctx.role),
|
|
100
105
|
requestStageFix: (w, findings, change) => ctx.pi.requestStageFix(w, findings, change, cfg),
|
|
101
106
|
pinAndReset: (w, sha) => ctx.pinAndReset(w, sha),
|
|
102
107
|
finishAbortedRun: () => ctx.finishAbortedRun(userPrompt, wt),
|