| Item | Detail |
|---|---|
| Status | Built and verified — V1 + V2 + V3 + V4 extensions complete |
| Build date | 2026-04-22 (V3 complete); 2026-04-24 (V4 complete); 2026-04-30 (V4 bug fixes — see v4 addendum) |
| Manifest coverage | 36 / 36 defects detected (100%) |
| Checks implemented | 45 / 45 (Tiers 1–6) |
| Python requirement | 3.6 or later (stdlib + sqlite3 only — no pip installs for CLI; flask + anthropic for V4 web interface) |
V1 covers Tiers 1–4 (28 checks, D01–D24).
V2 adds Tier 5 business logic (10 checks, D25–D30) — see diagnostic_engine_build_spec_v2.md.
V3 adds Tier 5 calculation + Tier 6 external reference (7 checks, D31–D36) — see diagnostic_engine_build_spec_v3_addendum.md.
V4 adds a Flask web application with AI-powered NL query interface, interactive dashboard, and AI narrative report — see diagnostic_engine_build_spec_v4_addendum.md.
Three deviations were identified and resolved during V1 build. See the relevant check entries in Section 3.2 for full detail.
| Check | Deviation | Status |
|---|---|---|
| CHK_T3_06 | TRANSACTION table contains only TXN_TYPE='PREM' — no SURR/CLAIM/MATURITY records exist. Substituted a payment-gap approach (>730 days between consecutive payments on SR/DC/MT policies). Fully documented in code. | Necessary adaptation — detects D18 correctly |
| CHK_T3_04 | Original spec said return both rate records per inversion pair; initial build returned only one. Fixed to return both via UNION. | Fixed |
| CHK_T4_02 | Original spec said group by PLAN_CODE and report count per plan; initial build returned individual RATE_IDs. Fixed to return plan codes with counts. | Fixed |
| Check | Deviation | Status |
|---|---|---|
| CHK_T5_11 | Detection threshold is 0.95 (spec says 0.90). 1/1.08 ≈ 0.926, which is above 0.90 — the spec threshold is mathematically impossible to trigger with an 8% modal loading omission. Raised to 0.95 to correctly detect the defect. | Necessary adaptation |
| CHK_T5_15 | Spec testing checklist said passes=True; check correctly fails with 8 records because the cumulative bonus check catches the same D30 unit-error. D30 is detected redundantly by both CHK_T5_06 and CHK_T5_15. |
Expected behaviour — manifest coverage unaffected |
| Location | Spec | As built |
|---|---|---|
| Progress lines | (1 issue) / (0 issues) |
(1 issue(s)) |
| Passed checks row | — no issues |
✓ no issues |
| Findings label | Seeded defect: |
Seeded defect(s): |
Build a Python diagnostic tool that connects to mock_pas.db, runs a library of
SQL-based risk checks organised across four tiers, and produces both a structured
findings file and a human-readable report.
The tool must:
- Identify all 24 seeded defects catalogued in
defect_manifest.json - Run generalised checks that would catch similar issues beyond the seeded defects
- Produce a scorecard comparing findings against the manifest (ground-truth validation)
- Require no external dependencies beyond Python standard library and
sqlite3(which is bundled with Python — no pip installs needed)
mock_pas/
├── mock_pas.db # Existing — do not modify
├── defect_manifest.json # Existing — read-only input
├── requirements.txt # V4: flask>=2.3.0, anthropic>=0.25.0
├── app.py # V4: Flask web application entry point
├── start_web.sh # V4: convenience start script (defaults to port 5001)
├── run_all.sh # Shell script: runs run_diagnostics.py then report.py (CLI)
├── diagnostic/
│ ├── checks.py # Check function library (one function per check)
│ ├── run_diagnostics.py # Runner: executes all checks, writes findings.json
│ ├── report.py # Reporter: reads findings.json, prints formatted report
│ ├── findings.json # Output: generated by run_diagnostics.py
│ ├── report.html # Output: generated by report.py
│ ├── nl_query.py # V4: NL→SQL engine (Anthropic API)
│ ├── report_ai.py # V4: AI narrative report generator (Anthropic API)
│ ├── draft_checks.py # V4: draft check CRUD and Python codegen
│ ├── draft_checks/ # V4: runtime-generated draft .json files
│ ├── templates/ # V4: Jinja2 HTML templates
│ │ ├── base.html
│ │ ├── home.html
│ │ ├── select_checks.html
│ │ ├── dashboard.html
│ │ ├── nl_query.html
│ │ ├── review_checks.html
│ │ └── coming_soon.html # placeholder for future pages
│ └── static/ # V4: CSS and JS
│ ├── style.css
│ └── app.js
└── reference_data/
├── regulatory_params.json
├── mortality_cso2001.csv
└── generate_reference_data.py
This file contains all check functions. It is a pure library — it does not run anything on import. The runner (run_diagnostics.py) imports and calls these functions.
Every check function must follow this exact signature and return format:
def check_<name>(conn: sqlite3.Connection) -> dict:
"""
One-sentence description of what this check detects.
Tier: <1|2|3|4>
Risk category: <Data Integrity|Structural|Actuarial Logic|Governance>
Seeded defects covered: <D01, D04> or "None — generalised check only"
"""
cursor = conn.cursor()
# --- SQL query ---
cursor.execute("""
SELECT ...
FROM ...
WHERE ...
""")
rows = cursor.fetchall()
affected_keys = [str(row[0]) for row in rows]
return {
"check_id": "CHK_<ID>", # e.g. "CHK_T1_01"
"tier": 1, # integer 1–4
"tier_label": "Data Integrity", # matches tier number
"severity": "HIGH", # HIGH / MEDIUM / LOW
"table": "POLICY", # primary table checked
"field": "SUM_ASSURED", # primary field checked (or "MULTIPLE")
"check_name": "Null sum assured on inforce policy",
"description": "One to two sentences explaining what was found and why it matters.",
"affected_count": len(affected_keys),
"affected_keys": affected_keys[:20], # cap display at 20 records
"passed": len(affected_keys) == 0,
"seeded_defects": ["D01"], # list of manifest IDs this covers
"remediation": "One sentence on how to fix."
}Rules:
passedis True only whenaffected_count== 0 (no issues found)affected_keysshould be the primary key of the affected table (POLICY_NO, RATE_ID, etc.)- Cap
affected_keysat 20 items in the return value to keep findings.json readable - Every function must handle SQL exceptions gracefully: wrap the execute in try/except,
return
"passed": Falseand"affected_count": -1with an error description if the query fails — this itself is a signal the schema may be malformed
Implement all 28 V1 checks below. V2 addendum (diagnostic_engine_build_spec_v2.md)
adds 10 Tier 5 checks (CHK_T5_01–T5_10) for a total of 38. V3 addendum
(diagnostic_engine_build_spec_v3_addendum.md) adds 7 more checks (CHK_T5_11–T5_15,
CHK_T6_01–T6_02) for a final total of 45. See §3.3 for the complete as-built registry.
Checks marked [SEEDED] directly target one or more seeded defects. Checks marked [GENERAL] are generalised — they catch whole categories of issues and will find the seeded defect as one instance among potentially many. Most checks should be GENERAL, using the seeded defect as a test case.
CHK_T1_01 — Null or zero sum assured on active policies [SEEDED: D01, D04]
Detect: POLICY records where STATUS_CODE = 'IF' AND (SUM_ASSURED IS NULL OR SUM_ASSURED <= 0)
Table: POLICY | Field: SUM_ASSURED | Severity: HIGH
CHK_T1_02 — Out-of-range premium rate values [SEEDED: D02]
Detect: PREMIUM_RATE records where RATE_PER_1000 <= 0 OR RATE_PER_1000 > 5000
Table: PREMIUM_RATE | Field: RATE_PER_1000 | Severity: HIGH
Note: 5000 per 1000 SA = 500% — no realistic product exceeds this
CHK_T1_03 — Impossible date of birth [SEEDED: D03]
Detect: LIFE records where DOB > DATE('now') OR DOB < '1900-01-01'
Table: LIFE | Field: DOB | Severity: HIGH
CHK_T1_04 — Spreadsheet error artifacts in text fields [SEEDED: D05]
Detect: LIFE records where any of ID_NO, FULL_NAME contain '#VALUE!', '#N/A',
'#REF!', '#DIV/0!', '#NUM!', '#NAME?', '#NULL!'
Also check: SYSTEM_PARAMETER.PARAM_VALUE for same patterns
Table: LIFE, SYSTEM_PARAMETER | Field: MULTIPLE | Severity: MEDIUM
Use: WHERE field LIKE '#%'
CHK_T1_05 — Null mandatory fields across core tables [SEEDED: D06]
Detect: Run NULL checks on the following NOT-logically-nullable fields:
POLICY: POLICY_NO, PLAN_CODE, STATUS_CODE, ISSUE_DATE, CURRENCY_CODE, AGE_CALC_METHOD
LIFE: POLICY_NO, LIFE_TYPE, FULL_NAME
BENEFIT: POLICY_NO, BENEFIT_TYPE, STATUS
PREMIUM_RATE: PLAN_CODE, GENDER, SMOKING_STATUS, RATE_PER_1000, EFFECTIVE_DATE
Return one result row per table/field combination that has any NULLs.
Table: MULTIPLE | Field: MULTIPLE | Severity: HIGH
CHK_T1_06 — Negative or zero benefit amounts on active benefits
Detect: BENEFIT records where STATUS = 'ACTIVE' AND (BENEFIT_AMOUNT IS NULL
OR BENEFIT_AMOUNT <= 0)
Table: BENEFIT | Field: BENEFIT_AMOUNT | Severity: HIGH
Note: generalised check — no specific seeded defect, but a realistic risk
CHK_T2_01 — Orphaned policy records (invalid plan code) [SEEDED: D07]
Detect: POLICY records where PLAN_CODE NOT IN (SELECT PLAN_CODE FROM PRODUCT)
Table: POLICY | Field: PLAN_CODE | Severity: HIGH
CHK_T2_02 — Multiple life assured records on same policy [SEEDED: D08]
Detect: POLICY_NO values in LIFE where COUNT of LIFE_TYPE = 'LA' > 1
SQL: SELECT POLICY_NO, COUNT(*) as cnt FROM LIFE
WHERE LIFE_TYPE = 'LA' GROUP BY POLICY_NO HAVING cnt > 1
Table: LIFE | Field: LIFE_TYPE | Severity: MEDIUM
CHK_T2_03 — Overlapping effective date ranges in premium rate table [SEEDED: D09]
Detect: PREMIUM_RATE records for the same (PLAN_CODE, GENDER, SMOKING_STATUS, ISSUE_AGE)
combination where date ranges overlap.
SQL approach: self-join on matching key columns where:
a.RATE_ID < b.RATE_ID
AND a.EFFECTIVE_DATE <= COALESCE(b.EXPIRY_DATE, '9999-12-31')
AND COALESCE(a.EXPIRY_DATE, '9999-12-31') >= b.EFFECTIVE_DATE
Return: both RATE_ID values via UNION (one SELECT for a.RATE_ID, one for b.RATE_ID)
Table: PREMIUM_RATE | Field: EFFECTIVE_DATE / EXPIRY_DATE | Severity: MEDIUM
CHK_T2_04 — Bonus records on non-participating policies [SEEDED: D10]
Detect: POLICY_BONUS records linked to policies on non-participating products
SQL: SELECT pb.* FROM POLICY_BONUS pb
JOIN POLICY p ON pb.POLICY_NO = p.POLICY_NO
JOIN PRODUCT pr ON p.PLAN_CODE = pr.PLAN_CODE
WHERE pr.PARTICIPATING_FLAG = 'N'
Table: POLICY_BONUS | Field: POLICY_NO | Severity: MEDIUM
CHK_T2_05 — Orphaned benefit records (non-existent policy) [SEEDED: D11]
Detect: BENEFIT records where POLICY_NO NOT IN (SELECT POLICY_NO FROM POLICY)
Table: BENEFIT | Field: POLICY_NO | Severity: LOW
CHK_T2_06 — Policy sold by terminated agent [SEEDED: D12]
Detect: POLICY records where AGENT_CODE references a terminated agent AND
ISSUE_DATE > AGENT.TERMINATION_DATE
SQL: SELECT p.POLICY_NO, p.ISSUE_DATE, a.AGENT_CODE, a.TERMINATION_DATE
FROM POLICY p JOIN AGENT a ON p.AGENT_CODE = a.AGENT_CODE
WHERE a.STATUS = 'TERMINATED'
AND a.TERMINATION_DATE IS NOT NULL
AND p.ISSUE_DATE > a.TERMINATION_DATE
Table: POLICY | Field: AGENT_CODE | Severity: LOW
CHK_T2_07 — Policies with no life assured record
Detect: POLICY_NO values in POLICY with no corresponding LIFE record
where LIFE_TYPE = 'LA'
SQL: SELECT p.POLICY_NO FROM POLICY p
LEFT JOIN LIFE l ON p.POLICY_NO = l.POLICY_NO AND l.LIFE_TYPE = 'LA'
WHERE l.LIFE_ID IS NULL
Table: POLICY | Field: POLICY_NO | Severity: HIGH
Note: Every policy must have exactly one life assured — this is a critical gap
CHK_T2_08 — Policies with no benefit record
Detect: POLICY_NO values in POLICY with STATUS_CODE = 'IF' and no BENEFIT records
Table: POLICY | Field: POLICY_NO | Severity: HIGH
Note: An inforce policy with no defined benefits cannot be administered
CHK_T3_01 — Lapsed policy with future next premium due date [SEEDED: D13]
Detect: POLICY records where STATUS_CODE = 'LP'
AND NEXT_PREM_DUE_DATE > DATE('now')
Table: POLICY | Field: NEXT_PREM_DUE_DATE | Severity: HIGH
CHK_T3_02 — Issue date after maturity date [SEEDED: D14]
Detect: POLICY records where MATURITY_DATE IS NOT NULL
AND ISSUE_DATE > MATURITY_DATE
Table: POLICY | Field: ISSUE_DATE / MATURITY_DATE | Severity: HIGH
CHK_T3_03 — Gaps in premium rate table by age [SEEDED: D15]
Detect: For each (PLAN_CODE, GENDER, SMOKING_STATUS) combination, find missing
integer ages in the sequence between MIN(ISSUE_AGE) and MAX(ISSUE_AGE).
SQL approach:
1. Get distinct age series per combination
2. Use a WITH RECURSIVE CTE to generate expected integer sequence
3. LEFT JOIN to find gaps
Return: PLAN_CODE, GENDER, SMOKING_STATUS, and list of missing ages
Table: PREMIUM_RATE | Field: ISSUE_AGE | Severity: MEDIUM
CHK_T3_04 — Non-monotonic premium rates with age (term products) [SEEDED: D16]
Detect: For TERM products (PRODUCT_TYPE = 'TERM'), find PREMIUM_RATE records where
the rate at age N is lower than the rate at age N-1 for the same combination.
SQL approach: self-join PREMIUM_RATE on same PLAN_CODE/GENDER/SMOKING_STATUS
where b.ISSUE_AGE = a.ISSUE_AGE + 1 AND b.RATE_PER_1000 < a.RATE_PER_1000
Return: Both rate records in each inversion pair via UNION
(one SELECT for a.RATE_ID at age N, one for b.RATE_ID at age N+1)
Table: PREMIUM_RATE | Field: RATE_PER_1000 | Severity: HIGH
CHK_T3_05 — Policy issued outside product entry age limits [SEEDED: D17]
Detect: POLICY records where ISSUE_AGE < PRODUCT.MIN_ENTRY_AGE
OR ISSUE_AGE > PRODUCT.MAX_ENTRY_AGE
SQL: JOIN POLICY to PRODUCT on PLAN_CODE
Table: POLICY | Field: ISSUE_AGE | Severity: HIGH
CHK_T3_06 — Premium collected after policy exit date [SEEDED: D18]
Detect: PREMIUM_HISTORY records with STATUS = 'PAID' where the policy
STATUS_CODE IN ('SR','DC','MT') and PAID_DATE is anomalously late
relative to the prior premium payment history for that policy.
IMPORTANT — as-built deviation from original spec:
The original spec called for joining to the TRANSACTION table on
TXN_TYPE IN ('SURR','CLAIM','MATURITY') to obtain the exit date.
In mock_pas.db, the TRANSACTION table contains only TXN_TYPE = 'PREM'
records — no surrender, claim, or maturity transaction records were seeded.
Following the spec literally would make this check permanently return zero results.
Substituted approach: for SR/DC/MT policies, flag any PAID premium whose
PAID_DATE is more than 730 days after the immediately preceding PAID premium
for the same policy. This reliably detects D18 (a 2019 payment on a policy
whose last normal payment was 2012) and the logic is documented in the code.
If the TRANSACTION table is later populated with proper exit-type records,
the original spec approach should be reinstated:
1. Identify exit transaction date per policy from TRANSACTION table
(TXN_TYPE IN ('SURR','CLAIM','MATURITY'), STATUS = 'POSTED')
2. Join to PREMIUM_HISTORY to find PAID premiums after that date
Table: PREMIUM_HISTORY | Field: PAID_DATE | Severity: MEDIUM
CHK_T3_07 — Surrender value factor exceeding 1.0 [SEEDED: D19]
Detect: SURRENDER_VALUE_FACTOR records where SV_FACTOR > 1.0
Also flag: SV_FACTOR < 0
Table: SURRENDER_VALUE_FACTOR | Field: SV_FACTOR | Severity: MEDIUM
CHK_T3_08 — Valuation basis referencing expired mortality table [SEEDED: D20]
Detect: VALUATION_BASIS records where EXPIRY_DATE IS NULL (i.e. currently active)
but the referenced MORT_TABLE_CODE has an EXPIRY_DATE < DATE('now')
SQL: SELECT vb.BASIS_CODE, vb.MORT_TABLE_CODE, mt.EXPIRY_DATE
FROM VALUATION_BASIS vb
JOIN MORTALITY_TABLE mt ON vb.MORT_TABLE_CODE = mt.TABLE_CODE
WHERE (vb.EXPIRY_DATE IS NULL OR vb.EXPIRY_DATE >= DATE('now'))
AND mt.EXPIRY_DATE < DATE('now')
GROUP BY vb.BASIS_CODE
Table: VALUATION_BASIS | Field: MORT_TABLE_CODE | Severity: HIGH
CHK_T3_09 — Issue age inconsistent with date of birth and issue date
Detect: POLICY records where the stored ISSUE_AGE materially differs from the
age derivable from LIFE.DOB and POLICY.ISSUE_DATE.
For ANB: expected age = year(ISSUE_DATE) - year(DOB) + 1 adjustment
For ALB: expected age = floor((ISSUE_DATE - DOB) / 365.25)
Flag where |stored_age - calculated_age| > 1
SQL: JOIN POLICY to LIFE (LIFE_TYPE='LA'), compute age, compare to ISSUE_AGE
Table: POLICY | Field: ISSUE_AGE | Severity: MEDIUM
CHK_T4_01 — Stale valuation parameters (not updated in 5+ years) [SEEDED: D21]
Detect: SYSTEM_PARAMETER records where PARAM_CATEGORY = 'VALUATION'
AND (LAST_UPD_DATE IS NULL
OR LAST_UPD_DATE < DATE('now', '-5 years'))
Table: SYSTEM_PARAMETER | Field: LAST_UPD_DATE | Severity: HIGH
CHK_T4_02 — Premium rate records with no expiry date [SEEDED: D22]
Detect: PREMIUM_RATE records where EXPIRY_DATE IS NULL, grouped by PLAN_CODE.
SQL: SELECT PLAN_CODE, COUNT(*) AS open_rate_count
FROM PREMIUM_RATE
WHERE EXPIRY_DATE IS NULL
GROUP BY PLAN_CODE
ORDER BY PLAN_CODE
Return: affected_keys are PLAN_CODE strings with counts e.g. "WL99 (52 records)".
affected_count is the number of distinct plan codes affected (not total records).
The description also reports the total record count across all plans.
Table: PREMIUM_RATE | Field: EXPIRY_DATE | Severity: MEDIUM
CHK_T4_03 — System parameter type mismatch [SEEDED: D23]
Detect: SYSTEM_PARAMETER records where PARAM_TYPE = 'NUMERIC'
but PARAM_VALUE cannot be cast to a real number.
SQL approach: Use CASE WHEN CAST(PARAM_VALUE AS REAL) IS NULL
AND PARAM_TYPE = 'NUMERIC' THEN 1 ELSE 0 END
Note: In SQLite, CAST of a non-numeric string returns 0, not NULL.
Use: WHERE PARAM_TYPE = 'NUMERIC'
AND PARAM_VALUE NOT GLOB '[0-9]*'
AND PARAM_VALUE NOT GLOB '-[0-9]*'
AND PARAM_VALUE NOT GLOB '[0-9]*.[0-9]*'
AND PARAM_VALUE != '0'
Note: '-[0-9]*' glob added (beyond original spec) to correctly pass
negative numeric values such as '-1.5'.
Table: SYSTEM_PARAMETER | Field: PARAM_VALUE | Severity: LOW
CHK_T4_04 — Overlapping effective date ranges in valuation basis [SEEDED: D24]
Detect: VALUATION_BASIS records where date ranges overlap.
SQL approach: self-join on all rows where:
a.BASIS_CODE < b.BASIS_CODE (avoid duplicate pairs)
AND a.EFFECTIVE_DATE <= COALESCE(b.EXPIRY_DATE, '9999-12-31')
AND COALESCE(a.EXPIRY_DATE, '9999-12-31') >= b.EFFECTIVE_DATE
Return: both BASIS_CODE values via UNION
Table: VALUATION_BASIS | Field: EFFECTIVE_DATE / EXPIRY_DATE | Severity: HIGH
CHK_T4_05 — Parameters with no effective date (unversioned hardcodes)
Detect: SYSTEM_PARAMETER records where EFFECTIVE_DATE IS NULL
AND EXPIRY_DATE IS NULL. These are floating parameters with no
temporal scope — a classic legacy system governance failure.
Table: SYSTEM_PARAMETER | Field: EFFECTIVE_DATE | Severity: MEDIUM
The following utilities reflect the as-built V3 final state (45 checks, Tiers 1–6). The V1 checks listed in §3.2 were extended by the V2 and V3 addendum specifications.
import os, json, csv
def get_db_connection(db_path: str) -> sqlite3.Connection:
"""Return a sqlite3 connection with row_factory set to sqlite3.Row."""
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
return conn
def load_reference_data(ref_dir: str) -> dict:
"""
Load external reference data from the reference_data/ directory.
Returns a dict with keys 'regulatory' and 'mortality_cso2001'.
Raises FileNotFoundError with a clear message if either file is missing.
Required for Tier 6 checks (CHK_T6_01, CHK_T6_02).
"""
reg_path = os.path.join(ref_dir, 'regulatory_params.json')
mort_path = os.path.join(ref_dir, 'mortality_cso2001.csv')
if not os.path.exists(reg_path):
raise FileNotFoundError(f"Regulatory params not found: {reg_path}")
if not os.path.exists(mort_path):
raise FileNotFoundError(f"Mortality reference table not found: {mort_path}")
with open(reg_path) as f:
regulatory = json.load(f)
mortality = {}
with open(mort_path, newline='') as f:
for row in csv.DictReader(f):
key = (row['GENDER'], int(row['AGE']), row['SMOKING_STATUS'])
mortality[key] = float(row['QX_REFERENCE'])
return {"regulatory": regulatory, "mortality_cso2001": mortality}
def get_all_checks() -> list:
"""Return an ordered list of all 45 check functions (Tiers 1–6)."""
return [
# Tier 1 — Data Integrity (6 checks)
check_t1_01_null_sum_assured,
check_t1_02_invalid_premium_rate,
check_t1_03_impossible_dob,
check_t1_04_spreadsheet_artifacts,
check_t1_05_null_mandatory_fields,
check_t1_06_invalid_benefit_amounts,
# Tier 2 — Structural (8 checks)
check_t2_01_orphaned_plan_code,
check_t2_02_duplicate_life_assured,
check_t2_03_overlapping_rate_dates,
check_t2_04_bonus_on_nonpar_policy,
check_t2_05_orphaned_benefits,
check_t2_06_terminated_agent,
check_t2_07_no_life_assured,
check_t2_08_no_benefit_record,
# Tier 3 — Actuarial Logic (9 checks)
check_t3_01_lapsed_future_prem_due,
check_t3_02_issue_after_maturity,
check_t3_03_rate_table_age_gaps,
check_t3_04_nonmonotonic_rates,
check_t3_05_issue_age_outside_limits,
check_t3_06_premium_after_exit,
check_t3_07_sv_factor_exceeds_one,
check_t3_08_expired_mortality_table,
check_t3_09_issue_age_inconsistent,
# Tier 4 — Governance (5 checks)
check_t4_01_stale_valuation_params,
check_t4_02_rate_no_expiry,
check_t4_03_param_type_mismatch,
check_t4_04_overlapping_val_basis,
check_t4_05_unversioned_parameters,
# Tier 5 — Business Logic / Calculation (15 checks)
# CHK_T5_01–T5_10 added by V2 addendum; CHK_T5_11–T5_15 added by V3 addendum
check_t5_01_smoker_rate_below_ns,
check_t5_02_ri_sar_exceeds_sa,
check_t5_03_policy_term_date_mismatch,
check_t5_04_ppt_exceeds_policy_term,
check_t5_05_bonus_year_exceeds_duration,
check_t5_06_bonus_amount_inconsistent,
check_t5_07_female_rate_above_male,
check_t5_08_ri_premium_implausible,
check_t5_09_par_policy_no_bonus,
check_t5_10_premium_below_rate_table,
check_t5_11_modal_premium_no_loading,
check_t5_12_substandard_no_extra_loading,
check_t5_13_nonpar_death_benefit_exceeds_sa,
check_t5_14_endowment_maturity_below_sa,
check_t5_15_cumulative_bonus_inconsistent,
# Tier 6 — External Reference / Regulatory (2 checks)
# Added by V3 addendum; gracefully skip when --ref-data not provided
check_t6_01_val_rate_exceeds_regulatory,
check_t6_02_qx_deviation_from_reference,
]This is the entry-point script. It imports checks.py, runs every check function,
and writes all results to findings.json.
"""
run_diagnostics.py
Usage: python run_diagnostics.py [--db path/to/mock_pas.db] [--manifest path/to/defect_manifest.json]
Defaults: looks for mock_pas.db and defect_manifest.json in the parent directory.
"""The script must:
-
Parse arguments — accept
--dband--manifestflags with sensible defaults (look one directory up from thediagnostic/folder). -
Load the manifest — read
defect_manifest.jsonand build a lookup dict keyed by defect_id so findings can be cross-referenced. -
Run all checks — iterate over
get_all_checks(), call each function with the connection, collect results. Print a one-line progress indicator for each check:[PASS] CHK_T1_01 — Null sum assured on inforce policy (0 issues) [FAIL] CHK_T1_02 — Out-of-range premium rate values (1 issue) -
Compute manifest coverage — for every finding where
passed = False, look up which seeded defect IDs the check covers. Build a set of discovered defect IDs and compare against the full manifest. Report:Manifest coverage: 36 / 36 defects detected Missed defects: (none) -
Write findings.json with the structure below.
{
"run_timestamp": "2026-04-22T14:32:00",
"database": "mock_pas.db",
"total_checks": 45,
"checks_passed": 16,
"checks_failed": 29,
"manifest_total": 36,
"manifest_detected": 36,
"manifest_missed": [],
"findings": [
{
"check_id": "CHK_T1_01",
"tier": 1,
"tier_label": "Data Integrity",
"severity": "HIGH",
"table": "POLICY",
"field": "SUM_ASSURED",
"check_name": "Null sum assured on inforce policy",
"description": "1 inforce policy has a NULL sum assured. This makes the policy
unadministrable — no claim, surrender, or valuation can proceed.",
"affected_count": 1,
"affected_keys": ["SG01-2010-000042"],
"passed": false,
"seeded_defects": ["D01"],
"remediation": "Recover SUM_ASSURED from original policy documents or TRANSACTION history."
}
]
}Reads findings.json and produces two outputs: a formatted console report and
report.html for browser viewing.
The following example reflects the V3 final state (45 checks, 36 seeded defects, Tiers 1–6). The V1 spec originally showed 28 checks and 4 tiers; V2 and V3 addenda extended those totals.
============================================================
POLICY ADMINISTRATION SYSTEM — DIAGNOSTIC REPORT
Database: mock_pas.db
Run date: 2026-04-22 14:32:00
Checks run: 45 | Passed: 16 | Failed: 29
============================================================
MANIFEST COVERAGE
Seeded defects detected: 36 / 36 (100.0%)
Undetected defect IDs: (none)
------------------------------------------------------------
SUMMARY BY TIER
------------------------------------------------------------
Tier 1 — Data Integrity 6 checks 3 failed 3 passed
Tier 2 — Structural 8 checks 6 failed 2 passed
Tier 3 — Actuarial Logic 9 checks 6 failed 3 passed
Tier 4 — Governance 5 checks 4 failed 1 passed
Tier 5 — Business Logic 15 checks 8 failed 7 passed
Tier 6 — External Reference 2 checks 2 failed 0 passed
TIER LEGEND
Tier 5 findings: teal left border in HTML report
Tier 6 findings: purple left border in HTML report
SUMMARY BY SEVERITY
HIGH findings: 23
MEDIUM findings: 5
LOW findings: 1
------------------------------------------------------------
FINDINGS (failed checks only, sorted by Tier then Severity)
------------------------------------------------------------
[HIGH] CHK_T1_01 | Tier 1 | POLICY.SUM_ASSURED
Null or zero sum assured on active policies
2 policies affected: SG01-2010-000042, SG01-2012-000031
An inforce policy cannot be administered without a sum assured.
Remediation: Recover from policy documents or TRANSACTION history.
Seeded defect(s): D01, D04
[HIGH] CHK_T1_02 | Tier 1 | PREMIUM_RATE.RATE_PER_1000
Out-of-range premium rate value
1 rate record affected: RATE_ID 74
A negative rate (-999) cannot produce a valid premium.
Remediation: Replace with correct rate from product pricing schedule.
Seeded defect(s): D02
... (continue for all failed checks)
------------------------------------------------------------
PASSED CHECKS
------------------------------------------------------------
CHK_T1_06 Negative benefit amounts on active benefits ✓ no issues
CHK_T5_07 Female rate above male rate ✓ no issues
CHK_T5_09 Par policy in force >2 years with no bonus ✓ no issues
CHK_T5_10 Annual premium below rate table ✓ no issues
... (list all passed checks in one compact block)
============================================================
END OF REPORT
============================================================
Generate report.html as a self-contained single file (no external CSS or JS
dependencies). Include:
- A header bar showing database name, run date, and overall pass/fail count
- A summary scorecard table: one row per tier, columns for checks run / failed / passed
- A severity badge count: HIGH / MEDIUM / LOW counts prominently displayed
- A manifest coverage section: X / 36 defects found, list any missed
- A findings section: one card per failed check, colour-coded by severity (red border = HIGH, amber = MEDIUM, grey = LOW) Additional left-accent by tier: teal (#0d9e75) for Tier 5, purple (#6f42c1) for Tier 6
- A tier accent legend in the report header explaining teal = Tier 5, purple = Tier 6
- A passed checks section: compact table listing check ID and name
Use only inline CSS. No Bootstrap, no external fonts, no JavaScript. Colours: HIGH = #dc3545 border, MEDIUM = #fd7e14 border, LOW = #6c757d border. Background: white. Font: system-ui, sans-serif. Keep it clean and printable.
#!/bin/bash
set -e
cd "$(dirname "$0")"
echo "Running diagnostic checks..."
python3 diagnostic/run_diagnostics.py --db mock_pas.db --manifest defect_manifest.json
echo ""
echo "Generating report..."
python3 diagnostic/report.py --findings diagnostic/findings.json
echo ""
echo "Done. Open diagnostic/report.html to view the full report."- Always use
conn.row_factory = sqlite3.Rowso rows behave like dicts - Never close the connection inside a check function — let the runner manage it
- Wrap every cursor.execute in try/except sqlite3.Error; return a failed result with description "SQL error: {e}" rather than raising — the runner should continue even if individual checks fail
- IMPORTANT:
TRANSACTIONis a reserved keyword in SQLite. Always quote it as"TRANSACTION"in SQL strings when referencing the TRANSACTION table.
This is the most complex check. Use a WITH RECURSIVE CTE:
WITH RECURSIVE age_series(plan_code, gender, smoking_status, age) AS (
SELECT plan_code, gender, smoking_status, MIN(issue_age)
FROM premium_rate
GROUP BY plan_code, gender, smoking_status
UNION ALL
SELECT a.plan_code, a.gender, a.smoking_status, a.age + 1
FROM age_series a
JOIN (
SELECT plan_code, gender, smoking_status, MAX(issue_age) as max_age
FROM premium_rate
GROUP BY plan_code, gender, smoking_status
) m ON a.plan_code = m.plan_code
AND a.gender = m.gender
AND a.smoking_status = m.smoking_status
WHERE a.age < m.max_age
)
SELECT a.plan_code, a.gender, a.smoking_status, a.age AS missing_age
FROM age_series a
LEFT JOIN premium_rate r ON a.plan_code = r.plan_code
AND a.gender = r.gender
AND a.smoking_status = r.smoking_status
AND a.age = r.issue_age
WHERE r.rate_id IS NULL;Return affected_keys as "PLAN_CODE/GENDER/SMOKING_STATUS/AGE" composite strings.
SQLite date arithmetic uses julianday():
SELECT p.policy_no,
p.issue_age AS stored_age,
p.age_calc_method,
l.dob,
p.issue_date,
CASE p.age_calc_method
WHEN 'ANB' THEN
CAST((julianday(p.issue_date) - julianday(l.dob)) / 365.25 AS INTEGER) + 1
WHEN 'ALB' THEN
CAST((julianday(p.issue_date) - julianday(l.dob)) / 365.25 AS INTEGER)
END AS calculated_age
FROM policy p
JOIN life l ON p.policy_no = l.policy_no AND l.life_type = 'LA'
WHERE l.dob IS NOT NULL
AND p.issue_age IS NOT NULL
AND ABS(p.issue_age -
CASE p.age_calc_method
WHEN 'ANB' THEN CAST((julianday(p.issue_date) - julianday(l.dob)) / 365.25 AS INTEGER) + 1
WHEN 'ALB' THEN CAST((julianday(p.issue_date) - julianday(l.dob)) / 365.25 AS INTEGER)
END) > 1;Build the mapping at startup:
with open(manifest_path) as f:
manifest = json.load(f)
# Build: defect_id -> defect metadata
manifest_lookup = {d["defect_id"]: d for d in manifest["defects"]}
# Build: check_id -> list of seeded defect IDs it covers
# (derived from the "seeded_defects" field in each check's return dict)After all checks run, determine coverage:
detected_defect_ids = set()
for result in all_results:
if not result["passed"]:
for did in result.get("seeded_defects", []):
detected_defect_ids.add(did)
all_defect_ids = set(manifest_lookup.keys())
missed = all_defect_ids - detected_defect_ids
coverage_pct = len(detected_defect_ids) / len(all_defect_ids) * 100Every check should be written as a general rule, not a lookup of a known bad key. Correct pattern:
# CORRECT — general check
WHERE STATUS_CODE = 'IF' AND SUM_ASSURED IS NULLIncorrect pattern:
# WRONG — hardcoded to known defect key
WHERE POLICY_NO = 'SG01-2010-000042'The seeded defect should be discovered as a natural result of the general rule — not targeted directly. This is what makes the tool genuinely useful on a real system.
-
python3 diagnostic/run_diagnostics.pyruns without errors -
findings.jsonis created and is valid JSON -
python3 diagnostic/report.pyruns without errors -
report.htmlis created and opens correctly in a browser - Console output shows progress line for each of the 45 checks (V3 final state)
-
manifest_detectedin findings.json is 36 / 36 (100%) - CHK_T1_01 finds SG01-2010-000042 in affected_keys (D01)
- CHK_T1_02 finds the negative rate record (D02) — RATE_ID 74
- CHK_T1_03 finds the future DOB record (D03) — LIFE_ID 8
- CHK_T3_02 finds the issue-after-maturity policy (D14) — SG01-2014-000073
- CHK_T3_04 finds the non-monotonic rate at age 55 for TRM20/M/NS (D16) — RATE_IDs 26, 27, 37, 38
- CHK_T4_01 finds the stale valuation parameter (D21) — PARAM_IDs 16, 1, 2
- Run against an empty database — all checks return passed=True gracefully (not verified during build; error-handling structure should handle it correctly)
- Run with a missing manifest file — script exits with a clear error message, not a Python traceback
- The age gap CTE check (CHK_T3_03) completes in under 10 seconds (achieved: 0.02s)
- findings.json affected_keys never exceeds 20 items per finding