Topic Promotion Governed Activation — 05 Scanner Expansion (2026-06-03)
05 — Objective D: Scanner Integration Expansion — PASS
Artifacts: sql/D_scanner.sql + sql/D_scanner_fix.sql. Function: fn_governance_scan(actor, mode) RETURNS scan_id.
Coverage (one bounded scan)
Ownership / topic / axis-registry / axis-assignment coverage + all drift outputs + candidate-state summaries → governance_scan_run (ledger: finding/new/resolved counts + summary jsonb) and governance_drift_state (idempotent upsert target; open/resolved lifecycle).
Design properties
Bounded (reads bounded views, never object-grain); idempotent (ON CONFLICT DO UPDATE increments scan_count); rerun-safe/resolve (unseen findings flip to resolved); no daemon (on-demand function).
Bug found & fixed (genuine test value)
First build keyed state on (detector_code, object_type, object_ref, value_code) — omitting axis_code. With a second per-assignment coverage rule, axis_assignment_missing emits one NULL-value row per axis for the same collection → collision → "ON CONFLICT cannot affect row a second time". Fix: added axis_code to state + unique key (detector_code, COALESCE(axis_code,'∅'), object_type, object_ref, COALESCE(value_code,'∅')) + SELECT DISTINCT in scanner CTE.
Idempotency test (3 passes, no data change)
pass1: 41 rows / scan_count 1 / new 41. pass2: 41 / 2 / 0. pass3: 41 / 3 / 0. Row count stable at 41 (bounded); scan_count increments uniformly.
Resolve-transition test (rollback-only)
Added containment coverage_rule → re-scan → coverage_rule_missing(containment) flipped to resolved; collision now cleanly split axis_assignment_missing containment=35 / topic=29 (no error). ROLLBACK restored 41 open. Proves both fix and resolve path on the exact triggering input.
Verdict
PASS. Expanded scanner idempotent, bounded, rerun-safe, daemon-free, verified incl. resolve transition.