tumwater 0.4.2 → 0.4.3
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} +25 -12
- 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 +1 -8
- 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 +9 -0
- 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 +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +50 -27
- 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 +1 -1
- package/dist/src/agent/subscription-account.js +3 -3
- 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 +58 -102
- package/dist/src/backlog/backlog-md.js +5 -5
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/backlog-release.js +12 -12
- package/dist/src/backlog/backlog-structure.js +114 -61
- package/dist/src/backlog/backlog.js +1 -1
- package/dist/src/baseline/main-baseline.js +148 -84
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +29 -65
- 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-scoped.js +40 -16
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/cli/cli-command-args.js +89 -52
- package/dist/src/cli/cli-flag-specs.js +42 -22
- package/dist/src/cli/cli-init.js +34 -12
- package/dist/src/cli/cli-query-commands.js +1 -1
- package/dist/src/cli/cli-repo-gate.js +2 -2
- package/dist/src/cli/cli-start.js +91 -58
- package/dist/src/cli/completion.js +82 -46
- package/dist/src/cli/config-commands.js +82 -71
- package/dist/src/cli/help.js +4 -6
- package/dist/src/cli/log-commands.js +43 -23
- package/dist/src/cli/notify-command.js +2 -2
- package/dist/src/cli/question-commands.js +86 -60
- package/dist/src/cli.js +181 -135
- package/dist/src/collections.js +31 -16
- package/dist/src/concurrency/check-slots.js +70 -0
- package/dist/src/concurrency/lock.js +22 -0
- 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 +5 -20
- 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 +153 -102
- package/dist/src/config/config-schema.js +9 -11
- 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 +4 -52
- package/dist/src/config/config-validation.js +4 -38
- package/dist/src/config/config-views.js +9 -34
- package/dist/src/config/config-write.js +6 -23
- package/dist/src/config/config.js +4 -5
- package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
- package/dist/src/doctor/doctor-backlog-plans.js +24 -24
- package/dist/src/doctor/doctor-backlog.js +7 -5
- package/dist/src/doctor/doctor-check-names.js +3 -6
- package/dist/src/doctor/doctor-checks.js +5 -5
- package/dist/src/doctor/doctor-claude-checks.js +44 -28
- package/dist/src/doctor/doctor-codex-checks.js +6 -6
- package/dist/src/doctor/doctor-contention-checks.js +44 -112
- package/dist/src/doctor/doctor-fallback-checks.js +2 -2
- package/dist/src/doctor/doctor-holds.js +6 -45
- 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 +120 -20
- 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 +3 -3
- 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 +30 -54
- package/dist/src/events/event-labels.js +1 -5
- package/dist/src/events/event-legacy.js +1 -3
- package/dist/src/events/{notify.js → event-notify.js} +2 -2
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/failure/failure-cluster.js +6 -6
- package/dist/src/failure/failure-data.js +8 -4
- package/dist/src/failure/failure-render.js +44 -34
- package/dist/src/failure/failure-state-change.js +4 -34
- 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 +27 -68
- 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} +7 -7
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- 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-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-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +22 -23
- package/dist/src/gui/gui-prompt-commands.js +23 -23
- package/dist/src/gui/gui-report-endpoints.js +1 -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 +51 -33
- 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-core.js +1 -1
- package/dist/src/merge/merge-drain.js +8 -11
- package/dist/src/merge/merge-pipeline.js +6 -7
- package/dist/src/merge/merge-post-review-drain.js +10 -13
- package/dist/src/merge/merge-requeue.js +1 -1
- package/dist/src/merge/merge-slot.js +0 -8
- package/dist/src/merge/merge-verify.js +8 -18
- package/dist/src/notes-cli.js +52 -42
- package/dist/src/operator/operator-commands.js +76 -44
- 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 +27 -32
- package/dist/src/orchestrator/orchestrator.js +8 -21
- package/dist/src/orchestrator/scheduling-admit.js +101 -68
- package/dist/src/orchestrator/scheduling-scope.js +1 -1
- package/dist/src/orchestrator/scheduling-skip.js +1 -1
- package/dist/src/orchestrator/startup-migrations.js +1 -1
- package/dist/src/paths.js +8 -7
- 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 +1 -1
- 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 +32 -6
- package/dist/src/pi/pi-watchdogs.js +12 -5
- 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 +49 -5
- package/dist/src/process/process.js +8 -8
- package/dist/src/process/run-marker.js +31 -13
- 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/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/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/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +1 -1
- package/dist/src/review/review-precheck.js +11 -1
- package/dist/src/review/review.js +40 -153
- package/dist/src/review/secret-leaks.js +26 -16
- 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 +1 -14
- package/dist/src/roles/role-view.js +9 -21
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/run-apply.js +12 -6
- 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 +135 -84
- 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 +119 -50
- package/dist/src/run/stage-check.js +49 -32
- 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/runner-outcome.js +6 -1
- package/dist/src/runner/runner-pi.js +3 -10
- package/dist/src/runner/runner-recovery.js +1 -1
- package/dist/src/runner/{revision.js → runner-revision.js} +1 -1
- package/dist/src/runner/runner-run.js +97 -35
- package/dist/src/runner/runner-state-load.js +12 -9
- package/dist/src/runner/runner.js +41 -25
- package/dist/src/scheduling/claims.js +0 -4
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-data.js +2 -2
- package/dist/src/status/status-role-notes.js +2 -2
- package/dist/src/status/status-roles.js +9 -17
- 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 +98 -65
- package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +2 -15
- package/dist/src/team/team-polls.js +28 -126
- package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
- package/dist/src/team/team-state.js +4 -13
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +20 -15
- package/dist/src/text/text.js +3 -3
- package/dist/src/ui/alert-feeds.js +49 -73
- package/dist/src/ui/badges.js +3 -13
- package/dist/src/ui/gui/gui-client-boot.js +2 -2
- package/dist/src/ui/gui/gui-client-composer.js +1 -1
- package/dist/src/ui/gui/gui-client-drawer-config.js +7 -17
- package/dist/src/ui/gui/gui-client-drawer.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-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 +4 -4
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/progress-data.js +53 -40
- package/dist/src/ui/run-progress-model.js +9 -2
- package/dist/src/ui/status-model.js +2 -24
- package/dist/src/ui/status-payload.js +3 -7
- package/dist/src/ui/status-render.js +5 -9
- package/dist/src/ui/team-alert-backlog.js +6 -6
- 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 +7 -8
- package/dist/src/ui/transcript-run.js +27 -16
- 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 +1 -7
- 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 +159 -92
- 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 +3 -3
- 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 -228
- package/dist/src/ui/tui/tui-pane-history.js +61 -70
- package/dist/src/ui/tui/tui-pane-pending.js +284 -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 +11 -7
- 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 +29 -32
- package/dist/src/ui/tui/tui.js +3 -4
- package/dist/src/verdict/fix-claim.js +76 -54
- package/dist/src/verdict/refusal.js +1 -1
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- 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/backlog/{question-answer.js → backlog-question-answer.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
- /package/dist/src/runner/{leftover.js → runner-leftover.js} +0 -0
|
@@ -93,35 +93,24 @@ const pushReason = async (findings, check) => {
|
|
|
93
93
|
if (reason)
|
|
94
94
|
findings.push(reason);
|
|
95
95
|
};
|
|
96
|
-
/** The findings
|
|
97
|
-
*
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (files.includes("PLANS.md")) {
|
|
111
|
-
const { head, base } = await headAndBase(wt, mainBranch, "PLANS.md");
|
|
112
|
-
// Finished plans are archived in PLANS-DONE.md (plans/backlog-archive.md): a requires clause
|
|
113
|
-
// naming a merged part resolves there, on both sides of the comparison alike.
|
|
114
|
-
const archived = parseEntryDetails(readTextOrNull(path.join(wt, PLANS_ARCHIVE)) ?? "", "Done").map((e) => e.title);
|
|
115
|
-
const dependency = base === undefined ? undefined : planDependencyFinding(head, base, archived);
|
|
116
|
-
if (dependency !== undefined)
|
|
117
|
-
findings.push(dependency);
|
|
118
|
-
const refusal = base === undefined ? undefined : ungroundedRefusalFinding("PLANS.md", head, base);
|
|
119
|
-
if (refusal !== undefined)
|
|
120
|
-
findings.push(refusal);
|
|
121
|
-
}
|
|
96
|
+
/** The PLANS.md record findings: a requires clause naming a part the archive lacks, and an
|
|
97
|
+
* ungrounded refusal that cites another part. */
|
|
98
|
+
async function plansFindings(wt, mainBranch) {
|
|
99
|
+
const { head, base } = await headAndBase(wt, mainBranch, "PLANS.md");
|
|
100
|
+
// Finished plans are archived in PLANS-DONE.md (plans/backlog-archive.md): a requires clause
|
|
101
|
+
// naming a merged part resolves there, on both sides of the comparison alike.
|
|
102
|
+
const archived = parseEntryDetails(readTextOrNull(path.join(wt, PLANS_ARCHIVE)) ?? "", "Done").map((e) => e.title);
|
|
103
|
+
const dependency = base === undefined ? undefined : planDependencyFinding(head, base, archived);
|
|
104
|
+
const refusal = base === undefined ? undefined : ungroundedRefusalFinding("PLANS.md", head, base);
|
|
105
|
+
return [dependency, refusal].filter((f) => f !== undefined);
|
|
106
|
+
}
|
|
107
|
+
/** The finished-archive findings: duplicates of entries the archive already holds, plus the
|
|
108
|
+
* acceptance greps of the plan entries the change finished (for a PLANS.md change). */
|
|
109
|
+
async function archiveFindings(wt, mainBranch, files) {
|
|
122
110
|
const baseRev = FINISHED_ARCHIVES.some((s) => files.includes(s.file) || files.includes(s.archive))
|
|
123
111
|
? await changeBaseRev(wt, mainBranch).catch(() => undefined)
|
|
124
112
|
: undefined;
|
|
113
|
+
const findings = [];
|
|
125
114
|
for (const spec of FINISHED_ARCHIVES) {
|
|
126
115
|
if (baseRev === undefined || !(files.includes(spec.file) || files.includes(spec.archive)))
|
|
127
116
|
continue;
|
|
@@ -144,12 +133,18 @@ export async function stageCheckFindings(wt, mainBranch, exemptPaths, role) {
|
|
|
144
133
|
})().catch(() => []);
|
|
145
134
|
findings.push(...greps);
|
|
146
135
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
136
|
+
return findings;
|
|
137
|
+
}
|
|
138
|
+
/** The BUGS.md record finding: an ungrounded refusal that cites another bug. */
|
|
139
|
+
async function bugsFindings(wt, mainBranch) {
|
|
140
|
+
const { head, base } = await headAndBase(wt, mainBranch, "BUGS.md");
|
|
141
|
+
const refusal = base === undefined ? undefined : ungroundedRefusalFinding("BUGS.md", head, base);
|
|
142
|
+
return refusal === undefined ? [] : [refusal];
|
|
143
|
+
}
|
|
144
|
+
/** The stray-placeholder findings: a `_None yet._` line a changed record still leaves in a
|
|
145
|
+
* section that holds entries. */
|
|
146
|
+
async function placeholderFindings(wt, mainBranch, files) {
|
|
147
|
+
const findings = [];
|
|
153
148
|
for (const file of PLACEHOLDER_FILES) {
|
|
154
149
|
if (!files.includes(file))
|
|
155
150
|
continue;
|
|
@@ -158,6 +153,28 @@ export async function stageCheckFindings(wt, mainBranch, exemptPaths, role) {
|
|
|
158
153
|
if (stray !== undefined)
|
|
159
154
|
findings.push(stray);
|
|
160
155
|
}
|
|
156
|
+
return findings;
|
|
157
|
+
}
|
|
158
|
+
/** The findings likely to get this uncommitted change rejected at merge, or an empty list when
|
|
159
|
+
* it is clean: the merge gate's own deterministic checks plus the staging-only heuristics (plan
|
|
160
|
+
* dependencies, path drift). Checks the worktree at `wt` against
|
|
161
|
+
* `mainBranch`'s merge-base; `exemptPaths` are the config's review-exemption globs that scope
|
|
162
|
+
* the false-fix check.
|
|
163
|
+
*
|
|
164
|
+
* Never throws: a check that fails yields no finding, so a git hiccup at staging time defers to
|
|
165
|
+
* the gate instead of failing the run. */
|
|
166
|
+
export async function stageCheckFindings(wt, mainBranch, exemptPaths, role) {
|
|
167
|
+
const files = await changedFiles(wt).catch(() => null);
|
|
168
|
+
if (files === null || files.length === 0)
|
|
169
|
+
return [];
|
|
170
|
+
const findings = [];
|
|
171
|
+
await pushReason(findings, backlogStructureReason(wt, mainBranch, files));
|
|
172
|
+
if (files.includes("PLANS.md"))
|
|
173
|
+
findings.push(...(await plansFindings(wt, mainBranch)));
|
|
174
|
+
findings.push(...(await archiveFindings(wt, mainBranch, files)));
|
|
175
|
+
if (files.includes("BUGS.md"))
|
|
176
|
+
findings.push(...(await bugsFindings(wt, mainBranch)));
|
|
177
|
+
findings.push(...(await placeholderFindings(wt, mainBranch, files)));
|
|
161
178
|
await pushReason(findings, protectedEditReason(wt, mainBranch, files, role));
|
|
162
179
|
await pushReason(findings, secretLeakReason(wt, mainBranch, files));
|
|
163
180
|
if (isExemptDiff(files, exemptPaths))
|
|
@@ -16,6 +16,34 @@ export const MAX_STALE_PATHS = 10;
|
|
|
16
16
|
export function diffPlusPath(line) {
|
|
17
17
|
return line.slice(4).replace(/^b\//, "");
|
|
18
18
|
}
|
|
19
|
+
/** Parse a `-U0` unified diff into each file's removed and added content lines, marker-stripped.
|
|
20
|
+
* The `diff --git` header names the file, and git's `---`/`+++` old/new path headers carry no
|
|
21
|
+
* content; `@@` hunk headers, context lines and the `\ No newline` marker match neither branch,
|
|
22
|
+
* so the walk records only real `-`/`+` content lines. Consumers keep only what they want — a
|
|
23
|
+
* JS-file filter, a trim-and-min-length rule, a compound-name extraction — from the per-file
|
|
24
|
+
* removed/added lists; stage-path-checks's movedSourceFindings and stage-name-checks's
|
|
25
|
+
* declaredNameDeltas each walked this same grammar before the helper. stage-prose-checks's
|
|
26
|
+
* detachedDocFindings and addedDiffLines are deliberately not folded here: both need each hunk's
|
|
27
|
+
* start-line count from the `@@` header, which this shape drops. */
|
|
28
|
+
export function diffRemovedAdded(diff) {
|
|
29
|
+
const files = [];
|
|
30
|
+
let cur;
|
|
31
|
+
for (const line of diff.split("\n")) {
|
|
32
|
+
const header = /^diff --git a\/.* b\/(.*)$/.exec(line);
|
|
33
|
+
if (header) {
|
|
34
|
+
cur = { path: header[1], removed: [], added: [] };
|
|
35
|
+
files.push(cur);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (!cur)
|
|
39
|
+
continue;
|
|
40
|
+
if (line.startsWith("-") && !line.startsWith("---"))
|
|
41
|
+
cur.removed.push(line.slice(1));
|
|
42
|
+
else if (line.startsWith("+") && !line.startsWith("+++"))
|
|
43
|
+
cur.added.push(line.slice(1));
|
|
44
|
+
}
|
|
45
|
+
return files;
|
|
46
|
+
}
|
|
19
47
|
/** Parse `git grep -n` stage output into `file:line:text` hits. `git grep -n` prints one
|
|
20
48
|
* `path:line:text` per match, and the four name/path checks (stage-name-checks.ts's
|
|
21
49
|
* staleNameFindings and movedNameFindings, stage-path-checks.ts's movedSourceFindings and
|
|
@@ -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 [
|