@consiliency/agent-board-schema 1.51.0 → 1.52.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 (39) hide show
  1. package/package.json +1 -1
  2. package/schema-manifest.json +1 -1
  3. package/tests/agent_board_agent_liveness.test.sql +3 -1
  4. package/tests/agent_board_atomic_fleet_onboarding.test.sql +33 -36
  5. package/tests/agent_board_authbound_boundary.test.sql +4 -3
  6. package/tests/agent_board_board_claim_policy.test.sql +17 -6
  7. package/tests/agent_board_claim_subject_gate.test.sql +7 -8
  8. package/tests/agent_board_claimkey.test.sql +9 -6
  9. package/tests/agent_board_claimpolicy_race.test.sql +1466 -342
  10. package/tests/agent_board_consent_emission.test.sql +5 -4
  11. package/tests/agent_board_consent_gating.test.sql +20 -9
  12. package/tests/agent_board_enroll_resumption.test.sql +1 -1
  13. package/tests/agent_board_enrollment_audit_content.test.sql +5 -4
  14. package/tests/agent_board_federated_identity_injective_slug.test.sql +3 -1
  15. package/tests/agent_board_federated_lock_order_class.test.sql +69 -52
  16. package/tests/agent_board_fedsession.test.sql +3 -1
  17. package/tests/agent_board_fedsession_provider_absence_gate.test.sql +9 -4
  18. package/tests/agent_board_fedsession_stored_identity_gate.test.sql +18 -6
  19. package/tests/agent_board_fk_lock_edges.test.sql +64 -5
  20. package/tests/agent_board_fleet_domain_reclaim.test.sql +2 -4
  21. package/tests/agent_board_fleet_machine_hijack.test.sql +2 -4
  22. package/tests/agent_board_fleet_machine_reactivation.test.sql +5 -1
  23. package/tests/agent_board_fleet_machine_reclaim.test.sql +2 -4
  24. package/tests/agent_board_invite_no_downgrade.test.sql +24 -18
  25. package/tests/agent_board_ledger_authority.test.sql +29 -21
  26. package/tests/agent_board_mint_harden.test.sql +5 -5
  27. package/tests/agent_board_outbox_wake_ordering.test.sql +4 -3
  28. package/tests/agent_board_ownerless_renew_quota.test.sql +7 -2
  29. package/tests/agent_board_portal_offline_invariant.test.sql +5 -4
  30. package/tests/agent_board_receipt_subject_gate.test.sql +7 -8
  31. package/tests/agent_board_rolereset.test.sql +5 -3
  32. package/tests/agent_board_rowid.test.sql +34 -15
  33. package/tests/agent_board_signer_lifecycle.test.sql +24 -13
  34. package/tests/agent_board_source_strip_helper.test.sql +2 -1
  35. package/tests/agent_board_task_event_origin_posture.test.sql +21 -9
  36. package/tests/agent_board_wake_recovery.test.sql +53 -26
  37. package/tests/agent_board_write_enforcement_class.test.sql +130 -12
  38. package/tests/catalog/function_execute_privileges.json +1923 -0
  39. package/tests/fixtures/test-prereqs.sql +644 -237
@@ -50,7 +50,7 @@ select p.oid, p.proname
50
50
  from pg_proc p
51
51
  join pg_namespace n on n.oid = p.pronamespace
52
52
  where n.nspname = 'agent_board'
53
- and agent_board_test.stripped_body(p.oid) ~* 'insert\s+into\s+(agent_board\.)?task_events';
53
+ and agent_board_test.stripped_body(p.oid) ~ agent_board_test.stmt_re('insert into', 'task_events');
54
54
 
55
55
  create temp table _op_class on commit drop as
56
56
  with recursive walk as (
@@ -60,8 +60,7 @@ with recursive walk as (
60
60
  from pg_proc p
61
61
  join pg_namespace n on n.oid = p.pronamespace
62
62
  join walk w
63
- on agent_board_test.stripped_body(p.oid)
64
- ~* ('(^|[^a-z0-9_.])(agent_board\.)?' || w.proname || '\s*\(')
63
+ on agent_board_test.stripped_body(p.oid) ~ agent_board_test.call_re(w.proname)
65
64
  where n.nspname = 'agent_board' and p.oid <> w.oid and w.depth < 5
66
65
  )
67
66
  select oid, proname, min(depth) as depth from walk group by oid, proname;
@@ -106,11 +105,12 @@ select is(
106
105
  (select coalesce(string_agg(c.proname, ', ' order by c.proname), '')
107
106
  from _op_class c
108
107
  where (select count(*) from regexp_matches(
109
- agent_board_test.stripped_body(c.oid, true), '_append_task_event\s*\(', 'g')) <> 0
108
+ agent_board_test.stripped_body(c.oid, true), agent_board_test.call_re('_append_task_event'), 'g')) <> 0
110
109
  and (select count(*) from regexp_matches(
111
- agent_board_test.stripped_body(c.oid, true), '_append_task_event\s*\(', 'g'))
110
+ agent_board_test.stripped_body(c.oid, true), agent_board_test.call_re('_append_task_event'), 'g'))
112
111
  <> (select count(*) from regexp_matches(
113
- agent_board_test.stripped_body(c.oid, true), '_append_task_event\s*\(\s*''', 'g'))),
112
+ agent_board_test.stripped_body(c.oid, true),
113
+ agent_board_test.re(agent_board_test.call_frag('_append_task_event') || '\s*'''), 'g'))),
114
114
  '',
