@bongos/core 1.19.316 → 1.19.317

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.
@@ -0,0 +1,165 @@
1
+ // modules/lifecycle/db-claim-reads.js — read-only claim + grade lookups (no state transitions).
2
+ //
3
+ // Carved out of modules/lifecycle/db.js by task 1003204 (goal 1000079, the R03
4
+ // de-monolith plan) — a STRUCTURAL move, not a rewrite: the code below is the
5
+ // original db.js lines 4774-4915 unchanged. db.js remains the facade that
6
+ // re-exports every name, so every existing db call site is unchanged.
7
+ //
8
+ // THE BOUNDARY (ADR 0093 §1): this file reaches core ONLY through the published
9
+ // doorway ../../src/module-api.js — never a core internal, never a sibling module.
10
+
11
+ 'use strict';
12
+
13
+ const api = require('../../src/module-api');
14
+ const { pool } = api;
15
+
16
+ async function getActiveClaimsForBuilder(builderId) {
17
+ // release_requested_* (task 1736): a goal owner/manager's flag-for-release
18
+ // nudge rides on the holder's own claim read so /me (and the surfaces built
19
+ // on it) can show "someone asked you to wrap this up".
20
+ const { rows } = await pool.query(
21
+ `SELECT c.id AS claim_id, c.task_id, c.worktree_name, c.claimed_at,
22
+ c.head_sha, c.reserved_migration_number,
23
+ c.release_requested_by, c.release_requested_at, c.release_requested_note,
24
+ t.title, t.touches, t.priority, t.credits_reward, t.version_id, t.module_key
25
+ FROM claims c JOIN tasks t ON t.id = c.task_id
26
+ WHERE c.builder_id = $1 AND c.released_at IS NULL
27
+ ORDER BY c.claimed_at`,
28
+ [builderId]
29
+ );
30
+ return rows;
31
+ }
32
+
33
+ // Task-scoped dev box (goal 1000051 / R92, ADR 0148 §2). The set-union of the
34
+ // module KEYS a builder's active claims put in scope: each active claim's task →
35
+ // its goal → goal.scope_modules (the ADR 0086 scope wall), unioned + deduped
36
+ // across all the builder's active claims. This is the "what code does this
37
+ // builder's current work need" signal the dev-box source-access resolver
38
+ // (modules/dev-box/box-access.js, wired in R93/R94) maps to git sparse-checkout
39
+ // globs via the doorway `moduleScopeMap`. Returns [] when the builder holds no
40
+ // active claim, or when every claimed task carries no goal / an empty
41
+ // scope_modules — the box then falls back to BASE-only (the pre-existing Bongos
42
+ // instance). KEYS only; glob resolution is the box module's job (it reaches
43
+ // moduleScopeMap through the doorway, never a core-internal require — the
44
+ // R90/ADR-0148 boundary). Honours the #538 deps.pool test seam
45
+ // (NODE_ENV==='test' only), like the goals reads above.
46
+ async function moduleScopeForActiveClaims(builderId, deps = {}) {
47
+ const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
48
+ const id = Number(builderId);
49
+ if (!Number.isInteger(id)) {
50
+ throw new Error('moduleScopeForActiveClaims: builderId must be numeric');
51
+ }
52
+ const { rows } = await activePool.query(
53
+ `SELECT DISTINCT m AS module_key
54
+ FROM claims c
55
+ JOIN tasks t ON t.id = c.task_id
56
+ JOIN goals g ON g.id = t.goal_id
57
+ CROSS JOIN LATERAL unnest(g.scope_modules) AS m
58
+ WHERE c.builder_id = $1 AND c.released_at IS NULL
59
+ ORDER BY m`,
60
+ [id]
61
+ );
62
+ return rows.map((r) => r.module_key);
63
+ }
64
+
65
+ // Task 1002729 — the companion signal moduleScopeForActiveClaims cannot carry.
66
+ // That query INNER JOINs goals and unnests scope_modules, so it returns [] for
67
+ // BOTH "this builder holds no claim" and "this builder holds claims whose goals
68
+ // declare no scope_modules". Those must not be treated alike: the first is a
69
+ // genuinely unscoped box (BASE-only is right), the second is a builder actively
70
+ // working who would be handed a box with no modules/ at all. Live evidence for
71
+ // why this matters: 247 of 248 source grants on the cloudbongos fleet resolved to
72
+ // BASE, including an Archon holding 7 active claims — every one of those claims
73
+ // sat on a goal with scope_modules = {}. Counts CLAIMS, deliberately not joining
74
+ // goals, so it stays true regardless of how the goal wall is (or isn't) declared.
75
+ async function activeClaimCountForBuilder(builderId, deps = {}) {
76
+ const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
77
+ const id = Number(builderId);
78
+ if (!Number.isInteger(id)) {
79
+ throw new Error('activeClaimCountForBuilder: builderId must be numeric');
80
+ }
81
+ const { rows } = await activePool.query(
82
+ `SELECT count(*)::int AS n FROM claims WHERE builder_id = $1 AND released_at IS NULL`,
83
+ [id]
84
+ );
85
+ return rows.length ? Number(rows[0].n) : 0;
86
+ }
87
+
88
+ // task 1642 — every active claim bound to a given CLI session. "Active" is the
89
+ // claims-table convention (released_at IS NULL); there is NO `status` column on
90
+ // claims (a common misread — the task's status lives on `tasks`). Keyed on
91
+ // creator_session_id (the auth-token session shared across a builder's worktrees),
92
+ // NOT builder_id. This backs the one-active-claim guard (shouldRefuseSessionClaim),
93
+ // which further discriminates by `worktree_name` — the real commingle boundary —
94
+ // so two claims can coexist for one auth session as long as they land in different
95
+ // worktrees. NULL-session callers must never reach this — they are session-UNBOUND
96
+ // web claims (bind_session=false) that any CLI session may adopt; the route
97
+ // short-circuits before calling here.
98
+ async function getActiveClaimsForSession(creatorSessionId) {
99
+ if (!creatorSessionId) return [];
100
+ const { rows } = await pool.query(
101
+ // task 1002709: requested_worktree_name rides along because the guard keys on
102
+ // it — comparing the incoming REQUESTED name against a stored DISAMBIGUATED
103
+ // one is what let a second same-tree claim through. worktree_name stays
104
+ // selected: callers still render the on-disk name.
105
+ `SELECT c.id AS claim_id, c.task_id, c.creator_session_id, c.worktree_name,
106
+ c.requested_worktree_name, c.claimed_at,
107
+ t.title
108
+ FROM claims c JOIN tasks t ON t.id = c.task_id
109
+ WHERE c.creator_session_id = $1 AND c.released_at IS NULL
110
+ ORDER BY c.claimed_at`,
111
+ [creatorSessionId]
112
+ );
113
+ return rows;
114
+ }
115
+
116
+ // The builder who most recently held a claim on this task — active OR resolved.
117
+ // Used by the `--regrade` ownership guard (#678): when a ship lands at
118
+ // `completed` but the subagent grader fails, the claim is already consumed, so
119
+ // the only record of "whose work this is" lives in the claims ledger. Returns
120
+ // the latest claim row by claim time (id is the tiebreak, monotonic), joined to
121
+ // the builder for github_login, plus the claim's head_sha (the grader's
122
+ // last-resort baseline), or null if the task was never claimed. Deliberately
123
+ // does NOT return the holder's rank — the regrade gate keys off the CALLER's
124
+ // rank (from /me), and rank is sensitive metadata not previously on this read
125
+ // surface (grader hacker finding, #678). Pure read, builder-agnostic.
126
+
127
+ async function getLastClaimHolder(taskId) {
128
+ const { rows } = await pool.query(
129
+ `SELECT c.builder_id, c.head_sha, c.released_at, c.outcome, c.claimed_at,
130
+ b.github_login
131
+ FROM claims c
132
+ JOIN builders b ON b.id = c.builder_id
133
+ WHERE c.task_id = $1
134
+ ORDER BY c.claimed_at DESC, c.id DESC
135
+ LIMIT 1`,
136
+ [taskId]
137
+ );
138
+ return rows[0] ?? null;
139
+ }
140
+
141
+ // getTaskGrade — fetch the current grade row for a task (V4.R08 diff-hash
142
+ // reuse). Returns { diff_hash, passed, score, threshold, grader_kind,
143
+ // grader_version, rubric_scores, signals, notes_md } or null when no grade
144
+ // exists yet. Used by ship.js before the grade step to detect "same diff →
145
+ // skip the panel".
146
+ async function getTaskGrade(taskId) {
147
+ const { rows } = await pool.query(
148
+ `SELECT diff_hash, passed, score, threshold, grader_kind, grader_version,
149
+ rubric_scores, signals, notes_md
150
+ FROM task_grades
151
+ WHERE task_id = $1
152
+ LIMIT 1`,
153
+ [taskId]
154
+ );
155
+ return rows[0] ?? null;
156
+ }
157
+
158
+ module.exports = {
159
+ activeClaimCountForBuilder,
160
+ getActiveClaimsForBuilder,
161
+ getActiveClaimsForSession,
162
+ getLastClaimHolder,
163
+ getTaskGrade,
164
+ moduleScopeForActiveClaims,
165
+ };