5000x · runtime/350 · auto-refresh trigger production pilot
03 — runtime/350 · auto-refresh trigger 5000x production pilot
1. Why a second pilot
4000x's runtime/340 proved migration 024 fires durably once, on one
row, on information_unit. Three invariants from sandbox/230 were
covered only under BEGIN/ROLLBACK:
- statement-level granularity (a bulk UPDATE over N rows fires the trigger ONCE, not N times);
- both triggers (
information_unitANDiu_metadata_tag) share the same gate + function; - closing the gate mid-transaction silences the trigger in real time.
runtime/350 promotes all three to durable production proofs.
2. Sequence (single transaction, COMMIT)
350.1 precondition: gate=false
350.2 UPDATE dot_config SET value='true' WHERE key='iu_core.three_axis_auto_refresh_enabled'
350.3 BULK UPDATE information_unit SET updated_at=now() WHERE id IN (3 enacted)
350.4 → 1 refresh_log row added (statement-level: 3 rows, 1 fire) ✓ PASS
350.5 BULK UPDATE iu_metadata_tag SET assigned_at=assigned_at WHERE id IN (3)
350.6 → 2 refresh_log rows total since marker (IU side + tag side) ✓ PASS
350.7 UPDATE dot_config SET value='false'
350.8 UPDATE information_unit again (single row, post-close)
350.9 → still 2 rows since marker — gate closed silenced trigger ✓ PASS
350.10 final_state: gate=false / cache_healthy=t / in_sync=t / errors=0 ✓ PASS
350.11 fail-closed: exception if rows ≠ 2 or trigger_error_log > 0 ✓ PASS
350.12 rename the 2 audit rows actor=iu_lifecycle_trigger → iu_5000x_pilot
COMMIT
3. Durable side effects
iu_three_axis_envelope_refresh_log: +2 rows underactor='iu_5000x_pilot', bothoutcome='skipped_in_sync',dry_run=false,forced=false,upserted_count=0,deleted_count=0,drift_pre/post={…}.iu_three_axis_envelope_trigger_error_log: unchanged at 0 rows.dot_config.iu_core.three_axis_auto_refresh_enabled: back tofalse(wastrueonly inside this transaction).- All other tables unchanged.
The actor='iu_5000x_pilot' tag (350.12) lets the rollback delete
exactly this run's rows without disturbing the 4000x runtime/340 rows
(actor='iu_lifecycle_trigger') or any prior macro's audit rows.
4. Why SET assigned_at = assigned_at is the right tag-side trigger probe
iu_metadata_tag has no updated_at column — it has assigned_at.
The semantically-cleanest no-op rewrite is therefore
SET assigned_at = assigned_at. Postgres still treats this as a row
update for MVCC purposes (a new tuple is written), so the AFTER UPDATE
FOR EACH STATEMENT trigger fires. Statement-level granularity is
preserved: 3 updated rows → exactly one trigger invocation.
5. Why this does not introduce drift
- The IU-side UPDATE only touches
updated_at, which is not a column the envelope projects.fn_iu_three_axis_envelope_drift_checksees no change inview_countortable_count; outcome staysskipped_in_sync. - The tag-side UPDATE rewrites
assigned_atto itself — the column is not part ofiu_metadata_tag_registry-driven derivation, so axis B's derived tags don't move; outcome staysskipped_in_sync.
Both UPDATEs are pure no-ops at the envelope level; the trigger is the only durable observer.
6. Rollback
sql/iu-core/runtime/rollback/350_…rollback.sql:
DELETE FROM public.iu_three_axis_envelope_refresh_log
WHERE actor = 'iu_5000x_pilot';
A single-row predicate, scoped only to this run. Does NOT touch:
- runtime/340 rows (
actor='iu_lifecycle_trigger') — these stay as historical record; - the gate value (already
falseat COMMIT); - the trigger function, triggers, or error log (migration 024 rollback handles those).
7. Five-layer impact
| layer | impact |
|---|---|
| PG | +2 audit rows under actor='iu_5000x_pilot'; gate transient |
| Directus | none |
| Nuxt | none |
| AgentData | +1 KB report (this doc) |
| Qdrant | none |