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
|
@@ -238,8 +238,35 @@ export async function topAndAnnounce(root, args, command, top) {
|
|
|
238
238
|
* $VISUAL/$EDITOR and the saved buffer goes through `editText`. A saved buffer, positional or
|
|
239
239
|
* `--file` text that rebuilds the entry byte-for-byte writes and commits nothing (`unchanged
|
|
240
240
|
* <command> <n>: <title>`), and an out-of-range position fails
|
|
241
|
-
* before the editor launches.
|
|
241
|
+
* before the editor launches. The flag vocabulary, editor round-trip, unchanged report and
|
|
242
|
+
* commit-plus-hold announcement each live in their own named helper below. */
|
|
242
243
|
export async function editAndAnnounce(root, args, command, edit, editText) {
|
|
244
|
+
const { path, json, positionals, position } = parseEditArgs(args, command);
|
|
245
|
+
await requireReadyRepo(root);
|
|
246
|
+
let text = null;
|
|
247
|
+
if (path === null && positionals.length === 0) {
|
|
248
|
+
const opened = openEditInEditor(root, command, position);
|
|
249
|
+
if (!opened.changed) {
|
|
250
|
+
sayUnchangedEdit(command, position, json, opened.file, opened.title);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
text = opened.text;
|
|
254
|
+
}
|
|
255
|
+
else if (path !== null)
|
|
256
|
+
text = readPromptFileArg(path);
|
|
257
|
+
const edited = text === null ? edit(position, positionals) : editText(position, text);
|
|
258
|
+
if (edited.unchanged) {
|
|
259
|
+
sayUnchangedEdit(command, position, json, edited.file, edited.title);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
await announceEdited(root, command, position, json, edited);
|
|
263
|
+
}
|
|
264
|
+
/** The flag vocabulary of the `--edit` shell: `--file <path>` is extracted and the repeat
|
|
265
|
+
* of the flag refused; the remaining tokens go through scanProseFlags (the `--json` boolean
|
|
266
|
+
* at most once, an `--edit <n>` position required here through scanProseFlags' one valued
|
|
267
|
+
* flag), and a stray positional alongside `--file` is refused. `fail` returns never, so on
|
|
268
|
+
* return `position` is a `number`. */
|
|
269
|
+
function parseEditArgs(args, command) {
|
|
243
270
|
const fileFlag = args.indexOf("--file");
|
|
244
271
|
const path = fileFlag >= 0 ? promptFileValue(args[fileFlag + 1]) : null;
|
|
245
272
|
const rest = path === null ? args : args.filter((_, i) => i !== fileFlag && i !== fileFlag + 1);
|
|
@@ -253,32 +280,36 @@ export async function editAndAnnounce(root, args, command, edit, editText) {
|
|
|
253
280
|
rejectStrayWithFile(positionals[0]);
|
|
254
281
|
if (position === null)
|
|
255
282
|
fail(`--edit needs a position (usage: tumwater ${command} --edit <n>)`);
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const edited = text === null ? edit(position, positionals) : editText(position, text);
|
|
278
|
-
if (edited.unchanged) {
|
|
279
|
-
sayUnchanged(edited.file, edited.title);
|
|
280
|
-
return;
|
|
283
|
+
return { path, json, positionals, position };
|
|
284
|
+
}
|
|
285
|
+
/** The no-op wording when an edit changes nothing: a `--json` `{ file, title, changed: false }`
|
|
286
|
+
* payload, else a single `unchanged <command> <n>: <title>` line. */
|
|
287
|
+
function sayUnchangedEdit(command, position, json, file, title) {
|
|
288
|
+
if (json)
|
|
289
|
+
sayJson({ file, title, changed: false });
|
|
290
|
+
else
|
|
291
|
+
say(`unchanged ${command} ${position}: ${title}`);
|
|
292
|
+
}
|
|
293
|
+
/** The editor round-trip taken when the `--edit` shell has neither `--file` nor positionals:
|
|
294
|
+
* the entry rendered as `--show` prints it (showEntry/renderShownEntry) opens in
|
|
295
|
+
* $VISUAL/$EDITOR, and a saved buffer that trims to the same text reports `{ changed: false }`
|
|
296
|
+
* with the backing file's name and the shown title; otherwise the saved text is returned as
|
|
297
|
+
* `{ changed: true, text }`. */
|
|
298
|
+
function openEditInEditor(root, command, position) {
|
|
299
|
+
const shown = showEntry(root, command, position);
|
|
300
|
+
const before = renderShownEntry(shown);
|
|
301
|
+
const after = editInEditor(before, `${command}-${position}.md`);
|
|
302
|
+
if (after.trim() === before.trim()) {
|
|
303
|
+
return { changed: false, file: command === "bug" ? "BUGS.md" : "PLANS.md", title: shown.title };
|
|
281
304
|
}
|
|
305
|
+
return { changed: true, text: after };
|
|
306
|
+
}
|
|
307
|
+
/** The commit-and-announce tail of the `--edit` shell: the changed entry is committed through
|
|
308
|
+
* commitOperatorBacklogEdit with the `Edit <command>: <title>` subject (warned on an
|
|
309
|
+
* uncommitted worktree), the still-held role from the one before `position` is picked for the
|
|
310
|
+
* hold report, and the `--json` payload (with `hold` when held) or the prose `edited …` line
|
|
311
|
+
* plus stillHeldNote is printed. */
|
|
312
|
+
async function announceEdited(root, command, position, json, edited) {
|
|
282
313
|
warnIfUncommitted(await commitOperatorBacklogEdit(root, edited.file, stampedSubject("operator", `Edit ${command}: ${edited.title}`)), edited.file); // vocabulary-ok: the tumwater(operator): commit prefix is a machine string
|
|
283
314
|
const holds = backlogHolds(root);
|
|
284
315
|
const hold = (command === "bug" ? holds.bugs : holds.plans)[position - 1] ?? null;
|
|
@@ -10,53 +10,21 @@
|
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { statOrNull } from "../files/files.js";
|
|
12
12
|
import { allLiveClaims } from "../runner/runner-state-load.js";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
13
|
+
import { firstTrimmedLineStartingWith } from "../text/text.js";
|
|
14
|
+
import { NEEDS_REVIEW_PREFIX } from "../roles/role-guidance.js";
|
|
15
15
|
import { baseRoleOf } from "../roles/runner-ids.js";
|
|
16
16
|
import { roleLabel } from "../roles/role-labels.js";
|
|
17
17
|
import { actionableEntryRangesFor } from "./backlog-index-data.js";
|
|
18
18
|
import { entryKey, partKey, plannedPartKeys, plansWaitingOn, requiredParts, seriesPart } from "./backlog-parts.js";
|
|
19
19
|
import { finishedFiles, openBugEntries, plannedPlanEntries } from "./backlog.js";
|
|
20
|
-
/** The number of holds an entry's count must reach before a Needs-review note
|
|
21
|
-
* becomes a `held-for-operator` hold (the operator's decision, surfaced by parts 2/3 and 3/3). */
|
|
22
|
-
export const OPERATOR_HOLD_THRESHOLD = 3;
|
|
23
|
-
/** The held count a body records: the integer in the first Needs-review note
|
|
24
|
-
* line's `(held N)` tag, else 0. A present but non-numeric `(held …)` tag is 0 (and stops the
|
|
25
|
-
* scan), so a malformed tag never throws. Pure over the body text — the data-layer read half of
|
|
26
|
-
* the convergence backstop. No writer emits a `(held N)` tag anymore and the `**Previously held
|
|
27
|
-
* N**` markers left on disk are never read, so a freshly written flat note always reads 0 and a
|
|
28
|
-
* new hold never escalates to the operator's desk. */
|
|
29
|
-
export function heldCount(body) {
|
|
30
|
-
const tagRe = new RegExp(`${escapeRegExp(HELD_TAG_PREFIX)}(\\d+)\\)`);
|
|
31
|
-
for (const raw of body.split("\n")) {
|
|
32
|
-
const line = raw.trimStart();
|
|
33
|
-
const note = line.startsWith(NEEDS_REVIEW_PREFIX);
|
|
34
|
-
if (!note)
|
|
35
|
-
continue;
|
|
36
|
-
const tag = tagRe.exec(line);
|
|
37
|
-
if (tag !== null)
|
|
38
|
-
return Number(tag[1]);
|
|
39
|
-
if (line.includes(HELD_TAG_PREFIX))
|
|
40
|
-
return 0;
|
|
41
|
-
}
|
|
42
|
-
return 0;
|
|
43
|
-
}
|
|
44
|
-
/** The `(held N)` count on a single Needs-review note line, or undefined when the
|
|
45
|
-
* line is not such a note or carries no numeric tag. The per-line form of heldCount, so a writer
|
|
46
|
-
* that drops notes (withoutHeldForOperatorNotes) can gate on the same threshold without scanning
|
|
47
|
-
* the whole body. */
|
|
48
|
-
function lineHeldCount(line) {
|
|
49
|
-
const trimmed = line.trimStart();
|
|
50
|
-
const isNote = trimmed.startsWith(NEEDS_REVIEW_PREFIX);
|
|
51
|
-
if (!isNote)
|
|
52
|
-
return undefined;
|
|
53
|
-
const tag = new RegExp(`${escapeRegExp(HELD_TAG_PREFIX)}(\\d+)\\)`).exec(trimmed);
|
|
54
|
-
return tag === null ? undefined : Number(tag[1]);
|
|
55
|
-
}
|
|
56
20
|
/** The `**Refused …` note prefix a refusing run writes. Every hold note — this one and the
|
|
57
21
|
* imported Needs-review prefix — is matched as a line prefix (hasNoteLine)
|
|
58
22
|
* so a mention in prose never holds an entry. */
|
|
59
23
|
const REFUSED_PREFIX = "**Refused ";
|
|
24
|
+
/** The legacy marker the old increment rule left beside a Needs-review note's prior held count
|
|
25
|
+
* (`**Previously held N−1**` beside the live `(held N)`), dropped by withoutNeedsReviewNotes so
|
|
26
|
+
* a release leaves no orphan. No writer emits it anymore; only survives on live notes. */
|
|
27
|
+
const PRIOR_HELD_PREFIX = "**Previously held ";
|
|
60
28
|
/** True when `body` carries a line (ignoring leading indentation) that begins with `prefix`.
|
|
61
29
|
* The one home of the hold-note rule: a note holds its entry only when written as its own
|
|
62
30
|
* line, never when its prefix is quoted inside prose. */
|
|
@@ -80,6 +48,25 @@ export function refusalNote(body) {
|
|
|
80
48
|
export function needsReviewNote(body) {
|
|
81
49
|
return noteText(body, NEEDS_REVIEW_PREFIX);
|
|
82
50
|
}
|
|
51
|
+
/** True when `entry` is parked with the plan role — its Needs-review note is the feature
|
|
52
|
+
* role's too-large note, the automatic convergence hold, or a final-round rejection, all of
|
|
53
|
+
* which the plan role clears by splitting or replanning (role-catalog.ts `plan` find). False
|
|
54
|
+
* for the bugfix role's wait-for-the-next-failure note ("by bugfix: …"), which parks the
|
|
55
|
+
* entry for selfcheck to lift when the cluster fails again (selfcheck step 4) and which no
|
|
56
|
+
* role can act on until then. entryHold returns "needs-review" for every such note (all of
|
|
57
|
+
* them park the entry), so the stalled-with-the-plan-role surfaces — the stalled-entries team
|
|
58
|
+
* alert (team-alert-backlog.ts) and the doctor's `stalled entries` row
|
|
59
|
+
* (checkStalledEntries) — classify by this agreed subset, keeping the bugfix parks out of the
|
|
60
|
+
* plan-role blame. Reads the note's text after the NEEDS_REVIEW_PREFIX once, so the alert and
|
|
61
|
+
* the doctor row cannot disagree on which notes the plan role clears; the eligibility mark
|
|
62
|
+
* (holdMark) continues to hold every needs-review note, a by-bugfix park included. */
|
|
63
|
+
export function isPlanStalled(entry) {
|
|
64
|
+
const note = needsReviewNote(entry.body);
|
|
65
|
+
if (note === null)
|
|
66
|
+
return false;
|
|
67
|
+
// A "by bugfix:" note is a wait for evidence that selfcheck clears, not plan-role work.
|
|
68
|
+
return !note.includes("by bugfix");
|
|
69
|
+
}
|
|
83
70
|
/** `lines` without any line starting with `prefix` (the same line-prefix rule hasNoteLine
|
|
84
71
|
* applies). When a dropped note sat between two blank lines, the blank line after it goes too,
|
|
85
72
|
* so releasing leaves one blank line where the note was. Every other line is kept. */
|
|
@@ -101,33 +88,25 @@ function withoutNoteLines(lines, prefix) {
|
|
|
101
88
|
export function withoutRefusalNotes(lines) {
|
|
102
89
|
return withoutNoteLines(lines, REFUSED_PREFIX);
|
|
103
90
|
}
|
|
104
|
-
/** `lines`
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (c !== undefined && c >= OPERATOR_HOLD_THRESHOLD) {
|
|
117
|
-
priorHeld.add(c - 1);
|
|
118
|
-
drop.add(i);
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
if (priorHeld.size === 0)
|
|
91
|
+
/** `lines` without its Needs-review note lines and the legacy `**Previously held N**`
|
|
92
|
+
* markers the old increment rule left beside them, the release's lift when the entry has no
|
|
93
|
+
* Refused note: a `--release` that finds no Refused note drops every Needs-review note line so
|
|
94
|
+
* the entry becomes eligible, and takes each `**Previously held N**` marker in the slice with
|
|
95
|
+
* it, so the lift leaves no orphan whether or not the note still carries a `(held N)` tag.
|
|
96
|
+
* Nothing changes when the slice carries no Needs-review note, so a marker that sits on an
|
|
97
|
+
* eligible entry stays put. Every other line, Refused notes included, is kept, so the
|
|
98
|
+
* Needs-review lift never lifts a refusal. */
|
|
99
|
+
export function withoutNeedsReviewNotes(lines) {
|
|
100
|
+
// A lift only happens when the slice carries a Needs-review note; without one, the slice
|
|
101
|
+
// (and any legacy prior-held marker on it) is left byte for byte.
|
|
102
|
+
if (!lines.some((raw) => raw.trimStart().startsWith(NEEDS_REVIEW_PREFIX)))
|
|
122
103
|
return lines;
|
|
104
|
+
const drop = new Set();
|
|
123
105
|
lines.forEach((raw, i) => {
|
|
124
|
-
if (drop.has(i))
|
|
125
|
-
return;
|
|
126
106
|
const trimmed = raw.trimStart();
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (count !== null && priorHeld.has(Number(count[0])))
|
|
107
|
+
// Every Needs-review note goes, and so does each `**Previously held N**` marker the old
|
|
108
|
+
// increment rule wrote with it.
|
|
109
|
+
if (trimmed.startsWith(NEEDS_REVIEW_PREFIX) || trimmed.startsWith(PRIOR_HELD_PREFIX))
|
|
131
110
|
drop.add(i);
|
|
132
111
|
});
|
|
133
112
|
const out = [];
|
|
@@ -140,7 +119,7 @@ export function withoutHeldForOperatorNotes(lines) {
|
|
|
140
119
|
while (j < lines.length && drop.has(j))
|
|
141
120
|
j++;
|
|
142
121
|
// A dropped run between two blank lines drops the blank after it too, so the lift leaves
|
|
143
|
-
// one blank line where the note was (the same rule withoutNoteLines applies to one).
|
|
122
|
+
// one blank line where the note was (the same rule withoutNoteLines applies to one note).
|
|
144
123
|
if (j < lines.length && (lines[i - 1] ?? "x").trim() === "" && (lines[j] ?? "x").trim() === "")
|
|
145
124
|
j++;
|
|
146
125
|
i = j - 1;
|
|
@@ -209,13 +188,10 @@ export function entryHold(entry, planned) {
|
|
|
209
188
|
export function entryHoldAgainst(entry, plannedParts) {
|
|
210
189
|
if (hasNoteLine(entry.body, REFUSED_PREFIX))
|
|
211
190
|
return "refused";
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return
|
|
216
|
-
? "held-for-operator" // vocabulary-ok: the operator-held string names the operator hold decision
|
|
217
|
-
: "needs-review";
|
|
218
|
-
}
|
|
191
|
+
// A Needs-review note — the feature role's too-large note or the convergence hold's — parks the
|
|
192
|
+
// entry for the plan role to split or replan, at any count: nothing escalates to the operator.
|
|
193
|
+
if (hasNoteLine(entry.body, NEEDS_REVIEW_PREFIX))
|
|
194
|
+
return "needs-review";
|
|
219
195
|
const refs = requiredParts(entry.title);
|
|
220
196
|
if (refs.length === 0)
|
|
221
197
|
return null;
|
|
@@ -235,8 +211,6 @@ export function holdMark(hold) {
|
|
|
235
211
|
return " [blocked: refused]";
|
|
236
212
|
if (hold === "needs-review")
|
|
237
213
|
return " [blocked: needs review]";
|
|
238
|
-
if (hold === "held-for-operator")
|
|
239
|
-
return " [blocked: held for operator]"; // vocabulary-ok: this hold names the operator-held wording for the operator decision
|
|
240
214
|
return ` [blocked: requires ${hold.blockedBy.join(", ")}]`;
|
|
241
215
|
}
|
|
242
216
|
/** The line `plan|bug --release` and `--top` print when the entry they touched is still held:
|
|
@@ -315,42 +289,22 @@ export function backlogMarks(root) {
|
|
|
315
289
|
bugs: holds.bugs.map((h, i) => mark(h, working.bugs[i])),
|
|
316
290
|
};
|
|
317
291
|
}
|
|
318
|
-
/** A planned plan or open bug whose hold is `"held-for-operator"`, in file order: the
|
|
319
|
-
* entries parked until the operator lifts them with `tumwater plan --release` or `tumwater
|
|
320
|
-
* bug --release` (the `operator-held` team alert's input), each carrying the backlog file
|
|
321
|
-
* and its 0-based position so a surface opening the row can land on it, the same RefusedEntry
|
|
322
|
-
* shape refusedEntries returns. The convergence hold writes only on plans, but a role's own
|
|
323
|
-
* too-large note can sit on a bug and reach this threshold, so a bug can be held this way too.
|
|
324
|
-
* A missing file yields `[]`. */
|
|
325
|
-
export function operatorHeldEntries(root) {
|
|
326
|
-
const holds = backlogHolds(root);
|
|
327
|
-
const out = [];
|
|
328
|
-
const entriesOf = (entries, held, file) => {
|
|
329
|
-
entries.forEach((e, i) => {
|
|
330
|
-
if (held[i] === "held-for-operator")
|
|
331
|
-
out.push({ file, index: i, title: e.title }); // vocabulary-ok: the held-for-operator hold's key
|
|
332
|
-
});
|
|
333
|
-
};
|
|
334
|
-
entriesOf(plannedPlanEntries(root), holds.plans, "plans");
|
|
335
|
-
entriesOf(openBugEntries(root), holds.bugs, "bugs");
|
|
336
|
-
return out;
|
|
337
|
-
}
|
|
338
292
|
/** The display noun for a backlog-file discriminator — the `"plans" | "bugs"` `file` on
|
|
339
293
|
* RefusedEntry and StalledEntry (and the same discriminator the prompt's named-hold wording
|
|
340
294
|
* and the doctor's Stalled row take): BUGS.md is a "Bug" and PLANS.md a "Plan", and the
|
|
341
295
|
* lowercase form is the `Refused plan|bug:` label and the `tumwater plan|bug --release`
|
|
342
|
-
* command noun
|
|
296
|
+
* command noun. */
|
|
343
297
|
export function entryKindLabel(file) {
|
|
344
298
|
return file === "bugs" ? "Bug" : "Plan";
|
|
345
299
|
}
|
|
346
|
-
/** The planned plans and open bugs
|
|
347
|
-
*
|
|
348
|
-
* `stalled-entries` team alert's input
|
|
349
|
-
* hold. Same `backlogHolds(root)` + the
|
|
350
|
-
* uses, so the alert and the doctor's
|
|
351
|
-
* `
|
|
352
|
-
*
|
|
353
|
-
* excluded
|
|
300
|
+
/** The planned plans and open bugs parked with the plan role — whose Needs-review hold the plan
|
|
301
|
+
* role clears by splitting or replanning (the feature role's too-large note, the convergence hold,
|
|
302
|
+
* a final-round rejection), in file order: the `stalled-entries` team alert's input, each
|
|
303
|
+
* carrying its backlog file, 0-based index and the hold. Same `backlogHolds(root)` + the
|
|
304
|
+
* planned/`openBugEntries` readers `refusedEntries` uses, so the alert and the doctor's
|
|
305
|
+
* `stalled entries` row classify through the same `entryHold` + `isPlanStalled` and cannot
|
|
306
|
+
* disagree on which holds are role-resolvable. The bugfix role's wait-for-the-next-failure
|
|
307
|
+
* parks ("by bugfix:") are excluded: selfcheck lifts them, not the plan role. A missing
|
|
354
308
|
* file yields `[]`. */
|
|
355
309
|
export function stalledEntries(root) {
|
|
356
310
|
const holds = backlogHolds(root);
|
|
@@ -358,7 +312,9 @@ export function stalledEntries(root) {
|
|
|
358
312
|
const entriesOf = (entries, held, file) => {
|
|
359
313
|
entries.forEach((e, i) => {
|
|
360
314
|
const hold = held[i];
|
|
361
|
-
|
|
315
|
+
// Only holds the plan role can clear surface as stalled: the bugfix role's
|
|
316
|
+
// wait-for-the-next-failure parks stay out of the plan-role blame.
|
|
317
|
+
if (hold === "needs-review" && isPlanStalled(e))
|
|
362
318
|
out.push({ file, index: i, title: e.title, hold });
|
|
363
319
|
});
|
|
364
320
|
};
|
|
@@ -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
|
|
@@ -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
|
});
|