115
115
  'OP-4 CLASS: every funnel caller passes an explicit literal posture -- a caller that does not is NAMED here');
116
116
 
@@ -120,8 +120,13 @@ select is(
120
120
  select is(
121
121
  (select coalesce(string_agg(d.proname, ', ' order by d.proname), '')
122
122
  from _op_direct d
123
+ -- The INSERT column-list arm ONLY (col_write_frag's arm 4), not col_write_re: that union also
124
+ -- accepts an INSERT with NO column list as a write of every column, and here a positional
125
+ -- insert that leaves origin_posture to its default is exactly the offender to name.
123
126
  where agent_board_test.stripped_body(d.oid)
124
- !~* 'insert\s+into\s+(agent_board\.)?task_events\s*\([^)]*origin_posture'),
127
+ !~ agent_board_test.re(agent_board_test.stmt_frag('insert into', 'task_events')
128
+ || '(?:\s+(?:' || agent_board_test.ci_re('as') || '\s+)?' || agent_board_test.ident_any_frag() || ')?'
129
+ || '\s*\([^)]*' || agent_board_test.col_frag('origin_posture') || '[^)]*\)')),
125
130
  '',
126
131
  'OP-5 CLASS: every DIRECT task_events INSERT names origin_posture -- one that does not is NAMED here');
127
132
 
@@ -314,7 +319,12 @@ select is(
314
319
  from pg_proc p join pg_namespace n on n.oid = p.pronamespace
315
320
  where n.nspname = 'agent_board'
316
321
  and p.prolang <> (select oid from pg_language where lanname = 'internal')
317
- and agent_board_test.stripped_body(p.oid, true) ~* 'set\s[^;]*\morigin_posture\s*='),
322
+ -- The relations that OWN the column come from the catalog, not a hand list: an
323
+ -- origin_posture column added to a second table joins this scan by itself.
324
+ and exists (select 1 from information_schema.columns c
325
+ where c.table_schema = 'agent_board' and c.column_name = 'origin_posture'
326
+ and agent_board_test.stripped_body(p.oid, true)
327
+ ~ agent_board_test.re(agent_board_test.col_assign_frag(c.table_name, 'origin_posture', agent_board_test.bare_ok(p.oid))))),
318
328
  '',
319
329
  'OP-19: nothing UPDATEs origin_posture -- including through a dynamic-SQL template, which keeping literals makes visible');
320
330
 
@@ -336,8 +346,10 @@ select is(
336
346
  from pg_proc p join pg_namespace n on n.oid = p.pronamespace
337
347
  where n.nspname = 'agent_board'
338
348
  and p.proname <> 'append_task_event'
349
+ -- bare_ok: a body that can reach the schema unqualified is scanned for the bare call too
350
+ -- (the hand-rolled form this replaced required the `agent_board.` qualifier).
339
351
  and agent_board_test.stripped_body(p.oid, true)
340
- ~ '(^|[^a-z0-9_.])agent_board\.append_task_event\s*\('),
352
+ ~ agent_board_test.call_re('append_task_event', agent_board_test.bare_ok(p.oid))),
341
353
  '',
342
354
  'OP-20: no in-schema function calls the PUBLIC append_task_event -- one that did would be stamped direct-rpc and named by nothing else');
343
355
 
