One-Roof Clone Axis/Topic — 05 Scanner/Candidate Integration (Obj D, PASS, idempotent)
05 — Objective D: Scanner / Candidate Integration
Verdict: PASS — committed. SQL: sql/D_scanner_candidate_integration_clone_COMMIT.sql. 9/9 checks PASS; idempotency and bounded growth proven.
What was added
v_axis_coverage_summary— one row per active axis withrequired_cells / gap_cells / ok / low_conf / stale / quarantine. This is the scanner's summary metric surface (read contract in doc 07).- An idempotent axis-finding scan (
pg_temp.run_axis_scan()) that UPSERTs into the persistent clone-localaxis_candidate_findingtable from the quality/gap/missing views.
The integrated read path (what the scanner now sees)
| axis | required | gap | ok | low_conf | stale | quarantine |
|---|---|---|---|---|---|---|
| responsibility | 210 | 0 | 0* | 0 | 0 | 0 |
| topic | 6 | 2 | 3 | 1 | 1 | 1 |
*responsibility coverage is owner-based, not assignment-based, so it has no assignment-quality rows — its health is the gap (0).
Findings written (clone-local, bounded)
| finding_type | severity | count |
|---|---|---|
| missing_owner | warning | 2 |
| low_confidence | warning | 1 |
| stale_topic | warning | 1 |
| quarantine | warning | 1 |
| missing_assignment | info | 29 |
| total | 34 |
Idempotency + bounded growth (the load-bearing property)
The scan was run twice in one transaction:
- pass 1 row count = 34; pass 2 row count = 34 → identical, no growth.
max(scan_count)= 2 → re-seen findings are updated (last_seen,scan_count+1), not duplicated — the PK(finding_type,object_type,object_ref,axis_code,value_code)+ON CONFLICT DO UPDATEmakes the scan convergent.- The finding set is bounded by
inventory × finding_types(a fixed ceiling), andmissing_assignmentisinfo-severity and capped at the unclassified-inventory count (29 ≤ 35). No object-grain explosion, no unbounded scan.
No daemon, no worker loop
The scan is a single deterministic pass invoked explicitly — there is no background worker, no pg_notify, no polling. Running it again is safe and converges. This matches the project's standing "no uncontrolled worker loop" rule: the cursor/worker machinery exists (gov_worker_cursor) but nothing here starts a loop; a future governed scheduler would call this same convergent function.
Relation to the existing candidate path
axis_candidate_finding is the axis analogue of governance_candidate_state/governance_candidate_object — a no-checked-forever, re-derivable finding store. It is kept separate (clone-local, additive) rather than overloading the existing candidate tables, so the existing scanner state is untouched and the axis findings can be dropped independently.