Issue: #199 — GOV-A4: Version Drift Detection Automation Last updated: 2026-03-01 Status: Active — automated detection, manual remediation
Drift detection prevents silent architectural inconsistency. When code changes without updating registries, documentation, or naming conventions, the system accumulates "drift" — small mismatches that erode trust and cause hard-to-diagnose failures.
This system provides automated detection at three levels:
| Level | Tool | Frequency |
|---|---|---|
| SQL runtime checks | governance_drift_check() |
Every QA run |
| Documentation freshness | scripts/check_doc_drift.py |
Weekly / CI |
| Migration ordering | scripts/check_migration_order.py |
Every commit |
8 automated checks, each returning pass or drift:
| # | Check Name | Severity | What It Detects | Delegates To |
|---|---|---|---|---|
| 1 | formula_weight_drift |
critical | Active formula weights ≠ stored fingerprint | check_formula_drift() |
| 2 | function_source_drift |
critical | Function body modified without updating registry | check_function_source_drift() |
| 3 | scoring_version_count |
critical | Multiple active scoring versions | Direct query |
| 4 | search_config_count |
critical | Multiple active search ranking configs | Direct query |
| 5 | scoring_function_exists |
critical | compute_unhealthiness_v32 missing from pg_proc |
Direct query |
| 6 | trigger_naming_convention |
medium | Products table triggers not following naming pattern | pg_trigger regex |
| 7 | stale_feature_flags |
medium | Expired feature flags still enabled | Direct query |
| 8 | source_hashes_populated |
medium | formula_source_hashes table is empty |
Direct query |
Running manually:
SELECT * FROM governance_drift_check();Persisting results:
SELECT log_drift_check(); -- Returns run_id UUIDQuerying history:
SELECT * FROM drift_check_results
WHERE checked_at > now() - interval '30 days'
ORDER BY checked_at DESC;Scans docs/ directory and flags .md files not updated within a threshold.
python scripts/check_doc_drift.py # Default 90-day threshold
python scripts/check_doc_drift.py --max-age 60 # Stricter threshold
python scripts/check_doc_drift.py --warn-only # Non-blocking modeValidates that all migration files have:
- Valid
YYYYMMDDHHMMSS_description.sqlnaming - Monotonically increasing timestamps (no duplicates, no out-of-order)
- Meaningful descriptions (≥ 3 characters)
python scripts/check_migration_order.py| Severity | Response Time | Action |
|---|---|---|
| critical | Immediate | Block deployment. Fix before merging any PR. |
| medium | Next sprint | Create remediation issue. Document in drift log. |
| low | Quarterly review | Track for pattern analysis. No immediate action. |
Escalation: If a critical drift persists for > 24h, escalate per
docs/INCIDENT_RESPONSE.md severity definitions.
All user-defined triggers on the products table must follow one of:
| Pattern | Example | Use Case |
|---|---|---|
trg_products_{purpose} |
trg_products_search_vector_update |
Standard triggers |
products_{NN}_{purpose} |
products_30_change_audit |
Ordered execution |
Non-conforming triggers are flagged by check #6. Internal triggers
(tgisinternal = true) are excluded from validation.
Drift checks run as part of RUN_QA.ps1 via QA__governance_drift.sql.
Documentation and migration checks run manually.
# .github/workflows/governance-drift.yml (planned)
name: Governance Drift Check
on:
schedule:
- cron: '0 6 * * 1' # Weekly Monday 6am UTC
workflow_dispatch: {}
pull_request:
paths:
- 'supabase/migrations/**'
- 'db/**'
- 'docs/**'
jobs:
drift-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 } # Full history for git log dates
- name: Check document freshness
run: python scripts/check_doc_drift.py --warn-only
- name: Check migration ordering
run: python scripts/check_migration_order.py
# SQL drift checks run via existing qa.yml against SupabaseDeployment timeline: Activate once the governance workflow backlog clears (target: after all GOV-* issues complete).
| Week | Action | Owner |
|---|---|---|
| 1st Monday | Run governance_drift_check() via Supabase Studio SQL Editor |
Maintainer |
| 1st Monday | Run python scripts/check_doc_drift.py locally |
Maintainer |
| 1st Monday | Run python scripts/check_migration_order.py locally |
Maintainer |
| 1st Monday | Review drift_check_results table for trends |
Maintainer |
Log results: Use SELECT log_drift_check(); to persist each run for trending.
The drift_check_results table stores all past drift check runs:
| Column | Type | Purpose |
|---|---|---|
id |
bigint | Auto-incrementing PK |
run_id |
uuid | Groups all checks from one log_drift_check() call |
check_name |
text | Which check |
severity |
text | critical / medium / low |
status |
text | pass / drift / skip |
detail |
text | Human-readable description |
checked_at |
timestamptz | When the check ran |
Retention: No automatic purging. Review quarterly for cleanup.
To add a new check to governance_drift_check():
- Add a new
RETURN QUERY SELECT ...block in the function body - Update the catalog table in this document (§2.1)
- Update the header comment in the migration with the new count
- Add/update QA check in
db/qa/QA__governance_drift.sql - Update
copilot-instructions.mdif total QA check count changes
Template:
-- ── Check N: {name} ─────────────────────────────────────
RETURN QUERY
SELECT '{check_name}'::text,
'{severity}'::text,
CASE WHEN {condition} THEN 'drift' ELSE 'pass' END,
'{human-readable detail}'::text;