@@ -200,22 +200,34 @@ cols as (
200
200
  insert into _wr_class(kind, pattern)
201
201
  select 'relations', string_agg(relname, '|' order by relname) from rels
202
202
  union all
203
- select 'columns', string_agg(attname, '|' order by length(attname) desc, attname) from cols;
203
+ -- The column arm is a col_ref_frag alternation, QUALIFIED OR BARE (`i.created_at <` and
204
+ -- `created_at <` are both gates; a bare-only form would miss the alias spelling). The
205
+ -- window below is measured from the start of the reference, so a qualified reference
206
+ -- reaches `[^<>=]{0,40}` further back than the old `\m…\M` did -- a difference that can
207
+ -- only ADD a match, which makes the exact-set claims below red, never green (measured on
208
+ -- the 26 members: identical).
209
+ select 'columns', '(?:' || string_agg(agent_board_test.col_ref_frag(attname), '|' order by length(attname) desc, attname) || ')' from cols;
204
210
 
205
211
  create or replace function pg_temp._wr_cursor_gates(p_text text) returns text
206
212
  language sql stable as $fn$
207
213
  select coalesce(
208
- (select string_agg(distinct regexp_replace(m[1], '[[:space:]]+', ' ', 'g'), ' | ')
214
+ -- A qualifier on a column-first match is dropped from the report, so the strings
215
+ -- WR-8 pins stay `created_at <`, whichever alias the body spells. (This rewrites a
216
+ -- MATCH, not source text, so it is a plain regex rather than a family composition.)
217
+ (select string_agg(distinct regexp_replace(regexp_replace(m[1], '^[^.]*[.][[:space:]]*', ''),
218
+ '[[:space:]]+', ' ', 'g'), ' | ')
209
219
  from _wr_class c,
210
220
  regexp_matches(
211
221
  -- literals KEPT: this file's class members put real predicates in
212
222
  -- dynamic SQL (see the _wr_members header). Comments are removed.
213
223
  agent_board_test.strip_sql($1, true),
214
- '(\m(' || c.pattern || ')\M[^<>=]{0,40}(?:<=|>=|<|>)'
215
- || '|(?:<=|>=|<|>)[^<>=]{0,40}\m(' || c.pattern || ')\M'
216
- || '|\m(' || c.pattern || ')\M[^<>=]{0,20}\mbetween\M'
217
- || '|\mbetween\M[^<>=]{0,40}\m(' || c.pattern || ')\M)',
218
- 'gi') m
224
+ -- case-folding comes from the family (ci_re / ident_re), not from an `i` flag,
225
+ -- which the director's `(?c)` would override
226
+ agent_board_test.re('(' || c.pattern || '[^<>=]{0,40}(?:<=|>=|<|>)'
227
+ || '|(?:<=|>=|<|>)[^<>=]{0,40}' || c.pattern
228
+ || '|' || c.pattern || '[^<>=]{0,20}\m' || agent_board_test.ci_re('between') || '\M'
229
+ || '|\m' || agent_board_test.ci_re('between') || '\M[^<>=]{0,40}' || c.pattern || ')'),
230
+ 'g') m
219
231
  where c.kind = 'columns'),
220
232
  '')
221
233
  $fn$;
@@ -270,7 +282,12 @@ select p.proname, 'function', pg_get_functiondef(p.oid), agent_board_test.stripp
270
282
  join pg_namespace n on n.oid = p.pronamespace
271
283
  where n.nspname = 'agent_board'
272
284
  and p.prolang <> (select oid from pg_language where lanname = 'internal')
273
- and pg_get_functiondef(p.oid) ~* ('(' || (select pattern from _wr_class where kind = 'relations') || ')');
285
+ -- membership by RELATION REFERENCE in the stripped body (literals kept, because the
286
+ -- members this file exists for reference their relations inside dynamic SQL), rather
287
+ -- than a substring of the raw definition; measured: the same 15 functions.
288
+ and agent_board_test.stripped_body(p.oid, true) ~ agent_board_test.re(
289
+ (select string_agg(agent_board_test.qname_frag(x, agent_board_test.bare_ok(p.oid)), '|')
290
+ from unnest(string_to_array((select pattern from _wr_class where kind = 'relations'), '|')) x));
274
291
 
275
292
  -- ---------------------------------------------------------------------------
276
293
  -- WR-1 EXISTENCE
@@ -353,7 +370,7 @@ select is(
353
370
 
354
371
  select ok(
355
372
  (select agent_board_test.stripped_viewdef(to_regclass('agent_board.v_agent_wake_recovery'), true)
356
- !~* '\morder[[:space:]]+by\M'),
373
+ !~ agent_board_test.re('\m' || agent_board_test.ci_re('order by') || '\M')),
357
374
  'WR-4 the wake recovery view carries no ORDER BY -- the unhandled set is a SET, not a cursor stream');
358
375
 
359
376
  -- ---------------------------------------------------------------------------
@@ -367,16 +384,20 @@ select is(
367
384
  coalesce(
368
385
  (select array_agg(btrim(btrim(split_part(e, '::', 1)), '''') order by btrim(btrim(split_part(e, '::', 1)), ''''))
369
386
  from unnest(string_to_array(
370
- substring(pg_get_viewdef(to_regclass('agent_board.v_agent_wake_recovery'))
371
- from 'action_state = ANY \(ARRAY\[([^]]*)\]'), ',')) e),
387
+ -- literals KEPT: the state set being read IS a list of string literals
388
+ substring(agent_board_test.stripped_viewdef(to_regclass('agent_board.v_agent_wake_recovery'), true)
389
+ from agent_board_test.re(agent_board_test.col_ref_frag('action_state') || '\s*=\s*' || agent_board_test.ci_re('any')
390
+ || '\s*\(\s*' || agent_board_test.ci_re('array') || '\s*\[([^]]*)\]')), ',')) e),
372
391
  array['VIEW-STATE-SET-NOT-FOUND']),
373
392
  coalesce(
374
393
  (select array_agg(btrim(btrim(split_part(e, '::', 1)), '''') order by btrim(btrim(split_part(e, '::', 1)), ''''))
375
394
  from unnest(string_to_array(
395
+ -- source-scan: loud-literal pg_get_expr deparses a stored predicate tree -- no comment or literal can hide in it
376
396
  substring((select pg_get_expr(i.indpred, i.indrelid)
377
397
  from pg_index i
378
398
  where i.indexrelid = to_regclass('agent_board.idx_agent_board_inbox_items_agent_pending'))
379
- from 'action_state = ANY \(ARRAY\[([^]]*)\]'), ',')) e),
399
+ from agent_board_test.re(agent_board_test.col_ref_frag('action_state') || '\s*=\s*' || agent_board_test.ci_re('any')
400
+ || '\s*\(\s*' || agent_board_test.ci_re('array') || '\s*\[([^]]*)\]')), ',')) e),
380
401
  array['INDEX-STATE-SET-NOT-FOUND']),
