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
|
@@ -3,8 +3,8 @@ import { daysBetween } from "../text/datetime.js";
|
|
|
3
3
|
* QUESTIONS.md text, with no filesystem access — every function here takes the markdown text as
|
|
4
4
|
* an argument. Split from src/backlog/backlog.ts, which keeps the stat-cached file readers
|
|
5
5
|
* (sectionEntries, the root-based plannedPlans/openBugs/openQuestions family, and the
|
|
6
|
-
* Done/Fixed date scan): consumers who only parse markdown (question-answer.ts's
|
|
7
|
-
* walks, backlog-write.ts's section appends, backlog-structure.ts's stranding checks)
|
|
6
|
+
* Done/Fixed date scan): consumers who only parse markdown (backlog-question-answer.ts's
|
|
7
|
+
* QUESTIONS.md walks, backlog-write.ts's section appends, backlog-structure.ts's stranding checks)
|
|
8
8
|
* import from here without reaching the file/cache layer, and a caller who parses a string never
|
|
9
9
|
* pays for a stat. Both layers share one fenceTracker state machine, so independent readers can
|
|
10
10
|
* never disagree about what is body content. */
|
|
@@ -87,7 +87,7 @@ export function headingTitle(line, prefix) {
|
|
|
87
87
|
* code block. The single home of the fence-aware heading guard (`!fenced.inside(line) &&
|
|
88
88
|
* line.startsWith(prefix)` plus headingTitle) that every line-level reader of the backlog
|
|
89
89
|
* docs repeats: sectionLines' section boundaries and parseEntryDetails' entry boundaries here,
|
|
90
|
-
* and question-answer.ts's walks of QUESTIONS.md (the Open/Answered scan, the `### ` block
|
|
90
|
+
* and backlog-question-answer.ts's walks of QUESTIONS.md (the Open/Answered scan, the `### ` block
|
|
91
91
|
* split, and the Answered section's start); that file's two pure next-`## ` boundary walks
|
|
92
92
|
* go through nextSectionHeading below instead. Readers that walk
|
|
93
93
|
* whole documents collecting heading lines go through fenceAwareHeadingLines instead; callers
|
|
@@ -120,7 +120,7 @@ export function sectionLines(md, sectionTitle) {
|
|
|
120
120
|
/** The index of the next `## ` section heading at or after `from`, fence-aware through the
|
|
121
121
|
* caller's `fenced` tracker — or `lines.length` when none follows. The section-end boundary
|
|
122
122
|
* rule as an index, for readers that must cut or splice at the boundary rather than collect
|
|
123
|
-
* its content (question-answer.ts cuts the Open section at its end and inserts a moved
|
|
123
|
+
* its content (backlog-question-answer.ts cuts the Open section at its end and inserts a moved
|
|
124
124
|
* block before Answered's next `## `; backlog-write.ts cuts the section a filed entry is
|
|
125
125
|
* appended to). Shares fencedHeadingTitle's guard with sectionLines, so
|
|
126
126
|
* a boundary found here is the same boundary sectionLines would stop at. */
|
|
@@ -418,7 +418,7 @@ export function lastDate(text, dateRe) {
|
|
|
418
418
|
* about when an entry opened. dateRe is cloned with the g flag so matchAll sees every date; a
|
|
419
419
|
* text with no date yields null. Exported so the heading stamp parsers that apply the same
|
|
420
420
|
* filing rule to a single title line — `filingDate` (backlog.ts) and `questionFilingDate`
|
|
421
|
-
* (question-answer.ts) — share the one first-match mechanism instead of re-stating it;
|
|
421
|
+
* (backlog-question-answer.ts) — share the one first-match mechanism instead of re-stating it;
|
|
422
422
|
* entryDates selects it with `first`, so the walker stays single-homed like lastDate's
|
|
423
423
|
* completion rule. */
|
|
424
424
|
export function firstDate(text, dateRe) {
|
|
@@ -16,25 +16,37 @@ function parseRef(chunk, fallbackSeries) {
|
|
|
16
16
|
const m = /^(?:([\s\S]+?)\s+)?([0-9]+[a-z]?)\/(\d+)(?:\s*[–-]\s*(\d+)\/(\d+))?$/i.exec(text);
|
|
17
17
|
if (m === null)
|
|
18
18
|
return { refs: [], named: null };
|
|
19
|
-
|
|
20
|
-
const series = named || fallbackSeries;
|
|
21
|
-
if (series === null || series === "")
|
|
22
|
-
return { refs: [], named: named || null };
|
|
19
|
+
const { named, series } = resolveRefSeries(m[1], fallbackSeries);
|
|
23
20
|
const of = Number(m[3]);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
if (series === null || !Number.isInteger(of) || of < 1)
|
|
22
|
+
return { refs: [], named };
|
|
23
|
+
return { refs: refsFromMatch(series, of, m[2], m[4] ?? null), named };
|
|
24
|
+
}
|
|
25
|
+
/** Whichever series a ref chunk resolves to: a captured name with any trailing `parts`/`part`
|
|
26
|
+
* keyword dropped, or — for a bare `parts a/n` — the clause's last-named series (null when it
|
|
27
|
+
* named none, so a bare ref there resolves to nothing). */
|
|
28
|
+
function resolveRefSeries(matchName, fallbackSeries) {
|
|
29
|
+
const named = (matchName?.trim() ?? "").replace(/\s+parts?$/i, "");
|
|
30
|
+
if (named !== "")
|
|
31
|
+
return { named, series: named };
|
|
32
|
+
return { named: null, series: fallbackSeries === "" ? null : fallbackSeries };
|
|
33
|
+
}
|
|
34
|
+
/** Build the resolved refs for a chunk whose regex matched: the one part, or an inclusive
|
|
35
|
+
* numeric range. The part token arrives verbatim (so `2a` survives); a range is numeric on
|
|
36
|
+
* both ends (the lettered parts never range) and expands to that many single refs. */
|
|
37
|
+
function refsFromMatch(series, of, startText, toText) {
|
|
38
|
+
const start = startText.toLowerCase();
|
|
39
|
+
if (toText === null)
|
|
40
|
+
return [{ series, part: start, of }];
|
|
29
41
|
// A range is numeric on both ends (the lettered parts never range); expand it inclusively.
|
|
30
42
|
const from = Number(start);
|
|
31
|
-
const to = Number(
|
|
43
|
+
const to = Number(toText);
|
|
32
44
|
if (!Number.isInteger(from) || !Number.isInteger(to) || to < from)
|
|
33
|
-
return
|
|
45
|
+
return [];
|
|
34
46
|
const parts = [];
|
|
35
47
|
for (let p = from; p <= to; p++)
|
|
36
48
|
parts.push({ series, part: String(p), of });
|
|
37
|
-
return
|
|
49
|
+
return parts;
|
|
38
50
|
}
|
|
39
51
|
/** The stable identity of an entry heading: its `(planned …)`/`(done …)` stamp suffix
|
|
40
52
|
* removed through the shared stripEntryStamp, whitespace collapsed, lowercased. Adding a
|
|
@@ -181,12 +193,45 @@ const MISWORDED_DEPENDENCY_RE = /\b(?:blocked by|blocked on|after|follows|depend
|
|
|
181
193
|
* Refused and needs-review entries count like the rest, since their clauses still decide when
|
|
182
194
|
* they unblock. */
|
|
183
195
|
export function planDependencyProblems(planned, doneTitles) {
|
|
196
|
+
const known = dependencyKnownParts(planned, doneTitles);
|
|
197
|
+
const { nodes, dangling, unparsed, misworded } = dependencyEntryScan(planned, known);
|
|
198
|
+
return { cycle: firstDependencyCycle(nodes), dangling, unparsed, misworded };
|
|
199
|
+
}
|
|
200
|
+
/** The part keys that count as known for dangling checks: every still-planned entry's part plus
|
|
201
|
+
* every done title's part — a dangling ref is one no planned or done heading carries. */
|
|
202
|
+
function dependencyKnownParts(planned, doneTitles) {
|
|
184
203
|
const known = plannedPartKeys(planned);
|
|
185
204
|
for (const t of doneTitles) {
|
|
186
205
|
const part = seriesPart(t);
|
|
187
206
|
if (part !== null)
|
|
188
207
|
known.add(partKey(part.series, part.part));
|
|
189
208
|
}
|
|
209
|
+
return known;
|
|
210
|
+
}
|
|
211
|
+
/** The misworded refs a still-planned `Series, part i/n:` entry contributes: the series-parts its
|
|
212
|
+
* trailing-or-open parenthetical names through a dependency verb with no requires clause holding
|
|
213
|
+
* them, each `title`+`ref` deduped against the refs already collected across the whole planned
|
|
214
|
+
* list (a duplicate title contributes one ref, not one per occurrence). */
|
|
215
|
+
function miswordedDependencyRefs(entry, own, refs, plannedKeys, misworded) {
|
|
216
|
+
let meta = trailingParenthetical(entry.title);
|
|
217
|
+
if (meta === "")
|
|
218
|
+
meta = openParenthetical(entry.title);
|
|
219
|
+
const held = new Set(refs.map((ref) => partKey(ref.series, ref.part)));
|
|
220
|
+
for (const m of meta.matchAll(MISWORDED_DEPENDENCY_RE)) {
|
|
221
|
+
for (const ref of parseRef(m[1], own.series).refs) {
|
|
222
|
+
const key = partKey(ref.series, ref.part);
|
|
223
|
+
if (!plannedKeys.has(key) || held.has(key))
|
|
224
|
+
continue;
|
|
225
|
+
const name = `${ref.series} ${ref.part}/${ref.of}`;
|
|
226
|
+
if (!misworded.some((w) => w.title === entry.title && w.ref === name))
|
|
227
|
+
misworded.push({ title: entry.title, ref: name });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/** The one-pass scan over the planned entries: a clause that parses to no refs pushes its title to
|
|
232
|
+
* `unparsed`, a ref `known` does not carry lands in `dangling`, and each entry with a series part
|
|
233
|
+
* contributes its misworded refs and a graph node whose deps are its required parts' keys. */
|
|
234
|
+
function dependencyEntryScan(planned, known) {
|
|
190
235
|
const dangling = [];
|
|
191
236
|
const unparsed = [];
|
|
192
237
|
const misworded = [];
|
|
@@ -203,25 +248,18 @@ export function planDependencyProblems(planned, doneTitles) {
|
|
|
203
248
|
const own = seriesPart(entry.title);
|
|
204
249
|
if (own === null)
|
|
205
250
|
continue;
|
|
206
|
-
|
|
207
|
-
if (meta === "")
|
|
208
|
-
meta = openParenthetical(entry.title);
|
|
209
|
-
const held = new Set(refs.map((ref) => partKey(ref.series, ref.part)));
|
|
210
|
-
for (const m of meta.matchAll(MISWORDED_DEPENDENCY_RE)) {
|
|
211
|
-
for (const ref of parseRef(m[1], own.series).refs) {
|
|
212
|
-
const key = partKey(ref.series, ref.part);
|
|
213
|
-
if (!plannedKeys.has(key) || held.has(key))
|
|
214
|
-
continue;
|
|
215
|
-
const name = `${ref.series} ${ref.part}/${ref.of}`;
|
|
216
|
-
if (!misworded.some((w) => w.title === entry.title && w.ref === name))
|
|
217
|
-
misworded.push({ title: entry.title, ref: name });
|
|
218
|
-
}
|
|
219
|
-
}
|
|
251
|
+
miswordedDependencyRefs(entry, own, refs, plannedKeys, misworded);
|
|
220
252
|
nodes.set(partKey(own.series, own.part), {
|
|
221
253
|
name: `${own.series} ${own.part}/${own.of}`,
|
|
222
254
|
deps: refs.map((ref) => partKey(ref.series, ref.part)),
|
|
223
255
|
});
|
|
224
256
|
}
|
|
257
|
+
return { nodes, dangling, unparsed, misworded };
|
|
258
|
+
}
|
|
259
|
+
/** The first dependency cycle the depth-first walk finds, as `Series i/n` names in walk order with
|
|
260
|
+
* the first name repeated last (a self-reference is a one-hop cycle), or null when the graph is
|
|
261
|
+
* acyclic. The `open`/`done` state map and the walk order decide which cycle reports first. */
|
|
262
|
+
function firstDependencyCycle(nodes) {
|
|
225
263
|
const state = new Map();
|
|
226
264
|
const stack = [];
|
|
227
265
|
const visit = (key) => {
|
|
@@ -252,7 +290,7 @@ export function planDependencyProblems(planned, doneTitles) {
|
|
|
252
290
|
if (cycle !== null)
|
|
253
291
|
break;
|
|
254
292
|
}
|
|
255
|
-
return
|
|
293
|
+
return cycle;
|
|
256
294
|
}
|
|
257
295
|
/** The prerequisite refs of `title`'s `requires … landed` clause when none of them is still in
|
|
258
296
|
* `plannedParts` (built by plannedPartKeys), else []. A heading with no parseable clause is never
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* output helpers, so every surface can drive it without ending the process.
|
|
8
8
|
*
|
|
9
9
|
* Shared by every read/answer surface: the CLI's `tumwater questions` command body wraps it in
|
|
10
|
-
* src/cli/question-commands.ts (the say/fail/editor layer), while the TUI's answer box
|
|
10
|
+
* src/cli/cli-question-commands.ts (the say/fail/editor layer), while the TUI's answer box
|
|
11
11
|
* (src/ui/tui/tui-keys.ts) and the dashboard's POST route (src/gui/gui-endpoint-commands.ts)
|
|
12
12
|
* import the non-exiting answer core directly. It lives with the rest of the BUGS/PLANS/QUESTIONS
|
|
13
13
|
* markdown (src/backlog/), not under src/cli/, so the non-command surfaces never depend on the
|
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
/** The hold-lifting and reordering writes of `tumwater bug|plan --release <n>` and `--top <n>`:
|
|
2
|
-
* lift an entry's Refused or
|
|
2
|
+
* lift an entry's Refused or Needs-review note, or move
|
|
3
3
|
* it to the head of its open
|
|
4
4
|
* section. Each comes as an exiting writer (the CLI) and a non-exiting `try…` core that refuses as
|
|
5
5
|
* data (the TUI and dashboard); entry lookup and the section's file target are
|
|
6
6
|
* backlog-write.ts's. */
|
|
7
7
|
import { backlogTarget, findEntry, locateDashboardEntry, readEntryLines } from "./backlog-write.js";
|
|
8
|
-
import {
|
|
8
|
+
import { withoutNeedsReviewNotes, withoutRefusalNotes } from "./backlog-eligibility.js";
|
|
9
9
|
import { writeTextAtomic } from "../files/files.js";
|
|
10
10
|
import { fail } from "../cli/cli-output.js";
|
|
11
11
|
import { trimTrailingBlankLines } from "../text/text.js";
|
|
12
12
|
import { withPrimaryWorktreeLock } from "../concurrency/lock.js";
|
|
13
|
-
/** The prose naming the note
|
|
14
|
-
* exit-announcers so their released lines
|
|
13
|
+
/** The prose naming the note kind a release removed — plural, since a release lifts every line
|
|
14
|
+
* of the chosen kind — shared by the CLI, TUI and the exit-announcers so their released lines
|
|
15
|
+
* cannot drift. */
|
|
15
16
|
export function liftedNotePhrase(lifted) {
|
|
16
17
|
return lifted === "refused"
|
|
17
|
-
? "the Refused
|
|
18
|
-
: "the
|
|
18
|
+
? "the Refused notes"
|
|
19
|
+
: "the Needs review notes";
|
|
19
20
|
}
|
|
20
21
|
/** Drop the Refused note from `entry`'s slice of `lines`, in place, and return its kind; an
|
|
21
|
-
* entry with none loses its
|
|
22
|
-
* threshold, plus its **Previously held N** line). Null (and `lines` untouched) when the entry
|
|
22
|
+
* entry with none loses its Needs-review note. Null (and `lines` untouched) when the entry
|
|
23
23
|
* carries neither, shared by releaseEntry and tryReleaseEntry so they cannot drift. */
|
|
24
24
|
function liftRefusal(lines, entry) {
|
|
25
25
|
const slice = lines.slice(entry.start - 1, entry.end);
|
|
26
26
|
let kept = withoutRefusalNotes(slice);
|
|
27
27
|
if (kept.length === slice.length) {
|
|
28
|
-
kept =
|
|
28
|
+
kept = withoutNeedsReviewNotes(slice);
|
|
29
29
|
if (kept.length === slice.length)
|
|
30
30
|
return null;
|
|
31
31
|
lines.splice(entry.start - 1, slice.length, ...kept);
|
|
32
|
-
return "
|
|
32
|
+
return "needs-review";
|
|
33
33
|
}
|
|
34
34
|
lines.splice(entry.start - 1, slice.length, ...kept);
|
|
35
35
|
return "refused";
|
|
@@ -51,7 +51,7 @@ export function tryReleaseEntry(root, kind, n, expectedTitle) {
|
|
|
51
51
|
return { status: "released", file: fileName, title: entry.title, lifted };
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
|
-
/** Lift the Refused or
|
|
54
|
+
/** Lift the Refused or Needs-review note from the
|
|
55
55
|
* `n`th entry (1-based, as `--list` numbers them) of the kind's open section and write the
|
|
56
56
|
* file, under the same lock the cancel and edit writers hold, returning which hold it lifted.
|
|
57
57
|
* An out-of-range position fails with locateEntry's wording; an entry with neither note
|
|
@@ -62,7 +62,7 @@ export function releaseEntry(root, kind, n) {
|
|
|
62
62
|
const { file, lines, entry } = readEntryLines(root, fileName, sectionTitle, n);
|
|
63
63
|
const lifted = liftRefusal(lines, entry);
|
|
64
64
|
if (lifted === null)
|
|
65
|
-
fail(`${kind} #${n} "${entry.title}" carries no Refused or
|
|
65
|
+
fail(`${kind} #${n} "${entry.title}" carries no Refused or Needs review note`);
|
|
66
66
|
writeTextAtomic(file, lines.join("\n"));
|
|
67
67
|
return { file: fileName, title: entry.title, lifted };
|
|
68
68
|
});
|
|
@@ -215,6 +215,113 @@ export function doubleBlockFixedEntries(md) {
|
|
|
215
215
|
}
|
|
216
216
|
return found;
|
|
217
217
|
}
|
|
218
|
+
/** The duplicate-heading rule (a): the first `## ` title `head` carries more of than `base` did,
|
|
219
|
+
* or undefined when none. A base that already carries a duplicate never blocks unrelated edits —
|
|
220
|
+
* this fires only when the head's count EXCEEDS the base's, so a change that removes a duplicate
|
|
221
|
+
* always passes. */
|
|
222
|
+
function duplicateSectionReason(file, headCounts, baseCounts) {
|
|
223
|
+
for (const [title, count] of headCounts)
|
|
224
|
+
if (count > 1 && count > (baseCounts.get(title) ?? 0))
|
|
225
|
+
return (`${file} adds another "## ${title}" heading — readers take the first section of a ` +
|
|
226
|
+
`name, so anything under the duplicate is invisible; keep one "## ${title}" per file`);
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
/** The dropped-known-section rule (b): the first `## ` title the base carried but `head` dropped,
|
|
230
|
+
* restricted to this file's KNOWN_SECTIONS, or undefined when none. A `## ` title that is not one
|
|
231
|
+
* of this file's known sections is not load-bearing: ADDING one is a legitimate project extension
|
|
232
|
+
* (this repo grew `## Verified`, a doc-only stack can grow `## Notes`), so a new one passes — but
|
|
233
|
+
* DROPPING one is the repair for a misplaced heading, so this rule guards only known sections and
|
|
234
|
+
* leaves an unknown one free to be demoted to `### ` under a known section (BUGS.md 2026-10-23). */
|
|
235
|
+
function droppedSectionReason(file, headCounts, baseCounts) {
|
|
236
|
+
// A `## ` title that is not one of this file's known sections (KNOWN_SECTIONS) is not
|
|
237
|
+
// load-bearing: ADDING one is a legitimate project extension (this repo grew `## Verified`,
|
|
238
|
+
// a doc-only stack can grow `## Notes`), so a new one passes — but DROPPING one is the repair
|
|
239
|
+
// for a misplaced heading, so rule (b) guards only known sections and leaves an unknown one
|
|
240
|
+
// free to be demoted to `### ` under a known section (BUGS.md 2026-10-23).
|
|
241
|
+
const known = KNOWN_SECTIONS[file] ?? [];
|
|
242
|
+
for (const title of baseCounts.keys())
|
|
243
|
+
if (!headCounts.has(title) && known.includes(title))
|
|
244
|
+
return (`${file} drops the "## ${title}" section the base had — entries filed under it would ` +
|
|
245
|
+
`be invisible to every section reader; restore "## ${title}"`);
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
/** The re-added-archive-heading rule: for PLANS.md's and BUGS.md's archives only, rejects a head
|
|
249
|
+
* that adds a copy of a `### ` heading the base archive already carries (readdedHeadings) —
|
|
250
|
+
* finished history pasted back, never a new finished entry — or undefined for every other file.
|
|
251
|
+
* An archive only grows by new headings: a copy of one the base archive already carries is
|
|
252
|
+
* finished history pasted back (readdedHeadings), never a new finished entry.
|
|
253
|
+
*/
|
|
254
|
+
function readdedHeadingReason(file, base, head) {
|
|
255
|
+
if (file === PLANS_ARCHIVE || file === BUGS_ARCHIVE) {
|
|
256
|
+
const readded = readdedHeadings(base, head);
|
|
257
|
+
if (readded.length > 0)
|
|
258
|
+
return (`${file} adds ${plural(readded.length, '"### " heading', '"### " headings')} it already carries ` +
|
|
259
|
+
`(first: "${truncate(readded[0], 160)}") — the archive holds each finished entry once, so the ` +
|
|
260
|
+
`copies duplicate its history; remove them, and edit an archived entry in place instead`);
|
|
261
|
+
}
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
/** The misplaced-plan rule (part 4/4), for PLANS.md and its archive only, else undefined: rejects
|
|
265
|
+
* a head that ADDS a `### ` entry directly under `## Done` whose joined heading metadata carries
|
|
266
|
+
* `(planned YYYY-MM-DD` but no `done YYYY-MM-DD`, and whose key the base's PLANS.md and archive
|
|
267
|
+
* never carried in ANY section — a plan filed into the wrong section. strandedPlanEntries already
|
|
268
|
+
* does the fence-aware, joined-metadata reading the first two conditions need; the base's key set
|
|
269
|
+
* — over both files, since tumwater moves a finished plan from one to the other
|
|
270
|
+
* (plans/backlog-archive.md) — supplies the third. Judged against the merge-base (the same base
|
|
271
|
+
* the heading rules use), so a Planned → Done move of an entry the base already had passes and a
|
|
272
|
+
* pre-existing stranded entry (whose key the base has) never blocks unrelated merges. */
|
|
273
|
+
async function misplacedPlanReason(file, wt, baseRev, base, head) {
|
|
274
|
+
// Part 4/4's rule, PLANS.md and its archive (see this function's doc comment): a NEW plan
|
|
275
|
+
// filed directly under ## Done with no done date. strandedPlanEntries already does the
|
|
276
|
+
// fence-aware, joined-metadata reading the first two conditions need; the base's key set —
|
|
277
|
+
// over both files, since tumwater moves a finished plan from one to the other
|
|
278
|
+
// (plans/backlog-archive.md) — supplies the third.
|
|
279
|
+
if (file === "PLANS.md" || file === PLANS_ARCHIVE) {
|
|
280
|
+
const baseKeys = new Set([
|
|
281
|
+
...planHeadingKeys(file === "PLANS.md" ? base : await fileContentAt(wt, baseRev, "PLANS.md")),
|
|
282
|
+
...planHeadingKeys(file === PLANS_ARCHIVE ? base : await fileContentAt(wt, baseRev, PLANS_ARCHIVE)),
|
|
283
|
+
]);
|
|
284
|
+
const newcomer = strandedPlanEntries(head).find((e) => e.section === "Done" && !baseKeys.has(planHeadingKey(e.title)));
|
|
285
|
+
if (newcomer)
|
|
286
|
+
// The fault and its fix differ by file: in PLANS.md a NEW plan (the base never carried
|
|
287
|
+
// it in any section) was pasted under ## Done unfinished and belongs under ## Planned;
|
|
288
|
+
// in the archive the same heading is a finished plan the commit-time sweep already moved
|
|
289
|
+
// here, whose only fault is a missing `done YYYY-MM-DD` token — telling its author to
|
|
290
|
+
// "file it under ## Planned" is the opposite of the fix and wastes a revision.
|
|
291
|
+
return file === PLANS_ARCHIVE
|
|
292
|
+
? `${file} files "${newcomer.title}" directly under "## Done" with no done date — the ` +
|
|
293
|
+
`entry is finished and archived, so the only fix is to add \`done YYYY-MM-DD\` to its ` +
|
|
294
|
+
`"### " heading; do not move it back under "## Planned"`
|
|
295
|
+
: `${file} files "${newcomer.title}" directly under "## Done" with no done date — a plan ` +
|
|
296
|
+
`written into the done section is invisible to every Planned reader; file it under ` +
|
|
297
|
+
`"## Planned" instead`;
|
|
298
|
+
}
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
/** The hidden-fixed-record rule, for BUGS.md and its archive only, else undefined: rejects a head
|
|
302
|
+
* whose multi-block `## Fixed` entries (doubleBlockFixedEntries) the base did not carry — a new
|
|
303
|
+
* headingless record merged into a neighbor's body — or undefined when none. A new headingless
|
|
304
|
+
* record cannot hide in a Fixed entry's body: reject a head whose multi-block entries the base did
|
|
305
|
+
* not carry (an existing one never blocks unrelated edits, matching the heading-set rules above).
|
|
306
|
+
* The Fix paragraph is the guard's unit, so a merged second block would otherwise sit inside the
|
|
307
|
+
* neighbor it was symbol-checked against. BUGS-FIXED.md holds the same entries once archived.
|
|
308
|
+
*/
|
|
309
|
+
function hiddenFixedRecordReason(file, base, head) {
|
|
310
|
+
// A new headingless record cannot hide in a Fixed entry's body: reject a head whose
|
|
311
|
+
// multi-block entries the base did not carry (an existing one never blocks unrelated
|
|
312
|
+
// edits, matching the heading-set rules above). The Fix paragraph is the guard's unit, so
|
|
313
|
+
// a merged second block would otherwise sit inside the neighbor it was symbol-checked
|
|
314
|
+
// against. BUGS-FIXED.md holds the same entries once archived.
|
|
315
|
+
if (file === "BUGS.md" || file === BUGS_ARCHIVE) {
|
|
316
|
+
const baseTitles = new Set(doubleBlockFixedEntries(base));
|
|
317
|
+
const newcomer = doubleBlockFixedEntries(head).find((title) => !baseTitles.has(title));
|
|
318
|
+
if (newcomer)
|
|
319
|
+
return (`${file}'s Fixed entry "${newcomer}" holds more than one **Symptom:**/**Fix:** block — ` +
|
|
320
|
+
`a record with no "### " heading has merged into it, so the hidden record is invisible ` +
|
|
321
|
+
`to every section reader; give it its own "### " heading`);
|
|
322
|
+
}
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
218
325
|
/** The first structural fault a diff merging into `mainBranch` leaves in a backlog file, or
|
|
219
326
|
* undefined when none: for each touched backlog file it compares the `## ` heading set of the tree
|
|
220
327
|
* being merged (`wt`) against the diff's merge-base — the same base falseFixReason measures
|
|
@@ -268,67 +375,13 @@ export async function backlogStructureReason(wt, mainBranch, files) {
|
|
|
268
375
|
const base = await fileContentAt(wt, baseRev, file);
|
|
269
376
|
const headCounts = sectionTitleCounts(head);
|
|
270
377
|
const baseCounts = sectionTitleCounts(base);
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
// for a misplaced heading, so rule (b) guards only known sections and leaves an unknown one
|
|
279
|
-
// free to be demoted to `### ` under a known section (BUGS.md 2026-10-23).
|
|
280
|
-
const known = KNOWN_SECTIONS[file] ?? [];
|
|
281
|
-
for (const title of baseCounts.keys())
|
|
282
|
-
if (!headCounts.has(title) && known.includes(title))
|
|
283
|
-
return (`${file} drops the "## ${title}" section the base had — entries filed under it would ` +
|
|
284
|
-
`be invisible to every section reader; restore "## ${title}"`);
|
|
285
|
-
// An archive only grows by new headings: a copy of one the base archive already carries is
|
|
286
|
-
// finished history pasted back (readdedHeadings), never a new finished entry.
|
|
287
|
-
if (file === PLANS_ARCHIVE || file === BUGS_ARCHIVE) {
|
|
288
|
-
const readded = readdedHeadings(base, head);
|
|
289
|
-
if (readded.length > 0)
|
|
290
|
-
return (`${file} adds ${plural(readded.length, '"### " heading', '"### " headings')} it already carries ` +
|
|
291
|
-
`(first: "${truncate(readded[0], 160)}") — the archive holds each finished entry once, so the ` +
|
|
292
|
-
`copies duplicate its history; remove them, and edit an archived entry in place instead`);
|
|
293
|
-
}
|
|
294
|
-
// Part 4/4's rule, PLANS.md and its archive (see this function's doc comment): a NEW plan
|
|
295
|
-
// filed directly under ## Done with no done date. strandedPlanEntries already does the
|
|
296
|
-
// fence-aware, joined-metadata reading the first two conditions need; the base's key set —
|
|
297
|
-
// over both files, since tumwater moves a finished plan from one to the other
|
|
298
|
-
// (plans/backlog-archive.md) — supplies the third.
|
|
299
|
-
if (file === "PLANS.md" || file === PLANS_ARCHIVE) {
|
|
300
|
-
const baseKeys = new Set([
|
|
301
|
-
...planHeadingKeys(file === "PLANS.md" ? base : await fileContentAt(wt, baseRev, "PLANS.md")),
|
|
302
|
-
...planHeadingKeys(file === PLANS_ARCHIVE ? base : await fileContentAt(wt, baseRev, PLANS_ARCHIVE)),
|
|
303
|
-
]);
|
|
304
|
-
const newcomer = strandedPlanEntries(head).find((e) => e.section === "Done" && !baseKeys.has(planHeadingKey(e.title)));
|
|
305
|
-
if (newcomer)
|
|
306
|
-
// The fault and its fix differ by file: in PLANS.md a NEW plan (the base never carried
|
|
307
|
-
// it in any section) was pasted under ## Done unfinished and belongs under ## Planned;
|
|
308
|
-
// in the archive the same heading is a finished plan the commit-time sweep already moved
|
|
309
|
-
// here, whose only fault is a missing `done YYYY-MM-DD` token — telling its author to
|
|
310
|
-
// "file it under ## Planned" is the opposite of the fix and wastes a revision.
|
|
311
|
-
return file === PLANS_ARCHIVE
|
|
312
|
-
? `${file} files "${newcomer.title}" directly under "## Done" with no done date — the ` +
|
|
313
|
-
`entry is finished and archived, so the only fix is to add \`done YYYY-MM-DD\` to its ` +
|
|
314
|
-
`"### " heading; do not move it back under "## Planned"`
|
|
315
|
-
: `${file} files "${newcomer.title}" directly under "## Done" with no done date — a plan ` +
|
|
316
|
-
`written into the done section is invisible to every Planned reader; file it under ` +
|
|
317
|
-
`"## Planned" instead`;
|
|
318
|
-
}
|
|
319
|
-
// A new headingless record cannot hide in a Fixed entry's body: reject a head whose
|
|
320
|
-
// multi-block entries the base did not carry (an existing one never blocks unrelated
|
|
321
|
-
// edits, matching the heading-set rules above). The Fix paragraph is the guard's unit, so
|
|
322
|
-
// a merged second block would otherwise sit inside the neighbor it was symbol-checked
|
|
323
|
-
// against. BUGS-FIXED.md holds the same entries once archived.
|
|
324
|
-
if (file === "BUGS.md" || file === BUGS_ARCHIVE) {
|
|
325
|
-
const baseTitles = new Set(doubleBlockFixedEntries(base));
|
|
326
|
-
const newcomer = doubleBlockFixedEntries(head).find((title) => !baseTitles.has(title));
|
|
327
|
-
if (newcomer)
|
|
328
|
-
return (`${file}'s Fixed entry "${newcomer}" holds more than one **Symptom:**/**Fix:** block — ` +
|
|
329
|
-
`a record with no "### " heading has merged into it, so the hidden record is invisible ` +
|
|
330
|
-
`to every section reader; give it its own "### " heading`);
|
|
331
|
-
}
|
|
378
|
+
const reason = duplicateSectionReason(file, headCounts, baseCounts) ??
|
|
379
|
+
droppedSectionReason(file, headCounts, baseCounts) ??
|
|
380
|
+
readdedHeadingReason(file, base, head) ??
|
|
381
|
+
(await misplacedPlanReason(file, wt, baseRev, base, head)) ??
|
|
382
|
+
hiddenFixedRecordReason(file, base, head);
|
|
383
|
+
if (reason !== undefined)
|
|
384
|
+
return reason;
|
|
332
385
|
}
|
|
333
386
|
return undefined;
|
|
334
387
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { cachedTextByStat } from "../files/stat-cache.js";
|
|
3
|
+
import { DAY_MS } from "../text/datetime.js";
|
|
3
4
|
import { entryDates, entryLags, finishedArchiveOf, firstDate, lastDate, parseEntryDetails } from "./backlog-md.js";
|
|
4
5
|
import { CHECK_DECLARATION_MARKER } from "../roles/role-guidance.js";
|
|
5
6
|
/** The stat-cached file readers of the project backlog data shown on both dashboards: planned
|
|
@@ -81,7 +82,7 @@ export function sectionCompletionLags(root, fileName, sectionTitle, dateRe) {
|
|
|
81
82
|
* because the stamp is the entry's own and the body may name other dates.
|
|
82
83
|
* null when the heading carries no such clause. Routed through backlog-md.ts's firstDate -
|
|
83
84
|
* the one first-match mechanism - with this module's own stamp regex.
|
|
84
|
-
* questionFilingDate (question-answer.ts) is the same recipe for the `asked by` verb. */
|
|
85
|
+
* questionFilingDate (backlog-question-answer.ts) is the same recipe for the `asked by` verb. */
|
|
85
86
|
const STAMP_DATE_RE = /(?:planned|reported|found by)\b[^)]*?\b(\d{4}-\d{2}-\d{2})/;
|
|
86
87
|
export function filingDate(title) {
|
|
87
88
|
return firstDate(title, STAMP_DATE_RE);
|
|
@@ -104,7 +105,7 @@ export function completionDate(title, dateRe) {
|
|
|
104
105
|
/** Whole days between a filing stamp and `now`, floored at 0 so a future-dated entry reads as
|
|
105
106
|
* fresh (0d) rather than negative. Shared by the suffix and the `--list --json` `ageDays` field. */
|
|
106
107
|
export function ageInDays(since, now = Date.now()) {
|
|
107
|
-
return Math.max(0, Math.floor((now - Date.parse(since)) /
|
|
108
|
+
return Math.max(0, Math.floor((now - Date.parse(since)) / DAY_MS));
|
|
108
109
|
}
|
|
109
110
|
/** The ` · since YYYY-MM-DD (Nd)`/`(today)` tail a listing line gains for its entry's filing
|
|
110
111
|
* stamp — the one place the wording and the today/`Nd` choice live, so the plans/bugs list
|