@consiliency/agent-board-schema 1.45.0 → 1.50.0

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.
Files changed (35) hide show
  1. package/fixtures/already-applied.json +24 -0
  2. package/migrations/20260926000100_agent_board_ownerless_quota_census.sql +268 -0
  3. package/migrations/20260927000100_agent_board_fleet_domain_no_downgrade.sql +393 -0
  4. package/migrations/20260928000100_agent_board_ownerless_quota_renew_convergence.sql +562 -0
  5. package/migrations/20260929000100_agent_board_advisory_revert_cap_reset.sql +371 -0
  6. package/migrations/20260930000100_agent_board_claim_policy_race.sql +922 -0
  7. package/migrations/20261001000100_agent_board_claimpolicy_lock_grant_revoke.sql +158 -0
  8. package/package.json +1 -1
  9. package/schema-manifest.json +57 -3
  10. package/src/tooling.test.mjs +52 -2
  11. package/tests/agent_board_board_claim_policy.test.sql +261 -1
  12. package/tests/agent_board_claim_subject_gate.test.sql +11 -2
  13. package/tests/agent_board_claimkey.test.sql +17 -2
  14. package/tests/agent_board_claimpolicy_race.test.sql +1374 -0
  15. package/tests/agent_board_fleet_machine_hijack.test.sql +270 -1
  16. package/tests/agent_board_fleet_machine_reclaim.test.sql +28 -2
  17. package/tests/agent_board_ownerless_quota_census.test.sql +406 -0
  18. package/tests/agent_board_ownerless_renew_quota.test.sql +580 -0
  19. package/tests/catalog/p_metadata_parameters.json +28 -0
  20. package/tests/fixtures/test-prereqs.sql +186 -0
  21. package/tests/isolation/expected/claimpolicy_acquire_revert_race.out +66 -0
  22. package/tests/isolation/expected/claimpolicy_board_scope.out +73 -0
  23. package/tests/isolation/expected/claimpolicy_claimrow_lock_order.out +80 -0
  24. package/tests/isolation/expected/claimpolicy_isolation_refusal.out +114 -0
  25. package/tests/isolation/expected/claimpolicy_renew_revert_race.out +66 -0
  26. package/tests/isolation/expected/claimpolicy_writer_keyshare.out +49 -0
  27. package/tests/isolation/expected/fleet_domain_no_downgrade_demote_race.out +447 -0
  28. package/tests/isolation/specs/claimpolicy_acquire_revert_race.spec +118 -0
  29. package/tests/isolation/specs/claimpolicy_board_scope.spec +115 -0
  30. package/tests/isolation/specs/claimpolicy_claimrow_lock_order.spec +200 -0
  31. package/tests/isolation/specs/claimpolicy_isolation_refusal.spec +162 -0
  32. package/tests/isolation/specs/claimpolicy_renew_revert_race.spec +150 -0
  33. package/tests/isolation/specs/claimpolicy_writer_keyshare.spec +107 -0
  34. package/tests/isolation/specs/fleet_domain_no_downgrade_demote_race.spec +292 -0
  35. package/tests/seeds/20261001000100_agent_board_claimpolicy_lock_grant_revoke.sql.pre.sql +99 -0