381
402
  'WR-5 the view''s action_state set equals idx_agent_board_inbox_items_agent_pending''s predicate set -- editing either one alone fails here');
382
403
 
@@ -578,7 +599,8 @@ select is(
578
599
  -- The scoping is a catalog fact, not only a behavioural one: a future edit that
579
600
  -- drops the predicate but leaves the fixture passing for another reason fails here.
580
601
  select ok(
581
- (select pg_get_viewdef(to_regclass('agent_board.v_agent_wake_recovery')) ~ 'current_agent_id\(\)'),
602
+ (select agent_board_test.stripped_viewdef(to_regclass('agent_board.v_agent_wake_recovery'))
603
+ ~ agent_board_test.call_re('current_agent_id')),
582
604
  'WR-6 the view definition itself scopes on current_agent_id() -- recipient scoping is server-side, never a caller''s filter');
583
605
 
584
606
  -- ---------------------------------------------------------------------------
@@ -617,16 +639,19 @@ select is(
617
639
  'created_at <',
618
640
  'WR-8 mark_retention_metadata''s ONLY pre-commit comparison is `created_at <` (an age cutoff) -- it never compares seq, and never gates on a lower bound');
619
641
 
620
- -- `ops_retention_reason` is a jsonb KEY -- a string literal -- so it is read from
621
- -- the RAW definition, which is true regardless of strip mode. The companion half
622
- -- is read from `body`, i.e. through the shared strip: the UPDATE it looks for is
623
- -- inside the `$retention$` dynamic-SQL block, which survives because this file
624
- -- keeps literals (see the _wr_members header) but would NOT survive the default
625
- -- literal-stripping mode. Restated so the next reader does not "simplify" the two
626
- -- halves into one column.
642
+ -- `ops_retention_reason` is a jsonb KEY -- a string literal -- and the UPDATE it
643
+ -- accompanies is inside the `$retention$` dynamic-SQL block. Both are read from
644
+ -- `body`, which KEEPS literals (see the _wr_members header): the key survives as a
645
+ -- string, and the UPDATE survives because it is one. Neither would survive the
646
+ -- default literal-stripping mode, and neither needs the raw `src` -- comments are
647
+ -- the only thing the strip removes here. Restated so the next reader does not
648
+ -- "simplify" the strip mode away. The UPDATE's target is the format() placeholder
649
+ -- `agent_board.%1$I` -- the sweep is parameterised over the relation -- so this
650
+ -- matches THAT spelling, not a relation name: a statement matcher would need a name
651
+ -- the body does not contain.
627
652
  select ok(
628
- (select src ~ 'ops_retention_reason'
629
- and body ~ 'update agent_board'
653
+ (select body ~ 'ops_retention_reason'
654
+ and body ~ agent_board_test.re('\m' || agent_board_test.ci_re('update') || '\s+' || agent_board_test.ci_re('agent_board') || '\s*\.\s*%1\$I\M')
630
655
  from _wr_members where name = 'mark_retention_metadata'),
631
656
  'WR-8 mark_retention_metadata stamps retention metadata through an UPDATE sweep -- the age cutoff drives retention, not a fetch');
632
657
 
@@ -650,7 +675,7 @@ select is(
650
675
  'WR-8 claim_callback_outbox_events'' ONLY pre-commit gate is `available_at <=` -- a scheduling predicate, never a lower bound against a caller cursor');
651
676
 
652
677
  select ok(
653
- (select body !~* '\mavailable_at\M[^<>=]{0,40}(?:>=|>)'
678
+ (select body !~ agent_board_test.re(agent_board_test.col_ref_frag('available_at') || '[^<>=]{0,40}(?:>=|>)')
654
679
  from _wr_members where name = 'claim_callback_outbox_events'),
655
680
  'WR-8 that gate is an UPPER bound (<=) and never a lower bound -- a `available_at >` would be a cursor, and is what this distinguishes');
656
681
 
@@ -683,7 +708,7 @@ select ok(
683
708
  'WR-9 claim_callback_outbox_events partitions the queue on outbox_status -- the lossless property #187 relies on, restated positively here');
684
709
 
685
710
  select ok(
686
- (select src ~ 'action_state = ANY'
711
+ (select body ~ agent_board_test.re(agent_board_test.col_ref_frag('action_state') || '\s*=\s*' || agent_board_test.ci_re('any') || '\M')
687
712
  from _wr_members where name = 'v_agent_wake_recovery'),
688
713
  'WR-9 v_agent_wake_recovery partitions the inbox on action_state -- status, never an ordinal');
689
714
 
@@ -700,12 +725,14 @@ select is(
700
725
  from pg_proc p
701
726
  where p.pronamespace = 'agent_board'::regnamespace
702
727
  and p.prolang <> (select oid from pg_language where lanname = 'internal')
703
- and agent_board_test.stripped_body(p.oid, true) ~* 'action_state[[:space:]]*=[[:space:]]*[^=]'
704
728
  -- The schema qualification is OPTIONAL on purpose (board review F3): a writer
705
729
  -- using `set search_path` plus a bare `update inbox_items` was invisible to
706
730
  -- the qualified form, and WR-10 stayed green naming only one writer. Same
707
731
  -- class as the WR-7 name filter -- two name-based enumerators in one file.
708
- and agent_board_test.stripped_body(p.oid, true) ~* 'update[[:space:]]+(agent_board\.)?inbox_items\M'),
732
+ -- col_write_re keeps the qualifier optional and binds the column to THIS relation's
733
+ -- write (the hand-rolled pair accepted `action_state =` anywhere in a body that also
734
+ -- updated inbox_items, a WHERE predicate included).
735
+ and agent_board_test.stripped_body(p.oid, true) ~ agent_board_test.col_write_re('inbox_items', 'action_state')),
709
736
  'update_inbox_item_state',
710
737
  'WR-10 exactly ONE function writes inbox_items.action_state -- the only way out of the unhandled set');
711
738
 
@@ -332,7 +332,7 @@ set search_path = extensions, public, agent_board;
332
332
 
333
333
  begin;
334
334
 
335
- select plan(23);
335
+ select plan(25);
336
336
 
337
337
  -- ===========================================================================================
338
338
  -- THE CLASS, derived from pg_catalog. Nothing below is a list of names.
@@ -357,10 +357,7 @@ select p.oid as fn_oid, t.relname, t.relkind
357
357
  where p.pronamespace = 'agent_board'::regnamespace
358
358
  and p.prokind in ('f','p')
359
359
  and p.prolang <> (select oid from pg_language where lanname = 'internal')
360
- and lower(agent_board_test.stripped_body(p.oid, true))
361
- ~ ('(insert\s+into|merge\s+into|update|delete\s+from|truncate|copy)(\s+table)?(\s+only)?\s+'
362
- || '(("?agent_board"?\s*\.\s*)?' || t.relname || '\M'
363
- || '|("?agent_board"?\s*\.\s*)?"' || t.relname || '")');
360
+ and agent_board_test.stripped_body(p.oid, true) ~ agent_board_test.write_re(t.relname);
364
361
 
365
362
  create temp table _wec_closure on commit drop as
366
363
  select * from agent_board_test.fn_call_closure();
@@ -385,8 +382,17 @@ select * from agent_board_test.fn_call_closure();
385
382
  -- then satisfy the resolver arm — fail-CLOSED, and no agent_board function is written that way
386
383
  -- today (measured: this pattern and the digits-only one agree on ALL 67 shipped raisers; they
387
384
  -- differ only on the decoy).
385
+ --
386
+ -- Composed from the family (#437 round 1): the keywords are `ci_re` so the case rule lives in the
387
+ -- pattern, not in a `lower()` around each subject, and the site the lint traces `via pat` scores
388
+ -- the composition rather than a hand alternation on an absence form. The three uses below are
389
+ -- `!~ (select pat from _wec_raise_pat)`, which the lint judges by THIS expression.
388
390
  create temp table _wec_raise_pat on commit drop as
389
- select 'errcode\s*=\s*''?42501|sqlstate\s+''?42501|insufficient_privilege'::text as pat;
391
+ select agent_board_test.re(
392
+ agent_board_test.ci_re('errcode') || '\s*=\s*''?42501'
393
+ || '|' || agent_board_test.ci_re('sqlstate') || '\s+''?42501'
394
+ || '|' || agent_board_test.ci_re('insufficient_privilege')
395
+ ) as pat;
390
396
 
391
397
  -- THE POPULATION, per-OID and TRANSITIVE: an authenticated-executable root that reaches a direct
392
398
  -- writer through the call closure. `fn_call_closure()` is REFLEXIVE (every function reaches
@@ -580,7 +586,7 @@ select is(
580
586
  where cl.root_oid = p.oid
581
587
  and cl.reached_name in ('_resolved_actor_agent_id', 'current_agent_id',
582
588
  '_require_platform_admin_user_id'))
583
- and lower(agent_board_test.stripped_body(p.oid, true)) !~ 'auth\s*\.\s*uid\s*\('),
589
+ and agent_board_test.stripped_body(p.oid, true) !~ agent_board_test.call_re('auth.uid', false)),
584
590
  array[]::text[],
585
591
  '#325 HALF A: EVERY authenticated-executable writer derives its actor from the SESSION — see the header on why this spelling is fail-OPEN against a dead closure and which assertions cover that');
586
592
 
@@ -599,7 +605,7 @@ select is(
599
605
  select is(
600
606
  (select coalesce(array_agg(p.sig order by p.sig), array[]::text[])
601
607
  from _wec_pop p
602
- where lower(agent_board_test.stripped_body(p.oid, true)) ~ 'auth\s*\.\s*uid\s*\('),
608
+ where agent_board_test.stripped_body(p.oid, true) ~ agent_board_test.call_re('auth.uid', false)),
603
609
  array['agent_board.self_enroll_human(text)']::text[],
604
610
  '#325: exactly ONE writer derives from raw auth.uid() — self_enroll_human CREATES the identity a session would resolve to, so it cannot use the resolver; scored on the own body because the closure form matches all 59 and would make Half A vacuous');
605
611
 
@@ -649,7 +655,7 @@ select is( -- pgtap-allow-raise-checked
649
655
  and (ap.proname ~ '^_(current_)?agent_(can|has)_'
650
656
  or ap.proname ~ '^_grant_within_'
651
657
  or ap.proname in ('_invite_permitted', '_membership_consent_present')))
652
- and lower(agent_board_test.stripped_body(p.oid, true)) !~ (select pat from _wec_raise_pat)) x),
658
+ and agent_board_test.stripped_body(p.oid, true) !~ (select pat from _wec_raise_pat)) x),
653
659
  array[
654
660
  'agent_board.block_board_inviter(uuid,uuid,uuid,jsonb) -> _resolved_actor_agent_id',
655
661
  'agent_board.create_or_find_thread(uuid,text,text,text,jsonb) -> _resolved_actor_agent_id',
@@ -698,7 +704,7 @@ select is(
698
704
  and (ap.proname ~ '^_(current_)?agent_(can|has)_'
699
705
  or ap.proname ~ '^_grant_within_'
700
706
  or ap.proname in ('_invite_permitted', '_membership_consent_present')))
701
- and lower(agent_board_test.stripped_body(p.oid, true)) !~ (select pat from _wec_raise_pat)
707
+ and agent_board_test.stripped_body(p.oid, true) !~ (select pat from _wec_raise_pat)
702
708
  and not exists (
703
709
  select 1 from _wec_closure cl
704
710
  where cl.root_oid = p.oid
@@ -750,9 +756,9 @@ select ok(
750
756
  -- exclusion is currently the STRICTER choice, so a failure here means WEC-8 is being harder than
751
757
  -- it needs to be, not that it has gone soft.
752
758
  select ok(
753
- lower(agent_board_test.stripped_body(
759
+ agent_board_test.stripped_body(
754
760
  (select oid from pg_proc where pronamespace = 'agent_board'::regnamespace
755
- and proname = 'current_agent_id'), true)) !~ (select pat from _wec_raise_pat),
761
+ and proname = 'current_agent_id'), true) !~ (select pat from _wec_raise_pat),
756
762
  '#325: current_agent_id does NOT raise — it returns NULL on an unresolved principal, which is why it is not counted as a refusal in WEC-8');
757
763
 
758
764
  -- ===========================================================================================
@@ -1012,6 +1018,118 @@ select is(
1012
1018
  '88',
1013
1019
  '#325: 88 writing referential actions (74 CASCADE + 14 SET NULL) reach the 37 class relations — bump this number only after confirming the new edge is intended; see WEC-22, which names parents independently');
1014
1020
 
1021
+ -- ===========================================================================================
1022
+ -- WEC-24/25 — THE CROSS-CHECK (Consiliency/message-board#416 / #417 / #424, plan change 9).
1023
+ -- `write_re` is the suite's only definition of "writes" (the lint in
1024
+ -- scripts/check-test-source-scan-literals.mjs is what makes that true). This bounds it from the
1025
+ -- other side with a PARSER: plpgsql_check's plpgsql_show_dependency_tb, created inside this
1026
+ -- transaction and rolled back with it. For every plpgsql function in agent_board, the class
1027
+ -- relations the regex says it WRITES must be a SUBSET of the relations the parser says it
1028
+ -- TOUCHES.
1029
+ --
1030
+ -- WHAT THIS BOUNDS, STATED NARROWLY. The parser does not distinguish a read from a write, so a
1031
+ -- regex "writer" of a relation the function legitimately READS is still a subset member and is
1032
+ -- NOT caught here. The check convicts only a regex writer of a relation the parser never
1033
+ -- touches: a name matched in a comment or a literal, a look-alike identifier, or a relation
1034
+ -- named only inside dynamic SQL. The reverse direction (parser dependency, regex silent) is
1035
+ -- deliberately NOT asserted: dynamic SQL yields no parser dependency at all, and every function
1036
+ -- that reads A and writes B would be flagged on A — a list of false positives with a reason each.
1037
+ --
1038
+ -- DYNAMIC SQL. A body that EXECUTEs a COMPUTED string is parser-blind: `execute format('update
1039
+ -- agent_board.boards …')` or `q := 'update agent_board.boards …'; execute q;` is a regex writer
1040
+ -- (literals are KEPT in `_wec_direct`'s subject, because a raise's errcode literal must be
1041
+ -- visible) with no dependency — a FALSE over-match verdict. Measured before writing this: the
1042
+ -- parser DOES read a CONSTANT `execute 'update agent_board.boards …'` (it reports the relation),
1043
+ -- so the plan's m32 fixture in that spelling flips nothing; the variable-held spelling is the
1044
+ -- mutation. The subset check therefore SKIPS any function whose
1045
+ -- literal-stripped body contains an `execute` token, and WEC-25 names those functions
1046
+ -- positively. Measured on this tree: exactly `mark_retention_metadata`, whose dynamic targets —
1047
+ -- from its own allowlist literal — are messages, task_events, presence, callback_outbox_events,
1048
+ -- callback_delivery_attempts, inbox_items, delivery_recipients and board_memberships. That last
1049
+ -- one is a CLASS relation and the write is `update agent_board.%1$I …` inside format():
1050
+ -- invisible to `write_re('board_memberships')` AND to the parser. WEC-25 is therefore not
1051
+ -- decoration: it is the only assertion in this suite that names a writer of board_memberships
1052
+ -- neither definition of "writes" can see, and a second dynamic writer fails it by name. Its
1053
+ -- TARGET list above is prose: nothing here pins it (recorded as unpinned, plan m33).
1054
+ --
1055
+ -- TRIGGER FUNCTIONS need the relation they fire on to be parsed (NEW/OLD are typed by it); each
1056
+ -- is parsed once per pg_trigger attachment and the dependency sets are unioned. An UNATTACHED
1057
+ -- trigger function cannot be parsed and is reported as `PARSER-UNREADABLE`, not skipped.
1058
+ --
1059
+ -- PREREQUISITE. If plpgsql_check is not available on the cluster, WEC-24 reads
1060
+ -- `PREREQUISITE-MISSING plpgsql_check` and FAILS by name — never `skip`: undetermined is not a
1061
+ -- pass (AGENTS.md rule 4). Both the local cluster and CI's `supabase start` image ship it.
1062
+ -- ===========================================================================================
1063
+ do $$
1064
+ begin
1065
+ if exists (select 1 from pg_available_extensions where name = 'plpgsql_check') then
1066
+ execute 'create extension if not exists plpgsql_check with schema extensions';
1067
+ end if;
1068
+ end $$;
1069
+
1070
+ -- The parser's relation set for one function, reached through dynamic SQL so that an absent
1071
+ -- extension yields NULL here instead of a parse error that would abort the file (#113 class).
1072
+ create function pg_temp._wec_parser_rels(p_fn oid, p_rel oid) returns text[]
1073
+ language plpgsql as $$
1074
+ declare v_rels text[]; v_ns text;
1075
+ begin
1076
+ select e.extnamespace::regnamespace::text into v_ns from pg_extension e where e.extname = 'plpgsql_check';
1077
+ if v_ns is null then
1078
+ return null;
1079
+ end if;
1080
+ execute format($q$ select coalesce(array_agg(distinct d.schema || '.' || d.name), '{}'::text[])
1081
+ from %I.plpgsql_show_dependency_tb($1::regprocedure, $2::regclass) d
1082
+ where d.type = 'RELATION' $q$, v_ns)
1083
+ into v_rels using p_fn, p_rel;
1084
+ return v_rels;
1085
+ end $$;
1086
+
1087
+ create temp table _wec_plpgsql on commit drop as
1088
+ select p.oid as fn_oid, p.proname::text as proname,
1089
+ p.prorettype = 'trigger'::regtype as is_trigger,
1090
+ agent_board_test.stripped_body(p.oid) ~ agent_board_test.re('\m' || agent_board_test.ci_re('execute') || '\M') as dynamic_sql
1091
+ from pg_proc p
1092
+ where p.pronamespace = 'agent_board'::regnamespace
1093
+ and p.prokind in ('f', 'p')
1094
+ and p.prolang = (select oid from pg_language where lanname = 'plpgsql');
1095
+
1096
+ create temp table _wec_parser on commit drop as
1097
+ select f.fn_oid,
1098
+ (select count(*) from pg_extension where extname = 'plpgsql_check') > 0 as parser_present,
1099
+ f.is_trigger and not exists (select 1 from pg_trigger tg where tg.tgfoid = f.fn_oid) as unattached_trigger,
1100
+ (select array_agg(distinct r)
1101
+ from (select tg.tgrelid from pg_trigger tg where tg.tgfoid = f.fn_oid
1102
+ union all select 0::oid where not f.is_trigger) tr
1103
+ cross join lateral unnest(coalesce(pg_temp._wec_parser_rels(f.fn_oid, tr.tgrelid), '{}'::text[])) as r) as rels
1104
+ from _wec_plpgsql f;
1105
+
1106
+ select is(
1107
+ (select case
1108
+ when not bool_and(parser_present) then 'PREREQUISITE-MISSING plpgsql_check'
1109
+ else coalesce(string_agg(x.entry, ', ' order by x.entry), '<none>')
1110
+ end
1111
+ from _wec_parser pr
1112
+ left join lateral (
1113
+ select f.proname || ':' || w.relname as entry
1114
+ from _wec_plpgsql f
1115
+ join _wec_direct w on w.fn_oid = f.fn_oid
1116
+ where f.fn_oid = pr.fn_oid
1117
+ and not f.dynamic_sql
1118
+ and (pr.unattached_trigger
1119
+ or not ('agent_board.' || w.relname = any (coalesce(pr.rels, '{}'::text[]))))
1120
+ union all
1121
+ select f.proname || ':PARSER-UNREADABLE'
1122
+ from _wec_plpgsql f
1123
+ where f.fn_oid = pr.fn_oid and pr.unattached_trigger and not f.dynamic_sql
1124
+ ) x on true),
1125
+ '<none>',
1126
+ 'WEC-24 (change 9): every class relation `write_re` says a static plpgsql body writes is a relation the parser says that body touches — a regex writer the parser cannot see (comment, literal, look-alike, dynamic SQL) is named here as `function:relation`');
1127
+
1128
+ select is(
1129
+ (select coalesce(string_agg(proname, ', ' order by proname), '<none>') from _wec_plpgsql where dynamic_sql),
1130
+ 'mark_retention_metadata',
1131
+ 'WEC-25 (change 9): the plpgsql bodies WEC-24 skips for dynamic SQL are exactly these — mark_retention_metadata writes board_memberships through format(), which neither write_re nor the parser can see; a second dynamic writer fails here by name');
1132
+
1015
1133
  select * from finish();
1016
1134
 
1017
1135
  rollback;