tumwater 0.4.0 → 0.4.2
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 +4 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +5 -2
- package/dist/src/agent/backend-traits.js +21 -0
- package/dist/src/agent/claude/claude-backend.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +181 -182
- package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
- package/dist/src/agent/claude/claude-write-guard.js +5 -4
- package/dist/src/agent/codex/codex-backend.js +1 -1
- package/dist/src/agent/codex/codex-rollout.js +48 -45
- package/dist/src/agent/codex/codex-stream.js +269 -215
- package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
- package/dist/src/agent/codex/codex-write-guard.js +3 -3
- package/dist/src/agent/image-input.js +18 -87
- 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 +2 -2
- package/dist/src/agent/opencode/opencode-config.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +101 -92
- package/dist/src/agent/run-agent.js +76 -382
- package/dist/src/agent/run-settle.js +25 -15
- package/dist/src/agent/run-spawn.js +226 -0
- package/dist/src/agent/run-stream.js +185 -0
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/agent/write-guard.js +5 -3
- package/dist/src/backlog/backlog-archive.js +86 -170
- package/dist/src/backlog/backlog-cli-view.js +89 -70
- package/dist/src/backlog/backlog-eligibility.js +51 -59
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +159 -0
- package/dist/src/backlog/backlog-index.js +7 -5
- package/dist/src/backlog/backlog-md.js +39 -28
- package/dist/src/backlog/backlog-release.js +21 -36
- package/dist/src/backlog/backlog-render.js +1 -1
- package/dist/src/backlog/backlog-structure.js +14 -145
- package/dist/src/backlog/backlog-write.js +31 -18
- package/dist/src/backlog/backlog.js +14 -4
- package/dist/src/backlog/question-answer.js +33 -15
- package/dist/src/baseline/main-baseline.js +71 -26
- package/dist/src/baseline/main-guardrails.js +76 -15
- package/dist/src/baseline/main-red.js +45 -94
- package/dist/src/baseline/main-watch.js +9 -3
- package/dist/src/brief/brief.js +40 -0
- package/dist/src/budget/budget-fallback-view.js +50 -8
- package/dist/src/budget/budget-wiring.js +15 -11
- package/dist/src/budget/budget.js +3 -2
- package/dist/src/build/build-check-events.js +4 -2
- package/dist/src/build/build-check-report.js +2 -1
- package/dist/src/build/build-check-scoped.js +70 -41
- package/dist/src/change/change-data.js +4 -2
- package/dist/src/cli/cli-args.js +9 -0
- package/dist/src/cli/cli-command-args.js +199 -98
- package/dist/src/cli/cli-flag-specs.js +6 -4
- package/dist/src/cli/cli-marker-commands.js +2 -2
- package/dist/src/cli/cli-query-commands.js +41 -6
- package/dist/src/cli/cli-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +46 -92
- package/dist/src/cli/cli-start.js +123 -47
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +19 -12
- package/dist/src/cli/log-commands.js +128 -107
- package/dist/src/cli/question-commands.js +82 -63
- package/dist/src/cli.js +396 -343
- package/dist/src/collections.js +46 -9
- package/dist/src/config/config-fallbacks.js +0 -4
- package/dist/src/config/config-live.js +179 -147
- package/dist/src/config/config-request.js +176 -51
- package/dist/src/config/config-validation-roles.js +271 -0
- package/dist/src/config/config-validation.js +10 -205
- package/dist/src/config/config-views.js +2 -7
- package/dist/src/config/config-write.js +205 -175
- package/dist/src/config/config.js +2 -1
- package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
- package/dist/src/doctor/doctor-backlog-plans.js +24 -25
- package/dist/src/doctor/doctor-backlog.js +34 -42
- package/dist/src/doctor/doctor-check-names.js +4 -2
- package/dist/src/doctor/doctor-checks.js +16 -4
- package/dist/src/doctor/doctor-claude-checks.js +2 -2
- package/dist/src/doctor/doctor-codex-checks.js +9 -20
- package/dist/src/doctor/doctor-contention-checks.js +52 -19
- package/dist/src/doctor/doctor-holds.js +22 -20
- package/dist/src/doctor/doctor-model-checks.js +8 -184
- package/dist/src/doctor/doctor-notify.js +2 -5
- package/dist/src/doctor/doctor-opencode-checks.js +90 -41
- package/dist/src/doctor/doctor-principles.js +26 -0
- package/dist/src/doctor/doctor-project-checks.js +69 -19
- package/dist/src/doctor/doctor-role-notes.js +33 -0
- package/dist/src/doctor/doctor.js +13 -6
- package/dist/src/doctor/probe.js +1 -1
- package/dist/src/events/event-format.js +307 -162
- package/dist/src/events/event-labels.js +0 -1
- package/dist/src/events/event-legacy.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +15 -2
- package/dist/src/events/notify.js +1 -1
- package/dist/src/failure/failure-cluster.js +4 -3
- package/dist/src/failure/failure-data.js +126 -94
- package/dist/src/failure/failure-render.js +58 -17
- package/dist/src/failure/failure-state-change.js +308 -27
- package/dist/src/failure/time-spend.js +139 -106
- package/dist/src/files/files.js +7 -0
- package/dist/src/files/json-object.js +15 -2
- package/dist/src/gates/budget-gates.js +90 -68
- package/dist/src/gates/gate-polls.js +92 -61
- package/dist/src/gates/role-cap-gates.js +1 -1
- package/dist/src/gates/startup-gate.js +28 -28
- package/dist/src/gates/streak-gate.js +1 -1
- package/dist/src/git/git.js +1 -1
- package/dist/src/git/slot-salvage.js +9 -9
- package/dist/src/git/slots-state.js +12 -3
- package/dist/src/git/worktree-pool-startup.js +127 -0
- package/dist/src/git/worktree-pool.js +1 -120
- package/dist/src/gui/gui-admin-endpoints.js +3 -4
- package/dist/src/gui/gui-args.js +1 -1
- package/dist/src/gui/gui-backlog-commands.js +20 -11
- package/dist/src/gui/gui-backlog-endpoints.js +2 -8
- package/dist/src/gui/gui-config-commands.js +51 -76
- package/dist/src/gui/gui-endpoint-commands.js +4 -2
- package/dist/src/gui/gui-endpoint-common.js +4 -3
- package/dist/src/gui/gui-history-endpoints.js +36 -27
- package/dist/src/gui/gui-operator-commands.js +43 -17
- package/dist/src/gui/gui-prompt-commands.js +27 -21
- package/dist/src/gui/gui-quarantine-commands.js +30 -0
- package/dist/src/gui/gui-report-endpoints.js +13 -9
- package/dist/src/gui/gui-server.js +5 -3
- package/dist/src/gui/gui-team-commands.js +1 -1
- package/dist/src/history/history-data.js +7 -1
- package/dist/src/history/history-render.js +15 -10
- package/dist/src/history/history.js +103 -68
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/pending-prompt.js +3 -2
- package/dist/src/inbox/prompt-commands.js +194 -161
- package/dist/src/init/init.js +104 -25
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +1 -1
- package/dist/src/merge/merge-git.js +5 -5
- package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
- package/dist/src/merge/merge-queue.js +2 -1
- package/dist/src/merge/merge-to-main.js +1 -1
- package/dist/src/merge/merge-verify.js +3 -2
- package/dist/src/operator/operator-commands.js +84 -52
- package/dist/src/operator/operator-intent.js +26 -3
- package/dist/src/operator/retire.js +2 -1
- package/dist/src/operator/revert.js +18 -12
- package/dist/src/orchestrator/orchestrator-poll.js +2 -2
- package/dist/src/orchestrator/orchestrator.js +1 -1
- package/dist/src/orchestrator/retention.js +6 -5
- package/dist/src/paths.js +10 -10
- package/dist/src/pi/pi-bin.js +2 -2
- package/dist/src/pi/pi-models.js +9 -0
- package/dist/src/pi/pi-stream.js +63 -41
- package/dist/src/pi/pi-watchdogs.js +84 -49
- package/dist/src/pi-extension/context-shake.js +55 -31
- package/dist/src/pi-extension/full-output.js +2 -3
- package/dist/src/process/process.js +24 -1
- package/dist/src/process/run-marker.js +2 -2
- package/dist/src/process/supervisor.js +24 -5
- package/dist/src/prompt/gate-notes.js +41 -16
- package/dist/src/prompt/notes-backlog.js +22 -29
- package/dist/src/prompt/principles.js +11 -0
- package/dist/src/prompt/prompt-followup.js +4 -12
- package/dist/src/prompt/prompt.js +15 -12
- package/dist/src/redeploy/redeploy-probes.js +11 -1
- package/dist/src/redeploy/redeployer.js +54 -7
- package/dist/src/redeploy/self-reload.js +7 -2
- package/dist/src/report/report-data.js +224 -320
- package/dist/src/report/report-fold.js +23 -231
- package/dist/src/report/report-main-red.js +70 -3
- package/dist/src/report/report-render.js +181 -48
- package/dist/src/report/report-since-data.js +218 -0
- package/dist/src/report/report-usage-fold.js +288 -0
- package/dist/src/report/report.js +2 -1
- package/dist/src/review/known-flakes.js +22 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review.js +16 -14
- package/dist/src/review/quarantine-render.js +29 -11
- package/dist/src/review/review-precheck.js +2 -1
- package/dist/src/roles/role-catalog.js +30 -29
- package/dist/src/roles/role-guidance.js +26 -43
- package/dist/src/roles/role-render.js +19 -0
- package/dist/src/roles/role-view.js +19 -5
- package/dist/src/roles/roles.js +2 -22
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/feedback-filings.js +10 -1
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-apply.js +14 -2
- package/dist/src/run/run-detail-data.js +50 -10
- package/dist/src/run/run-detail-render.js +32 -1
- package/dist/src/run/run-detail.js +44 -22
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-finalize.js +75 -46
- package/dist/src/run/run-prompt.js +271 -187
- package/dist/src/run/run-resume.js +32 -31
- package/dist/src/run/run-stage.js +116 -64
- package/dist/src/run/run-timing.js +18 -4
- package/dist/src/run/run-verdict.js +119 -83
- package/dist/src/run/stage-backlog-checks.js +4 -31
- package/dist/src/run/stage-check.js +4 -12
- package/dist/src/run/stage-checks-shared.js +15 -0
- package/dist/src/run/stage-name-checks.js +3 -13
- package/dist/src/run/stage-path-checks.js +26 -29
- package/dist/src/runner/leftover.js +13 -4
- package/dist/src/runner/revision.js +2 -2
- package/dist/src/runner/runner-fallback.js +1 -1
- package/dist/src/runner/runner-outcome.js +0 -5
- package/dist/src/runner/runner-pi.js +72 -49
- package/dist/src/runner/runner-run.js +19 -102
- package/dist/src/runner/runner-state-load.js +0 -10
- package/dist/src/runner/runner.js +1 -32
- package/dist/src/scheduling/claims.js +4 -11
- package/dist/src/status/status-data.js +140 -91
- package/dist/src/status/status-polls.js +41 -5
- package/dist/src/status/status-role-notes.js +53 -0
- package/dist/src/status/status-roles.js +10 -37
- package/dist/src/text/datetime.js +8 -1
- package/dist/src/text/format.js +13 -1
- package/dist/src/text/phrases.js +16 -2
- package/dist/src/text/text.js +12 -0
- package/dist/src/ui/alert-feeds.js +115 -0
- package/dist/src/ui/badges.js +3 -4
- package/dist/src/ui/gui/gui-client-boot.js +78 -57
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
- package/dist/src/ui/gui/gui-client-drawer.js +18 -2
- package/dist/src/ui/gui/gui-client-feed.js +3 -8
- package/dist/src/ui/gui/gui-client-history.js +16 -16
- package/dist/src/ui/gui/gui-client-model.js +9 -9
- package/dist/src/ui/gui/gui-client-operator.js +6 -10
- package/dist/src/ui/gui/gui-client-pending.js +4 -4
- package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
- package/dist/src/ui/gui/gui-client-report.js +70 -11
- package/dist/src/ui/gui/gui-client-roles.js +52 -12
- package/dist/src/ui/gui/gui-client-settings.js +31 -26
- package/dist/src/ui/gui/gui-client-team.js +28 -2
- package/dist/src/ui/gui/gui-client.js +33 -4
- package/dist/src/ui/gui/gui-icons.js +7 -1
- package/dist/src/ui/progress-data.js +2 -2
- package/dist/src/ui/run-progress-model.js +2 -2
- package/dist/src/ui/status-model.js +26 -16
- package/dist/src/ui/status-payload.js +149 -326
- package/dist/src/ui/status-render.js +131 -80
- package/dist/src/ui/team-alert-backlog.js +236 -0
- package/dist/src/ui/team-alert-shared.js +92 -0
- package/dist/src/ui/team-alert-status.js +326 -0
- package/dist/src/ui/team-alert-tail.js +238 -0
- package/dist/src/ui/team-alerts.js +16 -984
- package/dist/src/ui/transcript-tail.js +137 -94
- package/dist/src/ui/tui/tui-backlog.js +1 -1
- package/dist/src/ui/tui/tui-editor-answer.js +54 -44
- package/dist/src/ui/tui/tui-editor-attach.js +74 -63
- package/dist/src/ui/tui/tui-editor-backends.js +62 -56
- package/dist/src/ui/tui/tui-editor-brief.js +64 -54
- package/dist/src/ui/tui/tui-editor-budget.js +106 -89
- package/dist/src/ui/tui/tui-editor-config.js +2 -4
- package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
- package/dist/src/ui/tui/tui-editor-entry.js +171 -153
- package/dist/src/ui/tui/tui-editor-instances.js +2 -1
- package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
- package/dist/src/ui/tui/tui-editor-interval.js +39 -112
- package/dist/src/ui/tui/tui-editor-notes.js +28 -84
- package/dist/src/ui/tui/tui-editor-pause.js +210 -183
- package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
- package/dist/src/ui/tui/tui-editor-queued.js +74 -64
- package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
- package/dist/src/ui/tui/tui-editor-release.js +85 -0
- package/dist/src/ui/tui/tui-editor-retire.js +57 -46
- package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
- package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
- package/dist/src/ui/tui/tui-editor-search.js +70 -58
- package/dist/src/ui/tui/tui-editor-session.js +3 -0
- package/dist/src/ui/tui/tui-editors.js +4 -1
- package/dist/src/ui/tui/tui-frame.js +7 -3
- package/dist/src/ui/tui/tui-history.js +12 -10
- package/dist/src/ui/tui/tui-keys.js +6 -1
- package/dist/src/ui/tui/tui-pane-activity.js +62 -50
- package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +45 -26
- package/dist/src/ui/tui/tui-pane-history.js +296 -264
- package/dist/src/ui/tui/tui-pane-report.js +74 -57
- package/dist/src/ui/tui/tui-pane-session.js +6 -2
- package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
- package/dist/src/ui/tui/tui-pane.js +201 -174
- package/dist/src/ui/tui/tui-panes.js +211 -148
- package/dist/src/ui/tui/tui.js +4 -4
- package/dist/src/verdict/fix-claim.js +18 -6
- package/dist/src/verdict/refusal.js +32 -7
- package/package.json +1 -1
- package/dist/src/doctor/image-probe.js +0 -257
- /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
- /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Backlog eligibility: which PLANS.md / BUGS.md entries a work role may take now (plans,
|
|
2
|
-
* parallel-work-instances, part 2/7). An entry is held when its body carries a **Refused
|
|
3
|
-
* **Needs review
|
|
2
|
+
* parallel-work-instances, part 2/7). An entry is held when its body carries a **Refused …**
|
|
3
|
+
* or **Needs review …** note, or when
|
|
4
4
|
* its heading's trailing parenthetical
|
|
5
5
|
* (or a parenthetical left open by a wrapped heading) names a prerequisite (`requires parts
|
|
6
6
|
* 1/5–3/5 landed`) that is itself still listed under `## Planned`. The parsers are pure over
|
|
@@ -11,24 +11,26 @@ import path from "node:path";
|
|
|
11
11
|
import { statOrNull } from "../files/files.js";
|
|
12
12
|
import { allLiveClaims } from "../runner/runner-state-load.js";
|
|
13
13
|
import { escapeRegExp, firstTrimmedLineStartingWith } from "../text/text.js";
|
|
14
|
-
import { HELD_TAG_PREFIX,
|
|
14
|
+
import { HELD_TAG_PREFIX, NEEDS_REVIEW_PREFIX, PRIOR_HELD_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
|
-
import { actionableEntryRangesFor } from "./backlog-
|
|
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
|
|
20
|
+
/** The number of holds an entry's count must reach before a Needs-review note
|
|
21
21
|
* becomes a `held-for-operator` hold (the operator's decision, surfaced by parts 2/3 and 3/3). */
|
|
22
22
|
export const OPERATOR_HOLD_THRESHOLD = 3;
|
|
23
|
-
/** The held count a body records: the integer in the first Needs-review
|
|
24
|
-
* line's `(held N)` tag, else
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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. */
|
|
27
29
|
export function heldCount(body) {
|
|
28
30
|
const tagRe = new RegExp(`${escapeRegExp(HELD_TAG_PREFIX)}(\\d+)\\)`);
|
|
29
31
|
for (const raw of body.split("\n")) {
|
|
30
32
|
const line = raw.trimStart();
|
|
31
|
-
const note = line.startsWith(NEEDS_REVIEW_PREFIX)
|
|
33
|
+
const note = line.startsWith(NEEDS_REVIEW_PREFIX);
|
|
32
34
|
if (!note)
|
|
33
35
|
continue;
|
|
34
36
|
const tag = tagRe.exec(line);
|
|
@@ -37,29 +39,22 @@ export function heldCount(body) {
|
|
|
37
39
|
if (line.includes(HELD_TAG_PREFIX))
|
|
38
40
|
return 0;
|
|
39
41
|
}
|
|
40
|
-
for (const raw of body.split("\n")) {
|
|
41
|
-
const line = raw.trimStart();
|
|
42
|
-
if (!line.startsWith(PRIOR_HELD_PREFIX))
|
|
43
|
-
continue;
|
|
44
|
-
const count = /\d+/.exec(line.slice(PRIOR_HELD_PREFIX.length));
|
|
45
|
-
return count === null ? 0 : Number(count[0]);
|
|
46
|
-
}
|
|
47
42
|
return 0;
|
|
48
43
|
}
|
|
49
|
-
/** The `(held N)` count on a single Needs-review
|
|
44
|
+
/** The `(held N)` count on a single Needs-review note line, or undefined when the
|
|
50
45
|
* line is not such a note or carries no numeric tag. The per-line form of heldCount, so a writer
|
|
51
46
|
* that drops notes (withoutHeldForOperatorNotes) can gate on the same threshold without scanning
|
|
52
47
|
* the whole body. */
|
|
53
48
|
function lineHeldCount(line) {
|
|
54
49
|
const trimmed = line.trimStart();
|
|
55
|
-
const isNote = trimmed.startsWith(NEEDS_REVIEW_PREFIX)
|
|
50
|
+
const isNote = trimmed.startsWith(NEEDS_REVIEW_PREFIX);
|
|
56
51
|
if (!isNote)
|
|
57
52
|
return undefined;
|
|
58
53
|
const tag = new RegExp(`${escapeRegExp(HELD_TAG_PREFIX)}(\\d+)\\)`).exec(trimmed);
|
|
59
54
|
return tag === null ? undefined : Number(tag[1]);
|
|
60
55
|
}
|
|
61
56
|
/** The `**Refused …` note prefix a refusing run writes. Every hold note — this one and the
|
|
62
|
-
* imported Needs-review
|
|
57
|
+
* imported Needs-review prefix — is matched as a line prefix (hasNoteLine)
|
|
63
58
|
* so a mention in prose never holds an entry. */
|
|
64
59
|
const REFUSED_PREFIX = "**Refused ";
|
|
65
60
|
/** True when `body` carries a line (ignoring leading indentation) that begins with `prefix`.
|
|
@@ -79,9 +74,11 @@ function noteText(body, prefix) {
|
|
|
79
74
|
export function refusalNote(body) {
|
|
80
75
|
return noteText(body, REFUSED_PREFIX);
|
|
81
76
|
}
|
|
82
|
-
/** The text of `body`'s
|
|
83
|
-
|
|
84
|
-
|
|
77
|
+
/** The text of `body`'s Needs-review note, or null when it has none (entryHold's
|
|
78
|
+
* "needs-review" hold). A needs-review note parks the entry (an unreproducible bug, a plan
|
|
79
|
+
* judged too large, a rejected plan), so the stale-bug checks skip it like a refused one. */
|
|
80
|
+
export function needsReviewNote(body) {
|
|
81
|
+
return noteText(body, NEEDS_REVIEW_PREFIX);
|
|
85
82
|
}
|
|
86
83
|
/** `lines` without any line starting with `prefix` (the same line-prefix rule hasNoteLine
|
|
87
84
|
* applies). When a dropped note sat between two blank lines, the blank line after it goes too,
|
|
@@ -99,17 +96,12 @@ function withoutNoteLines(lines, prefix) {
|
|
|
99
96
|
}
|
|
100
97
|
return out;
|
|
101
98
|
}
|
|
102
|
-
/** `lines` without any Refused note line. Every other line, Needs-review,
|
|
103
|
-
*
|
|
99
|
+
/** `lines` without any Refused note line. Every other line, Needs-review notes included, is
|
|
100
|
+
* kept, so refusal lifting never lifts another hold. */
|
|
104
101
|
export function withoutRefusalNotes(lines) {
|
|
105
102
|
return withoutNoteLines(lines, REFUSED_PREFIX);
|
|
106
103
|
}
|
|
107
|
-
/** `lines`
|
|
108
|
-
* withoutRefusalNotes does. */
|
|
109
|
-
export function withoutWaitingNotes(lines) {
|
|
110
|
-
return withoutNoteLines(lines, WAITING_FOR_FAILURE_PREFIX);
|
|
111
|
-
}
|
|
112
|
-
/** `lines` with every Needs-review/Needs-replan note whose `(held N)` count reached
|
|
104
|
+
/** `lines` with every Needs-review note whose `(held N)` count reached
|
|
113
105
|
* OPERATOR_HOLD_THRESHOLD removed, together with its `**Previously held N−1**` line (the prior
|
|
114
106
|
* count the increment rule recorded beside the live `(held N)`), so a `--release` lift leaves
|
|
115
107
|
* neither behind and the entry becomes eligible. Drops nothing when no note is at or past the
|
|
@@ -203,9 +195,9 @@ export function waitingPartRanges(root, title) {
|
|
|
203
195
|
return out;
|
|
204
196
|
}
|
|
205
197
|
/** Why `entry` is held against the still-`planned` entries, or null when it may be taken: a
|
|
206
|
-
* `**Refused …` line (refused), a Needs-review line (needs-review), a
|
|
207
|
-
* (
|
|
208
|
-
*
|
|
198
|
+
* `**Refused …` line (refused), a Needs-review line (needs-review), or a prerequisite
|
|
199
|
+
* `(series, part)` still among `planned` ({ blockedBy }). Series compare case-insensitively;
|
|
200
|
+
* part tokens compare verbatim.
|
|
209
201
|
* Bodies mentioning "requires" are never consulted — only the heading's trailing or
|
|
210
202
|
* still-open parenthetical. */
|
|
211
203
|
export function entryHold(entry, planned) {
|
|
@@ -217,17 +209,13 @@ export function entryHold(entry, planned) {
|
|
|
217
209
|
export function entryHoldAgainst(entry, plannedParts) {
|
|
218
210
|
if (hasNoteLine(entry.body, REFUSED_PREFIX))
|
|
219
211
|
return "refused";
|
|
220
|
-
if (hasNoteLine(entry.body, NEEDS_REVIEW_PREFIX)
|
|
212
|
+
if (hasNoteLine(entry.body, NEEDS_REVIEW_PREFIX)) {
|
|
221
213
|
// A note whose held count reached OPERATOR_HOLD_THRESHOLD stops out of the role loop:
|
|
222
|
-
// neither the plan role (planBacklogFull
|
|
214
|
+
// neither the plan role (planBacklogFull) nor this role takes it.
|
|
223
215
|
return heldCount(entry.body) >= OPERATOR_HOLD_THRESHOLD
|
|
224
216
|
? "held-for-operator" // vocabulary-ok: the operator-held string names the operator hold decision
|
|
225
|
-
:
|
|
226
|
-
? "needs-review"
|
|
227
|
-
: "needs-replan";
|
|
217
|
+
: "needs-review";
|
|
228
218
|
}
|
|
229
|
-
if (hasNoteLine(entry.body, WAITING_FOR_FAILURE_PREFIX))
|
|
230
|
-
return "waiting-for-failure";
|
|
231
219
|
const refs = requiredParts(entry.title);
|
|
232
220
|
if (refs.length === 0)
|
|
233
221
|
return null;
|
|
@@ -247,12 +235,8 @@ export function holdMark(hold) {
|
|
|
247
235
|
return " [blocked: refused]";
|
|
248
236
|
if (hold === "needs-review")
|
|
249
237
|
return " [blocked: needs review]";
|
|
250
|
-
if (hold === "needs-replan")
|
|
251
|
-
return " [blocked: needs replan]";
|
|
252
238
|
if (hold === "held-for-operator")
|
|
253
239
|
return " [blocked: held for operator]"; // vocabulary-ok: this hold names the operator-held wording for the operator decision
|
|
254
|
-
if (hold === "waiting-for-failure")
|
|
255
|
-
return " [blocked: waiting for the next failure]";
|
|
256
240
|
return ` [blocked: requires ${hold.blockedBy.join(", ")}]`;
|
|
257
241
|
}
|
|
258
242
|
/** The line `plan|bug --release` and `--top` print when the entry they touched is still held:
|
|
@@ -359,13 +343,13 @@ export function operatorHeldEntries(root) {
|
|
|
359
343
|
export function entryKindLabel(file) {
|
|
360
344
|
return file === "bugs" ? "Bug" : "Plan";
|
|
361
345
|
}
|
|
362
|
-
/** The planned plans and open bugs whose hold is exactly `"needs-review"
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
* `
|
|
368
|
-
*
|
|
346
|
+
/** The planned plans and open bugs whose hold is exactly `"needs-review"`, in file order: the
|
|
347
|
+
* entries parked by a Needs-review note until the plan role splits or replans them (the
|
|
348
|
+
* `stalled-entries` team alert's input), each carrying its backlog file, 0-based index and the
|
|
349
|
+
* hold. Same `backlogHolds(root)` + the planned/`openBugEntries` readers `operatorHeldEntries`
|
|
350
|
+
* uses, so the alert and the doctor's `stalled entries` row classify through the same
|
|
351
|
+
* `entryHold` and cannot disagree on which holds are role-resolvable. A held-for-operator
|
|
352
|
+
* entry (a note at the held threshold) is
|
|
369
353
|
* excluded — it feeds `operatorHeldEntries` and the `operator-held` alert instead. A missing
|
|
370
354
|
* file yields `[]`. */
|
|
371
355
|
export function stalledEntries(root) {
|
|
@@ -374,7 +358,7 @@ export function stalledEntries(root) {
|
|
|
374
358
|
const entriesOf = (entries, held, file) => {
|
|
375
359
|
entries.forEach((e, i) => {
|
|
376
360
|
const hold = held[i];
|
|
377
|
-
if (hold === "needs-review"
|
|
361
|
+
if (hold === "needs-review")
|
|
378
362
|
out.push({ file, index: i, title: e.title, hold });
|
|
379
363
|
});
|
|
380
364
|
};
|
|
@@ -426,17 +410,25 @@ export function cancelledClaimNote(runner) {
|
|
|
426
410
|
? ""
|
|
427
411
|
: `${roleLabel(runner)} is still working on it — its run goes on; \`tumwater abort --role ${runner}\` stops it`;
|
|
428
412
|
}
|
|
413
|
+
/** The backlog file and section a claim-taking role's entries live under: BUGS.md's `## Open`
|
|
414
|
+
* for bugfix, PLANS.md's `## Planned` for every other base. An instance id resolves through its
|
|
415
|
+
* base role. The one home of the role → (file, section) mapping, so eligibility (eligibleEntries
|
|
416
|
+
* below) and claim staging (scheduling/claims.ts) read a claim's line range from the right file
|
|
417
|
+
* and cannot drift on which section a role owns. */
|
|
418
|
+
export function roleSection(role) {
|
|
419
|
+
return baseRoleOf(role) === "bugfix"
|
|
420
|
+
? { file: "BUGS.md", section: "Open" }
|
|
421
|
+
: { file: "PLANS.md", section: "Planned" };
|
|
422
|
+
}
|
|
429
423
|
/** The entries `role` may take now, in file order: PLANS.md's `## Planned` entries for
|
|
430
424
|
* feature, BUGS.md's `## Open` entries for bugfix, minus every entry entryHold holds, each with
|
|
431
425
|
* its stamp-free key, verbatim title and 1-based line range. Reads the primary checkout through
|
|
432
426
|
* the same stat-cached readers the index uses; a missing or unreadable file yields []. */
|
|
433
427
|
export function eligibleEntries(root, role) {
|
|
434
|
-
// An instance id (`bugfix-2`) resolves through its base role,
|
|
435
|
-
//
|
|
436
|
-
const
|
|
437
|
-
const
|
|
438
|
-
const section = bugfix ? "Open" : "Planned";
|
|
439
|
-
const entries = bugfix ? openBugEntries(root) : plannedPlanEntries(root);
|
|
428
|
+
// An instance id (`bugfix-2`) resolves through its base role, as roleSection does, so a
|
|
429
|
+
// claim's line range and the entry list are read from the right file.
|
|
430
|
+
const { file, section } = roleSection(role);
|
|
431
|
+
const entries = section === "Open" ? openBugEntries(root) : plannedPlanEntries(root);
|
|
440
432
|
const ranges = actionableEntryRangesFor(root, file, section);
|
|
441
433
|
const planned = plannedPlanEntries(root);
|
|
442
434
|
const parts = plannedPartKeys(planned);
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/** File brand-new BUGS.md `## Open` entries (operator bug reports, quarantined flaky tests) on
|
|
2
|
+
* the base branch as one `tumwater(operator):` commit, under the merge lock and the
|
|
3
|
+
* primary-worktree lock. Split out of backlog-archive.ts, whose name and doc cover archiving
|
|
4
|
+
* finished entries to the archive files, not appending new open entries. */
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { withLock, withPrimaryWorktreeLockAsync } from "../concurrency/lock.js";
|
|
8
|
+
import { readTextIfExists, writeTextAtomic } from "../files/files.js";
|
|
9
|
+
import { commitAuthorArgs, committerIdentArgs, git, gitTry } from "../git/git-run.js";
|
|
10
|
+
import { stampedSubject } from "../git/commit-message.js";
|
|
11
|
+
import { mergeLockDir } from "../paths.js";
|
|
12
|
+
import { plural } from "../text/phrases.js";
|
|
13
|
+
import { collapseWhitespace, errorFirstLine } from "../text/text.js";
|
|
14
|
+
import { formatDate } from "../text/datetime.js";
|
|
15
|
+
import { parseEntryDetails } from "./backlog-md.js";
|
|
16
|
+
import { appendBugEntryLocked } from "./backlog-write.js";
|
|
17
|
+
import { primaryOffBaseBranchReason } from "./backlog-archive.js";
|
|
18
|
+
/** The start of a quarantine entry's title for `test`, up to the date. appendBugEntryLocked
|
|
19
|
+
* collapses whitespace in the title, so the test part is collapsed the same way; collapsing the
|
|
20
|
+
* whole prefix would trim its trailing space. */
|
|
21
|
+
function quarantineTitlePrefix(test) {
|
|
22
|
+
return collapseWhitespace(`\`${test}\` failed, then passed on the same tree`) + " (quarantined by tumwater ";
|
|
23
|
+
}
|
|
24
|
+
/** Output fenced into a bug body: a fence of its own length cannot be closed by the quoted text. */
|
|
25
|
+
function fencedTail(tail) {
|
|
26
|
+
return ["~~~~", ...tail.replaceAll("~~~~", "~~~").split("\n").slice(-30), "~~~~"].join("\n");
|
|
27
|
+
}
|
|
28
|
+
/** File one BUGS.md `## Open` entry per given entry and commit BUGS.md on the base branch as one
|
|
29
|
+
* `tumwater(operator):` commit (the subject built from the number of entries actually appended is
|
|
30
|
+
* stamped under the operator role), under the merge lock and the primary-worktree lock exactly as
|
|
31
|
+
* archiveFinishedOnMain does. Skipped,
|
|
32
|
+
* writing nothing, when the primary checkout is off the base branch, BUGS.md has uncommitted
|
|
33
|
+
* changes, or BUGS.md exists but cannot be read; a failed append or commit restores BUGS.md.
|
|
34
|
+
* Every entry is appended verbatim as its own `## Open` heading — unless its `ifOpen` dedupe
|
|
35
|
+
* matches an existing heading, which is reused instead — and the returned headings are the
|
|
36
|
+
* entry headings in input order. */
|
|
37
|
+
export async function fileBugsOnMain(root, entries, subject, opts = {}) {
|
|
38
|
+
return withLock(mergeLockDir(root), () => withPrimaryWorktreeLockAsync(root, async () => {
|
|
39
|
+
const offBase = await primaryOffBaseBranchReason(root);
|
|
40
|
+
if (offBase !== null)
|
|
41
|
+
return { status: "skipped", reason: offBase };
|
|
42
|
+
const status = await gitTry(root, "status", "--porcelain", "--", "BUGS.md");
|
|
43
|
+
if (status === null)
|
|
44
|
+
return { status: "skipped", reason: "could not read git status" };
|
|
45
|
+
if (status.trim() !== "" && !status.startsWith("??")) {
|
|
46
|
+
return { status: "skipped", reason: "BUGS.md has uncommitted changes" };
|
|
47
|
+
}
|
|
48
|
+
const file = path.join(root, "BUGS.md");
|
|
49
|
+
let before;
|
|
50
|
+
try {
|
|
51
|
+
before = readTextIfExists(file);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
return { status: "skipped", reason: `BUGS.md could not be read: ${errorFirstLine(err)}` };
|
|
55
|
+
}
|
|
56
|
+
const open = before === null ? [] : parseEntryDetails(before, "Open").map((e) => e.title);
|
|
57
|
+
const headings = [];
|
|
58
|
+
let appended = 0;
|
|
59
|
+
const restoreBugs = async () => {
|
|
60
|
+
await gitTry(root, "reset", "-q", "--", "BUGS.md");
|
|
61
|
+
if (before === null)
|
|
62
|
+
fs.rmSync(file, { force: true });
|
|
63
|
+
else
|
|
64
|
+
writeTextAtomic(file, before);
|
|
65
|
+
};
|
|
66
|
+
try {
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
if (entry.ifOpen !== undefined) {
|
|
69
|
+
const existing = entry.ifOpen(open);
|
|
70
|
+
if (existing !== undefined) {
|
|
71
|
+
headings.push(existing);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
headings.push(appendBugEntryLocked(root, entry.title, entry.body));
|
|
76
|
+
appended++;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
await restoreBugs();
|
|
81
|
+
return { status: "skipped", reason: `a BUGS.md entry could not be written: ${errorFirstLine(err)}` };
|
|
82
|
+
}
|
|
83
|
+
if (appended === 0)
|
|
84
|
+
return { status: "filed", headings };
|
|
85
|
+
const commitSubject = stampedSubject("operator", // vocabulary-ok: the tumwater(operator): commit prefix is a machine string
|
|
86
|
+
subject(appended));
|
|
87
|
+
try {
|
|
88
|
+
await git(root, "add", "--", "BUGS.md");
|
|
89
|
+
await git(root, ...(await committerIdentArgs(root)), "commit", ...commitAuthorArgs(), "--only", "-m", commitSubject, "--", "BUGS.md");
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
await restoreBugs();
|
|
93
|
+
return { status: "skipped", reason: `the commit failed: ${errorFirstLine(err)}` };
|
|
94
|
+
}
|
|
95
|
+
return { status: "filed", headings };
|
|
96
|
+
}), opts.lockTimeoutMs);
|
|
97
|
+
}
|
|
98
|
+
/** File one BUGS.md `## Open` entry per newly quarantined test and commit BUGS.md on the base
|
|
99
|
+
* branch as one `tumwater(operator):` commit, delegating the locked write/commit to
|
|
100
|
+
* fileBugsOnMain and preserving the existing subject. A test whose quarantine entry is already
|
|
101
|
+
* under `## Open` (a filing whose markQuarantineFiled failed after the commit) gets that heading
|
|
102
|
+
* back and is not filed again — the dedupe runs inside fileBugsOnMain under the merge lock (via
|
|
103
|
+
* each entry's `ifOpen`), so concurrent quarantine filings cannot both append a duplicate, and
|
|
104
|
+
* the real skip on a dirty or off-base BUGS.md is enforced under the same lock. */
|
|
105
|
+
export async function fileQuarantineBugsOnMain(root, tests, opts = {}) {
|
|
106
|
+
const date = formatDate(new Date());
|
|
107
|
+
const toFile = [];
|
|
108
|
+
for (const { test, tail } of tests) {
|
|
109
|
+
const prefix = quarantineTitlePrefix(test);
|
|
110
|
+
const body = [
|
|
111
|
+
`The check failed on this test and then passed on a re-run of the same tree, so tumwater quarantined it: it counts as a known failure until this entry leaves \`## Open\`. Find why it is unstable and fix it.`,
|
|
112
|
+
...(tail ? ["", fencedTail(tail)] : []),
|
|
113
|
+
].join("\n");
|
|
114
|
+
toFile.push({
|
|
115
|
+
title: `${prefix}${date})`,
|
|
116
|
+
body,
|
|
117
|
+
// Dedupe under the lock, against the headings fileBugsOnMain reads: the reused heading
|
|
118
|
+
// (not a duplicate) is returned in headings and zipped back to this test below.
|
|
119
|
+
ifOpen: (open) => open.find((t) => t.startsWith(prefix)),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const outcome = await fileBugsOnMain(root, toFile,
|
|
123
|
+
// Size the subject by what was actually appended: tests whose `## Open` entry was already
|
|
124
|
+
// present are reused, not filed, so a mixed batch keeps the original "File N …" wording
|
|
125
|
+
// with N the count of new entries only.
|
|
126
|
+
(appended) => `File ${plural(appended, "quarantined flaky test", "quarantined flaky tests")} in BUGS.md`, opts);
|
|
127
|
+
if (outcome.status === "skipped")
|
|
128
|
+
return { status: "skipped", reason: outcome.reason };
|
|
129
|
+
// fileBugsOnMain's headings align with `tests` in order (reused headings or appended titles).
|
|
130
|
+
const bugs = new Map();
|
|
131
|
+
outcome.headings.forEach((heading, i) => bugs.set(tests[i].test, heading));
|
|
132
|
+
return { status: "filed", bugs };
|
|
133
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/** The data half of the `<backlog-index>` block every role prompt carries: the 1-based line
|
|
2
|
+
* ranges each actionable entry occupies in its file (actionableEntryRanges/
|
|
3
|
+
* actionableEntryRangesFor) and the collapsed blocked-series rows
|
|
4
|
+
* (groupBlockedSeries/partsLabel) that back it. Extracted from backlog-structure.ts
|
|
5
|
+
* (organized 2026-10-25) so that module keeps only the backlog's structural-correctness checks
|
|
6
|
+
* (stranded plans, duplicate and readded headings, the new-plan-under-Done gate) and this one
|
|
7
|
+
* holds what the multiple index readers share — the role prompt's index block
|
|
8
|
+
* (backlog-index.ts), the eligibility and claims readers, the archive, write and release
|
|
9
|
+
* paths, and the status/tui surfaces all ask for the same section walk of the same file, so
|
|
10
|
+
* the stat-cached range walk (actionableEntryRangesFor) is the one shared helper. The walk
|
|
11
|
+
* and series grouping are pure markdown reading — no pi, no git. Rendering of the block
|
|
12
|
+
* lives in backlog-index.ts; this module holds only the data the surfaces share. */
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { cachedTextByStat } from "../files/stat-cache.js";
|
|
15
|
+
import { fenceTracker, headingTitle } from "./backlog-md.js";
|
|
16
|
+
import { seriesPart } from "./backlog-parts.js";
|
|
17
|
+
/** The entries under one `## <sectionTitle>` section with their 1-based line ranges, in file
|
|
18
|
+
* order: the fence-aware walk sectionLines and parseEntryDetails share, carrying the line
|
|
19
|
+
* numbers neither returns. An entry starts at a `### ` line and ends at the line before the
|
|
20
|
+
* next `### ` or `## ` line (or EOF) — the same boundary parseEntryDetails closes bodies on —
|
|
21
|
+
* so a role reading lines `start`..`end` sees exactly the entry's heading and body. A `## ` or
|
|
22
|
+
* `### ` line inside a fenced code block is body content, never a boundary, through
|
|
23
|
+
* backlog-md.ts's shared fenceTracker. */
|
|
24
|
+
export function actionableEntryRanges(md, sectionTitle) {
|
|
25
|
+
const lines = md.split("\n");
|
|
26
|
+
const fenced = fenceTracker();
|
|
27
|
+
const entries = [];
|
|
28
|
+
let inSection = false;
|
|
29
|
+
let open = null;
|
|
30
|
+
// `end` is the 1-based number of the entry's last body line, i.e. the 0-based index of the
|
|
31
|
+
// next heading; at EOF it is the line count (the last 1-based line number).
|
|
32
|
+
const close = (end) => {
|
|
33
|
+
if (open !== null)
|
|
34
|
+
entries.push({ title: open.title, start: open.start, end });
|
|
35
|
+
open = null;
|
|
36
|
+
};
|
|
37
|
+
for (let i = 0; i < lines.length; i++) {
|
|
38
|
+
const line = lines[i] ?? "";
|
|
39
|
+
if (fenced.inside(line))
|
|
40
|
+
continue;
|
|
41
|
+
if (line.startsWith("## ")) {
|
|
42
|
+
close(i);
|
|
43
|
+
inSection = headingTitle(line, "## ") === sectionTitle;
|
|
44
|
+
}
|
|
45
|
+
else if (inSection && line.startsWith("### ")) {
|
|
46
|
+
close(i);
|
|
47
|
+
open = { title: headingTitle(line, "### "), start: i + 1 };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// A document ending in a newline splits to a trailing "" element; the last real line is one
|
|
51
|
+
// less, so an entry running to EOF ends there instead of at the phantom line.
|
|
52
|
+
close(md.endsWith("\n") ? lines.length - 1 : lines.length);
|
|
53
|
+
return entries;
|
|
54
|
+
}
|
|
55
|
+
/** Per-file cache of a section's actionable ranges (actionableEntryRangesFor). */
|
|
56
|
+
const actionableRangesCache = new Map();
|
|
57
|
+
/** `actionableEntryRanges` for one backlog file, stat-cached per (file path, section title):
|
|
58
|
+
* the per-poll and per-run readers (eligibility's eligibleEntries, claims' listedKeys) ask for
|
|
59
|
+
* the same section of the same file every couple of seconds, and BUGS.md's closed history —
|
|
60
|
+
* which the whole-file walk reads — grows without bound, so an unchanged file costs one stat
|
|
61
|
+
* instead of a full read, split, and section walk. Keyed by file path so distinct roots never
|
|
62
|
+
* collide, bounded inside cachedTextByStat. A missing or unreadable file reads as no ranges. The
|
|
63
|
+
* result is copied out, so each caller owns its data. */
|
|
64
|
+
export function actionableEntryRangesFor(root, file, sectionTitle) {
|
|
65
|
+
const full = path.join(root, file);
|
|
66
|
+
return (cachedTextByStat(actionableRangesCache, `${full}\u0000${sectionTitle}`, full, (md) => actionableEntryRanges(md, sectionTitle), (ranges) => ranges.slice()) ?? []);
|
|
67
|
+
}
|
|
68
|
+
/** True when `row` is a candidate for series-collapsing: it is a series part held only by an
|
|
69
|
+
* unmerged prerequisite (`{ blockedBy }`, not a Refused/needs-review note) and no live
|
|
70
|
+
* claim names it. Returns the row's series part when it collapses, else null. */
|
|
71
|
+
function collapsibleSeriesPart(row) {
|
|
72
|
+
if (row.claimer !== undefined)
|
|
73
|
+
return null;
|
|
74
|
+
if (typeof row.hold !== "object" || row.hold === null)
|
|
75
|
+
return null;
|
|
76
|
+
return seriesPart(row.title);
|
|
77
|
+
}
|
|
78
|
+
/** The collapsed part list for a series' blocked parts: each part as `i/n`, with a run of
|
|
79
|
+
* consecutive numeric parts (same total, adjacent values) joined into one en-dash range
|
|
80
|
+
* (`2/5–5/5`) and each lettered part (`2a`) listed singly. */
|
|
81
|
+
export function partsLabel(rows) {
|
|
82
|
+
const refs = rows.map((r) => seriesPart(r.title));
|
|
83
|
+
const pieces = [];
|
|
84
|
+
let start = null;
|
|
85
|
+
let prev = null;
|
|
86
|
+
const numeric = (p) => /^\d+$/.test(p);
|
|
87
|
+
const flush = () => {
|
|
88
|
+
if (start === null || prev === null)
|
|
89
|
+
return;
|
|
90
|
+
pieces.push(start.part === prev.part
|
|
91
|
+
? `${start.part}/${start.of}`
|
|
92
|
+
: `${start.part}/${start.of}–${prev.part}/${prev.of}`);
|
|
93
|
+
start = null;
|
|
94
|
+
prev = null;
|
|
95
|
+
};
|
|
96
|
+
for (const ref of refs) {
|
|
97
|
+
if (prev !== null &&
|
|
98
|
+
numeric(ref.part) &&
|
|
99
|
+
numeric(prev.part) &&
|
|
100
|
+
Number(ref.part) === Number(prev.part) + 1 &&
|
|
101
|
+
ref.of === prev.of) {
|
|
102
|
+
prev = ref;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
flush();
|
|
106
|
+
start = ref;
|
|
107
|
+
prev = ref;
|
|
108
|
+
}
|
|
109
|
+
flush();
|
|
110
|
+
return pieces.join(", ");
|
|
111
|
+
}
|
|
112
|
+
/** Renders `rows` to index lines, collapsing a multi-part series' blocked parts to one line per
|
|
113
|
+
* series (this plan, "Compact backlog index, part 1/1"): a long series' chain of held parts would
|
|
114
|
+
* otherwise fill every role prompt with lines no role can act on. An entry collapses when it is a
|
|
115
|
+
* series part held only by an unmerged prerequisite and no live claim names it; a series with two
|
|
116
|
+
* or more such parts renders as one line at the position of its first collapsed part, spanning from
|
|
117
|
+
* that part's start to the last's end, with the part list (`2/4–4/4`) and a `[blocked: each waits
|
|
118
|
+
* on an earlier part]` mark. A series with a single blocked part, an eligible part, a note-held
|
|
119
|
+
* (Refused/needs-review) part and a claimed part keep their ordinary lines, so the
|
|
120
|
+
* eligibility rules the feature role's charter teaches are unchanged; the collapsed mark keeps the
|
|
121
|
+
* `[blocked: …]` prefix it already skips. */
|
|
122
|
+
/** The grouping half of backlog-index's collapseBlockedSeries, on any rows carrying a title,
|
|
123
|
+
* hold and claimer: each series with two or more collapsible blocked parts becomes one `series`
|
|
124
|
+
* group at its first part's place; every other row stays a `row` group in order. */
|
|
125
|
+
export function groupBlockedSeries(rows) {
|
|
126
|
+
// Group the collapsible rows by series, keyed on the lowercased series name.
|
|
127
|
+
const bySeries = new Map();
|
|
128
|
+
for (const row of rows) {
|
|
129
|
+
const part = collapsibleSeriesPart(row);
|
|
130
|
+
if (part === null)
|
|
131
|
+
continue;
|
|
132
|
+
const key = part.series.toLowerCase();
|
|
133
|
+
const list = bySeries.get(key) ?? [];
|
|
134
|
+
list.push(row);
|
|
135
|
+
bySeries.set(key, list);
|
|
136
|
+
}
|
|
137
|
+
// Only a series with two or more blocked parts collapses; a lone blocked part keeps its
|
|
138
|
+
// exact `requires` line.
|
|
139
|
+
const collapse = new Set();
|
|
140
|
+
for (const [key, list] of bySeries)
|
|
141
|
+
if (list.length >= 2)
|
|
142
|
+
collapse.add(key);
|
|
143
|
+
const groups = [];
|
|
144
|
+
const skipped = new Set();
|
|
145
|
+
for (const row of rows) {
|
|
146
|
+
if (skipped.has(row))
|
|
147
|
+
continue;
|
|
148
|
+
const part = collapsibleSeriesPart(row);
|
|
149
|
+
if (part !== null && collapse.has(part.series.toLowerCase())) {
|
|
150
|
+
const group = bySeries.get(part.series.toLowerCase());
|
|
151
|
+
groups.push({ kind: "series", series: part.series, rows: group });
|
|
152
|
+
for (const g of group)
|
|
153
|
+
skipped.add(g);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
groups.push({ kind: "row", row });
|
|
157
|
+
}
|
|
158
|
+
return groups;
|
|
159
|
+
}
|
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
* entry (PLANS.md ## Planned, BUGS.md ## Open, QUESTIONS.md ## Open) plus the newest closed
|
|
3
3
|
* ones, each with the 1-based line range it occupies in its file — the bounded replacement for
|
|
4
4
|
* the per-run `grep -n '^##'` heading map. The entry-range parsing (actionableEntryRanges) and
|
|
5
|
-
* series collapsing (groupBlockedSeries, partsLabel) it builds on live in
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* series collapsing (groupBlockedSeries, partsLabel) it builds on live in
|
|
6
|
+
* backlog-index-data.ts; this module holds only the block's rendering and its per-file stat
|
|
7
|
+
* caches. Extracted from backlog-structure.ts (organized 2026-10-19, the data half split to
|
|
8
|
+
* backlog-index-data.ts 2026-10-25) so that module keeps only the backlog's structural
|
|
9
|
+
* checks. */
|
|
9
10
|
import path from "node:path";
|
|
10
11
|
import { cachedTextByStat } from "../files/stat-cache.js";
|
|
11
12
|
import { FINISHED_ARCHIVES } from "./backlog-md.js";
|
|
12
13
|
import { allLiveClaims } from "../runner/runner-state-load.js";
|
|
13
14
|
import { collapseWhitespace, truncate } from "../text/text.js";
|
|
14
|
-
import { actionableEntryRanges, groupBlockedSeries, partsLabel,
|
|
15
|
+
import { actionableEntryRanges, groupBlockedSeries, partsLabel, } from "./backlog-index-data.js";
|
|
16
|
+
import { stripEntryStamp } from "./backlog-structure.js";
|
|
15
17
|
import { entryHoldAgainst, holdMark } from "./backlog-eligibility.js";
|
|
16
18
|
import { plannedPartKeys } from "./backlog-parts.js";
|
|
17
19
|
import { plannedPlanEntries } from "./backlog.js";
|