Skip to content

Latest commit

 

History

History
371 lines (339 loc) · 90.2 KB

File metadata and controls

371 lines (339 loc) · 90.2 KB

Traceability Matrix: Elevarq Signals

Legend

  • BEHAVIORAL: Tests exercising actual code behavior (function calls, HTTP requests, data output)
  • STRUCTURAL: Tests verifying code structure (source scanning, file inspection)
  • INTEGRATION: Tests requiring live PostgreSQL (build-tag guarded)

Coverage Status values:

  • COVERED: rule is implemented and has at least one passing test.
  • PLANNED: rule is design-only at this point; implementation and tests land in a phased follow-up. The Test ID column lists the test cases the design anticipates.
Rule ID Rule Summary Test ID(s) Coverage Status Evidence Type Notes
R001 PostgreSQL connection with supplied params TC-SIG-001 COVERED BEHAVIORAL Connection config validates host/port/dbname/user, application_name, default port
R002 Execute only approved collector queries TC-SIG-002, TC-SIG-003 COVERED BEHAVIORAL Linter rejects DDL/DML/dangerous functions; accepts valid SELECT
R003 Collect diagnostic data from PG sources TC-SIG-004, TC-SIG-005 COVERED BEHAVIORAL Catalog contains 9+ required query IDs; sorted output verified
R004 Structured snapshot format (NDJSON) TC-SIG-006, TC-SIG-007, TC-SIG-023 COVERED BEHAVIORAL Encode/decode roundtrip; compression at 4096 threshold; data types preserved
R005 Snapshot metadata present TC-SIG-008 COVERED BEHAVIORAL Export produces metadata.json with schema_version, collector fields, RFC3339 timestamp
R006 Package snapshots into ZIP TC-SIG-009, TC-SIG-024, TC-SIG-128, TC-SIG-129 COVERED BEHAVIORAL ZIP contains required files; omits analyzer-only files; default no-data export is refused (no false-clean, R125); every emitted ZIP carries collection_status
R007 No scoring, recommendations, LLM TC-SIG-010, TC-SIG-011, TC-SIG-012 COVERED STRUCTURAL Source scan confirms no analysis/scoring/LLM modules present
R008 No external AI network calls TC-SIG-013 COVERED STRUCTURAL Source scan confirms no LLM transport code
R009 Suitable for OSS release TC-SIG-014 COVERED STRUCTURAL LICENSE present; no proprietary markers in source
R010 Stable CLI with commands TC-SIG-015 COVERED STRUCTURAL CLI help output includes required commands
R011 HTTP API endpoints (see Appendix A) TC-SIG-016, TC-SIG-017, TC-SIG-041 COVERED BEHAVIORAL /health 200 no auth; /status fields + no secret fields; /collect/now 202; /export ZIP
R012 Per-query and per-target timeouts TC-SIG-018 COVERED BEHAVIORAL Default timeouts verified; timeout options override correctly
R013 Three-layer read-only enforcement TC-SIG-019 COVERED BEHAVIORAL + STRUCTURAL Linter tested (10 cases); session param verified; ReadOnly tx verified
R014 Version and extension filtering TC-SIG-020 COVERED BEHAVIORAL Extension exclusion/inclusion; sorted output
R015 Cadence-based scheduling TC-SIG-021 COVERED BEHAVIORAL Duration constants; due-selection behavior; no catch-up
R016 Credentials never cached/exported TC-SIG-022 COVERED STRUCTURAL No password column in schema; no Password field in storage types
R017 Session read-only guard TC-SIG-025 COVERED BEHAVIORAL + STRUCTURAL Connection config sets read-only param; verified on acquired connection
R018 Refuse superuser role TC-SIG-026, TC-SIG-034 COVERED BEHAVIORAL Superuser hard failure blocks; default is blocking
R019 Refuse replication role TC-SIG-027 COVERED BEHAVIORAL Replication hard failure blocks
R020 Refuse bypassrls role TC-SIG-028 COVERED BEHAVIORAL BypassRLS hard failure blocks
R021 Read-only transaction enforcement TC-SIG-025 COVERED BEHAVIORAL + STRUCTURAL Dedicated connection; SET LOCAL timeouts inside transaction
R022 Session timeout enforcement TC-SIG-029 COVERED BEHAVIORAL + STRUCTURAL Default timeouts; SET LOCAL verified; lock_timeout=5000
R023 Hard vs soft failure distinction TC-SIG-030 COVERED BEHAVIORAL Warnings do not block; hard failures do
R024 No secrets in logs/API/export TC-SIG-031, TC-SIG-041 COVERED BEHAVIORAL DSN redaction; /status excludes secret fields
R025 Actionable error messages TC-SIG-032 COVERED BEHAVIORAL Error contains remediation guidance
R026 Unsafe override model TC-SIG-033, TC-SIG-034, TC-SIG-042 COVERED BEHAVIORAL Override option; default false; export metadata with bypass reasons
R027 Configuration via YAML + env vars TC-SIG-040 COVERED BEHAVIORAL YAML loading, env override precedence, default values
R028 Config file search order TC-SIG-040 COVERED BEHAVIORAL Defaults applied when no file found
R029 Single-target container mode via env TC-SIG-040 COVERED BEHAVIORAL SIGNALS_TARGET_* creates target with correct fields and defaults
R030 Config validation at startup TC-SIG-040 COVERED BEHAVIORAL Validate catches short interval, zero retention, empty fields, multiple secrets, bad durations
R031 Initial forced collection TC-SIG-037 COVERED BEHAVIORAL First cycle fires immediately; collect_error event proves execution
R032 Overlap prevention TC-SIG-038 COVERED BEHAVIORAL Rapid CollectNow calls do not block (buffered channel dedup)
R033 Concurrent multi-target collection TC-SIG-039 COVERED BEHAVIORAL 3 targets with maxConcurrent=2; all 3 attempted; errors per target
R034 Commit failure blocks persistence TC-SIG-036 COVERED STRUCTURAL Commit error checked; return precedes downstream persistence
R035 Export metadata contract TC-SIG-042 COVERED BEHAVIORAL Export metadata contains unsafe_mode and dynamic bypass reasons
R036 Persistence guarantees TC-SIG-043 COVERED BEHAVIORAL Migration creates tables; instance ID stable; retention cleanup; batch insert atomic
R037 Dynamic column capture for version-sensitive views TC-SIG-044 COVERED BEHAVIORAL pg_stat_statements uses SELECT *; linter accepts; NDJSON preserves all dynamic columns including future/renamed ones
R038 Query failure isolation TC-SIG-045 COVERED BEHAVIORAL + STRUCTURAL Savepoint isolation in collector; ROLLBACK TO SAVEPOINT on failure; transaction not aborted
R039 Dynamic capture preserves safety model TC-SIG-046 COVERED BEHAVIORAL Dynamic query passes linter; no write keywords; extension gating preserved
R040 Server identity collection TC-SIG-047 COVERED BEHAVIORAL Registered, passes linter
R041 Extension inventory TC-SIG-047 COVERED BEHAVIORAL Registered, passes linter
R042 Checkpoint/bgwriter health TC-SIG-047 COVERED BEHAVIORAL Registered, passes linter
R043 Long-running transactions TC-SIG-047 COVERED BEHAVIORAL Registered, passes linter
R044 Lock-blocking chains TC-SIG-047 COVERED BEHAVIORAL Registered, passes linter
R045 Login roles inventory (no password hashes) TC-SIG-047 COVERED BEHAVIORAL Registered, passes linter, no pg_authid/password access
R046 Connection utilization TC-SIG-047 COVERED BEHAVIORAL Registered, passes linter
R047 Planner stats staleness TC-SIG-047 COVERED BEHAVIORAL Registered, passes linter
R048 pg_stat_statements reset check TC-SIG-047 COVERED BEHAVIORAL Registered, requires extension, gracefully skipped when absent
R049 Replication slot risk TC-SIG-048 COVERED BEHAVIORAL Registered, no extension required, graceful empty result when no slots
R050 Replication status/lag TC-SIG-048 COVERED BEHAVIORAL Registered, graceful empty result when no replicas
R051 Checkpointer stats (PG 17+) TC-SIG-048 COVERED BEHAVIORAL Registered, MinPGVersion=17 verified, excluded on PG 16
R052 Vacuum health diagnostic TC-SIG-048 COVERED BEHAVIORAL Registered, includes dead_pct/xid_age/reloptions, adds value over raw stats
R053 Idle-in-transaction offenders TC-SIG-048 COVERED BEHAVIORAL Registered, filters for idle-in-txn state, excludes own PID
R054 Database sizes TC-SIG-048 COVERED BEHAVIORAL Registered, passes linter
R055 Largest relations TC-SIG-048 COVERED BEHAVIORAL Registered, passes linter
R056 Temp I/O pressure TC-SIG-048 COVERED BEHAVIORAL Registered, passes linter, no secrets
R057 Schema constraint inventory TC-SIG-050 COVERED BEHAVIORAL pg_constraints_v1: registered, linter pass, 24h cadence, schema filter, unnest/ordinality multi-column, deterministic order, constraint validity (is_validated from convalidated, #342)
R058 Schema index definitions TC-SIG-050 COVERED BEHAVIORAL pg_indexes_v1: registered, linter pass, 24h cadence, schema filter, indexdef included, COALESCE tablespace
R059 Column planner statistics TC-SIG-051 COVERED BEHAVIORAL pg_stats_v1: registered, linter pass, 24h cadence, schema filter, no data samples, n_distinct/correlation included
R060 Column inventory with types TC-SIG-052 COVERED BEHAVIORAL pg_columns_v1: registered, linter pass, 24h cadence, schema filter, pg_attribute native, format_type, no default text, excludes system/dropped columns
R061 Schema namespace inventory TC-SIG-053 COVERED BEHAVIORAL pg_schemas_v1: registered, linter pass, 24h cadence, schema filter, pg_namespace + pg_roles join, nspname/nspowner/is_default
R062 View inventory TC-SIG-054 COVERED BEHAVIORAL pg_views_v1: registered, linter pass, 24h cadence, schema filter, inventory mode (no definition), pg_views source
R063 View definitions TC-SIG-054 COVERED BEHAVIORAL pg_views_definitions_v1: registered, linter pass, includes pg_get_viewdef definition column, separate from inventory
R064 Materialized view inventory TC-SIG-055 COVERED BEHAVIORAL pg_matviews_v1: registered, linter pass, 24h cadence, schema filter, inventory mode, ispopulated + hasindexes
R065 Materialized view definitions TC-SIG-055 COVERED BEHAVIORAL pg_matviews_definitions_v1: registered, linter pass, includes definition column, separate from inventory
R066 Partition topology TC-SIG-056 COVERED BEHAVIORAL pg_partitions_v1: registered, linter pass, 24h cadence, schema filter, pg_partitioned_table + pg_inherits + pg_get_partkeydef, parent/child with bounds
R067 Trigger inventory TC-SIG-057 COVERED BEHAVIORAL pg_triggers_v1: registered, linter pass, 24h cadence, schema filter, tgtype bitmask, excludes internal triggers, pg_trigger + pg_proc
R068 Trigger definitions TC-SIG-057 COVERED BEHAVIORAL pg_triggers_definitions_v1: registered, linter pass, includes pg_get_triggerdef, separate from inventory
R069 Function/procedure inventory TC-SIG-058 COVERED BEHAVIORAL pg_functions_v1: registered, linter pass, 24h cadence, PG 11+, schema filter, prokind, no body, pg_proc + pg_language
R070 Function body definitions TC-SIG-058 COVERED BEHAVIORAL pg_functions_definitions_v1: registered, linter pass, PG 11+, includes prosrc as body; HighSensitivity = true on the skip-path under R075 v2 (runs by default; opt-out via signals.high_sensitivity_collectors_enabled: false drops it as status=skipped, reason=config_disabled)
R071 Sequence inventory and health TC-SIG-059 COVERED BEHAVIORAL pg_sequences_v1: registered, linter pass, 24h cadence, schema filter, pg_sequences view, 9 output columns
R072 Collector execution status metadata TC-SIG-060 COVERED BEHAVIORAL collector_status.json: status types (success/partial/skipped/failed), reason categories, JSON shape, deterministic ordering, builder helpers, ZIP integration
R073 Multi-target export correctness TC-SIG-061 COVERED BEHAVIORAL Target-filtered query runs/results, target-scoped collector_status.json, BuildStatusFromRuns, error classification, GetTargetName
R074 Deterministic ordering TC-SIG-050..061 COVERED BEHAVIORAL All schema collectors use ORDER BY; collector_status sorted by ID; export ZIP in fixed file order
R075 High-sensitivity collectors: default-on with opt-out (revised 2026-05) TC-SIG-062 COVERED BEHAVIORAL signals.high_sensitivity_collectors_enabled defaults to true (collect-everything default). When set to false, the opt-out branches per collector via QueryDef.SensitiveColumns: redact-path collectors (live pg_stat_activity: long_running_txns_v1, blocking_locks_v1, idle_in_txn_offenders_v1, wraparound_blockers_v1) keep running with their declared columns NULL-ed at execution time; skip-path collectors (whole-row-sensitive — DDL definitions, sampled-value stats, RLS policies, rewrite rules) are dropped and appear with status=skipped, reason=config_disabled (EA-R001). metadata.json.high_sensitivity_collectors_enabled records the effective state.
R076 Strict startup config validation TC-SIG-063 COVERED BEHAVIORAL ValidateStrict returns (warnings, error); daemon aborts on hard errors (malformed env ints, duplicate target names, multiple credential sources, non-positive intervals, missing required fields); warnings logged and continue (sslmode=prefer in non-prod, very short intervals)
R077 Atomic collection-cycle persistence TC-SIG-064 COVERED BEHAVIORAL Query runs, query results, and snapshot row persist in a single SQLite transaction via InsertCollectionAtomic; partial state not observable to readers or exports
R078 Audit logging and export metadata TC-SIG-065 COVERED BEHAVIORAL audit_event slog records for startup config validation, per-target collection cycles, and export requests; export metadata.json carries signals_version, schema_version, generated_at, instance_id, target_name (when scoped), high_sensitivity_collectors_enabled, collector_status_schema_version; no secrets or query payloads in audit attributes
R079 Operational metrics endpoint TC-SIG-066 COVERED BEHAVIORAL Optional Prometheus /metrics endpoint, off by default, gated by signals.metrics_enabled; emits 12 operational counters/gauges/histograms with bounded labels (target, status, reason, error_category); never exposes SQL text, query results, definitions, hostnames, dbnames, usernames, or paths; inherits the API's bearer auth
R080 Per-collector export view TC-SIG-067, TC-SIG-130..137 (tests/signals_per_collector_export_test.go) COVERED BEHAVIORAL Optional per-collector/<query_id>.json directory in the export ZIP, off by default, gated by signals.export_per_collector_files; canonical NDJSON layout remains authoritative. Strict regrouping of the already-resolved in-scope run set used by query_runs.ndjson / query_results.ndjson (issue #322) — never re-queried with a narrower selector, so --snapshot-id, --target-id, --all, --since/--until, and the R084 default (active targets only, orphans/disabled excluded) all match the canonical files. Grouped by (target_id, query_id), latest-run-wins, so multi-target exports keep target attribution (per-collector/<target_id>/<query_id>.json) instead of one target silently winning a query_id-only grouping. A missing/corrupt payload for a successful in-scope run fails the export (same guard as query_results.ndjson), never a success stub.
R081 Version-aware query catalog TC-SIG-068 COVERED BEHAVIORAL Per-cycle discovery probe (server_version_num, major, extensions, database, current_user); per-major catalog files pg14..pg18 + pg19 placeholder; stable logical collector IDs; output column normalization across majors; PG 18 fixes pg_stat_io_v1 (read_bytes/write_bytes/extend_bytes vs op_bytes) and pg_stat_wal_v1 (wal_writes/wal_syncs vs wal_write/wal_sync); experimental fallback for PG 19
R082 (Phase 1) Optional target narrowing on POST /collect/now TC-SIG-069..073 COVERED BEHAVIORAL Empty body unchanged; valid subset accepted; unknown / disabled / empty-array targets → 400 with rejected list; cycle not triggered on any rejection.
R082 (Phase 2) request_id + reason + audit correlation TC-SIG-074..080 COVERED BEHAVIORAL Optional request_id (^[A-Za-z0-9_-]{1,32}$) + reason (^[A-Za-z0-9_-]{1,64}$); ULID generated when absent. New audit events collect_now_requested / collect_now_rejected / collect_now_dropped. request_id propagated through collection_started / collection_completed. actor=local_operator invariant — control_plane reserved for Phase 3. R078 denylist still in force; no secrets / SQL / payload bodies leak into audit attributes.
R082 (Phase 3) Mode flag + control_plane token + entitlement protocol TC-SIG-081..092 COVERED BEHAVIORAL Realised by R083 (v0.3.1). Phase 4 (collector profiles, entitlement metadata, status callbacks) remains future work and is out of scope for R083.
R083 Mode B authentication + configuration TC-SIG-081..092 (tests/signals_r083_managed_mode_test.go) COVERED BEHAVIORAL Implemented in v0.3.1. signals.mode: standalone | managed config flag (internal/config/config.go); separate control_plane_token_file/_env with constant-time dual-token comparison and actor derivation in the auth middleware (internal/api/server.go). control_plane actor only valid in mode=managed. Startup validation hard-errors for missing / duplicate / short token, conflicting _file+_env. Audit-event actor decoration in internal/safety/audit.go.
R084 Default export scope = latest run per collector per active target TC-SIG-093, TC-SIG-121 PLANNED BEHAVIORAL Default signalsctl export (no flags) returns the latest run of each collector per active target — the row with the largest collected_at per (target_id, query_id) — plus the snapshots those runs belong to. Supersedes the interim latest-snapshot-per-target default, which dropped lower-cadence collectors after a high-cadence cycle (issue #5). Full history remains behind --all.
R085 Explicit export selectors (--all, --snapshot-id, --since/--until, --target-id) TC-SIG-094, TC-SIG-095, TC-SIG-096 PLANNED BEHAVIORAL Operator-driven selectors widen or narrow the R084 default. --all and --snapshot-id are mutually exclusive (FC-08). Existing --target-id and time-range params plumb through unchanged; this rule formalises them in the CLI.
R086 Export metadata extensions: snapshot_count + run_scope + ingest_mode TC-SIG-097, TC-SIG-098, TC-SIG-122 PLANNED BEHAVIORAL Required metadata.json fields: snapshot_count (integer ≥ 0, distinct snapshots in the ZIP), run_scope ("latest-per-collector" for the R084 default | "snapshot" for selector scopes), and ingest_mode ("analyze" | "history_only"). Operator-driven exports always emit "analyze"; "history_only" is reserved for R087 backlog replay.
R087 Backlog replay shape (one ZIP per snapshot, ordered, mode-tagged) TC-SIG-099 PLANNED BEHAVIORAL DESIGN-ONLY in this PR slice. When implemented, a delivery-outage backlog of N pending snapshots replays as N independent ZIPs in collected_at ascending order; the first N−1 carry ingest_mode="history_only", the last carries ingest_mode="analyze".
R088 Out of scope: delivery transport for backlog replay n/a PLANNED n/a Stakes the boundary. Transport (HTTP push, signed upload, mTLS), spool storage shape (column on snapshots table vs new pending_deliveries table), retry policy, and observability metrics are explicitly deferred to a future spec slice tied to the runtime portion of R082.
R089 UpsertTarget idempotent — return real targets.id TC-SIG-100, TC-SIG-101, TC-SIG-102, TC-SIG-107 COVERED BEHAVIORAL Always SELECT the canonical targets.id after the UPSERT; never trust LastInsertId(). Closes the v0.3.x drift where AUTOINCREMENT reserved-but-wasted ids leaked into snapshots.target_id.
R090 Default scope filters orphaned target_id via JOIN TC-SIG-103, TC-SIG-104, TC-SIG-105, TC-SIG-106 COVERED BEHAVIORAL GetLatestRunsPerCollector (the R084 default scope) JOINs targets to ignore orphans; the legacy GetLatestSnapshotsPerTarget / GetLatestSnapshotForTarget helpers carry the same JOIN for any path still using them. --all still surfaces orphans for forensics.
R091 Min snapshot interval per logical target (default 60s) TC-SIG-110..114, TC-SIG-116, TC-SIG-117 + integration smoke (tests/signals_min_snapshot_integration_test.go, //go:build integration) COVERED BEHAVIORAL min_snapshot_interval enforced in collectTarget before any PG work. First cycle for a target always runs (no prior snapshot). Skip path emits collection_skipped audit + writes zero rows. Build-tagged integration smoke exercises the full Run() orchestration against a real PG (SIGNALS_TEST_PG_DSN gated) so orchestration-layer regressions surface beyond the unit-level decision tests.
R092 Explicit --force / force=true override TC-SIG-115 + integration smoke (tests/signals_min_snapshot_integration_test.go, //go:build integration) COVERED BEHAVIORAL Per-request override bypasses R091 only; forced=true recorded in audit. Does not bypass overlap protection (R032), per-query cadence (R015), or safety validation (R013/R018-R020). Integration smoke verifies the forced cycle produces a second snapshot when within the R091 window.
R093 cluster_identity_v1 collector TC-CLUSTERID-01..05 (tests/signals_cluster_identity_test.go) COVERED BEHAVIORAL Collector registered in internal/pgqueries/catalog_diagnostics.go; emits one row per cycle with inet_server_addr, inet_server_port, is_in_recovery, cluster_name (NULLIF empty), server_timezone, last_wal_receive_lsn, last_wal_replay_lsn, postmaster_start_time. Composite key (inet_server_addr, inet_server_port, postmaster_start_time) disambiguates clusters in v1. system_identifier is out of scope — see spec § Out of scope. Cadence 6h, RetentionLong.
R094 Export metadata.json carries target_identity block; per-snapshot identity in snapshots.ndjson TC-SIG-118..123 (tests/signals_export_target_identity_test.go) COVERED BEHAVIORAL Single-target exports embed {host, port, dbname, username} at the top level of metadata.json via internal/db/db.go::GetTargetIdentity and internal/export/export.go::writeMetadata. Multi-target exports additionally embed target_identity per row in snapshots.ndjson via writeSnapshots (per-target cache to avoid N+1 lookups). Unscoped exports omit the top-level metadata block. Orphan snapshots omit the per-row block when the target_id does not resolve (R090 case). Connection identity only — never password / secret-ref / sslmode (INV-SIGNALS-07).
R095 signalsctl doctor pre-flight verification TC-DOC-01..12 (internal/doctor/doctor_test.go) + CLI wiring (cmd/signalsctl/doctor.go) COVERED BEHAVIORAL CLI subcommand runs six independent operator-facing checks (C1 config_valid, C2 store_writable, C3 target_reachable, C4 role_safe, C5 collector_prerequisites, C6 snapshot_freshness). Reports union of findings; exit 0 = all OK, 1 = any FAIL, 2 = usage error. JSON output via --json; check filter via --check. Dependency rules: C4 auto-adds C3; C5 auto-adds C4+C3; C6 has no dependencies (reads daemon SQLite store independently). C5 reuses pgqueries.GatedIDsByReason to classify each registered collector as available / extension_missing / version_unsupported / config_disabled. C6 reads db.GetLatestSnapshotTimeByTargetName and compares against signals.poll_interval. Read-only and credential-safe per INV-DOC-01..03. Implementation: internal/doctor/ + cmd/signalsctl/doctor.go. Spec: specifications/doctor.md.
R096 signalsctl connect test classified connection diagnostic TC-CONN-01..10 (internal/conntest/conntest_test.go) + CLI wiring (cmd/signalsctl/connect.go) COVERED BEHAVIORAL Single-target / ad-hoc / multi-target connection diagnostic with classified failure categories (ok, dns, tcp, tls, auth, startup, role, password_resolve, config). Reuses collector.BuildSafeDSN (extracted from internal/doctor in this slice), collector.ResolvePassword, collector.ValidateRoleSafety, collector.RedactError, collector.RedactDSN. Exit 0 / 1 / 2 mirrors R095 semantics via shared usageError / failError types. INV-CONN-01..04 hold (read-only, no credential leakage, deterministic classification, config-declared output order). Implementation: internal/conntest/ (classify + run aggregator), cmd/signalsctl/connect.go (thin CLI shell). Spec: specifications/connect-test.md.
R097 Circuit breaker — per-target backoff + operator pause TC-CIRC-01..08 + auto-transition tests (internal/circuit/circuit_test.go) COVERED BEHAVIORAL Per-target in-memory state machine in internal/circuit/ (closed / open / paused). Auto: fail_threshold consecutive collectTarget errors → open; open_cooldown → back to closed. Manual: signalsctl collect pause/resume + POST /collect/pause//collect/resume set/clear paused. Manual priority over auto (INV-CIRC-02). State in memory only — restart resets all to closed (INV-CIRC-01); audit log preserves the pause/resume trail. Gating runs in collectTarget ahead of R091, so skipped cycle writes zero rows (INV-CIRC-03) with reason_category circuit_open / circuit_paused. /metrics exposes signals_circuit_state{target,state} gauge with one row per (target, state). Daemon constructs circuit.NewManager from signals.circuit config + threads it via collector.WithCircuitManager. Spec: specifications/circuit-breaker.md.
R098 Per-target sensitivity profiles TC-SENS-01..05 (tests/signals_sensitivity_profiles_test.go) COVERED BEHAVIORAL config.TargetConfig.Collectors adds optional profile: default | restricted | custom with include / exclude lists. Layered AFTER R081 version/extension gates and R075 daemon-wide gate (INV-SENS-01: profiles never widen eligibility). restricted drops every HighSensitivity=true collector for this target only. custom honours include allowlist + exclude deny-list (ValidateStrict rejects same-ID overlap, FC-20). Filter logic in pgqueries.FilterParams.{ProfileRestricted, IncludeOnly, Exclude}; collector wiring in collector.applyTargetProfile. Per-target drops surface in collector_status.json under reason=config_disabled (EA-R001 channel). Spec: specifications/sensitivity-profiles.md.
R099 Per-class retention tests/signals_retention_per_class_test.go + existing snapshot/runs cleanup tests + internal/db/retention_atomic_test.go (FC-23 atomicity) COVERED BEHAVIORAL signals.retention structured block adds per-class day counts (short/medium/long). cleanup() prunes query_runs per class via db.DeleteQueryRunsOlderThanByClass joining query_catalog.retention_class. Snapshot rows pruned by max(class_days) envelope so a snapshot stays alive while any class still needs it. Backward-compat: signals.retention_days (flat) and signals.retention (structured) are mutually exclusive (FC-21); flat alone behaves identically to today. Atomic pruning (FC-23, #327): both retention helpers (DeleteQueryRunsOlderThan, DeleteQueryRunsOlderThanByClass) wrap the query_results delete + query_runs delete in ONE SQLite transaction; a failure after the payload delete rolls both back, so retention is a safe producer of INV-SNAP-STATUS-PAYLOAD and cannot re-create the #312 orphaned-success failure. Spec section in features/signals/specification.md § Per-class retention (Atomic pruning).
R100 Configuration reload (SIGHUP + POST /reload) tests/signals_config_reload_test.go (5 tests + race) COVERED BEHAVIORAL Daemon supports in-place reload of the runtime-mutable subset of config. Triggers: SIGHUP signal handler in cmd/signals/main.go + POST /reload endpoint (internal/api/reload.go). Both run config.Load + config.ValidateStrict BEFORE applying — rejected reloads emit config_reload_rejected audit and leave running state untouched. On success: collector.Reload(newTargets) swaps the target list under runtimeMu, closes pools for removed and connection-modified targets, emits per-target add/remove/modify audit events. v1 scope = targets list only; poll_interval, retention, circuit thresholds remain set-at-construction (documented as future scope in spec § Configuration reload).
R100.1 Connection-identity pool invalidation on reload tests/signals_config_reload_test.go (TestReload_PoolInvalidatedPerConnectionField table [Host, Port, DBName, User, SSLMode, SSLRootCertFile, SSLCert, SSLKey, SSLKeyPassphraseFile, AuthMethod, Region, AzureClientID, GCPImpersonateServiceAccount, SecretRef, SecretJSONKey, PasswordFile, PasswordEnv, PgpassFile, MaxCacheTTL], TestReload_ProfileOnlyChangeKeepsPool, TestReload_NewConnectionResolvesWithNewConfig, TestConnIdentity_CoversEveryConnectionField) COVERED BEHAVIORAL Fixes #328. sameConnection compared only a subset (host/port/db/user/sslmode + 3 password-source fields) and silently omitted the TLS/credential/cache fields, so a reload that changed only AuthMethod, SecretRef, cloud-identity selectors, TLS CA/mTLS files, or MaxCacheTTL kept the stale pool — and the pool's BeforeConnect closure captured the OLD TargetConfig, so even future connections resolved the old credential. Fix: sameConnection now compares a derived connIdentity struct built from the exhaustive pool-affecting field set; TestConnIdentity_CoversEveryConnectionField reflects over TargetConfig so a new connection/credential field added to config forces a test-time update rather than a silent omission. Profile-only (collectors) and Enabled-only changes are excluded from the identity and do NOT drop the pool. internal/collector/collector.go (connIdentity, sameConnection, reload path).
R105 Index bloat estimate (index_bloat_estimate_v1) TC-IDXBLOAT-01..10 (tests/signals_index_bloat_estimate_test.go) COVERED BEHAVIORAL Sibling of R104 for indexes. Registered in internal/pgqueries/catalog_index_bloat.go. One row per non-system relkind IN ('i','I') index. Formula: expected ≈ CEIL(reltuples × (Σ key_avg_width + 8 + 4) / GREATEST(block_size - 24, 1)) × block_size. Constants differ from R104 (index tuple header 8, item pointer 4, page header 24) to match the index-tuple layout. Width sum is bounded by pg_index.indnkeyatts so INCLUDE columns are excluded. Expression-key indexes emit stats_missing = TRUE (avg_width lookup via pg_index.indkey → pg_attribute.attname → pg_stats can't resolve expression columns). Partitioned-index parents (relkind = 'I') surface with actual_size_bytes = 0. System schemas excluded (INV-SIGNALS-12). CTE structure: page, idx_widths, base, estimated. No extension dependency. Cadence6h, RetentionMedium, Timeout=30s. Spec: specifications/collectors/index_bloat_estimate_v1.md.
R104 Table bloat estimate (bloat_estimate_v1) TC-BLOAT-01..09 (tests/signals_bloat_estimate_test.go) COVERED BEHAVIORAL Statistical bloat estimate registered in internal/pgqueries/catalog_bloat.go. One row per non-system relkind IN ('r','m','p') relation with actual vs expected size, floored bloat_bytes, and bloat_ratio ∈ [0.000, 1.000] (or NULL when pg_stats rows are missing). Formula: expected ≈ CEIL(reltuples × (23 + 4 + Σ avg_width + 8) / GREATEST(block_size - 24, 1)) × block_size, page size from current_setting('block_size') so non-default compile sizes (4K/16K) work. Adjacent columns surface n_live_tup / n_dead_tup / last_autovacuum from pg_stat_user_tables for cross-corroboration. System schemas excluded (INV-SIGNALS-12). CTE structure: page, widths, base, estimated. No extension dependency — runs on managed PG (RDS / Aurora / Cloud SQL / AlloyDB / Azure Flex) where pgstattuple is unavailable. Cadence6h, RetentionMedium, Timeout=30s. Index-bloat sibling intentionally deferred to a follow-up issue. Spec: specifications/collectors/bloat_estimate_v1.md.
R103 Index hygiene summary (index_health_summary_v1) TC-IDXHEALTH-01..08 (tests/signals_index_health_summary_test.go) COVERED BEHAVIORAL Single collector registered in internal/pgqueries/catalog_index_health.go. One row per non-system index with derived health_findings text-array drawn from {unused, large_unused, invalid, not_ready, redundant, duplicate}. SQL uses CTEs (idx_cols, idx_meta) plus two LATERAL self-joins to compute duplicate_of (lower-OID twin with identical column_set on the same table) and redundant_with (strict left-prefix of a larger index on the same table). System schemas excluded (INV-SIGNALS-12). column_set ordering matches index definition (uses pg_index.indkey ordered by ordinality, bounded by indnkeyatts so INCLUDE columns are skipped). Expression indexes emit NULL column_set and never match duplicate/redundant. Cadence6h, RetentionMedium, Timeout=30s, ResultRowset, no MinPGVersion. Spec: specifications/collectors/index_health_summary_v1.md.
R102 In-flight operation visibility (pg_stat_progress_* family) TC-PROG-01..07 (tests/signals_pg_stat_progress_test.go) COVERED BEHAVIORAL Six collectors registered in internal/pgqueries/catalog_progress.go (vacuum / analyze / create_index / cluster / basebackup / copy). Category progress, Cadence5m, RetentionShort, ResultRowset, MinPGVersion: 14, sensitivity low. Empty rowset on quiet clusters is the success state. Column drift handled via RegisterOverride(17, ...) and RegisterOverride(18, ...) for pg_stat_progress_vacuum (PG 17 byte-denominated dead-tuple columns + num_dead_item_ids + indexes_total + indexes_processed; PG 18 added delay_time) and pg_stat_progress_copy (PG 17 added tuples_skipped). Canonical SQL emits the union with NULL stubs; per-major overrides flip which columns are populated. Drift allowlist in tests/signals_catalog_drift_test.go covers the SQL-level NULL-stub flip with citation. Spec: specifications/collectors/pg_stat_progress_family_v1.md.
R101 Logical replication slot health (pg_stat_replication_slots_v1) TC-RSLOTS-01..05 (tests/signals_pg_stat_replication_slots_test.go) COVERED BEHAVIORAL Collector registered in internal/pgqueries/catalog_survival.go against pg_stat_replication_slots (PG 14+). Emits one row per logical slot with explicit column projection (no SELECT *): slot_name, spill_txns, spill_count, spill_bytes, stream_txns, stream_count, stream_bytes, total_txns, total_bytes, stats_reset. Category replication, Cadence5m, RetentionShort, MinPGVersion: 14. Empty rowset on instances with no logical slots is the success state (INV). On PG < 14 the collector is excluded by R081 dispatch and surfaces in collector_status.json with reason=version_unsupported via EA-R001. Spec: specifications/collectors/pg_stat_replication_slots_v1.md.
EA-R001 Canonical channel: gated collectors → collector_status.json TC-EXTABS-01..05 COVERED BEHAVIORAL Cross-cutter specifications/extension-absent-emission.md (status: ACTIVE). Gating implemented in internal/pgqueries/registry.go::GatedIDsByReason (extension_missing / version_unsupported / config_disabled); internal/collector/collector.go writes the QueryRun rows with status=skipped. collector_status.json schema: internal/collector/status.go. Existing tests: tests/signals_collector_status_test.go, tests/signals_post_031_test.go (H-002). No rows written to query_results.ndjson for gated collectors.
EA-R002 collector_status.json present in every export ZIP TC-SIG-009 (TestExportZIPContainsRequiredFiles) COVERED BEHAVIORAL internal/export/export.go::Builder.WriteTo writes the file unconditionally (INV-SIGNALS-11). Export test asserts presence in the required-files list (tests/signals_export_test.go).
EA-R004 Analyzer surfaces per-collector reason via EvidenceCompleteness analyzer-side test COVERED BEHAVIORAL Implemented in the Elevarq Analyzer repo (~/Projects/elevarq/arq/internal/analyzer/evidence/completeness.go). Detectors distinguish ExtensionUnavailable / VersionUnsupported / ConfigDisabled / CollectorFailed via the reason field exposed on the Completeness model.
R106 pg_stat_statements self-filtering + fixed application_name TC-SIG-118, TC-SIG-119, TC-SIG-120 (tests/signals_conn_test.go, tests/signals_pg_stat_statements_filter_test.go) COVERED BEHAVIORAL Connection application_name sourced from collector.AppName (single constant); applied via pgx.ConnConfig.RuntimeParams in BuildConnConfig. pg_stat_statements_v1 SQL in internal/pgqueries/catalog.go joins pg_database to scope rows to current_database() (pg_stat_statements.dbid = (SELECT oid FROM pg_database WHERE datname = current_database())) and applies a NOT EXISTS correlated subquery against pg_stat_activity to suppress rows attributable to sessions where application_name = 'signals' (matching on useridusesysid and dbiddatid). SELECT * projection preserved for R037 dynamic-column capture; ID, category, retention class, and RequiresExtension unchanged. INV-SIGNALS-16/17/18.
R110 Export consistency + per-call local scope concurrency safety TC-SIG-138, TC-SIG-139 (tests/signals_export_consistency_test.go) COVERED BEHAVIORAL Exports take a shared read lock serialised against destructive retention writes (existing). Request-specific scope (resolved snapshots, resolved run set, snapshot-ID membership map, run-scope label) is local and immutable for one WriteTo call — carried in a per-call value threaded through every writer, never on the shared Builder (issue #323). Two concurrent WriteTo calls on the production shared Builder with different selectors (--snapshot-id/--target-id vs --all/default) each contain only their own scope; holds under go test -race. The only remaining Builder mutable state is set-once construction-time config, read-only for the daemon lifetime.
R109 Disabled/removed targets excluded from default export + /status; targets.enabled reconciled on startup/reload TC-SIG-125 PLANNED BEHAVIORAL GetLatestRunsPerCollector and the per-target snapshot helpers JOIN targets with t.enabled = 1; /status lists only enabled targets. ReconcileEnabledTargets(enabledNames) (called on startup + reload) soft-disables targets disabled-in-config or removed-from-config and re-enables present ones, without deleting snapshots (--all still surfaces them). Closes the drift where lazy UpsertTarget left a disabled/removed target at enabled=1. INV-SIGNALS-20, INV-SIGNALS-14.
R108 Budget-exhaustion completeness: remaining due collectors recorded skipped/budget_exhausted; cycle partial; persistence + savepoint recovery survive elapsed budget TC-SIG-124 (helpers: internal/collector/budget_exhaustion_test.go; live production loop: internal/collector/budget_exhaustion_live_test.go, //go:build integration, SIGNALS_TEST_PG_DSN-gated, runs under -race) COVERED BEHAVIORAL + INTEGRATION On per-cycle budget exhaustion the collection loop records a skipped run with reason=budget_exhausted for every remaining due collector (both budget break points), so query_runs rows == due collectors (INV-SIGNALS-19). The read-tx commit + SQLite persistence use a fresh context so an over-budget cycle still persists its complete inventory. Savepoint recovery (ROLLBACK TO SAVEPOINT / RELEASE) after a collector's own query fails on the exhausted budget also runs under a bounded fresh context, not the elapsed budget context — otherwise the recovery is rejected on the expired ctx, the cycle aborts before appending the remaining budget_exhausted runs, and the whole partial cycle is discarded (#329, the production-path regression lock). Cycle status is partial when any budget_exhausted skip occurred. Pure helpers budgetSkippedRuns + cycleStatus unit-tested; the live integration test forces the target budget to expire during a collector query and asserts M due collectors → M persisted query_runs (the attempted one failed/timeout, the rest skipped/budget_exhausted), atomic snapshot + payloads, and a partial cycle. #329 closes the #8 completeness gap on the production path.
R107 Collector freshness metadata (collected_at + cadence + freshness) TC-SIG-123 PLANNED BEHAVIORAL collector_status.json entries carry per-collector collected_at, expected cadence, and a freshness classification (fresh ≤ 2× cadence, stale > 2× cadence, never_run = eligible but no run in scope). Default export enumerates eligible-but-never-run collectors so consumers distinguish current / stale / missing coverage. Pairs with the latest-run-per-collector default scope (R084) and the Analyzer ingest side (Elevarq/Arq#907).
R111 Diagnostic DSN values libpq-quoted — no parameter injection via password/field values TC-SIG-126 (internal/collector/secrets_test.go) COVERED BEHAVIORAL collector.BuildSafeDSN quotes every string-valued field (wrap in '…', escape \ and '); hostile values (embedded sslmode=disable host=evil, quotes, backslashes, whitespace) round-trip as exact literals through pgx.ParseConfig without re-targeting the connection or downgrading TLS posture. Production path BuildConnConfig unchanged (already net/url-escaped). INV-SIGNALS-21. Spec: features/signals/specification.md R111; issue #69.
R112 Auth lockout never denies a valid bearer token — limiter gates only the invalid path TC-SIG-127 (internal/api/token_auth_lockout_test.go) COVERED BEHAVIORAL tokenAuthMiddleware validates the token (constant-time vs api.token / control_plane_token) before consulting the per-IP tokenRateLimiter; a valid token from a locked-out IP authenticates and clears the IP's failure counter, while invalid/missing tokens over the threshold still get 429 (brute-force throttle preserved). Fixes the shared-IP (NAT / proxy / co-located pod) operator-lockout DoS. INV-SIGNALS-22. Spec: features/signals/specification.md R112; issue #68.
R114 TimescaleDB collector family (12 members, category timescaledb): detection + capability flags, hypertables, dimensions, chunks (≤5000) + summary, approximate sizes, compression settings/stats, continuous aggregates, jobs/job_stats/job_errors (≤1000) TC-TSDB-01..11, TC-TSDB-13, TC-TSDB-15 (tests/signals_timescaledb_test.go); live scenarios in tests/signals_timescaledb_integration_test.go (//go:build integration, SIGNALS_TEST_TSDB_DSN-gated) COVERED BEHAVIORAL + INTEGRATION Implemented in internal/pgqueries/catalog_timescaledb.go (#73). Documented PUBLIC-readable timescaledb_information views + hypertable_approximate_detailed_size() / hypertable_compression_stats() only; no internal catalogs, no exact size functions. Dynamic columns (R037) absorb cross-version view drift. Redact-path sensitivity on view_definition / err_message (R075). INV-SIGNALS-24. Spec (ACTIVE): specifications/collectors/timescaledb_family_v1.md; design: docs/timescaledb-collectors-design.md; permissions: docs/postgres-role.md § TimescaleDB targets.
R115 Extension-version gating: discovery captures extversion; QueryDef.RequiresExtensionMinVersion gates under version_unsupported (extension_missing keeps precedence); SQLSTATE 42P01/42883 → structured object_missing reason; fail-open on unknown/unparsable versions TC-TSDB-12, TC-TSDB-14 (tests/signals_extension_version_gate_test.go) COVERED BEHAVIORAL Implemented in internal/pgqueries/{discovery,types,registry}.go + internal/collector/status.go classifier; wired through collector.go and doctor C5. First consumer: R114 (RequiresExtensionMinVersion: "2.14", detection collector exempt).
R113 Optional API TLS at the daemon (api.tls_cert_file / tls_key_file); all-or-nothing; min TLS 1.2 TC-SIG-128 (tests/signals_api_tls_config_test.go), TC-SIG-129 (internal/api/server_tls_test.go) COVERED BEHAVIORAL APIConfig.TLSCertFile/TLSKeyFile (+ SIGNALS_API_TLS_CERT_FILE/_KEY_FILE env overrides); validation hard-errors when exactly one is set (TC-SIG-128). Server.Start calls ServeTLS with MinVersion: tls.VersionTLS12 when both are set, plain Serve otherwise; HTTPS smoke with a self-signed cert proves the listener negotiates TLS and serves /health, plain path unchanged (TC-SIG-129). Closes the cleartext-bearer-token exposure on the Helm 0.0.0.0 bind. Helm: NetworkPolicy 0.0.0.0/0 placeholder fails render when the policy is enabled; NOTES warns on exposed-without-TLS-or-NetworkPolicy. INV-SIGNALS-23. Spec: features/signals/specification.md R113; issue #67.
R127 Scheduled per-database file export (#350): when export_on_collect + export_dest are set, the post-cycle hook writes the latest snapshot for EACH active target as its own export ZIP (Options{TargetID}) into export_dest — one file per database, never a combined multi-target archive (which a directory-sweeping consumer reads as a single database) TC-SIG-130 (TestScheduledExporter_ExportLatest_OneFilePerTarget), TC-SIG-131 (TestScheduledExporter_ExportLatest_SingleTargetIsScoped), TC-SIG-132 (TestScheduledExporter_ExportLatest_TargetListErrorWritesNothing), TC-SIG-133 (TestScheduledExporter_ExportLatest_WriteFailureLeavesNoFile) + TestScheduledExporter_exportFilename (internal/export/scheduled_test.go) COVERED BEHAVIORAL ScheduledExporter.ExportLatest enumerates targets via Builder.LatestTargetIDs (GetLatestSnapshotsPerTarget) and writes one target-scoped ZIP each; atomic temp+rename (INV-SIGNALS-R127-ATOMIC — no partial archive); flat unique filename <instance>-t<targetID>-<ns-UTC>.zip so same-cycle targets never collide (INV-SIGNALS-R127-UNIQUE); a per-target failure is returned to the collector post-cycle hook (cmd/signals/main.go SetAfterCycle), which logs and continues so collection is never disrupted. Fixes the multi-database demo collapsing to one database in the Analyzer (#368; timeseries-demo#322). Spec: features/signals/specification.md R127.

Credential providers — aws_rds_iam (#94)

Spec: features/signals/credential-provider-aws-rds-iam.md (ACTIVE), deriving from credential-providers.md (keystone, #93). Provider dispatch + token cache in internal/collector/credprovider.go; AWS minter/region/guidance in internal/collector/credprovider_aws.go; auth_method/region config + validation in internal/config/config.go. Unit tests internal/collector/credprovider_test.go + internal/config/authmethod_test.go; env-gated live smoke internal/collector/credprovider_live_test.go (//go:build integration, SIGNALS_INTEGRATION_LIVE=1).

Rule ID Rule Summary Test ID(s) Coverage Status Evidence Type Notes
SIGNALS-AUTH-AWS-AC-001 Token minted, applied as password, ExpiresAt ~15m; minter inputs (endpoint/region/db_user) correct TestResolveAWSMintsTokenAsPassword COVERED BEHAVIORAL Fake minter; no real AWS call (NFR003).
SIGNALS-AUTH-AWS-AC-002 Cache reuse within 3m skew; re-mint past ~12m; per-target isolation (distinct cache key) TestResolveAWSCacheRefreshAndIsolation COVERED BEHAVIORAL Fake clock + fake minter; cache key = ConnIdentity() + method.
SIGNALS-AUTH-AWS-AC-003 aws_rds_iam + any password source → hard startup error naming the target TestValidateRejectsAWSWithPasswordSource COVERED BEHAVIORAL FC-AWS-003; covers password_file/password_env/pgpass_file.
SIGNALS-AUTH-AWS-AC-004 aws_rds_iam requires verify-full in every env; rejects weaker; accepts verify-full TestValidateRejectsAWSWithoutVerifyFull, TestValidateAcceptsAWSVerifyFull COVERED BEHAVIORAL FC-AWS-004 / INV003.
SIGNALS-AUTH-AWS-AC-005 Mint failure → redacted, method-attributable error; no token returned/logged TestResolveAWSMintFailure COVERED BEHAVIORAL FC-AWS-001 + INV002.
SIGNALS-AUTH-AWS-AC-006 Region unresolved → connect-time target-scoped error, minter never called; missing region at startup → warning only (fail-soft) TestResolveAWSRegionUnresolved, TestValidateWarnsButDoesNotFailOnMissingAWSRegion COVERED BEHAVIORAL FC-AWS-005; startup warning vs connect-time failure split.
SIGNALS-AUTH-AWS-AC-007 Successful resolution logs metadata (auth_method/region/db_user/expires_at), never the token TestResolveAWSLogsMetadataNotToken COVERED BEHAVIORAL INV002/INV007; log-capture asserts token absent.
SIGNALS-AUTH-AWS-AC-008 Live passwordless connect + read-only snapshot; token re-minted across reconnect crossing the skew TestLive_AWSRDSIAMPasswordlessConnectAndRemint COVERED INTEGRATION //go:build integration + SIGNALS_INTEGRATION_LIVE=1; real minter, injectable clock forces real re-mint. Not in default CI.
SIGNALS-AUTH-AWS-AC-009 Grant/policy guidance names GRANT rds_iam TO "<user>" + rds-db:connect, no secret material TestAWSGrantGuidance COVERED BEHAVIORAL AWSGrantGuidance.
keystone FC001 / NFR003 Unimplemented auth_method → hard error naming bad method + supported set; empty defaults to password TestValidateRejectsUnsupportedAuthMethod, TestEffectiveAuthMethodDefaultsToPassword COVERED BEHAVIORAL SupportedAuthMethods gate; back-compat default.
keystone NFR003 (back-compat) Empty auth_method resolves via existing password source, no expiry, minter never called TestResolvePasswordPathUnchanged COVERED BEHAVIORAL Password path unchanged.

Credential providers — azure_entra (#95)

Spec: features/signals/credential-provider-azure-entra.md (ACTIVE), deriving from credential-providers.md (keystone, #93). Reuses the shared dispatch + token cache in internal/collector/credprovider.go; Azure minter/client-id/guidance in internal/collector/credprovider_azure.go; auth_method/azure_client_id config + validation in internal/config/config.go. Unit tests internal/collector/credprovider_azure_test.go + internal/config/authmethod_azure_test.go; env-gated live smoke internal/collector/credprovider_azure_live_test.go (//go:build integration, SIGNALS_INTEGRATION_LIVE=1).

Rule ID Rule Summary Test ID(s) Coverage Status Evidence Type Notes
SIGNALS-AUTH-AZURE-AC-001 Token acquired, applied as password, ExpiresAt = token expiry; minter called for the fixed scope TestResolveAzureMintsTokenAsPassword COVERED BEHAVIORAL Fake minter; no real Azure call (NFR003). Scope hard-pinned (INV004).
SIGNALS-AUTH-AZURE-AC-002 Cache reuse within 5m skew; re-acquire past ~70m of a 75m token; per-target isolation (distinct cache key) TestResolveAzureCacheRefreshAndIsolation COVERED BEHAVIORAL Fake clock + fake minter; cache key = ConnIdentity() + method (INV003/NFR001).
SIGNALS-AUTH-AZURE-AC-003 azure_entra + any password source → hard startup error naming the target TestValidateRejectsAzureWithPasswordSource COVERED BEHAVIORAL FC-AZURE-003; covers password_file/password_env/pgpass_file.
SIGNALS-AUTH-AZURE-AC-004 azure_entra requires verify-full in every env; rejects weaker; accepts verify-full; missing azure_client_id does not warn TestValidateRejectsAzureWithoutVerifyFull, TestValidateAcceptsAzureVerifyFull, TestValidateAcceptsAzureWithClientID COVERED BEHAVIORAL FC-AZURE-004 / INV003.
SIGNALS-AUTH-AZURE-AC-005 Mint failure → redacted, method-attributable error; no token returned/logged TestResolveAzureMintFailure COVERED BEHAVIORAL FC-AZURE-001 + INV002.
SIGNALS-AUTH-AZURE-AC-006 Identity failure → actionable error naming azure_client_id disambiguation; leaves no cached entry; other targets keep resolving TestResolveAzureIdentityErrorIsActionableAndIsolated COVERED BEHAVIORAL FC-AZURE-005; target-scoped, non-fatal to siblings.
SIGNALS-AUTH-AZURE-AC-007 Successful resolution logs metadata (auth_method/scope/db_user), never the token TestResolveAzureLogsMetadataNotToken COVERED BEHAVIORAL INV002/INV007; log-capture asserts token absent.
SIGNALS-AUTH-AZURE-AC-008 Live passwordless connect + read-only snapshot; token re-acquired across reconnect crossing the skew TestLive_AzureEntraPasswordlessConnectAndReacquire COVERED INTEGRATION //go:build integration + SIGNALS_INTEGRATION_LIVE=1; real minter, injectable clock forces real re-acquire. Not in default CI.
SIGNALS-AUTH-AZURE-AC-009 Guidance names pgaadauth_create_principal('<user>', ...) + display-name match note, no secret material TestAzureEntraGuidance COVERED BEHAVIORAL AzureEntraGuidance.
(design) client-id resolution azure_client_id config wins; else AZURE_CLIENT_ID env; else empty (chain default) TestResolveAzureClientIDResolution COVERED BEHAVIORAL Confirmed user-assigned MI disambiguation.

Credential providers — gcp_cloudsql_iam (#96)

Spec: features/signals/credential-provider-gcp-cloudsql-iam.md (ACTIVE), deriving from credential-providers.md (keystone, #93). Reuses the shared dispatch + token cache in internal/collector/credprovider.go; GCP minter/impersonation/guidance in internal/collector/credprovider_gcp.go; auth_method/gcp_impersonate_service_account config + validation in internal/config/config.go. Connection path: direct libpq + verify-full (token-as-password), reusing the existing BeforeConnect seam — not the Cloud SQL Go Connector. Unit tests internal/collector/credprovider_gcp_test.go + internal/config/authmethod_gcp_test.go; env-gated live smoke internal/collector/credprovider_gcp_live_test.go (//go:build integration, SIGNALS_INTEGRATION_LIVE=1).

Rule ID Rule Summary Test ID(s) Coverage Status Evidence Type Notes
SIGNALS-AUTH-GCP-AC-001 Token acquired, applied as password, ExpiresAt = token expiry; minter called for the fixed scope TestResolveGCPMintsTokenAsPassword, TestEffectiveAuthMethodGCPCloudSQLIAM COVERED BEHAVIORAL Fake minter; no real GCP call (NFR003). Scope …/auth/sqlservice.login hard-pinned (INV004).
SIGNALS-AUTH-GCP-AC-002 Cache reuse within 5m skew; re-acquire past ~55m of a 60m token; per-target isolation (distinct cache key) TestResolveGCPCacheRefreshAndIsolation COVERED BEHAVIORAL Fake clock + fake minter; cache key = ConnIdentity() + method (INV003/NFR001).
SIGNALS-AUTH-GCP-AC-003 gcp_cloudsql_iam + any password source → hard startup error naming the target TestValidateRejectsGCPWithPasswordSource COVERED BEHAVIORAL FC-GCP-003; covers password_file/password_env/pgpass_file.
SIGNALS-AUTH-GCP-AC-004 gcp_cloudsql_iam requires verify-full in every env; rejects weaker; accepts verify-full; missing impersonation SA does not warn TestValidateRejectsGCPWithoutVerifyFull, TestValidateAcceptsGCPVerifyFull, TestValidateAcceptsGCPWithImpersonation COVERED BEHAVIORAL FC-GCP-004 / INV003; direct-libpq path.
SIGNALS-AUTH-GCP-AC-005 Mint failure → redacted, method-attributable error; no token returned/logged TestResolveGCPMintFailure COVERED BEHAVIORAL FC-GCP-001 + INV002.
SIGNALS-AUTH-GCP-AC-006 Identity failure → actionable error naming gcp_impersonate_service_account/ADC remediation; leaves no cached entry; other targets keep resolving TestResolveGCPIdentityErrorIsActionableAndIsolated COVERED BEHAVIORAL FC-GCP-005; target-scoped, non-fatal to siblings.
SIGNALS-AUTH-GCP-AC-007 Successful resolution logs metadata (auth_method/scope/db_user), never the token TestResolveGCPLogsMetadataNotToken COVERED BEHAVIORAL INV002/INV007; log-capture asserts token absent.
SIGNALS-AUTH-GCP-AC-008 Live passwordless connect + read-only snapshot; token re-acquired across reconnect crossing the skew TestLive_GCPCloudSQLIAMPasswordlessConnectAndReacquire COVERED INTEGRATION //go:build integration + SIGNALS_INTEGRATION_LIVE=1; real minter, injectable clock forces real re-acquire. Not in default CI.
SIGNALS-AUTH-GCP-AC-009 Guidance names gcloud sql users create + the target role, no secret material TestGCPCloudSQLGuidance COVERED BEHAVIORAL GCPCloudSQLGuidance.
(design) impersonation resolution gcp_impersonate_service_account handed to the minter when set; else empty (ambient ADC) TestResolveGCPImpersonationResolution COVERED BEHAVIORAL Confirmed per-target service-account impersonation.

Credential providers — secret_store (#97, #108)

Spec: features/signals/credential-provider-secret-store.md (ACTIVE), deriving from credential-providers.md (keystone, #93). Backend inference (SecretBackend, ParsedSecretRef, InferSecretBackend, AWS region-from-ARN) in internal/config/secretref.go; auth_method/secret_ref/secret_json_key/max_cache_ttl config + validation in internal/config/config.go. Shared dispatch + per-target cache in internal/collector/credprovider.go; the secretFetcher seam, fetch/extract/cache/redaction/guidance logic in internal/collector/credprovider_secret.go; the production fetchers in internal/collector/credprovider_secret_aws.go (AWS Secrets Manager, GetSecretValue), internal/collector/credprovider_secret_ssm.go (AWS Systems Manager Parameter Store, GetParameter with WithDecryption=true), internal/collector/credprovider_secret_azure.go (Azure Key Vault, azsecrets.GetSecret via the default Azure credential chain), and internal/collector/credprovider_secret_gcp.go (GCP Secret Manager, AccessSecretVersion via ADC). Connection path: direct libpq + verify-full (fetched secret applied as password), reusing the existing BeforeConnect seam. All four backends — AWS Secrets Manager (#97), AWS Systems Manager Parameter Store (#157), Azure Key Vault and GCP Secret Manager (#108) — are production-wired; each secret_ref is routed to exactly its backend's SDK and no other (INV005). Each backend supplies no lease/TTL, so reuse between reconnects is governed entirely by the operator's max_cache_ttl (INV003). Unit tests internal/collector/credprovider_secret_test.go + internal/config/secretref_test.go + internal/config/authmethod_secret_test.go; env-gated live smoke internal/collector/credprovider_secret_live_test.go (//go:build integration, SIGNALS_INTEGRATION_LIVE=1).

Rule ID Rule Summary Test ID(s) Coverage Status Evidence Type Notes
SIGNALS-AUTH-SECRET-AC-001 secret_store + verify-full + valid secret_ref fetches the secret, applies it as the password; no inline password required TestResolveSecretStoreFetchesAsPassword, TestEffectiveAuthMethodSecretStore, TestValidateAcceptsSecretStore COVERED BEHAVIORAL Fake fetcher; no real cloud call (NFR003). Asserts backend + region eu-west-1 parsed from ARN.
SIGNALS-AUTH-SECRET-AC-002 Each reference shape selects the right backend (incl. secretsmanager vs ssm ARN split); an unrecognised shape aborts startup with an actionable error naming the four accepted forms TestInferSecretBackendShapes, TestInferSecretBackendRejectsUnrecognised, TestProductionSecretFetcherRouting COVERED BEHAVIORAL FC-SECRET-007. Both AWS ARN regions parsed authoritatively (empty-region / non-parameter ssm ARN rejected).
SIGNALS-AUTH-SECRET-AC-002a Parameter Store fetcher calls GetParameter with WithDecryption=true; SecureString decrypted & String passthrough applied; ttl is zero; no-value parameter fails TestAWSParameterStoreFetcherDecryptsAndAppliesValue, TestAWSParameterStoreFetcherNoValueFails, TestAWSParameterStoreFetcherPropagatesError COVERED BEHAVIORAL #157. Fake SSM client; asserts WithDecryption + ARN as Name; no real AWS call (NFR003).
SIGNALS-AUTH-SECRET-AC-003 With secret_json_key set, the named key's string value is the password; a non-JSON / missing-key / non-string / empty value fails without leaking the raw value TestResolveSecretStoreJSONKeyExtraction, TestResolveSecretStoreJSONKeyFailuresDoNotLeak, TestValidateAcceptsSecretStoreWithJSONKey COVERED BEHAVIORAL FC-SECRET-003 + INV002; raw value/marker asserted absent from every error path.
SIGNALS-AUTH-SECRET-AC-004 Cache bound = min(vault TTL if present, max_cache_ttl if set); neither set → re-fetch every reconnect (rotation pickup); per-target isolation TestResolveSecretStoreCacheTTLAndRotation COVERED BEHAVIORAL INV003/NFR001. 5 subtests: no-bound re-fetch+rotation, vault TTL, max_cache_ttl, min-bound, per-target cache-key isolation. Fake clock + fake fetcher.
SIGNALS-AUTH-SECRET-AC-005 secret_store + any inline password source → hard startup error naming the target TestValidateRejectsSecretStoreWithPasswordSource COVERED BEHAVIORAL FC-SECRET-005 / INV001; covers password_file/password_env/pgpass_file.
SIGNALS-AUTH-SECRET-AC-006 secret_store requires verify-full in every env; rejects weaker modes TestValidateRejectsSecretStoreWithoutVerifyFull COVERED BEHAVIORAL FC-SECRET-006 / INV004 (verify-full floor).
SIGNALS-AUTH-SECRET-AC-007 secret_store without secret_ref, or with an unrecognised one, aborts startup with an actionable error TestValidateRejectsSecretStoreWithoutRef, TestValidateRejectsSecretStoreWithBadRef COVERED BEHAVIORAL FC-SECRET-007.
SIGNALS-AUTH-SECRET-AC-008 Fetch error/timeout → redacted, method+backend-attributable error with an actionable IAM hint; no secret returned/logged; other targets keep collecting TestResolveSecretStoreFetchErrorIsActionableAndIsolated COVERED BEHAVIORAL FC-SECRET-001 + INV002; target-scoped, non-fatal to siblings.
SIGNALS-AUTH-SECRET-AC-009 Identity/permission failure → actionable error naming the required grant; no secret leaks TestResolveSecretStoreIdentityErrorIsActionable COVERED BEHAVIORAL FC-SECRET-002 + INV002.
SIGNALS-AUTH-SECRET-AC-010 Successful resolution logs metadata only (auth_method/backend/secret_ref/db_user/ttl_present/json_key_extracted), never the secret TestResolveSecretStoreLogsMetadataNotSecret COVERED BEHAVIORAL INV002/INV007; log-capture asserts secret value absent.
SIGNALS-AUTH-SECRET-AC-011 Live passwordless connect + read-only snapshot using a real vault fetch (required live content) TestLive_SecretStorePasswordlessConnect COVERED INTEGRATION //go:build integration + SIGNALS_INTEGRATION_LIVE=1; all four production fetchers wired, the SIGNALS_TEST_SECRET_REF shape selects which one runs (incl. an ssm Parameter Store ARN). Not in default CI.
SIGNALS-AUTH-SECRET-AC-012 Fetch-failure guidance names the exact IAM grant for the inferred backend + workload-identity note, no secret material TestSecretStoreGuidanceAWS, TestSecretStoreGuidanceParameterStore COVERED BEHAVIORAL SecretStoreGuidance; Secrets Manager path names secretsmanager:GetSecretValue, Parameter Store path names ssm:GetParameter + kms:Decrypt; both name the ARN region.
SIGNALS-AUTH-SECRET-AC-013 Live rotation validation (optional, opt-in): rotate the stored secret, force a re-fetch, reconnect with the new value TestLive_SecretStorePasswordlessConnect COVERED INTEGRATION Opt-in via SIGNALS_TEST_SECRET_ROTATE=1; rotation-on-reconnect fully covered in default CI by AC-004's unit cache test.
SIGNALS-AUTH-SECRET-INV004 (empty secret) Empty fetched value (or extracted key) is a hard failure, not an empty password TestResolveSecretStoreEmptySecretFails COVERED BEHAVIORAL FC-SECRET-004.
SIGNALS-AUTH-SECRET-INV005 (backend isolation) Only the inferred backend's SDK is invoked for a target; an unwired backend reports errSecretBackendUnavailable (not redacted, carries no secret) TestProductionSecretFetcherRouting COVERED BEHAVIORAL productionSecretFetcher routes on ref.Backend; AWS Secrets Manager / AWS Parameter Store / Azure / GCP all wired, each ref reaches only its own fetcher; nil guard still returns unavailable.
(design) Azure Key Vault URI parse Key Vault secret URI splits into vault URL + name + optional version; malformed URIs fail without leaking a parsed name TestParseAzureKeyVaultRef COVERED BEHAVIORAL parseAzureKeyVaultRef; pure helper feeding azsecrets.GetSecret.

Credential providers — mtls (#98)

Spec: features/signals/credential-provider-mtls.md (ACTIVE), deriving from credential-providers.md (keystone, #93). The only certificate-kind credential: Credential.ClientCert applied to cfg.TLSConfig.Certificates in the BeforeConnect hook (internal/collector/collector.go). Provider + cert loader in internal/collector/credprovider_mtls.go; sslcert/sslkey/sslkey_passphrase_file config + validation in internal/config/config.go. Unit tests internal/collector/credprovider_mtls_test.go + internal/config/authmethod_mtls_test.go.

Rule ID Rule Summary Test ID(s) Coverage Status Evidence Type Notes
SIGNALS-AUTH-MTLS-AC-001 Valid cert/key pair loads, applied as certificate kind, ExpiresAt = cert NotAfter; no password TestFileCertLoaderLoadsValidPair, TestResolveMTLSCertKindAndExpiry COVERED BEHAVIORAL In-test self-signed cert; no operator material (NFR003).
SIGNALS-AUTH-MTLS-AC-002 Encrypted key + correct passphrase loads; wrong passphrase fails without echoing passphrase/key TestFileCertLoaderEncryptedKey COVERED BEHAVIORAL FC-MTLS-003; stdlib legacy-PEM decryption (NFR001).
SIGNALS-AUTH-MTLS-AC-003 Loader re-reads files each call; rotated cert/key picked up without restart (no cache) TestFileCertLoaderRotation COVERED BEHAVIORAL INV-MTLS-003.
SIGNALS-AUTH-MTLS-AC-004 mtls without sslcert/sslkey → hard startup error naming both fields TestValidateRejectsMTLSWithoutCertOrKey COVERED BEHAVIORAL FC-MTLS-001.
SIGNALS-AUTH-MTLS-AC-005 mtls requires verify-full in every env; rejects weaker; accepts verify-full TestValidateRejectsMTLSWithoutVerifyFull, TestValidateAcceptsMTLS COVERED BEHAVIORAL FC-MTLS-004 / INV-MTLS-004.
SIGNALS-AUTH-MTLS-AC-006 mtls + any inline password source → hard startup error (auth is by certificate) TestValidateRejectsMTLSWithPasswordSource COVERED BEHAVIORAL FC-MTLS-005.
SIGNALS-AUTH-MTLS-AC-007 Mismatched pair / non-PEM key → redacted error; key material never in the error TestFileCertLoaderMismatchedPair, TestFileCertLoaderNonPEMKey COVERED BEHAVIORAL FC-MTLS-002 + INV-MTLS-001.

Guided connect — signalsctl connect --auto (#99)

Spec: features/signals/guided-connect.md (ACTIVE). Orchestrates the six stages (detect → resolve → diagnose → role-safety → guidance → emit) over the existing credential providers, conntest, and ValidateRoleSafety (INV003 — reuse, not reimplement). Detection (env-only, no network per NFR001) in internal/guidedconnect/detect.go; the injectable-seam orchestrator (Run, GuidanceFor, renderTargetBlock, appendTarget) in internal/guidedconnect/orchestrator.go. The credential-aware diagnostic is additive: collector.BuildConnConfigWithCredential + the exported collector.CredentialResolver/NewCredentialResolver seam + conntest.TestConnectionWithResolver (shared diagnosePool tail with the production password-only TestConnection, preserving the collector path). MTLSGuidance completes the five-method guidance set. CLI wiring in cmd/signalsctl/connect.go (connectAutoCmd); password fallback prompts only on a TTY, else reports FC006. Unit tests: internal/guidedconnect/detect_test.go, internal/guidedconnect/orchestrator_test.go, internal/collector/connbuild_test.go, internal/conntest/conntest_test.go (resolver path), cmd/signalsctl/connect_auto_test.go. No cloud/network/DB needed in default CI — all seams are faked.

Rule ID Rule Summary Test ID(s) Coverage Status Evidence Type Notes
SIGNALS-CONNECT-AC001 A correctly-configured passwordless target connects, passes the read-only safety check, and emits a secret-free targets: block TestRun_HappyPath_SecretFreeBlock, TestBuildConnConfigWithCredential_Password, TestBuildConnConfigWithCredential_Certificate COVERED BEHAVIORAL INV001/INV002; emitted block carries auth_method + sslmode: verify-full, never a credential.
SIGNALS-CONNECT-AC002 Each documented environment (ambient identity + matching host) proposes the documented auth_method; identity-without-host and host-without-identity fall back to password TestDetect_Table, TestDetect_HostDisambiguatesMultipleIdentities COVERED BEHAVIORAL Detection threshold = ambient cloud identity AND matching host pattern (NFR001 env-only).
SIGNALS-CONNECT-AC003 More than one cloud identity with a host matching none is reported ambiguous (with notes), never guessed TestDetect_Ambiguous, TestRun_AmbiguousReported COVERED BEHAVIORAL FC001; carries disambiguation notes naming --auth-method.
SIGNALS-CONNECT-AC004 No secret value is ever printed on any path — success, failure, or guidance TestRun_NoSecretEverPrinted, TestTestConnectionWithResolver_ResolveFailure, TestMTLSGuidance COVERED BEHAVIORAL INV001; resolver/diagnose detail is redacted; guidance carries no key material.
SIGNALS-CONNECT-AC005 A connect that succeeds but is rejected for a missing grant returns the exact GRANT/role guidance for the detected method TestRun_MissingGrantGuidance COVERED BEHAVIORAL FC004; table-driven across all six methods via GuidanceFor.
SIGNALS-CONNECT-AC006 An over-privileged role fails the read-only safety check and is reported without emitting a config block TestRun_OverPrivilegedRole COVERED BEHAVIORAL FC005; reuses ValidateRoleSafety (INV003); no block on failure.
SIGNALS-CONNECT-AC007 --write is dry-run by default; when set it appends a secret-free block to targets: (creating the key if absent) and refuses a duplicate name TestRun_DryRunWritesNothing, TestRun_WriteAppendsBlockAndRefusesDuplicate, TestRun_WriteCreatesTargetsKey COVERED BEHAVIORAL Atomic temp+verify+rename; duplicate-name refusal.
SIGNALS-CONNECT-AC008 Every supported auth_method has a detection path, a connection path, and method-specific guidance TestRun_CoversAllMethods, TestMTLSGuidance COVERED BEHAVIORAL GuidanceFor dispatches to the five provider guidance funcs + password fallback.
SIGNALS-CONNECT-INV004 (single method) Exactly one auth method per run; an explicit --auth-method overrides detection TestRun_AuthMethodOverridesDetection COVERED BEHAVIORAL Override wins over the detector.
SIGNALS-CONNECT-FC002 (resolve failure) An unresolvable credential short-circuits to password_resolve with a redacted detail and no dial TestTestConnectionWithResolver_ResolveFailure COVERED BEHAVIORAL INV001; returns before opening a socket.
SIGNALS-CONNECT-FC006 (no password source) Password method with no TTY and no supplied password is reported, never blocks waiting for input TestRun_PasswordFallbackNoSource COVERED BEHAVIORAL Non-interactive by default; prompt only on a TTY.
SIGNALS-CONNECT-usage Missing --user/--host or an unknown --auth-method is a usage error before any detection or dial TestRun_RequiresUserAndHost, TestConnectAutoCmd_RequiresUser COVERED BEHAVIORAL Validated up front; CLI maps to usageError.
R116 (owner-only degrade) An OwnerOnlyDegrade collector that hits 42501 is recorded skipped/privilege_owner_only (not failed); the same error on other collectors stays failed; cycle not partial TestClassifyQueryFailureOwnerOnlyPermissionDeniedDegradesToSkipped, TestClassifyQueryFailureOwnerOnlyNonPermissionStaysFailed, TestClassifyQueryFailureNonOwnerPermissionDeniedStaysFailed, TestPrivilegeOwnerOnlyIsNotBudgetExhausted, TestExtStatDataCollectorsAreOwnerOnlyDegrade, TestOrdinaryCollectorIsNotOwnerOnlyDegrade, TestOnlyExtStatDataCollectorsAreOwnerOnlyDegrade COVERED BEHAVIORAL specifications/owner_only_privilege_degradation.md (#200); pg_statistic_ext_data has PUBLIC SELECT revoked.
R117 (advisory dedup) A persistent per-cycle advisory is logged once per (target, collector, kind) per daemon run, not every poll TestWarnOnceDeduplicatesPerTargetQueryKind COVERED BEHAVIORAL specifications/owner_only_privilege_degradation.md (#200).
R118 (export run classification) Every query_runs.ndjson row carries status/reason verbatim from the persisted columns, so the R116 classification survives the snapshot boundary TestExportQueryRunsCarryStatusAndReason, TestExportQueryRunsOwnerOnlySkipDistinguishable, TestExportQueryRunsUnknownStatusVerbatim COVERED BEHAVIORAL specifications/export-query-run-status.md (#250); TC-EQRS-01..04.

Canonical collector inventory (#252)

Spec: specifications/collector-inventory.md (ACTIVE). A committed, machine-readable inventory of registered collector IDs (specifications/collectors/collector-inventory.json), generated from the query registry by cmd/gen-collector-inventory and held in sync by a CI gate. Gives out-of-module consumers (Workbench catalogue generator, Analyzer WPS-R008 freshness checks) a mechanical view of the wire-name enum — spec filenames diverged from registered IDs (family specs, spec-less IDs), which is how the downstream bundled catalogue went stale at 64 of 99 names (Elevarq/Analyzer#1378, Elevarq/Workbench#765). Implementation: internal/pgqueries/inventory.go; tests: internal/pgqueries/inventory_test.go; acceptance: specifications/collector-inventory.acceptance.md.

Rule ID Rule Summary Test ID(s) Coverage Status Evidence Type Notes
R119 (inventory = registry) Committed inventory lists exactly the registered IDs with their registered categories, no additions or omissions TestCollectorInventoryFileInSync, TestCollectorInventoryMatchesRegistry COVERED BEHAVIORAL TC-CINV-01/03; INV-CINV-01.
R120 (canonical encoding) Byte-stable canonical JSON: sorted by name, sorted keys, two-space indent, single trailing newline, no volatile fields TestCollectorInventoryCanonicalEncoding, TestCollectorInventoryFileInSync COVERED BEHAVIORAL TC-CINV-02; regeneration of an unchanged registry is a byte-identical no-op.
R121 (CI sync gate) Registry↔inventory drift fails CI naming the missing/extra IDs; malformed committed bytes fail with a parse error TestCollectorInventoryDriftIsNamed, TestCollectorInventoryMalformedFile, TestCollectorInventoryFileInSync COVERED BEHAVIORAL TC-CINV-04/05.
R122 (contract version) contract_version = 1 gates the document schema; changes are deliberate spec amendments TestCollectorInventoryCanonicalEncoding COVERED BEHAVIORAL TC-CINV-02.
R123 (#293) Decision-grade index health (index_health_summary_v2) TC-IHV2-01..14 (tests/signals_index_health_summary_v2_test.go) COVERED BEHAVIORAL Additive collector in internal/pgqueries/catalog_index_health.go; v1 (R103) unchanged. One row per non-system index with explicit safety/constraint/build state and a versioned semantic structure fingerprint. Usage counters (idx_scan/idx_tup_read/idx_tup_fetch) and state booleans emitted raw — never COALESCEd to 0/false (R-IHV2-01). Constraint backing from pg_constraint.conindid -> controlled primary/unique/exclusion/other; build_state from pg_stat_progress_create_index (reindex vs build via left(command,2)='RE', avoiding the CREATE/REINDEX linter keywords) distinguishing active build from invalid_residue/not_ready_residue. structure_fingerprint = md5 over a name/table-independent normalization of pg_get_indexdef plus uniqueness/exclusion facts; exact_duplicate_of set only on full fingerprint equality (R-IHV2-05), prefix_candidate_of a labelled review candidate, never a drop verdict (R-IHV2-06). System schemas excluded (INV-SIGNALS-12); deterministic order; passes the linter; PG14-18 (live-validated on 14 and 18). Unblocks Elevarq/Analyzer#1596. Spec: specifications/collectors/index_health_summary_v2.md.
R124 (#294) Concurrent-DDL capability evidence in index_health_summary_v2 TC-IHV2-15 + TC-IHV2-04 (tests/signals_index_health_summary_v2_test.go) COVERED BEHAVIORAL Extends R123: index_health_summary_v2 emits relation_kind (controlled index/partitioned_index/other) and a strict is_partitioned boolean from pg_class.relkind ('I' -> partitioned parent index -> is_partitioned = true; 'i' -> ordinary / partition-local -> false). relkind is NOT NULL so the fact is always known, never synthesized as false (R-IHV2-08). The analyzer requires is_partitioned = false before DROP INDEX CONCURRENTLY (unsupported on partitioned indexes; https://www.postgresql.org/docs/current/sql-dropindex.html). Partition-local indexes are distinguished from the partitioned parent. Live-validated on PG18 with a partitioned table + parent index + partition-local indexes + an ordinary index. Required by Elevarq/Analyzer#1596. Spec: specifications/collectors/index_health_summary_v2.md.
R125 (#340) No-false-clean export: default scope with no successful data is refused (HTTP 422), never a clean empty ZIP; distinguishes no_collection_yet vs last_collection_failed; --all permissive but marks collection_status TestExportRefusesDefaultNoData, TestExportRefusesDefaultFailedCycle, TestExportDefaultWithDataUnchanged, TestExportAllScopeMarksCollectionStatus (tests/signals_export_falseclean_test.go) COVERED BEHAVIORAL FC-05/INV-SIGNALS-25. Default no-data export returns 422 + export_rejected audit; every emitted metadata.json carries collection_status (ok/no_collection_yet/last_collection_failed). Composes with Analyzer#1885/#1887 consumer-side guards. TC-SIG-024/127/128/129.
R126 (#340) Persisted last-cycle outcome: a fully-failed cycle (cycleStatus=failed) durably records target + failure category + timestamp (no secrets); a later success clears it TestFailedCyclePersistsOutcome, TestSuccessfulCycleClearsFailedOutcome, TestCycleOutcomeCarriesNoSecrets (tests/signals_cycle_outcome_test.go) COVERED BEHAVIORAL INV-SIGNALS-07/25. Migration 005_target_cycle_outcomes.sql; feeds R125's last_collection_failed distinction. TC-SIG-126.

Collector output-contract verification (#314)

Spec: specifications/collector-output-contract.md (ACTIVE), with the supporting invariants in features/signals/appendix-a-api-contract.md §"Collector output-contract verification (#314)" (INV-OUTPUT-CONTRACT, INV-CHAR-TEXT-VERIFIED, INV-STATUS-PAYLOAD-VERIFIED). The missing STDD tests → artifact link: a live-PG integration harness that runs the full collection against a real PostgreSQL carrying representative schema (parent + child with an unindexed FK), exports a snapshot ZIP via the production export.Builder.WriteTo path, reads it back, and asserts the per-collector output contract for the catalog/schema collectors. This locks the #312 internal-"char" class permanently — the contype == "f" assertion is RED on pre-#313 uncast SQL and GREEN after. Test: tests/signals_collector_output_contract_integration_test.go (//go:build integration, SIGNALS_TEST_PG_DSN-gated); runs in the CI PG-version matrix 14/15/16/17/18. Acceptance: specifications/collector-output-contract.acceptance.md.

#326 extends the harness to the remaining internal-"char" schema collectors #314 left unexercised: it seeds a partitioned table, a trigger, and a SQL function and asserts pg_partitions_v1.partition_strategy, pg_triggers_v1/pg_triggers_definitions_v1.tg_enabled, and pg_functions_v1/pg_functions_definitions_v1.volatility decode as single-char strings (OC-R006). When a superuser DSN (SIGNALS_TEST_PG_SUPERUSER_DSN) provisions the FDW capability it also seeds a foreign table and asserts fdw_foreign_tables_v1.relkind == "f"; without the capability that leg is a documented skip (OC-R007). This is test-only — the production normalization already landed in #319/#321.

#316 closes the #314 fast-follow (NFR-02): the per-collector declared-column assertion (OC-R002) now spans the whole registry, not only the schema collectors. The declared columns are derived from each collector's spec ## Output columns table at test time (a spec parser

  • a family-source map for the definition-mode / MCV variants), so the assertion can never drift from the spec (INV-07). New cheap fixtures (a view, materialized view, enum + composite type, RLS policy, extended statistics, a SET-config function, a user rule) lift the schema collectors that were previously zero-row into the asserted set. Every collector that still emits no rows in the test environment is enumerated in an explicit zero-row allowlist with a per-collector reason — contention (blocking_locks_v1, pg_locks_summary_v1, …), in-flight operations (pg_stat_progress_*), replication (replication_status_v1, …), uninstalled extensions (pg_stat_statements_v1, pg_vector_columns_v1, the timescaledb_* family), privilege the pg_monitor role lacks (pg_statistic_ext_data_v1 / _mcv_v1), a non-default GUC (pg_prepared_xacts_v1), or rare user-only catalog objects (pg_operators_v1, pg_aggregates_v1, pg_casts_v1, pg_collations_v1, pg_text_search_v1, fdw_user_mappings_v1) — and a collector emitting no rows that is NOT allowlisted fails the harness (OC-R008), so coverage is honest and a newly-added collector cannot slip through unclassified. The assertions are proven real by a mutation spot-check: renaming pg_indexes_v1.indexname's SQL alias turns the sweep RED. No production output-contract violation was found — the missing column hits during development were all spec-parsing artifacts (definition-mode columns wrongly attributed to inventory collectors, and the version-variant/scalar collectors), resolved by the family-source map and the column-dynamic classification, not by weakening any assertion. Test helper: tests/signals_collector_output_contract_columns.go.

#320 completes the type family the internal-"char" bug (#312) belonged to. "char" (OID 18) was one member of a class: pgx maps each PostgreSQL type to a Go type, queryToMaps stores it, and encoding/json serializes it — so a type whose exported JSON form the Analyzer misreads is a silent contract violation exactly like char. char was fixed centrally (#319); #320 audits and locks the remaining classes by exported JSON shape, cross-checked against the live Analyzer consumer, in a new sibling harness TestIntegration_CollectorTypeContractAgainstRealPG (tests/signals_collector_type_contract_integration_test.go) sharing the #314/#316/#326 fixture. It was measured, not guessed on a live PG 14–18: numeric→JSON number (pgx pgtype.Numeric; a NaN is the string "NaN"), jsonb→object (map[string]any), arrays→array ([]any), timestamps→RFC3339 string (time.Time), oid→number (uint32), booltrue/false. No real mismatch was found — the char class was uniquely broken (pgx's default QCharCodec returns an integer) whereas every other class has a correct pgx codec, so #320 adds no production SQL change; it is a regression lock (OC-R009) plus an explicit type-class coverage map (OC-R010). Two nuances measurement caught that guessing would have missed: (a) the redacted FDW option columns (fdw_options, server_options, foreign_table_options) are deliberately rendered text[]map[string]string object by the redaction post-processor (a contract the Analyzer ingestion test pins), so they are asserted as objects; (b) pg_policies_v1.permissive is a PG text column ("PERMISSIVE"/"RESTRICTIVE"), correctly a string, not a bool. bytea has no collector column and is recorded not-exercised. The assertions are proven real by mutation spot-checks (asserting a bool as a number, and a column that emits no non-null value, both go RED). Test file: tests/signals_collector_type_contract_integration_test.go.

Rule ID Rule Summary Test ID(s) Coverage Status Evidence Type Notes
OC-R001 (collect against live PG) Full collection runs against a real PostgreSQL target and exports a snapshot ZIP via the production export path TestIntegration_CollectorOutputContractAgainstRealPG COVERED INTEGRATION //go:build integration + SIGNALS_TEST_PG_DSN; CI matrix PG 14-18. Not in default unit CI.
OC-R002 (declared columns present) Every registered collector (#316 — not only catalog/schema) with a non-empty payload has its spec-derived declared columns (parsed from specifications/collectors/<id>.md ## Output columns, never hand-copied) present in each query_results payload object; column-dynamic collectors (SELECT */version-variant/scalar) are row-presence-only TestIntegration_CollectorOutputContractAgainstRealPG COVERED INTEGRATION TC-OC-03; INV-OUTPUT-CONTRACT/INV-07; #316. Mutation-verified RED on a renamed pg_indexes_v1.indexname alias.
OC-R003 (char-type is text) Internal-"char" columns (contype/relkind/relpersistence/provolatile/prokind) decode as single-char strings; seeded FK yields contype == "f" TestIntegration_CollectorOutputContractAgainstRealPG COVERED INTEGRATION TC-OC-01/02; INV-CHAR-TEXT-VERIFIED; the #312 regression lock. Also caught + fixed an uncast pg_class_storage_v1.relpersistence residual.
OC-R004 (status↔payload joinable) Every exported status=success/row_count=N run has exactly one joinable payload with N objects TestIntegration_CollectorOutputContractAgainstRealPG COVERED INTEGRATION TC-OC-04; INV-STATUS-PAYLOAD-VERIFIED; end-to-end proof of INV-SNAP-STATUS-PAYLOAD (#312).
INV-SNAP-STATUS-PAYLOAD (retention producer, #327) Retention cleanup deletes a run's payload + its run row atomically; a failure after the payload delete leaves BOTH tables intact, so no success run is ever stripped of its joinable payload internal/db/retention_atomic_test.go (TestDeleteQueryRunsOlderThan_AtomicOnSecondStepFailure, TestDeleteQueryRunsOlderThanByClass_AtomicOnSecondStepFailure, TestDeleteQueryRuns*_InvariantHoldsAfterCleanup) COVERED BEHAVIORAL FC-23; failure-injection proving the transaction rolls back both deletes on a second-step failure; post-cleanup asserts every remaining success run has exactly one joinable payload with row_count rows. Covers both the legacy flat helper and the per-class production helper.
OC-R005 (char-type normalized at connection boundary) OID 18 decodes as text for ALL collectors via a pooled-connection type registration; the columns #313 missed — relkind (catalog_bloat_v1/catalog_index_bloat_v1/fdw_*_v1), provolatile/volatility (functions), attidentity (identity, "" when non-set) — decode as strings, never JSON numbers TestIntegration_CollectorOutputContractAgainstRealPG COVERED INTEGRATION TC-OC-06; INV-04; the #319 central-fix regression lock (root cause of Elevarq/Analyzer#1871).
OC-R006 (char sweep covers remaining schema collectors) The char whitelist includes the aliases partition_strategy (pg_partitions_v1), tg_enabled (pg_triggers_v1/pg_triggers_definitions_v1), volatility (the output alias of provolatile in pg_functions_v1/pg_functions_definitions_v1), and attidentity; a seeded partitioned table, trigger, and function make each collector emit a single-char string for its aliased char column, never a JSON number TestIntegration_CollectorOutputContractAgainstRealPG COVERED INTEGRATION TC-OC-07; INV-05; regression-locks #319 across the schema collectors #314 missed (#326).
OC-R007 (FDW char capability-gated) When a superuser DSN provisions postgres_fdw + a foreign server + GRANT USAGE, a seeded foreign table makes fdw_foreign_tables_v1 emit relkind == "f" (single-char string, never a number); absent the capability the FDW fixture + assertion are skipped with a documented reason and every other assertion still runs TestIntegration_CollectorOutputContractAgainstRealPG COVERED INTEGRATION TC-OC-08; INV-05/INV-06; capability-gated FDW leg of #326.
OC-R008 (no silent coverage gap) Every registered collector is accounted for: rows-asserted (OC-R002), column-dynamic row-presence, or zero-row allowlisted with a documented reason; a collector emitting no rows that is NOT allowlisted fails the harness naming it; a coverage report enumerates the asserted count + the not-exercised allowlist TestIntegration_CollectorOutputContractAgainstRealPG COVERED INTEGRATION TC-OC-09; INV-07; #316. Local PG-17 run: 55/100 declared-column-asserted + 2 column-dynamic row-presence; 43 not-exercised (allowlisted with reasons).
OC-R009 (type classes land in Analyzer-expected JSON form) Each audited PostgreSQL type class beyond internal-"char" (numeric→number, jsonb→object, array→array, FDW-option→object, timestamp→RFC3339 string, oid→number, bool→true/false) is asserted against the exported ZIP read back through encoding/json, exercised by a seeded non-null value; a NaN numeric is the string "NaN" (documented exception); a wrong shape or a vacuous (no non-null) assertion fails the harness TestIntegration_CollectorTypeContractAgainstRealPG COVERED INTEGRATION TC-OC-10; INV-08; the #320 type-fidelity regression lock (general case of the #312 contype bug). Cross-checked vs the live Analyzer consumer (toFloat64/relidToInt64, map[string]any jsonb parsing, []any array coercion, RFC3339 time.Parse, asBool). Mutation-verified RED on a bool-as-number assertion and a no-such-column vacuous assertion. FDW-option-object leg is capability-gated (superuser DSN).
OC-R010 (no silent type-class gap) Every audited type class is accounted for: ≥1 seeded asserted combination, or a recorded not-exercised reason (bytea — no collector column; regclass-text — only on capability-gated collectors); a class with zero asserted combinations and no reason fails the harness; a per-class coverage report is logged TestIntegration_CollectorTypeContractAgainstRealPG COVERED INTEGRATION TC-OC-11; INV-08; #320. Local PG-18 run (FDW capability present): numeric/oid/timestamp/bool/array 5 each, jsonb 3, fdw-option-object 3 asserted; bytea + regclass-text not-exercised. PG-15 run (no FDW): fdw-option-object not-exercised (capability gate), rest asserted.