@@ -0,0 +1,1374 @@
1
+ -- #405 — the stale-policy race family: the CATALOG half.
2
+ --
3
+ -- WHAT THIS FILE CAN AND CANNOT SEE, said first because the division is the whole point. A pgTAP
4
+ -- file runs in ONE rolled-back transaction, so it cannot produce the interleaving this issue is
5
+ -- about, and it cannot change its own isolation level (that must be the transaction's first
6
+ -- statement). Every BEHAVIOURAL claim about the race — that a reader waits, that it re-reads the
7
+ -- committed policy, that a REPEATABLE READ caller is refused, that two boards do not serialise
8
+ -- against each other — lives in the isolation specs:
9
+ --
10
+ -- claimpolicy_acquire_revert_race.spec the advisory-SKIP path, the one a "lock before the
11
+ -- agent lock" placement never reaches
12
+ -- claimpolicy_renew_revert_race.spec the re-read AND the claim-state revalidation
13
+ -- claimpolicy_board_scope.spec the twins: a different board does not serialise
14
+ -- claimpolicy_isolation_refusal.spec REPEATABLE READ is refused, by name
15
+ -- claimpolicy_writer_keyshare.spec the writer-side KEY SHARE cycle, measured both ways
16
+ -- claimpolicy_claimrow_lock_order.spec reclaim_fleet_machine against the policy writer, and
17
+ -- the claim-row -> board-lock inversion CONSTRUCTED by
18
+ -- hand, so the quiet permutation is evidence about the
19
+ -- code rather than about the probe
20
+ --
21
+ -- WHAT THIS FILE PINS is the part a spec cannot: that the CLASS is complete and comes from the
22
+ -- catalog, that each exemption is asserted positively rather than by absence, and that the two
23
+ -- premises the advisory lock rests on — `set_board_claim_policy` is the sole writer of
24
+ -- `boards.claim_policy`, and `threads.board_id` never moves once set — are still true.
25
+ --
26
+ -- WHAT AN ALL-GREEN RUN OF THIS FILE ESTABLISHES, AND WHAT IT DOES NOT. Read this before citing a
27
+ -- green as evidence. Most assertions here are TRIPWIRES over source text and catalog state, and a
28
+ -- tripwire that fails OPEN is green for the same reason a correct one is: 35/35 here, 124/124 for
29
+ -- the suite and 34/34 for the isolation goldens are all consistent with a pattern that silently
30
+ -- matches nothing. Six review rounds found exactly that six times — a window too small for a real
31
+ -- upsert, an alternation with no path to `for key share`, a complement keyed by name so an overload
32
+ -- escaped it. Each was green until someone injected the shape and watched. So: a green run means no
33
+ -- assertion's expected value changed; it does NOT mean the properties are held. Only a MUTATION
34
+ -- that produces a NAMED failure establishes that a given property is pinned at all.
35
+ --
36
+ -- WHAT LIVES ONLY IN THE ISOLATION SUITE, so nobody reads this file as the whole gate: the race
37
+ -- itself (a reader waits and then observes the COMMITTED policy, including acquire's advisory-skip
38
+ -- path), renew's claim revalidation after the wait, the 0A000 refusal by name at three isolation
39
+ -- levels, cross-board non-serialisation, and the writer-side KEY SHARE vs FOR UPDATE
40
+ -- discrimination. Nothing here replaces claimpolicy_board_scope.spec. ISOLATION IS NOT OPTIONAL CI
41
+ -- FOR THIS FEATURE — a green pgTAP run with the isolation suite skipped establishes none of those.
42
+ --
43
+ -- THE MATCHERS' BEHAVIOUR TABLE lives in tests/fixtures/test-prereqs.sql beside the functions, and
44
+ -- its ACCEPT rows are the cheap half. Four defects shipped in a matcher whose accept rows all
45
+ -- passed: a literal space inside a multiword verb (so `INSERT\nINTO` evaded), a raw lower-case
46
+ -- column token (so `SET CLAIM_POLICY` evaded), `$` missing from the identifier class (so a DISTINCT
47
+ -- function `x$_claimgate_lock_board_policy` was credited as the protected helper), and a caller
48
+ -- passing `'i'` (so `"AGENT_BOARD"."claims"` folded onto `agent_board.claims`). Every one is a
49
+ -- REJECT row, and none was derivable from the spellings this file happens to contain — the corpus
50
+ -- uses single-space `INSERT INTO` and unquoted `agent_board`, so a green suite said nothing about
51
+ -- any of them. Reject rows must come from PostgreSQL's identifier and keyword rules, not from the
52
+ -- current bodies; a matcher's whole job is discrimination, so most of its table is what it refuses.
53
+ --
54
+ -- THREE LITERAL `agent_board\.` SPELLINGS SURVIVE ON PURPOSE, named here so "every site uses the
55
+ -- shared matcher" is not read as "no literal exists" — it is 67 matcher invocations across 41 lines
56
+ -- PLUS these three. They are PROBES OF ONE KNOWN BODY rather than population derivations, which is
57
+ -- a different instrument: a population matcher that misses a spelling drops a member SILENTLY,
58
+ -- while a probe that misses its target returns 0 or false and takes its assertion RED. Both were
59
+ -- mutation-checked rather than argued:
60
+ -- * CPR-12's first-board-wins guard — rewriting the guarded writer as `update "agent_board"."threads"`
61
+ -- makes the label fall to UNGUARDED and CPR-12 fails.
62
+ -- * CPR-3c's two position probes — rewriting renew's post-lock re-read as unqualified `from claims`
63
+ -- makes the probe return 0 and CPR-3c fails.
64
+ -- Converting them would be harmless but would not buy anything, and the exemption is cheaper to
65
+ -- audit written down than rediscovered.
66
+ --
67
+ -- ONE AXIS THAT HAS BEEN WRONG IN BOTH DIRECTIONS, called out because it keeps recurring: whether an
68
+ -- assertion should key on proname or on OID/signature depends on the question it asks, and there is
69
+ -- no safe default. A property of the NAME ("nothing called X is executable by a non-owner", CPR-22)
70
+ -- needs the name scan, and pinning a signature there lets an overload carry the grant. A property of
71
+ -- a SPECIFIC function (CPR-25a's guard shape, CPR-23/24's ACLs) needs to_regprocedure, and scanning
72
+ -- by name there returns multiple rows — which for a scalar subquery ABORTS the file and emits no
73
+ -- `not ok` at all. A set-membership test (CPR-14b) must compare OIDs, or one censused overload
74
+ -- excludes a same-named sibling from the complement. All three were live defects, not hypotheticals.
75
+ --
76
+ -- CPR-1 IS THE ONE THIS FILE EXISTS FOR. An advisory lock only excludes callers that also take it.
77
+ -- So "which functions resolve a board's claim policy" is not a documentation question here, it is
78
+ -- the soundness question, and a list written by hand is exactly what this repo's rule 1 forbids.
79
+ -- The set is derived from `pg_proc` + `pg_get_viewdef` on every run; a fifth resolver fails CPR-1
80
+ -- and NAMES ITSELF instead of silently inheriting an exemption nobody decided.
81
+ create extension if not exists pgtap with schema extensions;
82
+ set search_path = extensions, public, agent_board;
83
+
84
+ begin;
85
+
86
+ select plan(41);
87
+
88
+ -- ---------------------------------------------------------------------------
89
+ -- Fixture: one board, one thread on it, one thread with NO board (the constitutive-'inherit'
90
+ -- case that CPR-19/20 pin the composition against).
91
+ -- ---------------------------------------------------------------------------
92
+ create temp table _cpr (board uuid, thread uuid, free_thread uuid, board_b uuid) on commit drop;
93
+
94
+ do $$
95
+ declare
96
+ v_sfx text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
97
+ v_owner uuid := gen_random_uuid();
98
+ v_owner_user uuid;
99
+ v_board uuid;
100
+ v_thread uuid := gen_random_uuid();
101
+ v_free uuid := gen_random_uuid();
102
+ -- A SECOND board, existing only for CPR-22a. It must not be the board CPR-25 locks: the whole
103
+ -- pgTAP file is ONE transaction, `pg_advisory_xact_lock` holds to the end of it, and re-taking a
104
+ -- key already held adds no pg_locks row. Sharing a board would make CPR-22a's state check pass on
105
+ -- CPR-25's lock no matter what CPR-22a itself did — measured, not feared: with the helper call
106
+ -- deleted from set_board_claim_policy, the shared-board version of CPR-22a and its state check
107
+ -- both stayed GREEN.
108
+ v_board_b uuid;
109
+ begin
110
+ v_owner_user := public.ensure_github_user('cpr-own-' || v_sfx, 'cpr-own-' || v_sfx || '@example.test',
111
+ 'cpr-own-' || v_sfx, 'CPR Owner');
112
+ insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
113
+ values (v_owner, 'agent', 'cpr-own-' || v_sfx, 'CPR Owner', 'active', v_owner_user, v_owner_user);
114
+
115
+ -- create_board seeds the creator's owner membership (ROOMOWN), which is what
116
+ -- set_board_claim_policy's gate tests.
117
+ perform set_config('request.jwt.claim.role', 'authenticated', true);
118
+ perform set_config('request.jwt.claim.sub', v_owner_user::text, true);
119
+ select board_id into v_board
120
+ from agent_board.create_board(v_owner, 'cpr-' || v_sfx, 'CPR Board', 'claim policy race', '{}'::jsonb, 'private');
121
+ -- Opted out by direct UPDATE rather than through the RPC. Deliberate: the RPC now takes the
122
+ -- lock helper, so a MUTATION of that helper would abort this file in its FIXTURE and emit no
123
+ -- `not ok` at all -- rule 3's exact failure shape. Set the column directly and CPR-25 fails
124
+ -- BY NAME instead. The RPC's own transitions are BCP-11..21's subject, not this file's.
125
+ update agent_board.boards set claim_policy = 'advisory' where id = v_board;
126
+
127
+ insert into agent_board.threads(id, thread_type, created_by_agent_id, subject, board_id)
128
+ values (v_thread, 'coordination', v_owner, 'CPR board thread', v_board),
129
+ (v_free, 'coordination', v_owner, 'CPR boardless thread', null);
130
+
131
+ select board_id into v_board_b
132
+ from agent_board.create_board(v_owner, 'cpr-b-' || v_sfx, 'CPR Board B', 'claim policy race (CPR-22a)', '{}'::jsonb, 'private');
133
+ update agent_board.boards set claim_policy = 'advisory' where id = v_board_b;
134
+
135
+ insert into _cpr(board, thread, free_thread, board_b) values (v_board, v_thread, v_free, v_board_b);
136
+ end $$;
137
+
138
+ -- ===========================================================================
139
+ -- THE CLASS (rule 1). Catalog-derived on every run.
140
+ -- ===========================================================================
141
+
142
+ -- CPR-1. Everything in `agent_board` whose COMMENT-STRIPPED body (or view definition) mentions
143
+ -- `claim_policy` at all. Seven names, and the split between them is CPR-2/CPR-5's job — this
144
+ -- assertion's only claim is that there are no OTHERS.
145
+ --
146
+ -- STRIPPED, and that is not a stylistic preference: run the same query over RAW `prosrc` on this
147
+ -- schema and `set_board_claim_policy` also appears as a caller of `_claimgate_board_claim_policy`,
148
+ -- because its PROSE mentions the helper. A scan that counts a comment as code has already shipped
149
+ -- in this repository once.
150
+ select is(
151
+ (select string_agg(nm, ', ' order by nm) from (
152
+ select p.proname as nm
153
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
154
+ where n.nspname = 'agent_board'
155
+ and agent_board_test.stripped_body(p.oid, true) ~ 'claim_policy'
156
+ union
157
+ select c.relname
158
+ from pg_class c join pg_namespace n on n.oid = c.relnamespace
159
+ where n.nspname = 'agent_board' and c.relkind in ('v', 'm')
160
+ and agent_board_test.stripped_viewdef(c.oid, true) ~ 'claim_policy'
161
+ ) s),
162
+ '_claimgate_board_claim_policy, _claimgate_board_claim_policy_of, _claimgate_lock_board_policy, '
163
+ 'acquire_claim, renew_claim, set_board_claim_policy, v_ownerless_claim_quota',
164
+ 'CPR-1 CLASS: these are all the ROUTINE BODIES and VIEW/MATVIEW DEFINITIONS in agent_board that mention claim_policy — the range this query actually covers; a check constraint, column default or index mentioning the column is outside it, and safely so, since none of those can resolve a policy for a caller or yield a lease. A fifth resolver fails here and names itself');
165
+
166
+ -- CPR-2. Of those, the DECIDERS — the ones that act on the answer — must serialise. Asserted as
167
+ -- the exact set of callers of the lock helper, so a decider added without the lock is absent here
168
+ -- and a non-decider that takes it is present unexpectedly. Either direction fails.
169
+ select is(
170
+ (select coalesce(string_agg(p.proname, ', ' order by p.proname), '')
171
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
172
+ where n.nspname = 'agent_board'
173
+ and p.proname <> '_claimgate_lock_board_policy'
174
+ and agent_board_test.stripped_body(p.oid, true) ~ '_claimgate_lock_board_policy'),
175
+ 'acquire_claim, renew_claim, set_board_claim_policy',
176
+ 'CPR-2 CLASS: these are the deciders that take the board policy lock — the write paths and the writer itself');
177
+
178
+ -- CPR-3. AND TAKE IT BEFORE THE DECISION, which is the correction that makes this fix different
179
+ -- from the one that was falsified in review. The first design said "before the agent-scoped lock";
180
+ -- in acquire_claim that lock lives INSIDE the branch a stale 'advisory' read SKIPS, so on the
181
+ -- dangerous path it is never reached. Source ORDER is asserted per function against the marker
182
+ -- that IS that function's decision.
183
+ select is(
184
+ (select string_agg(x.proname || '=' ||
185
+ case when regexp_instr(b.body, agent_board_test.call_re('_claimgate_lock_board_policy'), 1, 1, 0) = 0 then 'NO-LOCK'
186
+ when regexp_instr(b.body, x.decision, 1, 1, 0, 'i') = 0 then 'NO-DECISION'
187
+ when regexp_instr(b.body, agent_board_test.call_re('_claimgate_lock_board_policy'), 1, 1, 0)
188
+ < regexp_instr(b.body, x.decision, 1, 1, 0, 'i')
189
+ then 'lock-first'
190
+ else 'DECISION-FIRST' end, ', ' order by x.proname)
191
+ from (values
192
+ ('acquire_claim', agent_board_test.call_re('_claimgate_board_claim_policy_of')),
193
+ ('renew_claim', agent_board_test.call_re('_claimgate_board_claim_policy_of')),
194
+ ('set_board_claim_policy', 'into\s+v_prev_policy')
195
+ ) as x(proname, decision)
196
+ join pg_proc p on p.proname = x.proname
197
+ join pg_namespace n on n.oid = p.pronamespace and n.nspname = 'agent_board'
198
+ cross join lateral (select agent_board_test.stripped_body(p.oid, true) as body) b),
199
+ 'acquire_claim=lock-first, renew_claim=lock-first, set_board_claim_policy=lock-first',
200
+ 'CPR-3 ORDER: in each decider the board lock precedes the policy decision in source order — the population is a restatement of CPR-2''s catalog-derived set with a per-function decision marker, so a FOURTH decider fails CPR-2 before this list can go stale; the behavioural half is claimpolicy_acquire_revert_race.spec');
201
+
202
+ -- CPR-4. acquire_claim takes TWO advisory locks. Board first, always: it is the only function that
203
+ -- takes the actor-scoped quota lock, so one consistent order here is the whole cycle argument.
204
+ -- CPR-4 REMOVED (#405 completeness review). It read acquire_claim alone; CPR-4a derives the same
205
+ -- ordering over every function taking the actor-scoped quota lock and so strictly contains it.
206
+ -- Confirmed by mutation before removal: inverting acquire_claim's two locks trips CPR-4a (it tripped
207
+ -- CPR-4 and CPR-4a together). The universal clause CPR-4's description used to carry is CPR-4a's.
208
+
209
+
210
+ -- CPR-4a. THE CLAUSE CPR-4'S SENTENCE USED TO CARRY. It ended "and nothing in the schema takes them
211
+ -- the other way" — a claim about every function, backed by a query that read acquire_claim and
212
+ -- nothing else. The population is derived instead: every function taking the actor-scoped quota
213
+ -- lock, each labelled with the order of its two lock events.
214
+ --
215
+ -- The marker is `pg_advisory_xact_lock(hashtext(` and it does NOT match the board helper, which
216
+ -- calls `hashtextextended` — the character after `hashtext` differs, so the two stay distinguishable
217
+ -- without a negative lookahead. `\s*` between tokens rather than a plain substring, so a caller
218
+ -- spelling the call across a line break is still seen.
219
+ --
220
+ -- upsert_delegation_policy is in the population and takes NO board lock, which is why it cannot
221
+ -- invert anything; it is asserted positively here rather than filtered out, so it having to change
222
+ -- is a decision someone makes rather than an exemption that erodes.
223
+ select is(
224
+ (with q as (
225
+ select p.proname,
226
+ regexp_instr(agent_board_test.stripped_body(p.oid, true),
227
+ agent_board_test.call_re('_claimgate_lock_board_policy'), 1, 1, 0) as board_pos,
228
+ regexp_instr(agent_board_test.stripped_body(p.oid, true),
229
+ 'pg_(try_)?advisory_xact_lock\s*\(\s*hashtext\s*\(', 1, 1, 0, 'i') as quota_pos
230
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
231
+ where n.nspname = 'agent_board'
232
+ and agent_board_test.stripped_body(p.oid, true) ~* 'pg_(try_)?advisory_xact_lock\s*\(\s*hashtext\s*\('
233
+ )
234
+ select coalesce(string_agg(proname || '=' ||
235
+ (case when board_pos = 0 then 'no-board-lock'
236
+ when board_pos < quota_pos then 'board-then-agent'
237
+ else 'AGENT-THEN-BOARD' end), ', ' order by proname), '<none>')
238
+ from q),
239
+ 'acquire_claim=board-then-agent, upsert_delegation_policy=no-board-lock',
240
+ 'CPR-4a ORDER (schema-wide): every function taking the actor-scoped quota lock either never takes the board lock or takes the board lock FIRST — the universal half of CPR-4, derived from the catalog rather than asserted about one function');
241
+
242
+ -- CPR-5. THE EXEMPTION, ASSERTED POSITIVELY. `v_ownerless_claim_quota` resolves board policy at
243
+ -- query time (#393's OQ-29) and is deliberately NOT serialised. The claim that makes that safe is
244
+ -- "it cannot produce a lease", and this asserts exactly that rather than asserting it is absent
245
+ -- from CPR-2: its ONLY rewrite rule is the view's own `_RETURN` SELECT (ev_type 1). An
246
+ -- `ON UPDATE ... DO INSTEAD` rule added to it would fail here.
247
+ select is(
248
+ (select format('rules=%s, user_triggers=%s',
249
+ (select string_agg(r.rulename || ':' || r.ev_type::text, ',' order by r.rulename)
250
+ from pg_rewrite r where r.ev_class = 'agent_board.v_ownerless_claim_quota'::regclass),
251
+ (select count(*) from pg_trigger t
252
+ where t.tgrelid = 'agent_board.v_ownerless_claim_quota'::regclass
253
+ and not t.tgisinternal)::text)),
254
+ 'rules=_RETURN:1, user_triggers=0',
255
+ 'CPR-5 EXEMPTION (positive): the census resolver is SELECT-only — its only rewrite rule is _RETURN AND it carries no INSTEAD OF trigger, the other way a view accepts writes; so a stale policy read there yields a report line and can never yield a lease');
256
+
257
+ -- ===========================================================================
258
+ -- THE SOLE-WRITER PROOF. #406 argued it from has_table_privilege + zero triggers, which does not
259
+ -- exclude column-level grants, rules, other definer routines, or dynamic SQL. Four bounds, each
260
+ -- asserted, because an advisory lock only excludes callers that also take it.
261
+ -- ===========================================================================
262
+
263
+ -- CPR-6. Source bound.
264
+ select is(
265
+ (select coalesce(string_agg(p.proname, ', ' order by p.proname), '')
266
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
267
+ where n.nspname not in ('pg_catalog', 'information_schema', 'agent_board_test')
268
+ and n.nspname !~ '^pg_'
269
+ and p.prolang <> (select oid from pg_language where lanname = 'internal')
270
+ and agent_board_test.stripped_body(p.oid, true)
271
+ ~ (agent_board_test.stmt_re('update|merge into|insert into', 'boards')
272
+ || '[^;]*' || agent_board_test.col_frag('claim_policy')
273
+ -- COPY, parsed by POSITION rather than by token. Three things this gets right that a
274
+ -- verb-plus-token test did not:
275
+ -- * `copy boards from …` with NO column list writes EVERY column, claim_policy
276
+ -- included, and contains no claim_policy token — the widest form of the verb.
277
+ -- * the direction word must follow the relation and its optional column list, so
278
+ -- `copy boards to '/tmp/from.csv'` cannot match on a PATH containing "from".
279
+ -- (Literals are kept by this scan, so the path really is in the text.)
280
+ -- * a column list that does NOT name claim_policy (`copy boards (slug) from …`)
281
+ -- writes only that column and is correctly NOT a member: the optional group fails
282
+ -- to match it, and the direction word is then not where the pattern requires.
283
+ || '|' || agent_board_test.ci_re('copy') || '\s+(?:' || agent_board_test.ci_re('only') || '\s+)?'
284
+ || agent_board_test.qname_frag('boards')
285
+ || '\s*(?:\([^)]*' || agent_board_test.col_frag('claim_policy') || '[^)]*\)\s*)?'
286
+ || agent_board_test.ci_re('from') || '(?![A-Za-z0-9_])'
287
+ -- INSERT WITH NO COLUMN LIST, the same shape as COPY without one and a PRE-EXISTING
288
+ -- hole rather than one this change introduced: `insert into boards values (…)` and
289
+ -- `insert into boards select …` write EVERY column, claim_policy included, and carry
290
+ -- no claim_policy token. COPY was special-cased and INSERT was not.
291
+ -- The INSERT-without-column-list rule is a NAMED OBJECT, embedded VERBATIM — the frag,
292
+ -- not `replace(re, '(?c)', '')`. CPR-0's rows compile this same frag, so row and
293
+ -- production are one object rather than two strings that agree today.
294
+ || '|' || agent_board_test.insert_all_columns_frag('boards')
295
+ || '|\m' || agent_board_test.ci_re('select') || '(?![A-Za-z0-9_])[^;]*\m'
296
+ || agent_board_test.ci_re('into') || '(?![A-Za-z0-9_])[^;]*'
297
+ || agent_board_test.qname_frag('boards') || '[^;]*' || agent_board_test.col_frag('claim_policy'))),
298
+ 'set_board_claim_policy',
299
+ 'CPR-6 SOLE WRITER (source): across EVERY non-system schema, exactly one function has a statement targeting boards that mentions claim_policy — a STATEMENT test rather than an enumeration of assignment spellings, so the multi-column form `set (slug, claim_policy) = (…)` lands here too, and a SECURITY DEFINER writer in another schema cannot bypass the premise by living outside agent_board');
300
+
301
+ -- CPR-7. Column-privilege bound — the one #406's table-level check cannot give. A grant of UPDATE
302
+ -- on `claim_policy` alone to `authenticated` passes `has_table_privilege(..., 'UPDATE')=false` at
303
+ -- table level in neither direction reliably, and would be invisible to CPR-6 entirely.
304
+ select is(
305
+ (select coalesce(string_agg(distinct grantee, ','), '<none>')
306
+ from information_schema.column_privileges
307
+ where table_schema = 'agent_board' and table_name = 'boards' and privilege_type = 'UPDATE'
308
+ and grantee <> (select relowner::regrole::text from pg_class where oid = 'agent_board.boards'::regclass)),
309
+ '<none>',
310
+ 'CPR-7 SOLE WRITER (column privileges): no non-owner grantee holds UPDATE on ANY column of agent_board.boards, claim_policy included');
311
+
312
+ -- CPR-8. Rule and trigger bound.
313
+ select is(
314
+ format('rules=%s, user_triggers=%s, anon=%s, authenticated=%s, service_role=%s',
315
+ (select count(*) from pg_rewrite where ev_class = 'agent_board.boards'::regclass)::text,
316
+ (select count(*) from pg_trigger where tgrelid = 'agent_board.boards'::regclass and not tgisinternal)::text,
317
+ has_table_privilege('anon', 'agent_board.boards', 'update')::text,
318
+ has_table_privilege('authenticated', 'agent_board.boards', 'update')::text,
319
+ has_table_privilege('service_role', 'agent_board.boards', 'update')::text),
320
+ 'rules=0, user_triggers=0, anon=false, authenticated=false, service_role=false',
321
+ 'CPR-8 SOLE WRITER (rules/triggers/table privileges): boards carries no rewrite rule and no user trigger, and none of the three PostgREST roles holds table-level UPDATE — the general bound over ALL grantees is CPR-7, whose information_schema source expands a table-level grant per column and so sees a grant to any role, not just these three');
322
+
323
+ -- CPR-9. Dynamic-SQL bound. A `set claim_policy` assembled at run time is invisible to CPR-6, so
324
+ -- the population that COULD assemble one is bounded instead, and each member is cleared
325
+ -- positively: get_bundle_compatibility contains no write at all, and mark_retention_metadata's
326
+ -- table allowlist is a literal that does not contain `boards` (it only ever assigns
327
+ -- metadata/payload). Literals are KEPT by this scan, which is what makes the allowlist visible.
328
+ select is(
329
+ (select string_agg(p.proname || ':mentions_boards=' ||
330
+ (agent_board_test.stripped_body(p.oid, true) ~ agent_board_test.qname_re('boards'))::text ||
331
+ '/mentions_threads=' ||
332
+ (agent_board_test.stripped_body(p.oid, true) ~ agent_board_test.qname_re('threads'))::text ||
333
+ '/mentions_claims=' ||
334
+ (agent_board_test.stripped_body(p.oid, true) ~ agent_board_test.qname_re('claims'))::text, ', ' order by p.proname)
335
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
336
+ where n.nspname = 'agent_board'
337
+ and agent_board_test.stripped_body(p.oid, true) ~* '\mexecute\M'),
338
+ 'get_bundle_compatibility:mentions_boards=false/mentions_threads=false/mentions_claims=false, mark_retention_metadata:mentions_boards=false/mentions_threads=false/mentions_claims=false',
339
+ 'CPR-9 SOLE WRITER (EXECUTE token): the functions in agent_board that contain an `execute` token are these and neither names boards, threads or claims — a third, or either one gaining any of the three, fails here. The range is that token, not dynamic SQL in general: a statement assembled and run some other way is outside it. The claims field is what makes a dynamic claims writer visible, since CPR-14a strips literals and cannot see one');
340
+
341
+ -- CPR-10. The writer's ROW lock is still `for no key update` and not `for update`. This is not
342
+ -- cosmetic: `for update` conflicts with the FOR KEY SHARE an FK reference to boards takes, which
343
+ -- turns "hold KEY SHARE, then want the advisory lock" into a real cycle.
344
+ -- claimpolicy_writer_keyshare.spec MEASURES both directions; this makes the source change visible
345
+ -- too, so the two fail together rather than one silently drifting.
346
+ select is(
347
+ (select format('for_no_key_update=%s, for_update=%s',
348
+ (agent_board_test.stripped_body(p.oid, true) ~ 'for\s+no\s+key\s+update')::text,
349
+ (agent_board_test.stripped_body(p.oid, true) ~ 'for\s+update')::text)
350
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
351
+ where p.oid = to_regprocedure('agent_board.set_board_claim_policy(uuid,text)')),
352
+ 'for_no_key_update=true, for_update=false',
353
+ 'CPR-10 LOCK ORDER: set_board_claim_policy row-locks boards with FOR NO KEY UPDATE, which does not conflict with the FK''s FOR KEY SHARE — measured both ways in claimpolicy_writer_keyshare.spec');
354
+
355
+ -- ===========================================================================
356
+ -- THE OTHER PREMISE: WHICH BOARD. Both write paths lock the board they derived from the claim's
357
+ -- thread, so `threads.board_id` must not move under them.
358
+ -- ===========================================================================
359
+
360
+ -- CPR-11. The writer set for UPDATEs of agent_board.threads.
361
+ --
362
+ -- THE SCHEMA QUALIFIER IS OPTIONAL IN THE PATTERN, DELIBERATELY. This scan used to require the
363
+ -- literal `agent_board.threads`, which made it blind to `update threads` resolved through a
364
+ -- function's own `search_path` — the identical write, spelled one way the regex did not admit.
365
+ -- Measured, not theorised: injecting a function with `set search_path to agent_board` and a bare
366
+ -- `update threads set board_id = …` left this assertion GREEN. The qualifier is now an optional
367
+ -- group, so both spellings land in the same population.
368
+ select is(
369
+ (select coalesce(string_agg(p.proname, ', ' order by p.proname), '')
370
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
371
+ where n.nspname = 'agent_board'
372
+ and agent_board_test.stripped_body(p.oid, true) ~ agent_board_test.stmt_re('update', 'threads')),
373
+ '_next_thread_message_sequence, _upsert_delivery_recipient',
374
+ 'CPR-11 BOARD IMMUTABILITY: these are the functions that UPDATE agent_board.threads, qualified or not — a third has to be decided against this premise rather than inheriting it');
375
+
376
+ -- CPR-12. THE POPULATION IS A STATEMENT TEST, NOT A SPELLING LIST. It matched `board_id =` after an
377
+ -- UPDATE or INSERT, which enumerated two spellings of assignment and called that the class. Three
378
+ -- valid forms were unmatched: the multi-column form `set (board_id, …) = (…)`, a
379
+ -- `MERGE … WHEN MATCHED THEN UPDATE SET board_id`, and a plain INSERT naming board_id in its COLUMN
380
+ -- LIST rather than an assignment. The population is now "a statement targeting threads that mentions
381
+ -- board_id at all", which admits every spelling of assignment including ones nobody has thought of;
382
+ -- it is deliberately WIDER than assignment (an UPDATE merely reading board_id in its WHERE lands
383
+ -- here too), because a false member is labelled and must be decided about, while a missed one is
384
+ -- invisible. Measured: the widening leaves today's population unchanged.
385
+ --
386
+ -- CPR-12. And the only one that assigns `board_id` does it FIRST-BOARD-WINS. That guard is what
387
+ -- makes the transition null -> board and never board -> board, which is why locking the board
388
+ -- derived before the wait is locking the right board. The one drift it leaves — a null board
389
+ -- STAMPED during the wait — is handled by resolving policy from the locked id rather than by
390
+ -- re-walking the thread; CPR-20 pins the constitutive 'inherit' that stamp would have to displace.
391
+ select is(
392
+ (select coalesce(string_agg(p.proname || '[' ||
393
+ (case when agent_board_test.stripped_body(p.oid, true)
394
+ ~ 'update\s+agent_board\.threads\s+t\s+set\s+board_id\s*=\s*p_board_id\s+where\s+t\.board_id\s+is\s+null'
395
+ then 'first-board-wins' else 'UNGUARDED' end) || ']', ', ' order by p.proname), '<none>')
396
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
397
+ where n.nspname not in ('pg_catalog', 'information_schema', 'agent_board_test')
398
+ and n.nspname !~ '^pg_'
399
+ and p.prolang <> (select oid from pg_language where lanname = 'internal')
400
+ and agent_board_test.stripped_body(p.oid, true)
401
+ ~ (agent_board_test.stmt_re('update|merge into|insert into', 'threads') || '[^;]*' || agent_board_test.col_frag('board_id'))),
402
+ '_upsert_delivery_recipient[first-board-wins]',
403
+ 'CPR-12 BOARD IMMUTABILITY: the sole writer of threads.board_id stamps only where it is null (OBSGATE first-board-wins), so a board can be gained but never moved');
404
+
405
+ -- CPR-13. And no session can do it directly.
406
+ select is(
407
+ format('anon=%s, authenticated=%s, service_role=%s',
408
+ has_table_privilege('anon', 'agent_board.threads', 'update')::text,
409
+ has_table_privilege('authenticated', 'agent_board.threads', 'update')::text,
410
+ has_table_privilege('service_role', 'agent_board.threads', 'update')::text),
411
+ 'anon=false, authenticated=false, service_role=false',
412
+ 'CPR-13 BOARD IMMUTABILITY: none of the PostgREST roles holds table-level UPDATE on agent_board.threads — the bound over ALL grantees is CPR-13a, so board_id is reachable only through the definer function CPR-12 clears');
413
+
414
+ -- ---------------------------------------------------------------------------
415
+ -- CPR-13a/b/c. THE AXIS PARITY THIS PREMISE WAS MISSING.
416
+ --
417
+ -- `boards.claim_policy` is bounded on five axes (CPR-6 source, CPR-7 column privileges, CPR-8
418
+ -- rules/triggers/table privileges, CPR-9 dynamic SQL, CPR-10 lock strength). `threads.board_id`
419
+ -- had three, and the two premises carry the SAME weight: the fix locks the board id observed
420
+ -- before the wait, so `null -> board` is safe (a mid-wait stamp reads as the enforcing 'inherit')
421
+ -- but `board -> board` would let a caller lock board X and skip as 'advisory' while the claim now
422
+ -- sits on enforcing board Y.
423
+ --
424
+ -- The gap was MEASURED, not inferred. Against the pre-#408 file all three of these violations left
425
+ -- the suite 26/26 GREEN: a column-level `grant update (board_id) on threads to authenticated`, a
426
+ -- new unqualified unguarded `update threads set board_id` writer, and a trigger on threads. The
427
+ -- identical column grant on `boards` went red on CPR-7 — that control is what shows the asymmetry
428
+ -- was in the assertions rather than in the code, which measured clean on every axis.
429
+ -- ---------------------------------------------------------------------------
430
+
431
+ -- CPR-13a. The column-privilege bound. THIS IS THE ONE CPR-13 CANNOT GIVE: after
432
+ -- `grant update (board_id) on agent_board.threads to authenticated`,
433
+ -- `has_table_privilege(...,'UPDATE')` is still FALSE while `has_column_privilege(...)` is TRUE, so
434
+ -- CPR-13 stays green while a session really can move the column. Mirrors CPR-7 for boards.
435
+ select is(
436
+ (select coalesce(string_agg(distinct grantee, ','), '<none>')
437
+ from information_schema.column_privileges
438
+ where table_schema = 'agent_board' and table_name = 'threads' and privilege_type = 'UPDATE'
439
+ and grantee <> (select relowner::regrole::text from pg_class where oid = 'agent_board.threads'::regclass)),
440
+ '<none>',
441
+ 'CPR-13a BOARD IMMUTABILITY (column privileges): no non-owner grantee holds UPDATE on ANY column of agent_board.threads, board_id included — the bound CPR-13''s table-level test cannot express');
442
+
443
+ -- CPR-13b. The rule and trigger bound. A `before update` trigger on threads rewrites board_id
444
+ -- underneath the definer function CPR-12 clears, and an `on update do instead` rule redirects the
445
+ -- write entirely — neither is visible to a source scan of the function bodies. Mirrors CPR-8.
446
+ select is(
447
+ format('rules=%s, user_triggers=%s',
448
+ (select count(*) from pg_rewrite where ev_class = 'agent_board.threads'::regclass)::text,
449
+ (select count(*) from pg_trigger where tgrelid = 'agent_board.threads'::regclass and not tgisinternal)::text),
450
+ 'rules=0, user_triggers=0',
451
+ 'CPR-13b BOARD IMMUTABILITY (rules/triggers): threads carries no rewrite rule and no user trigger, so nothing rewrites board_id beneath the first-board-wins guard CPR-12 pins');
452
+
453
+ -- CPR-13c. The dynamic-SQL bound is CPR-9's, EXTENDED IN PLACE rather than duplicated here: its
454
+ -- format string now carries `mentions_threads` beside `mentions_boards` for each of the two
455
+ -- EXECUTE-bearing functions. A sibling assertion would have restated CPR-9's "exactly two
456
+ -- functions contain an EXECUTE" population claim in a second place, and two copies of one
457
+ -- exact-set claim drift. What remains for this slot is the bound CPR-9 does not give: that
458
+ -- `threads` is not reachable from a session by any privilege path at all, column grants included,
459
+ -- which CPR-13 + CPR-13a give jointly — asserted here as one positive statement rather than left
460
+ -- to be composed by a reader.
461
+ -- CPR-13c REMOVED (#405 completeness review). It asserted the CONJUNCTION of CPR-13 (table-level
462
+ -- UPDATE for the three PostgREST roles) and CPR-13a (non-owner column grants) and detected nothing
463
+ -- either of them missed. Confirmed by mutation before removal, both directions: a TABLE grant trips
464
+ -- CPR-13 and CPR-13a; a COLUMN grant trips CPR-13a. Two assertions pinning one property is how a
465
+ -- later editor deletes the wrong one, so the conjunction went and the two bounds stayed.
466
+
467
+
468
+ -- ===========================================================================
469
+ -- LOCK ORDER (b): the claim-row -> board-lock inversion, bounded by PRIVILEGE rather than by
470
+ -- convention. Every path here takes the board lock before touching a claims row; the inverse
471
+ -- holder would be a session sitting on a claims row lock and then entering a policy path.
472
+ -- ===========================================================================
473
+
474
+ -- CPR-14. No function takes one WITH AN EXPLICIT LOCKING CLAUSE — which is what this query asks,
475
+ -- and all it asks. The description used to say "no function takes a row lock on claims", and that
476
+ -- sentence is FALSE: `update agent_board.claims` takes row locks too, and five functions do it
477
+ -- (acquire_claim, renew_claim, set_board_claim_policy, reclaim_fleet_machine, release_claim). The
478
+ -- premise that actually holds is the ORDERED one, and it is CPR-14a's, not this one's: of those
479
+ -- five, only the three that also take the board lock reach it at all, and all three take the board
480
+ -- lock FIRST. This assertion is still worth having — an explicit `for update` on claims is the one
481
+ -- spelling that could appear anywhere in a body, including before the board lock — but a
482
+ -- description claiming the whole class was quoted as a premise by
483
+ -- claimpolicy_claimrow_lock_order.spec, which is how one false sentence became two.
484
+ select is(
485
+ (select coalesce(string_agg(p.proname, ', ' order by p.proname), '<none>')
486
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
487
+ where n.nspname = 'agent_board'
488
+ and agent_board_test.stripped_body(p.oid, true) ~ (agent_board_test.qname_re('claims') || '[^;]*\m' || agent_board_test.ci_re('for') || '\s+(?:' || agent_board_test.ci_re('no key') || '\s+)?(?:' || agent_board_test.ci_re('update') || '|' || agent_board_test.ci_re('share') || '|' || agent_board_test.ci_re('key share') || ')(?![A-Za-z0-9_])')),
489
+ '<none>',
490
+ 'CPR-14 LOCK ORDER: no function in agent_board takes a claims row lock through an EXPLICIT locking clause (for update / for no key update / for share / for key share) — the spelling that could sit anywhere in a body; the UPDATE-shaped row locks, which CPR-14a enumerates and orders, are not this assertion''s subject');
491
+
492
+ -- CPR-14a. THE PREMISE CPR-14'S DESCRIPTION USED TO CLAIM, ASSERTED AS FAR AS A SOURCE SCAN CAN.
493
+ -- A row lock on claims is taken by `update agent_board.claims` just as surely as by `for update`,
494
+ -- so the question that decides the cycle is not "does anything take one" but "does anything take
495
+ -- one and THEN want the board lock".
496
+ --
497
+ -- WHAT THIS IS: A REGRESSION TRIPWIRE, not an execution-order proof. It compares FIRST-OCCURRENCE
498
+ -- SOURCE POSITIONS in the stripped body. That is genuinely weaker than execution order and the
499
+ -- gaps are real: a branch that takes the row lock only on one path, a loop, or a CALLEE that
500
+ -- performs the UPDATE would all evade it. A source scan has a floor and this is it. What the
501
+ -- tripwire does deliver is worth having on its own: it fails CLOSED on a new claims writer (the
502
+ -- population is catalog-derived, so an addition changes the string and names itself) and it
503
+ -- relabels a same-body swap INVERTED — verified by moving renew_claim's own lock call after its
504
+ -- UPDATE, which produced `renew_claim[INVERTED]`.
505
+ --
506
+ -- THE ACTUAL EXECUTION-ORDER EVIDENCE IS ELSEWHERE, and this comment exists so nobody mistakes
507
+ -- this assertion for it: a pg_proc call-closure measurement over the applied bundle established
508
+ -- the two sets and their intersection — every function that row-locks claims, every function whose
509
+ -- CALL CLOSURE (not just direct body) reaches _claimgate_lock_board_policy, and the order in each
510
+ -- of the three members of the intersection. reclaim_fleet_machine and release_claim row-lock claims
511
+ -- and never reach the helper by any path; acquire_claim, renew_claim and set_board_claim_policy
512
+ -- take the board lock first. That measurement also confirmed there are no triggers on claims and no
513
+ -- FK referencing it, so no lock event is added by a path the call graph cannot see.
514
+ --
515
+ -- THE POPULATION AND THE COMPARISON ARE THE SAME EXPRESSION, which they were not at first: the
516
+ -- population admitted DELETE, MERGE and explicit locking clauses while the ordering comparison
517
+ -- searched only for UPDATE, so a DELETE-shaped row lock followed by the board lock was labelled
518
+ -- `board-lock-first`. Both now come from `row_pos`, and membership IS `row_pos` having been found.
519
+ --
520
+ -- UNIFYING THEM MOVED THE FAILURE MODE, which is why the shape list then had to be re-derived
521
+ -- rather than trusted. While the two disagreed, an unmatched shape was MISLABELLED; now an
522
+ -- unmatched shape falls out of the census ENTIRELY and is not flagged at all. `INSERT … ON
523
+ -- CONFLICT … DO UPDATE` locks and updates an existing row and matched NEITHER regex, so a function
524
+ -- using it and then taking the board lock would simply have vanished from the population while the
525
+ -- description claimed every new writer names itself. It is the third arm of `row_pos` now.
526
+ --
527
+ -- THE ARMS ARE A LIST, AND CALLING THEM "DERIVED" WAS ITSELF AN OVERSTATEMENT. Three regexes and
528
+ -- a five-name expected string are a hand list however carefully chosen. What IS asserted is the
529
+ -- COMPLEMENT, by CPR-14b below: everything whose body mentions claims either lands in one of these
530
+ -- arms' census or carries no write verb at all. So an unlisted write shape does not vanish — it
531
+ -- fails CPR-14b and names itself. That is the property the word "derived" was reaching for, and it
532
+ -- needed an assertion rather than a sentence.
533
+ --
534
+ -- THE PLAIN-INSERT ARM IS DELIBERATELY WIDER THAN "ROW LOCK". An `INSERT` into claims takes no lock
535
+ -- on an EXISTING row, so on a strict reading it does not belong. It is here because the property
536
+ -- that matters is participation in a LOCK CYCLE, not the lock's taxonomy: every insert form can
537
+ -- block on `claims_active_resource_unique` against a concurrent inserter, and acquire_claim (board
538
+ -- lock, then that unique insert) is the other half of exactly that cycle. Excluding it was
539
+ -- justified last round on the grounds that acquire_claim is in the census anyway through its
540
+ -- UPDATE — true of TODAY's body only, asserted nowhere, and worthless against a NEW insert-only
541
+ -- writer that takes the board lock afterwards. Including the arm asserts it instead. It also
542
+ -- subsumes the upsert arm that replaced it: `least()` finds the INSERT at or before any
543
+ -- `ON CONFLICT`, so `DO UPDATE` and `DO NOTHING` both enter the census with no window to get wrong.
544
+ --
545
+ -- ONE SHAPE REMAINS OUT OF MODEL: a row lock taken by a CALLEE rather than in this body. The
546
+ -- tripwire is per-body by construction; the call-closure measurement recorded above is what covers
547
+ -- that direction.
548
+ --
549
+ -- NO CHARACTER-BOUNDED WINDOWS, and that is a correction rather than a preference. The upsert arm
550
+ -- was written as `claims … [^;]{0,250} … on conflict`, and the gap between them spans the column
551
+ -- list AND the VALUES list: measured at 337 characters on a realistic claims upsert, so the arm did
552
+ -- not match the shape it was named for and such a writer dropped out of the census silently — the
553
+ -- exact fail-open the arm was added to close. `[^;]` alone already confines a match to a single
554
+ -- statement (literals are stripped here, so no `;` can appear inside one), which is the containment
555
+ -- the bound was reaching for. The bound added nothing but a number to get wrong.
556
+ select is(
557
+ (with bodies as (
558
+ select p.proname, agent_board_test.stripped_body(p.oid) as body
559
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
560
+ where n.nspname = 'agent_board'
561
+ and p.prolang <> (select oid from pg_language where lanname = 'internal')
562
+ ), posed as (
563
+ select proname,
564
+ regexp_instr(body, agent_board_test.call_re('_claimgate_lock_board_policy'), 1, 1, 0) as lock_pos,
565
+ least(
566
+ coalesce(nullif(regexp_instr(body,
567
+ agent_board_test.stmt_re('update|delete from|merge into', 'claims'),
568
+ 1, 1, 0, 'i'), 0), 2147483647),
569
+ coalesce(nullif(regexp_instr(body,
570
+ agent_board_test.qname_re('claims') || '[^;]*\m' || agent_board_test.ci_re('for') || '\s+(?:' || agent_board_test.ci_re('no key') || '\s+)?(?:' || agent_board_test.ci_re('update') || '|' || agent_board_test.ci_re('share') || '|' || agent_board_test.ci_re('key share') || ')(?![A-Za-z0-9_])',
571
+ 1, 1, 0, 'i'), 0), 2147483647),
572
+ coalesce(nullif(regexp_instr(body,
573
+ agent_board_test.stmt_re('insert into', 'claims'),
574
+ 1, 1, 0, 'i'), 0), 2147483647)
575
+ ) as row_pos
576
+ from bodies
577
+ )
578
+ select coalesce(string_agg(proname || '[' ||
579
+ (case when lock_pos = 0 then 'no-board-lock'
580
+ when lock_pos < row_pos then 'board-lock-first'
581
+ else 'INVERTED' end) || ']', ', ' order by proname), '<none>')
582
+ from posed
583
+ where row_pos < 2147483647),
584
+ 'acquire_claim[board-lock-first], reclaim_fleet_machine[no-board-lock], release_claim[no-board-lock], renew_claim[board-lock-first], set_board_claim_policy[board-lock-first]',
585
+ 'CPR-14a LOCK ORDER (source-position tripwire): for every function whose body takes a claims row lock by one of the modelled shapes — UPDATE, DELETE, MERGE, ANY INSERT (upsert or plain), or an explicit locking clause — the board-lock helper is either absent from that body or appears EARLIER IN THE SOURCE TEXT than the shape; a regression tripwire on source position, not a proof of execution order, with the out-of-model shapes named in the comment above');
586
+
587
+ -- CPR-14b. THE COMPLEMENT OF CPR-14a'S CENSUS, which is what turns three hand-written regexes into
588
+ -- a bounded claim. CPR-14a can only order the functions its arms MATCH; a claims writer using a
589
+ -- shape no arm knows falls out of the census entirely and is ordered by nothing. This asserts that
590
+ -- the leftovers are all READERS: every function whose body mentions claims but which CPR-14a did
591
+ -- not census carries no write verb anywhere in its body.
592
+ --
593
+ -- Deliberately BLUNT — the verb scan is whole-body, not claims-scoped, so a leftover that writes
594
+ -- some OTHER table fails this too. That is the intended behaviour and not a rough edge: such a
595
+ -- function has to be looked at and decided about, rather than inheriting an exemption nobody chose.
596
+ --
597
+ -- KEYED BY OID, NOT BY proname, and that was a real hole rather than a tidy-up. The complement was
598
+ -- computed as `proname not in (censused)`, so if ONE overload of a name was censused, ANOTHER
599
+ -- overload of the same name using an unmodelled write shape was excluded from the complement —
600
+ -- ignored by CPR-14a because its shape is unmodelled, and by CPR-14b because its NAME was already
601
+ -- censused. It vanished from both, which is exactly what this assertion exists to prevent. The
602
+ -- members are rendered as full signatures so the failure message distinguishes overloads too.
603
+ --
604
+ -- WHAT THIS COMPLEMENT BOUNDS, and what it does not — because "complement of a hand list" reads
605
+ -- like a total bound and is not one. It bounds: any agent_board routine whose stripped body
606
+ -- contains the whole word `claims`, qualified or unqualified, that CPR-14a's arms did not census.
607
+ -- It does NOT bound:
608
+ -- * a table whose name merely CONTAINS claims — a partition like `claims_p1` is not `\mclaims\M`.
609
+ -- Checked rather than assumed: agent_board.claims is not partitioned and has no children today,
610
+ -- so this is a bound on future schema shape, not a live hole.
611
+ -- * a writer in ANOTHER SCHEMA — outside this file's subject entirely.
612
+ -- * dynamic SQL, whose claims mention lives in a string literal `stripped_body` removes here;
613
+ -- CPR-9's `mentions_claims` field is what covers that direction.
614
+ -- * verbs outside the scanned set (`COPY … TO/FROM`, `SELECT … INTO`), which are write paths this
615
+ -- regex does not name.
616
+ select is(
617
+ (with bodies as (
618
+ select p.oid, p.proname, agent_board_test.stripped_body(p.oid) as body
619
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
620
+ where n.nspname = 'agent_board'
621
+ and p.prolang <> (select oid from pg_language where lanname = 'internal')
622
+ ), mentioners as (
623
+ select oid, proname, body from bodies where body ~ agent_board_test.qname_re('claims')
624
+ ), censused as (
625
+ select oid from mentioners
626
+ where body ~ agent_board_test.stmt_re('update|delete from|merge into', 'claims')
627
+ or body ~ (agent_board_test.qname_re('claims') || '[^;]*\m' || agent_board_test.ci_re('for') || '\s+(?:' || agent_board_test.ci_re('no key') || '\s+)?(?:' || agent_board_test.ci_re('update') || '|' || agent_board_test.ci_re('share') || '|' || agent_board_test.ci_re('key share') || ')(?![A-Za-z0-9_])')
628
+ or body ~ agent_board_test.stmt_re('insert into', 'claims')
629
+ )
630
+ select coalesce(string_agg(m.oid::regprocedure::text || '[' ||
631
+ (case when m.body ~* '\m(update|delete|insert|merge|truncate|copy)\M|\mselect\M[^;]*\minto\M'
632
+ then 'HAS-WRITE-VERB' else 'no-write-verb' end) || ']', ', ' order by m.oid::regprocedure::text), '<none>')
633
+ from mentioners m
634
+ where m.oid not in (select oid from censused)),
635
+ '_agent_can_access_thread(uuid,uuid)[no-write-verb], _claimgate_active_ownerless_claims(uuid)[no-write-verb], _claimgate_promotion_blockers(text)[no-write-verb]',
636
+ 'CPR-14b LOCK ORDER (census complement): every function mentioning claims that CPR-14a did NOT census is a pure reader — so a claims writer using a shape no arm matches fails HERE instead of vanishing from both assertions');
637
+
638
+ -- ---------------------------------------------------------------------------
639
+ -- CPR-3a/3b/3c. THE PREMISE EVERYTHING ELSE RESTS ON, WHICH NOTHING PINNED.
640
+ --
641
+ -- "Policy is resolved from the board id that was LOCKED" is the load-bearing claim of this whole
642
+ -- change, and no assertion coupled the two. CPR-3 checks only that the lock call appears BEFORE the
643
+ -- resolver in source order — so `policy_of(board_of_thread(p_thread_id))` written after
644
+ -- `lock(v_board_id)` passes the entire inventory while re-walking the thread and reopening exactly
645
+ -- the null->board drift the lock exists to exclude. Position is not coupling.
646
+ --
647
+ -- WHY THERE IS NO ISOLATION PERMUTATION FOR THE null->board CASE, measured rather than assumed: the
648
+ -- helper takes NO lock when the board is null (`if p_board_id is not null`). On a boardless thread
649
+ -- there is therefore no wait point, nothing blocks, and no interleaving can be constructed at that
650
+ -- position — a permutation there would be a probe pointed at nothing. What actually guards the case
651
+ -- is structural: the resolver is called with the SAME identifier that was locked (CPR-3a), so a
652
+ -- board stamped mid-flight cannot be observed by this path at all, and CPR-18 pins that the absent
653
+ -- board resolves to the ENFORCING answer. Those two together are the guarantee; the spec suite
654
+ -- carries the cases that DO have a wait point.
655
+ -- ---------------------------------------------------------------------------
656
+
657
+ -- CPR-3a. The resolver's argument is the identifier that was locked — captured from each body and
658
+ -- compared, not inferred from ordering.
659
+ select is(
660
+ (select string_agg(x.proname || '=' ||
661
+ (case when x.locked is null then 'NO-LOCK-ARG'
662
+ when x.resolved is null then 'NO-RESOLVER-ARG'
663
+ when x.locked = x.resolved then 'same-var(' || x.locked || ')'
664
+ else 'DIFFERENT-VAR(' || x.locked || ' vs ' || x.resolved || ')' end),
665
+ ', ' order by x.proname)
666
+ from (select p.proname,
667
+ substring(agent_board_test.stripped_body(p.oid)
668
+ from '"?_claimgate_lock_board_policy"?\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)') as locked,
669
+ substring(agent_board_test.stripped_body(p.oid)
670
+ from '"?_claimgate_board_claim_policy_of"?\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)') as resolved
671
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
672
+ where n.nspname = 'agent_board'
673
+ and p.prolang <> (select oid from pg_language where lanname = 'internal')
674
+ and agent_board_test.stripped_body(p.oid) ~ agent_board_test.call_re('_claimgate_board_claim_policy_of')
675
+ and p.proname <> '_claimgate_board_claim_policy') x),
676
+ 'acquire_claim=same-var(v_board_id), renew_claim=same-var(v_board_id)',
677
+ 'CPR-3a COUPLING: each path resolves the policy from the SAME identifier it locked — a resolver called on a freshly re-walked board (policy_of(board_of_thread(…))) is named DIFFERENT-VAR here, which CPR-3''s source-order check cannot see');
678
+
679
+ -- CPR-3b. EXACTLY ONE OF EACH CALL, which is what makes CPR-3a's first-match capture SOUND rather
680
+ -- than merely usually right. `substring` returns the FIRST match, so a body with a valid first pair
681
+ -- followed by a second resolver call on a different variable —
682
+ -- `policy_of(v_fresh_board_id)` in a later branch — satisfies CPR-3, CPR-3a and a mere
683
+ -- "at least one bare identifier" check, while that later variable can observe exactly the stamp the
684
+ -- captured-id reasoning excludes. Counting closes it by construction: a second call of either kind
685
+ -- is a failure that names itself, and the fix is then to decide about the second call rather than to
686
+ -- out-regex it.
687
+ --
688
+ -- Measured before choosing this bound: both legitimate bodies contain exactly one of each, so the
689
+ -- assertion costs nothing today. A body that genuinely needs two must fail here and be argued for.
690
+ --
691
+ -- TWO LIMITS THIS CANNOT REACH, stated rather than implied, because a source scan has a floor:
692
+ -- * SHADOWING. Equal captured NAMES are not equal BINDINGS — a nested PL/pgSQL block may declare
693
+ -- its own `v_board_id`, and this comparison would read the two as the same variable. Nothing
694
+ -- here resolves scopes; what bounds it is that both bodies are single-block and CPR-3b's count
695
+ -- makes a second declaration-plus-call pattern fail.
696
+ -- * DOMINANCE. CPR-3c pins that the re-read appears between the lock and the decision in SOURCE
697
+ -- ORDER; it does not prove the re-read executes on every path that reaches the decision. A
698
+ -- conditional re-read would pass. That is an execution-order claim, and the isolation specs are
699
+ -- where execution order is demonstrated.
700
+ select is(
701
+ (select string_agg(x.proname || '=locks:' || x.locks || '/resolvers:' || x.resolvers
702
+ || '/bare:' || x.bare::text, ', ' order by x.proname)
703
+ from (select p.proname,
704
+ (select count(*) from regexp_matches(agent_board_test.stripped_body(p.oid),
705
+ agent_board_test.call_re('_claimgate_lock_board_policy'), 'g')) as locks,
706
+ (select count(*) from regexp_matches(agent_board_test.stripped_body(p.oid),
707
+ agent_board_test.call_re('_claimgate_board_claim_policy_of'), 'g')) as resolvers,
708
+ agent_board_test.stripped_body(p.oid)
709
+ ~* '"?_claimgate_board_claim_policy_of"?\s*\(\s*[A-Za-z_][A-Za-z0-9_]*\s*\)' as bare
710
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
711
+ where n.nspname = 'agent_board'
712
+ and p.prolang <> (select oid from pg_language where lanname = 'internal')
713
+ and agent_board_test.stripped_body(p.oid) ~ agent_board_test.call_re('_claimgate_board_claim_policy_of')
714
+ and p.proname <> '_claimgate_board_claim_policy') x),
715
+ 'acquire_claim=locks:1/resolvers:1/bare:true, renew_claim=locks:1/resolvers:1/bare:true',
716
+ 'CPR-3b COUPLING (capture soundness): each path takes the board lock ONCE and resolves the policy ONCE, with a bare identifier — so CPR-3a''s first-match capture cannot be defeated by a second resolver call on a different variable in a later branch');
717
+
718
+ -- CPR-3c. renew re-reads the CLAIM after the lock and before the decision. The re-read is what makes
719
+ -- renew's answer reflect the writer's committed state rather than its pre-wait snapshot; it is
720
+ -- cheaply visible in source and was pinned only by prose.
721
+ select ok(
722
+ (select regexp_instr(b.body, agent_board_test.call_re('_claimgate_lock_board_policy'), 1, 1, 0) > 0
723
+ -- SEARCHED FROM THE LOCK, not from the top: renew reads the claim BEFORE the lock too (to
724
+ -- derive the board from the token), so a first-occurrence search finds that earlier read and
725
+ -- would report the property false while it holds. Measured on this bundle: lock@1863,
726
+ -- pre-lock read@367, re-read@1940, resolver@2309.
727
+ and regexp_instr(b.body, 'select[^;]*from\s+agent_board\.claims\M',
728
+ regexp_instr(b.body, agent_board_test.call_re('_claimgate_lock_board_policy'), 1, 1, 0), 1, 0, 'i') > 0
729
+ and regexp_instr(b.body, 'select[^;]*from\s+agent_board\.claims\M',
730
+ regexp_instr(b.body, agent_board_test.call_re('_claimgate_lock_board_policy'), 1, 1, 0), 1, 0, 'i')
731
+ < regexp_instr(b.body, agent_board_test.call_re('_claimgate_board_claim_policy_of'), 1, 1, 0)
732
+ from pg_proc p
733
+ cross join lateral (select agent_board_test.stripped_body(p.oid) as body) b
734
+ where p.oid = to_regprocedure('agent_board.renew_claim(uuid,timestamptz)')),
735
+ 'CPR-3c RE-READ: renew_claim re-SELECTs the claim AFTER taking the board lock and BEFORE resolving the policy — the ordering that makes its decision reflect the committed state rather than its pre-wait snapshot');
736
+
737
+ -- ---------------------------------------------------------------------------
738
+ -- CPR-14c. THE CALLEE DIRECTION, WHICH BOTH PER-BODY ASSERTIONS MISS BY CONSTRUCTION.
739
+ --
740
+ -- CPR-14a and CPR-14b each read ONE body. A function that delegates its claims row lock to a callee
741
+ -- and then takes the board lock itself is invisible to both: its own body contains no row-lock
742
+ -- shape, so CPR-14a never censuses it and CPR-14b classifies it as a reader. That direction was
743
+ -- covered by a one-time call-closure measurement recorded in a comment — which is rule 5's exact
744
+ -- shape, a fact about the moment it was written with nothing re-checking it.
745
+ --
746
+ -- IT IS ONE HALF OF A UNION, and the reason is a regression the rework caused. The membership
747
+ -- version it replaced would have named a NON-row-locking root that delegates; this order version
748
+ -- excludes such a root only if its own body never mentions the board lock, so replacing rather than
749
+ -- adding closed one case and opened another. CPR-14d below is the membership half, kept alongside
750
+ -- with today's members enumerated so a WRAPPER appears there and has to be decided about VISIBLY
751
+ -- rather than being carved out by a predicate nobody re-reads.
752
+ --
753
+ -- AND FIRST SOURCE POSITION IS NOT EXECUTION ORDER, stated because this file rejects that inference
754
+ -- elsewhere and must not rely on it here: a body with an early lock in one branch and
755
+ -- `rowlock_callee(); lock();` in another passes, since the first textual lock precedes the callee
756
+ -- while that second branch executes claims-lock first. Branch semantics are beyond a source scan.
757
+ -- What this assertion covers is a body whose FIRST lock mention follows a row-locking call site;
758
+ -- what it does not cover is which of several mentions a given path reaches.
759
+ --
760
+ -- IT COMPARES ORDER, NOT MEMBERSHIP, and the first version did not — which was unsound in BOTH
761
+ -- directions. That version excluded any root that itself row-locks claims, so a function taking the
762
+ -- board lock, row-locking claims safely AFTER it, and DELEGATING another claims row lock BEFORE it
763
+ -- was omitted — the exact composed inversion this assertion exists to catch. And it admitted every
764
+ -- ordinary WRAPPER of acquire_claim, because membership in two closures says nothing about order,
765
+ -- so the exemption-resistance the narrowing was justified by was never bought.
766
+ --
767
+ -- The predicate is now: the caller's own body references the board lock, and it CALLS a callee whose
768
+ -- closure reaches a claims row-locker, and that call site precedes the lock. A wrapper has no board
769
+ -- lock in its own body and is excluded on that ground rather than by a membership carve-out; a
770
+ -- self-row-locking root is no longer excluded at all.
771
+ --
772
+ -- ITS OWN LIMIT, stated: fn_call_closure()'s edges are name matches over stripped bodies, so a
773
+ -- callee invoked through dynamic SQL is invisible to it. CPR-9's EXECUTE-token bound is what covers
774
+ -- that direction.
775
+ select is(
776
+ (with fns as (
777
+ select p.oid, p.proname, agent_board_test.stripped_body(p.oid) as body
778
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
779
+ where n.nspname = 'agent_board'
780
+ and p.prolang <> (select oid from pg_language where lanname = 'internal')
781
+ ), rowlockers as (
782
+ select oid from fns
783
+ where body ~ agent_board_test.stmt_re('update|delete from|merge into', 'claims')
784
+ or body ~ agent_board_test.stmt_re('insert into', 'claims')
785
+ or body ~ (agent_board_test.qname_re('claims') || '[^;]*\m' || agent_board_test.ci_re('for') || '\s+(?:' || agent_board_test.ci_re('no key') || '\s+)?(?:' || agent_board_test.ci_re('update') || '|' || agent_board_test.ci_re('share') || '|' || agent_board_test.ci_re('key share') || ')(?![A-Za-z0-9_])')
786
+ ), reaches_rowlock as (
787
+ select distinct c.root_oid as oid
788
+ from agent_board_test.fn_call_closure() c
789
+ where c.reached_oid in (select oid from rowlockers)
790
+ ), edges as (
791
+ select r.proname as caller, e.proname as callee,
792
+ regexp_instr(r.body, '\m' || e.proname || '\M\s*\(', 1, 1, 0, 'i') as call_pos,
793
+ regexp_instr(r.body, agent_board_test.call_re('_claimgate_lock_board_policy'), 1, 1, 0) as lock_pos
794
+ from fns r join fns e on e.oid <> r.oid
795
+ where e.oid in (select oid from reaches_rowlock)
796
+ and r.body ~ agent_board_test.call_re(e.proname)
797
+ and regexp_instr(r.body, agent_board_test.call_re('_claimgate_lock_board_policy'), 1, 1, 0) > 0
798
+ )
799
+ select coalesce(string_agg(distinct caller || '->' || callee, ', '
800
+ order by caller || '->' || callee), '<none>')
801
+ from edges
802
+ where call_pos > 0 and call_pos < lock_pos),
803
+ '<none>',
804
+ 'CPR-14c LOCK ORDER (callee direction): no function calls a claims row-locking callee BEFORE it takes the board lock — compared by the position of the CALL SITE against the lock in the caller''s own body, so the composed inversion neither per-body assertion can see is named here');
805
+
806
+ -- CPR-14d. THE MEMBERSHIP HALF OF THE UNION, restored rather than replaced. CPR-14c asks about
807
+ -- ORDER and therefore says nothing about a root whose own body never mentions the board lock; this
808
+ -- one asks only WHO reaches both, and enumerates them. A legitimate wrapper of acquire_claim will
809
+ -- appear here and fail — that is intended: the exemption is then a visible edit to this expected
810
+ -- string with a reason beside it, rather than a predicate carve-out that silently covers shapes
811
+ -- nobody considered. The two together catch both the delegated inversion and the composed one.
812
+ select is(
813
+ (with fns as (
814
+ select p.oid, p.proname, agent_board_test.stripped_body(p.oid) as body
815
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
816
+ where n.nspname = 'agent_board'
817
+ and p.prolang <> (select oid from pg_language where lanname = 'internal')
818
+ ), rowlockers as (
819
+ select oid from fns
820
+ where body ~ agent_board_test.stmt_re('update|delete from|merge into', 'claims')
821
+ or body ~ agent_board_test.stmt_re('insert into', 'claims')
822
+ or body ~ (agent_board_test.qname_re('claims') || '[^;]*\m' || agent_board_test.ci_re('for') || '\s+(?:' || agent_board_test.ci_re('no key') || '\s+)?(?:' || agent_board_test.ci_re('update') || '|' || agent_board_test.ci_re('share') || '|' || agent_board_test.ci_re('key share') || ')(?![A-Za-z0-9_])')
823
+ )
824
+ select coalesce(string_agg(distinct c.root_name, ', ' order by c.root_name), '<none>')
825
+ from agent_board_test.fn_call_closure() c
826
+ where c.reached_name = '_claimgate_lock_board_policy'
827
+ and c.root_oid in (select root_oid from agent_board_test.fn_call_closure()
828
+ where reached_oid in (select oid from rowlockers))),
829
+ 'acquire_claim, renew_claim, set_board_claim_policy',
830
+ 'CPR-14d LOCK ORDER (callee direction, membership half): these are the functions that reach BOTH a claims row-locker and the board lock through their call closures — a fourth, wrapper or composer alike, is named here and must be decided about rather than exempted by a predicate');
831
+
832
+ -- ---------------------------------------------------------------------------
833
+ -- CPR-0 / CPR-0a. THE MATCHERS' BEHAVIOUR TABLE, AND A GATE ON THE TABLE ITSELF.
834
+ --
835
+ -- WHY BOTH. The identifier matchers drifted four times across three review rounds, and every
836
+ -- version passed its accept cases. So a table was added. Then the SAME defect appeared INSIDE the
837
+ -- table three times: rows that pass for a reason unrelated to the rule they are named for — first
838
+ -- r06 (tested `$` through an adjacency that rejected it earlier), then a12/r13 (compiled a
839
+ -- reconstruction rather than the production object), then a13/a14/a15 (keyword branches that the
840
+ -- subject text could never exercise). Each was found by a different reviewer, after the previous
841
+ -- fix, by the same procedure: revert a rule and see which rows move.
842
+ --
843
+ -- THREE OCCURRENCES IS ENOUGH EVIDENCE THAT A HAND-COMPILED REVERT MAP CANNOT BE TRUSTED, because
844
+ -- compiling it is the same act as writing the rows. So the map is mechanical now: CPR-0a reverts
845
+ -- each rule in turn, re-evaluates every row, and requires at least one row to flip. A rule with no
846
+ -- flipping row FAILS AND NAMES ITSELF — which is what would have caught all three, and none of
847
+ -- which a human reading the table did.
848
+ --
849
+ -- The rows are DATA rather than inline expressions so both assertions evaluate the same list: a
850
+ -- row CPR-0 checks and CPR-0a cannot re-evaluate would be back to two lists that agree today.
851
+ -- ---------------------------------------------------------------------------
852
+ create temp table _mrow (label text primary key, expr text, want boolean) on commit drop;
853
+
854
+ insert into _mrow(label, expr, want) values
855
+ -- ACCEPTS: spellings PostgreSQL resolves to OUR object
856
+ ('a01-qualified', $x$'update agent_board.claims set x' ~ agent_board_test.stmt_re('update','claims')$x$, true),
857
+ ('a02-unqualified', $x$'update claims set x' ~ agent_board_test.stmt_re('update','claims')$x$, true),
858
+ ('a03-upper-unquoted', $x$'UPDATE AGENT_BOARD.CLAIMS SET x' ~ agent_board_test.stmt_re('update','claims')$x$, true),
859
+ ('a04-quoted-lower', $x$'update "agent_board"."claims" set x' ~ agent_board_test.stmt_re('update','claims')$x$, true),
860
+ ('a05-verb-newline', $x$'insert' || chr(10) || 'into claims v' ~ agent_board_test.stmt_re('insert into','claims')$x$, true),
861
+ ('a06-verb-2spaces', $x$'delete from claims where x' ~ agent_board_test.stmt_re('delete from','claims')$x$, true),
862
+ ('a07-verb-tab', $x$'merge' || chr(9) || 'into claims m' ~ agent_board_test.stmt_re('merge into','claims')$x$, true),
863
+ -- a08 gates ci_re's OWN space rule, which a05-a07 do NOT: they exercise whitespace inside a verb
864
+ -- through stmt_re, which would still work if ci_re emitted a literal space and stmt_re rewrote it.
865
+ ('a08-cire-phrase', $x$'no' || chr(10) || 'key' ~ ('(?c)' || agent_board_test.ci_re('no key'))$x$, true),
866
+ ('a09-inheritance', $x$'update agent_board.boards* set x' ~ agent_board_test.stmt_re('update','boards')$x$, true),
867
+ ('a10-call-space', $x$'perform _claimgate_lock_board_policy (v)' ~ agent_board_test.call_re('_claimgate_lock_board_policy')$x$, true),
868
+ ('a11-call-quoted', $x$'perform agent_board."_claimgate_lock_board_policy"(v)' ~ agent_board_test.call_re('_claimgate_lock_board_policy')$x$, true),
869
+ ('a12-col-upper', $x$'set CLAIM_POLICY = x' ~ agent_board_test.col_frag('claim_policy')$x$, true),
870
+ ('a13-ins-novals', $x$'insert into agent_board.boards values (1,2)' ~ agent_board_test.insert_all_columns_frag('boards')$x$, true),
871
+ ('a14-ins-select', $x$'insert into agent_board.boards select (1)' ~ agent_board_test.insert_all_columns_frag('boards')$x$, true),
872
+ -- a15/a16 cover two INSERT FORMS the rule must census. They do NOT gate a keyword exclusion, and
873
+ -- that is stated rather than implied: DEFAULT is only ever followed by VALUES and OVERRIDING by
874
+ -- SYSTEM/USER, so neither can be mistaken for an alias-before-`(`. The exclusions for them were
875
+ -- therefore DEAD CODE and have been removed rather than kept with rows that cannot gate them.
876
+ ('a15-ins-default', $x$'insert into agent_board.boards default values' ~ agent_board_test.insert_all_columns_frag('boards')$x$, true),
877
+ ('a16-ins-overriding', $x$'insert into agent_board.boards overriding user value values (1)' ~ agent_board_test.insert_all_columns_frag('boards')$x$, true),
878
+ ('a17-ins-re-eq-frag', $x$agent_board_test.insert_all_columns_re('boards') = '(?c)' || agent_board_test.insert_all_columns_frag('boards')$x$, true),
879
+ -- REJECTS: spellings PostgreSQL resolves ELSEWHERE
880
+ ('a18-verb-rel-spaces', $x$'update agent_board.claims set x' ~ agent_board_test.stmt_re('update','claims')$x$, true),
881
+ ('r01-other-schema', $x$'update other.claims set x' ~ agent_board_test.stmt_re('update','claims')$x$, false),
882
+ ('r02-dot-space', $x$'perform other . _claimgate_lock_board_policy(v)' ~ agent_board_test.call_re('_claimgate_lock_board_policy')$x$, false),
883
+ ('r03-quoted-upper', $x$'update "AGENT_BOARD"."claims" set x' ~ agent_board_test.stmt_re('update','claims')$x$, false),
884
+ ('r04-prefix-word', $x$'update dashboards set x' ~ agent_board_test.stmt_re('update','boards')$x$, false),
885
+ ('r05-dollar-lead', $x$'perform x$_claimgate_lock_board_policy(v)' ~ agent_board_test.call_re('_claimgate_lock_board_policy')$x$, false),
886
+ ('r06-dollar-rel', $x$'from x$claims c' ~ agent_board_test.qname_re('claims')$x$, false),
887
+ ('r07-nonascii-lead', $x$'perform ' || chr(233) || '_claimgate_lock_board_policy(v)' ~ agent_board_test.call_re('_claimgate_lock_board_policy')$x$, false),
888
+ ('r08-nonascii-trail', $x$'from claims' || chr(233) || ' c' ~ agent_board_test.qname_re('claims')$x$, false),
889
+ ('r09-col-nonascii', $x$'set claim_policy' || chr(233) || ' = x' ~ agent_board_test.col_frag('claim_policy')$x$, false),
890
+ ('r10-col-dollar', $x$'set x$claim_policy = x' ~ agent_board_test.col_frag('claim_policy')$x$, false),
891
+ ('r11-i-flag', $x$regexp_instr('x "AGENT_BOARD"."claims" y', agent_board_test.qname_re('claims'), 1, 1, 0, 'i') > 0$x$, false),
892
+ ('r12-ci-operator', $x$'x "AGENT_BOARD"."claims" y' ~* agent_board_test.qname_re('claims')$x$, false),
893
+ ('r13-ins-collist', $x$'insert into agent_board.boards (slug) values (1)' ~ agent_board_test.insert_all_columns_frag('boards')$x$, false),
894
+ ('r14-ins-alias', $x$'insert into agent_board.boards as b(slug) values (1)' ~ agent_board_test.insert_all_columns_frag('boards')$x$, false),
895
+ ('r16-call-i-flag', $x$regexp_instr('perform "AGENT_BOARD"."_claimgate_lock_board_policy"(v)', agent_board_test.call_re('_claimgate_lock_board_policy'), 1, 1, 0, 'i') > 0$x$, false),
896
+ ('r15-ins-qalias', $x$'insert into agent_board.boards as "b"(slug) values (1)' ~ agent_board_test.insert_all_columns_frag('boards')$x$, false);
897
+
898
+ create function pg_temp.mverdict(p_expr text) returns boolean
899
+ language plpgsql as $f$
900
+ declare v boolean;
901
+ begin
902
+ execute format('select (%s)', p_expr) into v;
903
+ return coalesce(v, false);
904
+ exception when others then
905
+ -- A row whose expression cannot compile is a FAILED row, not a skipped one: returning null here
906
+ -- would let a broken pattern read as agreement with a `false` expectation.
907
+ return not (select want from _mrow where expr = p_expr);
908
+ end $f$;
909
+
910
+ select is(
911
+ (select coalesce(string_agg(label || '=' || pg_temp.mverdict(expr)::text, ', ' order by label), '<none>')
912
+ from _mrow),
913
+ (select coalesce(string_agg(label || '=' || want::text, ', ' order by label), '<none>') from _mrow),
914
+ 'CPR-0 MATCHER BEHAVIOUR: over PLAIN and DOUBLE-QUOTED identifiers, the matchers accept the spellings PostgreSQL resolves to our object and reject those it resolves elsewhere — Unicode-escape identifiers (U&"...") are OUT OF SCOPE and unhandled, so this is not a claim about every spelling');
915
+
916
+ -- CPR-0a. EVERY RULE HAS A ROW THAT FAILS WITHOUT IT — compiled by the database, not by hand.
917
+ -- ONE BEHAVIOUR PER ENTRY, AND EACH NAMES THE ROW THAT MUST FLIP. Both are corrections.
918
+ --
919
+ -- THE SPLIT PAID FOR ITSELF ON ITS FIRST RUN, which is the answer to "why are these so finely
920
+ -- divided": separating the behaviours immediately exposed FOUR matcher rules the coarser hand list
921
+ -- had never reverted — ident_re's quoted alternative, qname_frag's OPTIONAL qualifier, call_re's own
922
+ -- director, and stmt_re's `\s+` between verb and relation. Two of them had no row at all (a18, r16
923
+ -- were added for them). A reviewer tempted to recombine entries should read that as the cost.
924
+ --
925
+ -- NO DESCRIPTION CARRIES A COUNT, and the rule that replaced is worth recording because it was
926
+ -- WRONG in a way that took a reviewer to see. It said a count is "safe" when it sits beside its own
927
+ -- assertion's expected value, since the two would fail together. They do not. PROXIMITY IS NOT
928
+ -- DERIVATION: nothing computes the count from the expected value, so the assertion fails on drift
929
+ -- only until a maintainer updates the expected value — which is the normal maintenance path — and
930
+ -- from that moment the count is silently wrong with nothing failing.
931
+ --
932
+ -- Measured rather than argued: add a function to agent_board_test, and CPR-0b fails; update its
933
+ -- expected value the way a maintainer would, and it passes again while any count in its prose is now
934
+ -- false. So the "safe" category did not exist. There are only counts that are easier to notice and
935
+ -- counts that are harder, and the durable move is the one this package already made when its runner
936
+ -- dropped "28 tests" and "42-migration union": delete them. The expected values enumerate every
937
+ -- member by name; a reader who wants a number can count one that cannot drift.
938
+ --
939
+ -- THE RECURSION IS THE POINT, not an aside: the rule written to prevent stale counts made a claim
940
+ -- broader than its own mechanism supported — the same shape as every finding in this review, in the
941
+ -- sentence codifying the lesson from the previous one.
942
+ --
943
+ -- IN THESE TWO ASSERTIONS the expected values enumerate every entry by name, so no count appears in
944
+ -- either description: it would add nothing a reader cannot see and one more thing that can go stale.
945
+ --
946
+ -- COMPOUND ENTRIES LET A COVERED BRANCH CONCEAL AN UNCOVERED SIBLING: `keyword exclusions` reverted
947
+ -- VALUES and SELECT together, and `lead_guard` reverted its dot-whitespace and non-ASCII guards
948
+ -- together, so deleting one row still left the rule reporting as gated through the other. That is
949
+ -- the r06 defect at the RULE level — the map certifying a rule it only half tests.
950
+ --
951
+ -- AND "SOME ROW FLIPPED" IS NOT "THE ROW FOR THIS RULE FLIPPED". The gate now requires the NAMED
952
+ -- row, so a rule cannot borrow coverage from an unrelated one.
953
+ create temp table _mrule (rule_name text primary key, fn text, expect_row text, revert_sql text) on commit drop;
954
+
955
+ insert into _mrule(rule_name, fn, expect_row, revert_sql) values
956
+ ('ci_re: a space becomes \s+', 'agent_board_test.ci_re(text)', 'a08-cire-phrase', $r$
957
+ create or replace function agent_board_test.ci_re(p_text text) returns text
958
+ language sql immutable set search_path to pg_catalog as $b$
959
+ select coalesce(string_agg(case when c ~ '[A-Za-z]' then '[' || upper(c) || lower(c) || ']' else c end, ''), '')
960
+ from regexp_split_to_table(p_text, '') as c $b$; $r$),
961
+ ('ident_chars: $ is an identifier character', 'agent_board_test.ident_chars()', 'r05-dollar-lead', $r$
962
+ create or replace function agent_board_test.ident_chars() returns text
963
+ language sql immutable set search_path to pg_catalog as $b$ select 'A-Za-z0-9_' $b$; $r$),
964
+ ('ident_re: the quoted alternative', 'agent_board_test.ident_re(text)', 'a04-quoted-lower', $r$
965
+ create or replace function agent_board_test.ident_re(p_name text) returns text
966
+ language sql immutable set search_path to pg_catalog as $b$
967
+ select '(?:' || agent_board_test.ci_re(p_name) || ')' $b$; $r$),
968
+ ('lead_guard: reject a dot with whitespace', 'agent_board_test.lead_guard()', 'r02-dot-space', $r$
969
+ create or replace function agent_board_test.lead_guard() returns text
970
+ language sql immutable set search_path to pg_catalog as $b$
971
+ select '(?<![' || agent_board_test.ident_chars() || '."])(?<![^\x00-\x7F])' $b$; $r$),
972
+ ('lead_guard: reject a leading non-ASCII letter', 'agent_board_test.lead_guard()', 'r07-nonascii-lead', $r$
973
+ create or replace function agent_board_test.lead_guard() returns text
974
+ language sql immutable set search_path to pg_catalog as $b$
975
+ select '(?<![' || agent_board_test.ident_chars() || '."])(?<!\.\s*)' $b$; $r$),
976
+ ('trail_guard: reject a trailing non-ASCII letter', 'agent_board_test.trail_guard()', 'r08-nonascii-trail', $r$
977
+ create or replace function agent_board_test.trail_guard() returns text
978
+ language sql immutable set search_path to pg_catalog as $b$
979
+ select '(?![' || agent_board_test.ident_chars() || '])' $b$; $r$),
980
+ ('qname_frag: the qualifier is OPTIONAL', 'agent_board_test.qname_frag(text)', 'a02-unqualified', $r$
981
+ create or replace function agent_board_test.qname_frag(p_name text) returns text
982
+ language sql immutable set search_path to pg_catalog as $b$
983
+ select agent_board_test.lead_guard() || agent_board_test.ident_re('agent_board')
984
+ || '\s*\.\s*' || agent_board_test.ident_re(p_name) || agent_board_test.trail_guard() $b$; $r$),
985
+ ('qname_re: the (?c) director', 'agent_board_test.qname_re(text)', 'r11-i-flag', $r$
986
+ create or replace function agent_board_test.qname_re(p_name text) returns text
987
+ language sql immutable set search_path to pg_catalog as $b$
988
+ select agent_board_test.qname_frag(p_name) $b$; $r$),
989
+ ('call_frag: whitespace may precede the paren', 'agent_board_test.call_frag(text)', 'a10-call-space', $r$
990
+ create or replace function agent_board_test.call_frag(p_name text) returns text
991
+ language sql immutable set search_path to pg_catalog as $b$
992
+ select agent_board_test.lead_guard() || '(?:' || agent_board_test.ident_re('agent_board')
993
+ || '\s*\.\s*)?' || agent_board_test.ident_re(p_name) || '\(' $b$; $r$),
994
+ ('call_re: the (?c) director', 'agent_board_test.call_re(text)', 'r16-call-i-flag', $r$
995
+ create or replace function agent_board_test.call_re(p_name text) returns text
996
+ language sql immutable set search_path to pg_catalog as $b$
997
+ select agent_board_test.call_frag(p_name) $b$; $r$),
998
+ ('col_frag: the column token carries identifier rules', 'agent_board_test.col_frag(text)', 'a12-col-upper', $r$
999
+ create or replace function agent_board_test.col_frag(p_name text) returns text
1000
+ language sql immutable set search_path to pg_catalog as $b$
1001
+ select agent_board_test.lead_guard() || p_name || agent_board_test.trail_guard() $b$; $r$),
1002
+ ('stmt_re: \s+ between the verb and the relation', 'agent_board_test.stmt_re(text,text)', 'a18-verb-rel-spaces', $r$
1003
+ create or replace function agent_board_test.stmt_re(p_verbs text, p_rel text) returns text
1004
+ language sql immutable set search_path to pg_catalog as $b$
1005
+ select '(?c)(?:' || (select string_agg(agent_board_test.ci_re(v), '|')
1006
+ from regexp_split_to_table(p_verbs, '\|') as v)
1007
+ || ') (?:' || agent_board_test.ci_re('only') || '\s+)?' || agent_board_test.qname_frag(p_rel) $b$; $r$),
1008
+ ('insert rule: the VALUES exclusion', 'agent_board_test.insert_all_columns_frag(text)', 'a13-ins-novals', $r$
1009
+ create or replace function agent_board_test.insert_all_columns_frag(p_rel text) returns text
1010
+ language sql immutable set search_path to pg_catalog as $b$
1011
+ select agent_board_test.ci_re('insert into') || '\s+(?:' || agent_board_test.ci_re('only') || '\s+)?'
1012
+ || agent_board_test.qname_frag(p_rel)
1013
+ || '(?!(?:\s+(?:' || agent_board_test.ci_re('as') || '\s+)?(?!'
1014
+ || agent_board_test.ci_re('select') || '(?![A-Za-z0-9_$]))'
1015
+ || '(?:"[^"]*"|[A-Za-z_][A-Za-z0-9_$]*))?\s*\()' $b$; $r$),
1016
+ ('insert rule: the SELECT exclusion', 'agent_board_test.insert_all_columns_frag(text)', 'a14-ins-select', $r$
1017
+ create or replace function agent_board_test.insert_all_columns_frag(p_rel text) returns text
1018
+ language sql immutable set search_path to pg_catalog as $b$
1019
+ select agent_board_test.ci_re('insert into') || '\s+(?:' || agent_board_test.ci_re('only') || '\s+)?'
1020
+ || agent_board_test.qname_frag(p_rel)
1021
+ || '(?!(?:\s+(?:' || agent_board_test.ci_re('as') || '\s+)?(?!'
1022
+ || agent_board_test.ci_re('values') || '(?![A-Za-z0-9_$]))'
1023
+ || '(?:"[^"]*"|[A-Za-z_][A-Za-z0-9_$]*))?\s*\()' $b$; $r$),
1024
+ ('insert rule: no backtracking before the paren', 'agent_board_test.insert_all_columns_frag(text)', 'r13-ins-collist', $r$
1025
+ create or replace function agent_board_test.insert_all_columns_frag(p_rel text) returns text
1026
+ language sql immutable set search_path to pg_catalog as $b$
1027
+ select agent_board_test.ci_re('insert into') || '\s+(?:' || agent_board_test.ci_re('only') || '\s+)?'
1028
+ || agent_board_test.qname_frag(p_rel) || '\s*(?!\()' $b$; $r$),
1029
+ ('insert rule: the quoted-alias alternative', 'agent_board_test.insert_all_columns_frag(text)', 'r15-ins-qalias', $r$
1030
+ create or replace function agent_board_test.insert_all_columns_frag(p_rel text) returns text
1031
+ language sql immutable set search_path to pg_catalog as $b$
1032
+ select agent_board_test.ci_re('insert into') || '\s+(?:' || agent_board_test.ci_re('only') || '\s+)?'
1033
+ || agent_board_test.qname_frag(p_rel)
1034
+ || '(?!(?:\s+(?:' || agent_board_test.ci_re('as') || '\s+)?(?!'
1035
+ || agent_board_test.ci_re('values') || '(?![A-Za-z0-9_$])|'
1036
+ || agent_board_test.ci_re('select') || '(?![A-Za-z0-9_$]))'
1037
+ || '[A-Za-z_][A-Za-z0-9_$]*)?\s*\()' $b$; $r$),
1038
+ ('insert re: composed from the frag', 'agent_board_test.insert_all_columns_re(text)', 'a17-ins-re-eq-frag', $r$
1039
+ create or replace function agent_board_test.insert_all_columns_re(p_rel text) returns text
1040
+ language sql immutable set search_path to pg_catalog as $b$
1041
+ select agent_board_test.insert_all_columns_frag(p_rel) $b$; $r$);
1042
+
1043
+ create temp table _mflip (rule_name text, expect_row text, flipped text, named_row_flipped boolean) on commit drop;
1044
+
1045
+ do $mut$
1046
+ declare
1047
+ r record;
1048
+ v_save text;
1049
+ v_hits text;
1050
+ begin
1051
+ for r in select * from _mrule order by rule_name loop
1052
+ v_save := pg_get_functiondef(r.fn::regprocedure);
1053
+ execute r.revert_sql;
1054
+ select coalesce(string_agg(m.label, ' ' order by m.label), '<NONE>')
1055
+ into v_hits
1056
+ from _mrow m
1057
+ where pg_temp.mverdict(m.expr) <> m.want;
1058
+ execute v_save; -- restore before the next rule
1059
+ insert into _mflip
1060
+ values (r.rule_name, r.expect_row, v_hits,
1061
+ v_hits ~ ('(^| )' || r.expect_row || '( |$)'));
1062
+ end loop;
1063
+ end $mut$;
1064
+
1065
+ select is(
1066
+ (select coalesce(string_agg(rule_name || ' (wanted ' || expect_row || ', got ' || flipped || ')',
1067
+ '; ' order by rule_name), '<none>')
1068
+ from _mflip where not named_row_flipped),
1069
+ '<none>',
1070
+ 'CPR-0a REVERT MAP (mechanical): reverting each rule makes THE ROW NAMED FOR THAT RULE fail — one behaviour per entry and a named row per entry, so a compound revert cannot let a covered branch conceal an uncovered sibling and no rule can borrow coverage from an unrelated row — the map is compiled by reverting each rule and re-evaluating the rows, not by hand, because three separate rows have now certified axes they did not test and a human reading the table caught none of them');
1071
+
1072
+ -- CPR-0b. AND THE RULE INVENTORY IS RECONCILED AGAINST THE CATALOG, because _mrule is still written
1073
+ -- by hand and a matcher added without an entry would be SILENTLY UNGATED — which contradicts
1074
+ -- CPR-0a's own sentence. Deriving "every rule a regex-building function implements" from its source
1075
+ -- is not mechanical; deriving "every FUNCTION in this schema is either gated or positively exempted"
1076
+ -- is, and it closes the exposure that matters: a new matcher cannot arrive unnoticed.
1077
+ --
1078
+ -- Every function in agent_board_test is classified. A new matcher appears as `NOT-GATED` and fails;
1079
+ -- a new helper appears too and must be exempted deliberately. That is rule 1 applied to the gate's
1080
+ -- own inventory: the classification comes from pg_proc, and the exemptions are asserted positively
1081
+ -- rather than by absence.
1082
+ select is(
1083
+ (select string_agg(p.proname || '=' ||
1084
+ (case when exists (select 1 from _mrule r where r.fn like '%.' || p.proname || '(%')
1085
+ then 'gated' else 'NOT-GATED' end), ', ' order by p.proname)
1086
+ from pg_proc p where p.pronamespace = 'agent_board_test'::regnamespace),
1087
+ 'call_frag=gated, call_re=gated, ci_re=gated, col_frag=gated, fn_call_closure=NOT-GATED, '
1088
+ 'ident_chars=gated, ident_re=gated, insert_all_columns_frag=gated, insert_all_columns_re=gated, '
1089
+ 'lead_guard=gated, lock_edges=NOT-GATED, qname_frag=gated, qname_re=gated, '
1090
+ 'row_lock_rank=NOT-GATED, row_locks_conflict=NOT-GATED, stmt_re=gated, strip_sql=NOT-GATED, '
1091
+ 'stripped_body=NOT-GATED, stripped_def=NOT-GATED, stripped_viewdef=NOT-GATED, trail_guard=gated',
1092
+ 'CPR-0b RULE INVENTORY: every function in agent_board_test is either covered by a CPR-0a rule or positively listed as not a pattern matcher — the NOT-GATED entries are body readers and lock helpers, which build no regex, and the expected value names every one of them; a new matcher added without a rule appears here as NOT-GATED and fails by name');
1093
+
1094
+ -- CPR-15/16. And no session can take one either — the OPERATION, not a privilege proxy for it.
1095
+ -- `select ... for update` on claims is refused with 42501 to both roles that can read the table,
1096
+ -- because a row lock needs UPDATE/DELETE privilege and neither holds it at table or column level.
1097
+ -- anon cannot even SELECT. Run inside a DO block so the SET LOCAL role is unwound with the
1098
+ -- subtransaction throws_ok opens.
1099
+ select throws_ok(
1100
+ $$ do $cpr$ begin
1101
+ perform set_config('role', 'authenticated', true);
1102
+ perform 1 from agent_board.claims limit 1 for update;
1103
+ end $cpr$ $$,
1104
+ '42501', NULL,
1105
+ 'CPR-15 LOCK ORDER: an authenticated session cannot row-lock a claim DIRECTLY — `select ... for update` is refused 42501, so the hand-written inversion claimpolicy_claimrow_lock_order.spec constructs cannot be spelled that way; composition through granted definer RPCs is a separate path, narrowed in that spec''s header');
1106
+
1107
+ select throws_ok(
1108
+ $$ do $cpr$ begin
1109
+ perform set_config('role', 'service_role', true);
1110
+ perform 1 from agent_board.claims limit 1 for update;
1111
+ end $cpr$ $$,
1112
+ '42501', NULL,
1113
+ 'CPR-16 LOCK ORDER: nor can service_role spell it directly — the operator carve-out exempts it from the guards, not from the table grants; what service_role could do is compose it across two RPCs, which is why CPR-22 now pins the lock helper as grantable to nobody');
1114
+
1115
+ -- ===========================================================================
1116
+ -- THE HELPERS: contract and authorization.
1117
+ -- ===========================================================================
1118
+
1119
+ -- CPR-17/18. The null fallbacks, which are what make the "board stamped mid-wait" drift
1120
+ -- CONSERVATIVE rather than a hole: an absent board resolves to the ENFORCING policy.
1121
+ select is(
1122
+ (select agent_board._claimgate_board_of_thread(null)),
1123
+ null::uuid,
1124
+ 'CPR-17 NULL FALLBACK: a null thread has no board — so nothing is locked and nothing can go stale');
1125
+
1126
+ select is(
1127
+ (select agent_board._claimgate_board_claim_policy_of(null)),
1128
+ 'inherit',
1129
+ 'CPR-18 NULL FALLBACK: an absent board resolves to inherit, the ENFORCING answer — a board stamped onto a null-board thread mid-wait can therefore only make this path stricter, never permissive');
1130
+
1131
+ -- CPR-19/20. The composition is the SAME function it replaced. `_claimgate_board_claim_policy` is
1132
+ -- what the census view resolves through and what BCP-5 pins; decomposing it into the two halves
1133
+ -- the enforcement paths call separately must not have moved its answer on either case.
1134
+ select is(
1135
+ (select agent_board._claimgate_board_claim_policy(c.thread) from _cpr c),
1136
+ (select agent_board._claimgate_board_claim_policy_of(agent_board._claimgate_board_of_thread(c.thread)) from _cpr c),
1137
+ 'CPR-19 COMPOSITION: for a board-linked thread the thread-keyed resolver and the two halves agree — one definition of the walk, not two');
1138
+
1139
+ select is(
1140
+ (select agent_board._claimgate_board_claim_policy(c.free_thread) from _cpr c),
1141
+ 'inherit',
1142
+ 'CPR-20 COMPOSITION: a thread with NO board still resolves inherit — a claim cannot opt itself out by omitting the board (BCP-5''s rule, unchanged by the decomposition)');
1143
+
1144
+ -- CPR-21. And the board-linked fixture really is advisory, so CPR-19 is comparing something.
1145
+ -- Without this CPR-19 could agree on 'inherit' twice and prove nothing.
1146
+ select is(
1147
+ (select agent_board._claimgate_board_claim_policy(c.thread) from _cpr c),
1148
+ 'advisory',
1149
+ 'CPR-21 DISCRIMINATION: the fixture board is advisory, so CPR-19 compares a non-default answer rather than agreeing on the fallback twice');
1150
+
1151
+ -- CPR-22..24. AUTHORIZATION. The serialisation is only worth something if a session cannot take
1152
+ -- the lock on a key of its own choosing, or skip it; and the two read helpers must stay narrower
1153
+ -- than the thread-keyed resolver, which KEEPS its authenticated grant because the census view
1154
+ -- resolves through it.
1155
+ -- CPR-22. THE GRANT THIS PR REMOVED, AND WHY. The description here used to say the helper was
1156
+ -- "unreachable from a session" while its own expected string asserted `service_role=true` — and a
1157
+ -- service_role SESSION could indeed call it. That is not academic: with the grant in place, a
1158
+ -- single service_role transaction could call `renew_claim` (whose `update agent_board.claims` sits
1159
+ -- OUTSIDE the ownerless block, so the row lock is taken even though `_is_service_role()`
1160
+ -- short-circuits the board lock) and then call this helper DIRECTLY — assembling claim-row-lock
1161
+ -- then board-lock, the very inversion claimpolicy_claimrow_lock_order.spec constructs by hand. It
1162
+ -- was reproduced as a real `deadlock detected` before the grant was removed.
1163
+ --
1164
+ -- The grant was dead weight: the helper is NOT SECURITY DEFINER and its three callers ARE, owned
1165
+ -- by the same owner, so they reach it by ownership. Nothing in the schema, the TypeScript packages
1166
+ -- (client/cli/mcp/runtime), or the Edge Functions names it — derived from the tree, not assumed.
1167
+ -- ENUMERATED FROM THE ACL, ACROSS EVERY OVERLOAD OF THE NAME. Two hand lists have been removed
1168
+ -- from this assertion in successive rounds, and the second was introduced by the fix for the first.
1169
+ --
1170
+ -- Round 3 replaced four has_function_privilege() calls (public/anon/authenticated/service_role)
1171
+ -- with aclexplode, because a grant to any fifth role was invisible. Measured: with EXECUTE
1172
+ -- granted to a fifth role the old form returned all-false and PASSED.
1173
+ --
1174
+ -- Round 4 removed the SIGNATURE filter. That fix keyed the query to
1175
+ -- `to_regprocedure('…(uuid)')` while its own description claimed "an overload carrying one is
1176
+ -- named here" — a claim one `create function …(text)` away from false. The property actually
1177
+ -- wanted is about the NAME: nothing in agent_board called _claimgate_lock_board_policy should be
1178
+ -- executable by a non-owner, whatever its argument types. Enumerating overloads is strictly
1179
+ -- stronger than asserting none exists, and does not require that second fact to be kept true.
1180
+ --
1181
+ -- Round 5 corrected the WORD: the scan takes any pg_proc row, so it covers ROUTINES — procedures
1182
+ -- and aggregates as well as functions — while the description said "function overloads". The
1183
+ -- scan was right and the sentence was narrow; both now say routine. 20261001000100 has the same
1184
+ -- scope and had to change its REVOKE verb to match, because `on function` RAISES against a
1185
+ -- procedure.
1186
+ --
1187
+ -- Four fail-open modes closed here, each deliberately:
1188
+ -- * `proacl IS NULL` means DEFAULT privileges, and the default for a function is EXECUTE to
1189
+ -- PUBLIC. aclexplode(NULL) returns ZERO ROWS, so a plain enumeration would report '<none>' for
1190
+ -- the one state where PUBLIC really can call it. Reported as its own row per overload.
1191
+ -- * ZERO overloads would also aggregate to '<none>' and PASS — an absent function reported as a
1192
+ -- clean ACL. The count is checked first and says so instead.
1193
+ -- * grantee 0 is PUBLIC and renders as '-' through regrole; spelled out so a PUBLIC grant is
1194
+ -- legible in the failure message rather than looking like a dash.
1195
+ -- * a grantee OID with no pg_roles row is RENDERED, not raised on. `a.grantee::regrole` raises
1196
+ -- for a dangling OID, and a raise inside a pgTAP assertion aborts the file and emits NO
1197
+ -- `not ok` at all — rule 3's exact shape, and the same trap to_regprocedure avoided before.
1198
+ -- The migration's matching `raise` is correct precisely because it is NOT an assertion: there,
1199
+ -- skipping silently would leave a live grant standing.
1200
+ select is(
1201
+ (with fns as (
1202
+ select p.oid, p.proacl, p.proowner, p.oid::regprocedure::text as sig
1203
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
1204
+ where n.nspname = 'agent_board' and p.proname = '_claimgate_lock_board_policy'
1205
+ ), findings as (
1206
+ select f.sig || ' -> DEFAULT ACL (public holds EXECUTE implicitly)' as entry
1207
+ from fns f where f.proacl is null
1208
+ union all
1209
+ select f.sig || ' -> ' ||
1210
+ case when a.grantee = 0 then 'PUBLIC'
1211
+ else coalesce((select quote_ident(r.rolname) from pg_roles r where r.oid = a.grantee),
1212
+ 'OID ' || a.grantee::text) end
1213
+ from fns f cross join lateral aclexplode(f.proacl) a
1214
+ where a.privilege_type = 'EXECUTE' and a.grantee <> f.proowner
1215
+ )
1216
+ select case when (select count(*) from fns) = 0
1217
+ then 'NO FUNCTION NAMED agent_board._claimgate_lock_board_policy'
1218
+ else coalesce((select string_agg(entry, '; ' order by entry) from findings), '<none>')
1219
+ end),
1220
+ '<none>',
1221
+ 'CPR-22 AUTHORIZATION: no non-owner grantee holds EXECUTE on ANY ROUTINE named agent_board._claimgate_lock_board_policy — functions, procedures and aggregates alike, enumerated from each one''s ACL, so a grant to any role on any signature or kind is named here rather than slipping past a list of four roles or a filter on one signature');
1222
+
1223
+ -- ENUMERATED FROM THE ACL, like CPR-22 and for the same reason: three named roles are a hand list,
1224
+ -- and a grant to any fourth was invisible to the old form. Pinned to the two exact signatures with
1225
+ -- to_regprocedure — correct HERE (this is a property of these two specific functions, not of a
1226
+ -- name) and the opposite of CPR-22's name scan, which asks a property of the name. A missing
1227
+ -- function yields no row and fails by comparison rather than raising.
1228
+ select is(
1229
+ (select string_agg(x.nm || ':' || x.grantees, ', ' order by x.nm)
1230
+ from (select p.proname as nm,
1231
+ (select case
1232
+ when p.proacl is null then 'DEFAULT-ACL(public holds EXECUTE)'
1233
+ else coalesce((select string_agg(distinct g.who, ',' order by g.who)
1234
+ from (select case when a.grantee = 0 then 'PUBLIC'
1235
+ else coalesce((select r.rolname from pg_roles r where r.oid = a.grantee),
1236
+ 'OID ' || a.grantee::text) end as who
1237
+ from aclexplode(p.proacl) a
1238
+ where a.privilege_type = 'EXECUTE' and a.grantee <> p.proowner) g),
1239
+ '<none>') end) as grantees
1240
+ from pg_proc p
1241
+ where p.oid in (to_regprocedure('agent_board._claimgate_board_of_thread(uuid)'),
1242
+ to_regprocedure('agent_board._claimgate_board_claim_policy_of(uuid)'))) x),
1243
+ '_claimgate_board_claim_policy_of:service_role, _claimgate_board_of_thread:service_role',
1244
+ 'CPR-23 AUTHORIZATION: the two halves are granted to service_role and to NOBODY ELSE — enumerated from each ACL rather than asked of three named roles, so a grant to any other role is named here; they read threads/boards under SECURITY DEFINER, so an authenticated grant would expose any thread''s board past RLS');
1245
+
1246
+ -- ENUMERATED, because has_function_privilege COULD NOT DECIDE THIS ASSERTION'S OWN SENTENCE. It
1247
+ -- claims the ACL was preserved "rather than silently re-granting to PUBLIC" — but
1248
+ -- has_function_privilege('authenticated', …) returns TRUE when PUBLIC holds the grant, so the exact
1249
+ -- state the sentence rules out was indistinguishable from the one it asserts. The grantee list is
1250
+ -- read from the ACL instead, where PUBLIC appears by name.
1251
+ select is(
1252
+ (select (select case
1253
+ when p.proacl is null then 'DEFAULT-ACL(public holds EXECUTE)'
1254
+ else coalesce((select string_agg(distinct g.who, ',' order by g.who)
1255
+ from (select case when a.grantee = 0 then 'PUBLIC'
1256
+ else coalesce((select r.rolname from pg_roles r where r.oid = a.grantee),
1257
+ 'OID ' || a.grantee::text) end as who
1258
+ from aclexplode(p.proacl) a
1259
+ where a.privilege_type = 'EXECUTE' and a.grantee <> p.proowner) g),
1260
+ '<none>') end)
1261
+ from pg_proc p
1262
+ where p.oid = to_regprocedure('agent_board._claimgate_board_claim_policy(uuid)')),
1263
+ 'authenticated,service_role',
1264
+ 'CPR-24 AUTHORIZATION: the thread-keyed resolver''s non-owner grantees are exactly authenticated and service_role — the census view resolves through it, and a PUBLIC grant is now DISTINGUISHABLE from the authenticated one rather than reading as the same true');
1265
+
1266
+ -- CPR-25. The lock helper works on the level it demands. The REFUSAL is the interesting half and
1267
+ -- cannot be asserted here — `set transaction isolation level` must be a transaction's first
1268
+ -- statement and this file is already inside one — so it is pinned by name in
1269
+ -- claimpolicy_isolation_refusal.spec. This asserts the allow side, which is what would break if
1270
+ -- the guard's comparison were inverted.
1271
+ select lives_ok(
1272
+ $$ select agent_board._claimgate_lock_board_policy(
1273
+ (select board from _cpr)) $$,
1274
+ 'CPR-25 ISOLATION GUARD (allow side): under READ COMMITTED the lock is taken and returns — the refusal under REPEATABLE READ is pinned in claimpolicy_isolation_refusal.spec, which is the only place it can be');
1275
+
1276
+ -- CPR-26. The NULL is CAST, because an untyped one is ambiguous the moment a same-name overload
1277
+ -- exists: `_claimgate_lock_board_policy(null)` resolves against every overload and raises
1278
+ -- `function is not unique`. That fails closed rather than aborting, so it was not a hole — but it
1279
+ -- reported a defect in the WRONG assertion (this one) for a condition CPR-22 is the one to name.
1280
+ -- Found while probing a same-name procedure for CPR-22's routine scope.
1281
+ --
1282
+ -- CPR-26. And a null board is isolation-checked but locks nothing — the deliberate absence of a
1283
+ -- carve-out. Asserted so the branch is exercised rather than assumed unreachable.
1284
+ select lives_ok(
1285
+ $$ select agent_board._claimgate_lock_board_policy(null::uuid) $$,
1286
+ 'CPR-26 ISOLATION GUARD: a null board takes no lock (there is no board row to serialise on) and is still admitted under READ COMMITTED');
1287
+
1288
+ -- CPR-22a. AND THE REVOKE DID NOT BREAK THE DEFINER PATH. CPR-22 is a negative — it would stay
1289
+ -- green if the helper became uncallable by EVERYONE including its own callers, which would take
1290
+ -- the enforcement offline rather than securing it. So the positive half is asserted by RUNNING the
1291
+ -- path: `set_board_claim_policy` is SECURITY DEFINER, reaches the helper by ownership, and its
1292
+ -- advisory lock must actually be HELD afterwards.
1293
+ --
1294
+ -- NOT VACUOUS, and that is the whole design of this assertion. `lives_ok` alone would pass if the
1295
+ -- helper were never entered, so the state check is separate (rule 3): the advisory lock the helper
1296
+ -- takes is looked for in pg_locks for THIS backend. The board is flipped advisory -> inherit, the
1297
+ -- transition that reaches the lock; the fixture board is already 'advisory' (CPR-21 pins that), so
1298
+ -- the call is a real transition rather than a no-op.
1299
+ select lives_ok(
1300
+ $$ select agent_board.set_board_claim_policy((select board_b from _cpr), 'inherit') $$,
1301
+ 'CPR-22a AUTHORIZATION (positive half): with the helper granted to nobody, the SECURITY DEFINER writer still reaches it by ownership — the revoke closed the session path without taking enforcement offline');
1302
+
1303
+ -- The state half, keyed to BOARD B SPECIFICALLY. `count(*) > 0` over all advisory locks would be
1304
+ -- satisfied by the lock CPR-25 already holds on the other board and would prove nothing about this
1305
+ -- call; the key is recomputed here the same way the helper computes it, and split into the
1306
+ -- (classid, objid) pair pg_locks records for a bigint advisory key.
1307
+ select is(
1308
+ (select count(*)
1309
+ from pg_locks l, (select hashtextextended('agent_board.board_claim_policy:' ||
1310
+ (select board_b from _cpr)::text, 0) as k) x
1311
+ where l.locktype = 'advisory'
1312
+ and l.pid = pg_backend_pid()
1313
+ and l.classid = ((x.k >> 32) & 4294967295)::oid
1314
+ and l.objid = (x.k & 4294967295)::oid),
1315
+ 1::bigint,
1316
+ 'CPR-22a-state AUTHORIZATION: and the advisory lock for THAT board is genuinely held by this backend afterwards — so CPR-22a exercised the helper rather than passing on a lock some earlier assertion took');
1317
+
1318
+ -- CPR-25a. THE GUARD'S COMPARISON TEXT — and text is all it is. Read the scope note before
1319
+ -- treating a green here as a refusal guarantee.
1320
+ --
1321
+ -- WHAT IT ASSERTS: that the body contains an inequality against the single ACCEPTED level, spelled
1322
+ -- `v_isolation <> 'read committed'`. WHAT IT DOES NOT ASSERT: that this comparison is the one
1323
+ -- CONTROLLING the raise. It is a regex over the stripped body, so a dead or unrelated inequality
1324
+ -- elsewhere would satisfy it while a narrower operative guard admitted another level. Proving
1325
+ -- control flow is beyond what a source scan can do, and a longer regex would not change that — it
1326
+ -- would only make the same claim harder to audit.
1327
+ --
1328
+ -- SO THE BEHAVIOURAL PIN IS THE SPEC, NOT THIS: claimpolicy_isolation_refusal.spec's `sz` and `ru`
1329
+ -- permutations RUN a transaction at serializable and at read uncommitted and observe the 0A000 by
1330
+ -- name. This assertion earns its place beside them because a permutation only pins the levels it
1331
+ -- ENUMERATES: the guard's inequality form is what extends the refusal to a level no permutation
1332
+ -- names, and that form is textual, so a text assertion is the right instrument for it.
1333
+ --
1334
+ -- THE INDEPENDENCE, MEASURED RATHER THAN REASONED — and the first version of this comment got it
1335
+ -- wrong, which is worth leaving on the record given what this file is for. It claimed that
1336
+ -- inverting to `= 'read committed'` "fails only this one, because every permutation's level is
1337
+ -- still refused". Measured: that inversion refuses READ COMMITTED and ADMITS the other three, so
1338
+ -- it fails `sz`, `ru`, CPR-25, CPR-26 and both halves of CPR-22a as well. It is not an isolating
1339
+ -- mutation at all.
1340
+ --
1341
+ -- What actually separates the two instruments is a BEHAVIOUR-PRESERVING reformulation:
1342
+ -- `not (v_isolation = 'read committed')` is semantically identical — measured refusing repeatable
1343
+ -- read, serializable and read uncommitted while admitting read committed, exactly as before — so
1344
+ -- every permutation and every other assertion stays green, and ONLY this one goes red. That is the
1345
+ -- honest statement of what this assertion adds: it pins the FORM while the permutations pin the
1346
+ -- behaviour, and a change that alters the form without altering today's behaviour is caught here
1347
+ -- alone. Narrowing to `= 'repeatable read'` alters both, and fails `sz`, `ru` and this together.
1348
+ --
1349
+ -- `stripped_body(oid, true)` KEEPS literals, which is required and not incidental here: the marker
1350
+ -- IS the literal 'read committed' inside the comparison. Comments are still stripped, so prose
1351
+ -- mentioning the level cannot satisfy it.
1352
+ --
1353
+ -- PINNED TO THE (uuid) SIGNATURE, and here that is CORRECT — the opposite of CPR-22, deliberately.
1354
+ -- The two ask different questions. CPR-22 asks a property of the NAME ("no overload is executable
1355
+ -- by a non-owner"), so filtering to one signature would let another carry a grant. This asks a
1356
+ -- property of THE GUARD the three deciders actually call, which is the (uuid) one; an unrelated
1357
+ -- overload's body has no bearing on it.
1358
+ --
1359
+ -- to_regprocedure() rather than `p.proname = …`, and that is not stylistic. Selecting by NAME made
1360
+ -- this a scalar subquery over a SET: adding one overload made it return two rows, and
1361
+ -- `more than one row returned by a subquery used as an expression` ABORTED the whole file — 32 of
1362
+ -- 33 assertions ran and the missing one emitted no `not ok` at all. Rule 3's exact shape, found by
1363
+ -- running the overload mutation rather than by reading. Four scalar subqueries in this file carried
1364
+ -- the same latent abort and are all pinned now.
1365
+ select is(
1366
+ (select substring(agent_board_test.stripped_body(p.oid, true)
1367
+ from '(v_isolation\s*(?:<>|!=|=)\s*''[a-z ]+'')')
1368
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
1369
+ where p.oid = to_regprocedure('agent_board._claimgate_lock_board_policy(uuid)')),
1370
+ 'v_isolation <> ''read committed''',
1371
+ 'CPR-25a ISOLATION GUARD (comparison TEXT, not control flow): the guard''s body spells an inequality against the one accepted level rather than an equality against one refused level — this pins the FORM that extends refusal to levels no permutation enumerates; the refusal itself is demonstrated behaviourally by the sz and ru permutations in claimpolicy_isolation_refusal.spec');
1372
+
1373
+ select * from finish();
1374
+ rollback;