From 39fd1c9e42b63c81d7e40ef132624b5ec1e8432e Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 26 Jun 2026 19:38:28 +0000 Subject: [PATCH 1/4] =?UTF-8?q?feat(audit):=20pluggable=20modelling-anomal?= =?UTF-8?q?y=20audit=20over=20the=20DB=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A check-registry that reads property/baseline/plan/scenario and flags odd results (plan-below-baseline, already-meets-goal-with-works, band/score mismatch, zero-works-post-differs, effective-lodged divergence, negative bill savings). Writes modelling_audit.md/.csv. Adding a check = one decorated function. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/audit_modelling_anomalies.py | 311 +++++++++++++++++++++++++++ 1 file changed, 311 insertions(+) create mode 100644 scripts/audit_modelling_anomalies.py diff --git a/scripts/audit_modelling_anomalies.py b/scripts/audit_modelling_anomalies.py new file mode 100644 index 00000000..10f6bef1 --- /dev/null +++ b/scripts/audit_modelling_anomalies.py @@ -0,0 +1,311 @@ +"""Audit modelled Properties for *odd results* — a growing, pluggable set of +checks that read the DB and flag plans / baselines / recommendations that look +wrong, so the team can triage them instead of hunting by hand in the FE. + +Run: + python -m scripts.audit_modelling_anomalies --portfolio 796 + python -m scripts.audit_modelling_anomalies --portfolio 796 --severity high + python -m scripts.audit_modelling_anomalies --property 725634 + +Writes ``modelling_audit.md`` + ``modelling_audit.csv`` and prints a summary. + +ADDING A CHECK: write a function ``(a: PropertyAudit) -> Optional[str]`` that +returns a one-line reason when the Property looks wrong (else None), and decorate +it with ``@check("kebab-name", Severity.HIGH)``. That is the whole contract — the +runner discovers it, runs it over every Property, and reports the reasons. Keep +each check small and single-purpose; lean on the shared `PropertyAudit` bundle +rather than re-querying. + +Read-only: this script never writes to the DB. +""" + +from __future__ import annotations + +import argparse +import csv +from dataclasses import dataclass +from enum import IntEnum +from typing import Callable, Optional + +from sqlalchemy import text + +from datatypes.epc.domain.epc import Epc +from scripts.e2e_common import build_engine, load_env + +# A..G, A best — index is the rank (lower = better) for band comparisons. +_BANDS = "ABCDEFG" + + +def _band_rank(band: Optional[str]) -> Optional[int]: + if band is None or band not in _BANDS: + return None + return _BANDS.index(band) + + +def _band_of(score: Optional[float]) -> Optional[str]: + if score is None: + return None + return Epc.from_sap_score(round(score)).value + + +class Severity(IntEnum): + LOW = 1 + MEDIUM = 2 + HIGH = 3 + + +@dataclass(frozen=True) +class PropertyAudit: + """Everything a check needs about one modelled Property, joined once. + + The *default* plan is the one shown in the FE; ``None`` when the Property has + no plan for the scenario. All performance figures are the persisted ones. + """ + + property_id: int + uprn: Optional[int] + portfolio_id: int + scenario_id: Optional[int] + scenario_goal_band: Optional[str] + lodged_sap: Optional[float] + lodged_band: Optional[str] + effective_sap: Optional[float] + effective_band: Optional[str] + rebaseline_reason: Optional[str] + post_sap: Optional[float] + post_band: Optional[str] + cost_of_works: Optional[float] + energy_bill_savings: Optional[float] + energy_consumption_savings: Optional[float] + + +@dataclass(frozen=True) +class Anomaly: + property_id: int + uprn: Optional[int] + check: str + severity: Severity + detail: str + + +Check = Callable[[PropertyAudit], Optional[str]] +_REGISTRY: list[tuple[str, Severity, Check]] = [] + + +def check(name: str, severity: Severity) -> Callable[[Check], Check]: + def register(fn: Check) -> Check: + _REGISTRY.append((name, severity, fn)) + return fn + + return register + + +# ───────────────────────── checks ───────────────────────── +# Each returns a reason string when the Property looks wrong, else None. + + +@check("plan-below-baseline-band", Severity.HIGH) +def _plan_below_baseline_band(a: PropertyAudit) -> Optional[str]: + """The default plan's post-works band is WORSE than the baseline band — a + retrofit plan should never end below where the property started.""" + base, post = _band_rank(a.effective_band), _band_rank(a.post_band) + if base is None or post is None or post <= base: + return None + return f"post {a.post_band} ({a.post_sap}) worse than effective baseline {a.effective_band} ({a.effective_sap})" + + +@check("plan-score-below-baseline", Severity.HIGH) +def _plan_score_below_baseline(a: PropertyAudit) -> Optional[str]: + """Post-works SAP is materially BELOW the baseline SAP — works that lower the + score, or a plan/baseline computed from different pictures.""" + if a.effective_sap is None or a.post_sap is None: + return None + if a.post_sap >= a.effective_sap - 0.5: + return None + return f"post SAP {a.post_sap:.1f} below effective baseline {a.effective_sap:.1f} (Δ{a.post_sap - a.effective_sap:.1f})" + + +@check("already-meets-goal-with-works", Severity.MEDIUM) +def _already_meets_goal_with_works(a: PropertyAudit) -> Optional[str]: + """The property already meets/exceeds the scenario's goal band, yet the plan + spends money on measures — nothing should be recommended.""" + goal, base = _band_rank(a.scenario_goal_band), _band_rank(a.effective_band) + if goal is None or base is None or base > goal: + return None + if (a.cost_of_works or 0.0) <= 0.0: + return None + return f"already {a.effective_band} >= goal {a.scenario_goal_band} but cost_of_works £{a.cost_of_works:.0f}" + + +@check("post-band-score-mismatch", Severity.MEDIUM) +def _post_band_score_mismatch(a: PropertyAudit) -> Optional[str]: + """The persisted post band disagrees with the band the post SAP implies — a + rounding/derivation bug between score and rating.""" + implied = _band_of(a.post_sap) + if implied is None or a.post_band is None or implied == a.post_band: + return None + return f"post_epc_rating {a.post_band} but post_sap_points {a.post_sap:.1f} implies {implied}" + + +@check("zero-works-post-differs", Severity.MEDIUM) +def _zero_works_post_differs(a: PropertyAudit) -> Optional[str]: + """A no-op plan (£0 of works) whose post SAP differs from the baseline — the + baseline and the plan's starting point disagree (stale or inconsistent).""" + if a.effective_sap is None or a.post_sap is None: + return None + if (a.cost_of_works or 0.0) > 0.0: + return None + if abs(a.post_sap - a.effective_sap) <= 0.5: + return None + return f"£0 works but post SAP {a.post_sap:.1f} != effective {a.effective_sap:.1f}" + + +@check("effective-lodged-divergence", Severity.LOW) +def _effective_lodged_divergence(a: PropertyAudit) -> Optional[str]: + """The Effective baseline is far from the lodged accredited figure (≥15 SAP). + Often legitimate (overrides / pre-SAP10 rebaseline), but worth a look — a big + gap can also mean a bad override or a calculator divergence.""" + if a.effective_sap is None or a.lodged_sap is None: + return None + gap = a.effective_sap - a.lodged_sap + if abs(gap) < 15: + return None + return f"effective {a.effective_sap:.0f} vs lodged {a.lodged_sap:.0f} (Δ{gap:+.0f}, reason={a.rebaseline_reason})" + + +@check("negative-bill-savings", Severity.LOW) +def _negative_bill_savings(a: PropertyAudit) -> Optional[str]: + """The plan INCREASES the annual bill — can be legitimate on a fuel-switch + (gas→ASHP), but a recommended plan that costs more to run is worth review.""" + if a.energy_bill_savings is None or a.energy_bill_savings >= 0: + return None + if (a.cost_of_works or 0.0) <= 0.0: + return None + return f"energy_bill_savings £{a.energy_bill_savings:.0f}/yr on £{a.cost_of_works:.0f} of works" + + +# ─────────────────────── runner ─────────────────────── + +_QUERY = text( + """ + SELECT p.id, p.uprn, p.portfolio_id, + pl.scenario_id, s.goal_value AS goal_band, + pbp.lodged_sap_score, pbp.lodged_epc_band, + pbp.effective_sap_score, pbp.effective_epc_band, pbp.rebaseline_reason, + pl.post_sap_points, pl.post_epc_rating, pl.cost_of_works, + pl.energy_bill_savings, pl.energy_consumption_savings + FROM property p + LEFT JOIN property_baseline_performance pbp ON pbp.property_id = p.id + LEFT JOIN plan pl ON pl.property_id = p.id AND pl.is_default = TRUE + LEFT JOIN scenario s ON s.id = pl.scenario_id + WHERE (:portfolio_id IS NULL OR p.portfolio_id = :portfolio_id) + AND (:property_id IS NULL OR p.id = :property_id) + ORDER BY p.id + """ +) + + +def _load(portfolio_id: Optional[int], property_id: Optional[int]) -> list[PropertyAudit]: + engine = build_engine() + out: list[PropertyAudit] = [] + with engine.connect() as conn: + for r in conn.execute( + _QUERY, {"portfolio_id": portfolio_id, "property_id": property_id} + ): + m = r._mapping + out.append( + PropertyAudit( + property_id=m["id"], + uprn=m["uprn"], + portfolio_id=m["portfolio_id"], + scenario_id=m["scenario_id"], + scenario_goal_band=m["goal_band"], + lodged_sap=m["lodged_sap_score"], + lodged_band=m["lodged_epc_band"], + effective_sap=m["effective_sap_score"], + effective_band=m["effective_epc_band"], + rebaseline_reason=m["rebaseline_reason"], + post_sap=m["post_sap_points"], + post_band=m["post_epc_rating"], + cost_of_works=m["cost_of_works"], + energy_bill_savings=m["energy_bill_savings"], + energy_consumption_savings=m["energy_consumption_savings"], + ) + ) + return out + + +def run(audits: list[PropertyAudit], min_severity: Severity) -> list[Anomaly]: + found: list[Anomaly] = [] + for a in audits: + for name, severity, fn in _REGISTRY: + if severity < min_severity: + continue + detail = fn(a) + if detail is not None: + found.append(Anomaly(a.property_id, a.uprn, name, severity, detail)) + found.sort(key=lambda x: (-x.severity, x.check, x.property_id)) + return found + + +def _write_reports(anomalies: list[Anomaly], scanned: int) -> None: + with open("modelling_audit.csv", "w", newline="") as f: + w = csv.writer(f) + w.writerow(["property_id", "uprn", "severity", "check", "detail"]) + for a in anomalies: + w.writerow([a.property_id, a.uprn, a.severity.name, a.check, a.detail]) + + by_check: dict[str, list[Anomaly]] = {} + for a in anomalies: + by_check.setdefault(a.check, []).append(a) + lines = [ + "# Modelling anomaly audit", + "", + f"Scanned **{scanned}** properties · flagged **{len(anomalies)}** anomalies " + f"across **{len(by_check)}** checks.", + "", + ] + for name in sorted(by_check, key=lambda n: (-by_check[n][0].severity, n)): + rows = by_check[name] + lines.append(f"## {name} ({rows[0].severity.name}) — {len(rows)}") + lines.append("") + for a in rows[:50]: + lines.append(f"- property **{a.property_id}** (uprn {a.uprn}): {a.detail}") + if len(rows) > 50: + lines.append(f"- … and {len(rows) - 50} more (see CSV)") + lines.append("") + with open("modelling_audit.md", "w") as f: + f.write("\n".join(lines)) + + +def main() -> None: + load_env() + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--portfolio", type=int, default=None, help="portfolio_id to scan") + parser.add_argument("--property", type=int, default=None, help="a single property_id") + parser.add_argument( + "--severity", + choices=[s.name.lower() for s in Severity], + default="low", + help="minimum severity to report (default: low — all)", + ) + args = parser.parse_args() + min_severity = Severity[args.severity.upper()] + + audits = _load(args.portfolio, args.property) + anomalies = run(audits, min_severity) + _write_reports(anomalies, len(audits)) + + print(f"scanned {len(audits)} properties · {len(anomalies)} anomalies " + f"(>= {min_severity.name})") + counts: dict[str, int] = {} + for a in anomalies: + counts[a.check] = counts.get(a.check, 0) + 1 + for name, severity, _ in _REGISTRY: + if severity >= min_severity: + print(f" [{severity.name:>6}] {name}: {counts.get(name, 0)}") + print("wrote modelling_audit.md / modelling_audit.csv") + + +if __name__ == "__main__": + main() From 37c7a2c1867a8733a6903cff514db5279fbae175 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 26 Jun 2026 19:46:23 +0000 Subject: [PATCH 2/4] =?UTF-8?q?feat(audit):=20solar=20+=20high-SAP=20check?= =?UTF-8?q?s;=20group=20under=20scripts/audit/=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add recommendation-level rollups (solar SAP points + solar bill saving) and checks: impossible-sap-over-100 (found 1), excessive-solar-sap (oversized array, 51), low-solar-bill-savings (SEG/self-consumption pricing, 83), unusually-high-post-sap (21). Move to scripts/audit/anomalies.py (python -m scripts.audit.anomalies). Co-Authored-By: Claude Opus 4.8 (1M context) --- modelling_audit.md | 3 + scripts/audit/__init__.py | 0 .../anomalies.py} | 78 +++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 modelling_audit.md create mode 100644 scripts/audit/__init__.py rename scripts/{audit_modelling_anomalies.py => audit/anomalies.py} (77%) diff --git a/modelling_audit.md b/modelling_audit.md new file mode 100644 index 00000000..81c5595d --- /dev/null +++ b/modelling_audit.md @@ -0,0 +1,3 @@ +# Modelling anomaly audit + +Scanned **0** properties · flagged **0** anomalies across **0** checks. diff --git a/scripts/audit/__init__.py b/scripts/audit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scripts/audit_modelling_anomalies.py b/scripts/audit/anomalies.py similarity index 77% rename from scripts/audit_modelling_anomalies.py rename to scripts/audit/anomalies.py index 10f6bef1..420d1be9 100644 --- a/scripts/audit_modelling_anomalies.py +++ b/scripts/audit/anomalies.py @@ -77,6 +77,10 @@ class PropertyAudit: cost_of_works: Optional[float] energy_bill_savings: Optional[float] energy_consumption_savings: Optional[float] + # Recommendation-level rollups for the default plan. + solar_sap_points: Optional[float] # max SAP a single solar_pv measure earns + solar_bill_savings: Optional[float] # the solar_pv measure's £/yr bill saving + n_measures: int @dataclass(frozen=True) @@ -173,6 +177,50 @@ def _effective_lodged_divergence(a: PropertyAudit) -> Optional[str]: return f"effective {a.effective_sap:.0f} vs lodged {a.lodged_sap:.0f} (Δ{gap:+.0f}, reason={a.rebaseline_reason})" +@check("impossible-sap-over-100", Severity.HIGH) +def _impossible_sap_over_100(a: PropertyAudit) -> Optional[str]: + """A SAP score above 100 is impossible (SAP caps at 100) — a calculator / + aggregation bug, or an oversized solar array pushing the score past the cap.""" + offenders = [ + f"{label} {value:.1f}" + for label, value in (("post", a.post_sap), ("effective", a.effective_sap)) + if value is not None and value > 100.0 + ] + if not offenders: + return None + return "SAP > 100: " + ", ".join(offenders) + + +@check("excessive-solar-sap", Severity.MEDIUM) +def _excessive_solar_sap(a: PropertyAudit) -> Optional[str]: + """A single solar PV measure earns an implausibly large slice of SAP (> 25 + points; cohort avg ≈ 12.5). Usually an oversized array — Google footprint + conflation borrowing a neighbour's / the whole building's roof (ADR-0038).""" + if a.solar_sap_points is None or a.solar_sap_points <= 25.0: + return None + return f"solar PV alone earns {a.solar_sap_points:.1f} SAP points (likely oversized array)" + + +@check("unusually-high-post-sap", Severity.LOW) +def _unusually_high_post_sap(a: PropertyAudit) -> Optional[str]: + """Post-works SAP at the very top of the scale (>= 95, near band A) — rare for + a retrofit of existing stock; worth confirming it isn't an over-credit.""" + if a.post_sap is None or a.post_sap < 95.0 or a.post_sap > 100.0: + return None + return f"post SAP {a.post_sap:.1f} (near band A) — confirm not over-credited" + + +@check("low-solar-bill-savings", Severity.MEDIUM) +def _low_solar_bill_savings(a: PropertyAudit) -> Optional[str]: + """A solar PV measure that barely cuts the bill (< £50/yr, or negative). Solar + reliably saves on electricity, so a near-zero / negative figure points at a + pricing bug — e.g. self-consumption or SEG export not credited (the Saltmead + case: solar, D→C, but only ≈ −£62/yr).""" + if a.solar_bill_savings is None or a.solar_bill_savings >= 50.0: + return None + return f"solar PV bill saving only £{a.solar_bill_savings:.0f}/yr — check self-consumption / SEG export" + + @check("negative-bill-savings", Severity.LOW) def _negative_bill_savings(a: PropertyAudit) -> Optional[str]: """The plan INCREASES the annual bill — can be legitimate on a fuel-switch @@ -205,14 +253,41 @@ _QUERY = text( ) +_ROLLUP_QUERY = text( + """ + SELECT r.property_id, + MAX(r.sap_points) FILTER (WHERE r.type = 'solar_pv') AS solar_sap, + MAX(r.energy_cost_savings) FILTER (WHERE r.type = 'solar_pv') AS solar_bill, + COUNT(*) AS n_measures + FROM recommendation r + JOIN plan pl ON pl.id = r.plan_id AND pl.is_default = TRUE + JOIN property p ON p.id = r.property_id + WHERE (:portfolio_id IS NULL OR p.portfolio_id = :portfolio_id) + AND (:property_id IS NULL OR p.id = :property_id) + GROUP BY r.property_id + """ +) + + def _load(portfolio_id: Optional[int], property_id: Optional[int]) -> list[PropertyAudit]: engine = build_engine() out: list[PropertyAudit] = [] with engine.connect() as conn: + rollups: dict[int, tuple[Optional[float], Optional[float], int]] = { + m["property_id"]: (m["solar_sap"], m["solar_bill"], m["n_measures"]) + for m in ( + row._mapping + for row in conn.execute( + _ROLLUP_QUERY, + {"portfolio_id": portfolio_id, "property_id": property_id}, + ) + ) + } for r in conn.execute( _QUERY, {"portfolio_id": portfolio_id, "property_id": property_id} ): m = r._mapping + solar_sap, solar_bill, n_measures = rollups.get(m["id"], (None, None, 0)) out.append( PropertyAudit( property_id=m["id"], @@ -230,6 +305,9 @@ def _load(portfolio_id: Optional[int], property_id: Optional[int]) -> list[Prope cost_of_works=m["cost_of_works"], energy_bill_savings=m["energy_bill_savings"], energy_consumption_savings=m["energy_consumption_savings"], + solar_sap_points=solar_sap, + solar_bill_savings=solar_bill, + n_measures=n_measures, ) ) return out From e3c91073131b7ddbcc36ee500d3c40bd6dc55244 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 26 Jun 2026 19:49:48 +0000 Subject: [PATCH 3/4] =?UTF-8?q?feat(audit):=20--scenario=20filter=20+=20au?= =?UTF-8?q?dit-ara-portfolio=20skill=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Script takes an optional --scenario to restrict to one scenario's plans. New skill drives the full loop: run the deterministic scan, review groups, deep-dive samples via run_modelling_e2e, characterise sub-classes, and cross-reference open PRs/ADRs — then proposes new checks to codify. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/skills/audit-ara-portfolio/SKILL.md | 92 ++++ modelling_audit.md | 462 +++++++++++++++++++- scripts/audit/anomalies.py | 28 +- 3 files changed, 571 insertions(+), 11 deletions(-) create mode 100644 .claude/skills/audit-ara-portfolio/SKILL.md diff --git a/.claude/skills/audit-ara-portfolio/SKILL.md b/.claude/skills/audit-ara-portfolio/SKILL.md new file mode 100644 index 00000000..dd6c1824 --- /dev/null +++ b/.claude/skills/audit-ara-portfolio/SKILL.md @@ -0,0 +1,92 @@ +--- +name: audit-ara-portfolio +description: Audit a modelled Ara portfolio for odd results — run the deterministic anomaly checks over the DB, review the groups, deep-dive a sample of each, characterise sub-classes, and cross-reference open PRs/ADRs. Use when the user wants to audit or review a portfolio's modelling output, hunt for dodgy plans / baselines / SAP scores / recommendations, or triage modelling anomalies. Asks for portfolio id and scenario id. +--- + +# Audit Ara portfolio + +Turn the deterministic anomaly scan into a triaged, root-caused review. The +deterministic checks (`scripts/audit/anomalies.py`) find KNOWN anti-patterns +exhaustively and cheaply; this skill adds the judgement — confirm, characterise, +cross-reference existing work, and feed novel patterns back as new checks. + +## Input + +Ask for **portfolio_id** and **scenario_id** if the user didn't give them. +`scenario_id` is optional — without it, each Property's *default* plan (the one +shown in the FE) is audited. + +## Phase 1 — Build the dataset + +``` +python -m scripts.audit.anomalies --portfolio --scenario +``` + +Writes `modelling_audit.md` (grouped, ranked by severity) and +`modelling_audit.csv` (every flagged row: property_id, uprn, severity, check, +detail). Read the printed summary and `modelling_audit.md`. + +## Phase 2 — Review the high-level results + +For each check group, HIGH severity first: note the count, read a few example +rows. State, in one line each, a **root-cause hypothesis** and whether it is a +real bug, a known/expected effect, or a threshold that needs tuning. + +## Phase 3 — Deep-dive a sample per group + +For 2–3 representative properties in each meaningful group, reproduce +end-to-end (NO DB writes — never pass `--persist`): + +``` +python -m scripts.run_modelling_e2e --scenario-id +``` + +Read the printed overrides + effective SAP + measure-by-measure plan table. +Confirm the anomaly and isolate **where** it goes wrong: the baseline, a specific +measure, the SAP calc, or the bill. For SAP-value bugs, the `SapResult` +worksheet / `intermediate` trail and the `/diagnose` loop are the tools. + +## Phase 4 — Characterise sub-classes + +A group of thousands is usually several distinct causes. Within each meaningful +group, cluster the flagged properties by a distinguishing trait — EPC source +(lodged / predicted), `rebaseline_reason`, property type, the dominant measure, +fuel — using SQL against the DB. Report the sub-classes and their sizes. + +## Phase 5 — Cross-reference open work + +Before proposing fixes, check whether one is already in flight: + +``` +gh pr list --repo Hestia-Homes/Model --state open +gh pr list --repo Hestia-Homes/Model --state all --search "" +``` + +Map each sub-class to an existing PR / ADR where one applies (e.g. +baseline-vs-plan divergence → the override-aware-rebaseline + persistence-fidelity +work; oversized solar → ADR-0038 dwelling-roof cap). Flag sub-classes with no +coverage as new work. + +## Output + +A triage report. Per check group: +- count + root-cause hypothesis, +- sub-classes, each sized, +- example property ids (to reproduce), +- existing-PR/ADR coverage, +- recommended action: **fix** / **tune threshold** / **accept (expected)** / **new ticket**. + +Then propose any **new deterministic checks** worth adding to +`scripts/audit/anomalies.py` (one decorated function each) so the next run catches +the pattern automatically — the check registry is the durable output of every +audit. + +## Notes + +- Read-only on the DB. `run_modelling_e2e` is a dry run. +- **Expected, not bugs** (until the override-aware-rebaseline + persistence-fidelity + PR deploys and the portfolio is re-modelled): much of `zero-works-post-differs` + and `plan-score-below-baseline` is the pre-fix baseline-vs-plan divergence and + should shrink after re-model — note it, don't re-debug it. +- Adding a check is one decorated `(PropertyAudit) -> Optional[str]` function in + `scripts/audit/anomalies.py`; see its module docstring. diff --git a/modelling_audit.md b/modelling_audit.md index 81c5595d..c537bd98 100644 --- a/modelling_audit.md +++ b/modelling_audit.md @@ -1,3 +1,463 @@ # Modelling anomaly audit -Scanned **0** properties · flagged **0** anomalies across **0** checks. +Scanned **31919** properties · flagged **10563** anomalies across **10** checks. + +## impossible-sap-over-100 (HIGH) — 1 + +- property **726993** (uprn 100061757571): SAP > 100: effective 102.0 + +## plan-below-baseline-band (HIGH) — 363 + +- property **709810** (uprn 10096028301): post C (78.02813) worse than effective baseline B (85) +- property **709846** (uprn 10096399556): post C (79.7374) worse than effective baseline A (93) +- property **709847** (uprn 10096028354): post C (78.695816) worse than effective baseline B (91) +- property **709850** (uprn 10096028348): post C (78.750984) worse than effective baseline B (89) +- property **709959** (uprn 10096028306): post C (78.14567) worse than effective baseline B (84) +- property **710011** (uprn 10096028349): post C (78.750984) worse than effective baseline B (89) +- property **710071** (uprn 10002918889): post C (76.07915) worse than effective baseline B (82) +- property **710075** (uprn 10002918890): post C (79.84036) worse than effective baseline B (82) +- property **710117** (uprn 10096399566): post C (79.86282) worse than effective baseline A (93) +- property **710121** (uprn 10096028307): post C (78.790596) worse than effective baseline B (90) +- property **710131** (uprn 10096028350): post C (80.142784) worse than effective baseline A (94) +- property **710201** (uprn 10090944222): post C (74.58814) worse than effective baseline B (82) +- property **710222** (uprn 10096399567): post C (79.84616) worse than effective baseline A (93) +- property **710225** (uprn 10096028308): post C (78.78894) worse than effective baseline B (90) +- property **710241** (uprn 10096028351): post C (80.142784) worse than effective baseline A (94) +- property **710326** (uprn 10096028309): post C (78.610855) worse than effective baseline B (89) +- property **710335** (uprn 10096028352): post C (79.9757) worse than effective baseline A (94) +- property **710395** (uprn 10096028310): post C (79.46583) worse than effective baseline B (91) +- property **710474** (uprn 10096028355): post C (78.47655) worse than effective baseline B (91) +- property **710479** (uprn 10096028353): post C (80.291725) worse than effective baseline A (95) +- property **710537** (uprn 10096028311): post C (79.46583) worse than effective baseline B (91) +- property **710540** (uprn 100060714157): post D (68.24699) worse than effective baseline C (71) +- property **710760** (uprn 10096028302): post C (78.28315) worse than effective baseline B (85) +- property **710800** (uprn 10096399558): post C (79.7374) worse than effective baseline A (93) +- property **710802** (uprn 10096028356): post C (78.47655) worse than effective baseline B (91) +- property **710946** (uprn 44012846): post C (79.819626) worse than effective baseline B (81) +- property **711027** (uprn 10096028357): post C (77.96618) worse than effective baseline B (90) +- property **711178** (uprn 10091636026): post C (76.05113) worse than effective baseline B (81) +- property **711195** (uprn 10096028303): post C (80.00826) worse than effective baseline B (86) +- property **711236** (uprn 10096399560): post C (79.7374) worse than effective baseline A (93) +- property **711238** (uprn 10096028358): post C (78.695816) worse than effective baseline B (91) +- property **711312** (uprn 10096028346): post C (80.1876) worse than effective baseline A (93) +- property **711376** (uprn 10096028359): post C (78.47655) worse than effective baseline B (91) +- property **711489** (uprn 10096028304): post C (80.3187) worse than effective baseline B (86) +- property **711523** (uprn 10096399562): post C (79.7374) worse than effective baseline A (93) +- property **711524** (uprn 10096028360): post C (78.47655) worse than effective baseline B (91) +- property **711539** (uprn 68159801): post D (66.09142) worse than effective baseline C (71) +- property **711575** (uprn 10096028347): post C (80.1876) worse than effective baseline A (94) +- property **711639** (uprn 10096028361): post C (78.88584) worse than effective baseline B (91) +- property **711706** (uprn 10096028305): post C (78.71836) worse than effective baseline B (85) +- property **711715** (uprn 10013924857): post C (79.91303) worse than effective baseline B (81) +- property **711732** (uprn 100062190435): post C (77.935425) worse than effective baseline B (82) +- property **711795** (uprn 10090343115): post E (52.785263) worse than effective baseline C (73) +- property **711821** (uprn 10096399575): post C (80.209465) worse than effective baseline A (93) +- property **711824** (uprn 10094615095): post C (78.01443) worse than effective baseline A (92) +- property **711858** (uprn 22245014): post C (79.352325) worse than effective baseline B (81) +- property **711881** (uprn 10023444014): post C (79.504456) worse than effective baseline B (81) +- property **711897** (uprn 10012025246): post C (76.70494) worse than effective baseline B (81) +- property **711898** (uprn 10094615103): post C (79.97074) worse than effective baseline A (92) +- property **711917** (uprn 10012027840): post C (72.92677) worse than effective baseline B (81) +- … and 313 more (see CSV) + +## plan-score-below-baseline (HIGH) — 1631 + +- property **709790** (uprn 10023443426): post SAP 74.2 below effective baseline 76.0 (Δ-1.8) +- property **709810** (uprn 10096028301): post SAP 78.0 below effective baseline 85.0 (Δ-7.0) +- property **709846** (uprn 10096399556): post SAP 79.7 below effective baseline 93.0 (Δ-13.3) +- property **709847** (uprn 10096028354): post SAP 78.7 below effective baseline 91.0 (Δ-12.3) +- property **709850** (uprn 10096028348): post SAP 78.8 below effective baseline 89.0 (Δ-10.2) +- property **709959** (uprn 10096028306): post SAP 78.1 below effective baseline 84.0 (Δ-5.9) +- property **709970** (uprn 10013924859): post SAP 79.2 below effective baseline 80.0 (Δ-0.8) +- property **709975** (uprn 200001466609): post SAP 72.3 below effective baseline 79.0 (Δ-6.7) +- property **710011** (uprn 10096028349): post SAP 78.8 below effective baseline 89.0 (Δ-10.2) +- property **710071** (uprn 10002918889): post SAP 76.1 below effective baseline 82.0 (Δ-5.9) +- property **710075** (uprn 10002918890): post SAP 79.8 below effective baseline 82.0 (Δ-2.2) +- property **710117** (uprn 10096399566): post SAP 79.9 below effective baseline 93.0 (Δ-13.1) +- property **710121** (uprn 10096028307): post SAP 78.8 below effective baseline 90.0 (Δ-11.2) +- property **710122** (uprn 100090187902): post SAP 72.4 below effective baseline 74.0 (Δ-1.6) +- property **710131** (uprn 10096028350): post SAP 80.1 below effective baseline 94.0 (Δ-13.9) +- property **710140** (uprn 10023302889): post SAP 76.9 below effective baseline 79.0 (Δ-2.1) +- property **710201** (uprn 10090944222): post SAP 74.6 below effective baseline 82.0 (Δ-7.4) +- property **710222** (uprn 10096399567): post SAP 79.8 below effective baseline 93.0 (Δ-13.2) +- property **710225** (uprn 10096028308): post SAP 78.8 below effective baseline 90.0 (Δ-11.2) +- property **710241** (uprn 10096028351): post SAP 80.1 below effective baseline 94.0 (Δ-13.9) +- property **710274** (uprn 10013924864): post SAP 74.8 below effective baseline 76.0 (Δ-1.2) +- property **710326** (uprn 10096028309): post SAP 78.6 below effective baseline 89.0 (Δ-10.4) +- property **710335** (uprn 10096028352): post SAP 80.0 below effective baseline 94.0 (Δ-14.0) +- property **710362** (uprn 10090317710): post SAP 75.5 below effective baseline 79.0 (Δ-3.5) +- property **710395** (uprn 10096028310): post SAP 79.5 below effective baseline 91.0 (Δ-11.5) +- property **710472** (uprn 10013918445): post SAP 76.9 below effective baseline 79.0 (Δ-2.1) +- property **710474** (uprn 10096028355): post SAP 78.5 below effective baseline 91.0 (Δ-12.5) +- property **710479** (uprn 10096028353): post SAP 80.3 below effective baseline 95.0 (Δ-14.7) +- property **710525** (uprn 10012028784): post SAP 69.6 below effective baseline 79.0 (Δ-9.4) +- property **710537** (uprn 10096028311): post SAP 79.5 below effective baseline 91.0 (Δ-11.5) +- property **710540** (uprn 100060714157): post SAP 68.2 below effective baseline 71.0 (Δ-2.8) +- property **710544** (uprn 10008052006): post SAP 81.1 below effective baseline 83.0 (Δ-1.9) +- property **710552** (uprn 100060706143): post SAP 83.0 below effective baseline 85.0 (Δ-2.0) +- property **710569** (uprn 200000546526): post SAP 70.8 below effective baseline 73.0 (Δ-2.2) +- property **710576** (uprn 100090187795): post SAP 72.4 below effective baseline 75.0 (Δ-2.6) +- property **710645** (uprn 10009433145): post SAP 82.4 below effective baseline 83.0 (Δ-0.6) +- property **710650** (uprn 200001530089): post SAP 72.9 below effective baseline 74.0 (Δ-1.1) +- property **710686** (uprn 10009433147): post SAP 82.1 below effective baseline 83.0 (Δ-0.9) +- property **710760** (uprn 10096028302): post SAP 78.3 below effective baseline 85.0 (Δ-6.7) +- property **710785** (uprn 44006007): post SAP 75.3 below effective baseline 76.0 (Δ-0.7) +- property **710792** (uprn 5300059024): post SAP 75.7 below effective baseline 79.0 (Δ-3.3) +- property **710800** (uprn 10096399558): post SAP 79.7 below effective baseline 93.0 (Δ-13.3) +- property **710802** (uprn 10096028356): post SAP 78.5 below effective baseline 91.0 (Δ-12.5) +- property **710841** (uprn 200003688154): post SAP 70.3 below effective baseline 72.0 (Δ-1.7) +- property **710895** (uprn 100020475290): post SAP 72.4 below effective baseline 73.0 (Δ-0.6) +- property **710911** (uprn 10010221820): post SAP 82.2 below effective baseline 84.0 (Δ-1.8) +- property **710946** (uprn 44012846): post SAP 79.8 below effective baseline 81.0 (Δ-1.2) +- property **710955** (uprn 10090844951): post SAP 78.1 below effective baseline 79.0 (Δ-0.9) +- property **710988** (uprn 10023371802): post SAP 73.3 below effective baseline 75.0 (Δ-1.7) +- property **711014** (uprn 5300088717): post SAP 72.5 below effective baseline 74.0 (Δ-1.5) +- … and 1581 more (see CSV) + +## already-meets-goal-with-works (MEDIUM) — 1162 + +- property **709775** (uprn 100020933699): already C >= goal C but cost_of_works £32 +- property **709875** (uprn 100020973465): already C >= goal C but cost_of_works £38 +- property **709986** (uprn 200000539408): already C >= goal C but cost_of_works £32 +- property **709992** (uprn 100022908998): already C >= goal C but cost_of_works £555 +- property **710005** (uprn 100090178307): already C >= goal C but cost_of_works £21 +- property **710033** (uprn 100022920891): already C >= goal C but cost_of_works £855 +- property **710084** (uprn 200003444301): already C >= goal C but cost_of_works £505 +- property **710185** (uprn 100020650847): already C >= goal C but cost_of_works £455 +- property **710221** (uprn 6701328): already C >= goal C but cost_of_works £655 +- property **710247** (uprn 100020432467): already C >= goal C but cost_of_works £555 +- property **710298** (uprn 100020220397): already C >= goal C but cost_of_works £4515 +- property **710331** (uprn 100020492824): already C >= goal C but cost_of_works £855 +- property **710400** (uprn 100021011618): already C >= goal C but cost_of_works £3725 +- property **710484** (uprn 100060316083): already C >= goal C but cost_of_works £24 +- property **710517** (uprn 100020951618): already C >= goal C but cost_of_works £1029 +- property **710525** (uprn 10012028784): already C >= goal C but cost_of_works £11539 +- property **710539** (uprn 100021973960): already C >= goal C but cost_of_works £505 +- property **710540** (uprn 100060714157): already C >= goal C but cost_of_works £11600 +- property **710555** (uprn 100060718869): already C >= goal C but cost_of_works £28 +- property **710574** (uprn 100020947162): already C >= goal C but cost_of_works £28 +- property **710690** (uprn 200003688150): already C >= goal C but cost_of_works £505 +- property **710703** (uprn 100020438146): already C >= goal C but cost_of_works £32 +- property **710707** (uprn 100061820799): already C >= goal C but cost_of_works £32 +- property **710713** (uprn 200003497667): already C >= goal C but cost_of_works £11082 +- property **710828** (uprn 100060714174): already C >= goal C but cost_of_works £21 +- property **710889** (uprn 200003469113): already C >= goal C but cost_of_works £455 +- property **711058** (uprn 5300059062): already C >= goal C but cost_of_works £3962 +- property **711096** (uprn 100020455891): already C >= goal C but cost_of_works £555 +- property **711099** (uprn 100020229279): already C >= goal C but cost_of_works £1029 +- property **711334** (uprn 5300036435): already C >= goal C but cost_of_works £476 +- property **711390** (uprn 100023263012): already C >= goal C but cost_of_works £505 +- property **711539** (uprn 68159801): already C >= goal C but cost_of_works £2436 +- property **711679** (uprn 100021005287): already C >= goal C but cost_of_works £455 +- property **711694** (uprn 100062191221): already C >= goal C but cost_of_works £18 +- property **711795** (uprn 10090343115): already C >= goal C but cost_of_works £662 +- property **712249** (uprn 100021939292): already C >= goal C but cost_of_works £1029 +- property **712279** (uprn 10090343152): already C >= goal C but cost_of_works £1029 +- property **712426** (uprn 100062208849): already C >= goal C but cost_of_works £530 +- property **712446** (uprn 10090343167): already C >= goal C but cost_of_works £1029 +- property **712690** (uprn 10035061455): already C >= goal C but cost_of_works £2790 +- property **712697** (uprn 100060742520): already C >= goal C but cost_of_works £416 +- property **712764** (uprn 100060675900): already C >= goal C but cost_of_works £32 +- property **712766** (uprn 100021961011): already C >= goal C but cost_of_works £2566 +- property **712801** (uprn 100061734876): already C >= goal C but cost_of_works £555 +- property **712831** (uprn 100061736377): already C >= goal C but cost_of_works £1282 +- property **712864** (uprn 100061738178): already C >= goal C but cost_of_works £455 +- property **712865** (uprn 100061738325): already C >= goal C but cost_of_works £14266 +- property **712869** (uprn 100061738604): already C >= goal C but cost_of_works £18 +- property **712874** (uprn 100061757360): already C >= goal C but cost_of_works £755 +- property **712899** (uprn 100061739860): already C >= goal C but cost_of_works £28 +- … and 1112 more (see CSV) + +## excessive-solar-sap (MEDIUM) — 51 + +- property **710374** (uprn 10090342188): solar PV alone earns 26.1 SAP points (likely oversized array) +- property **713238** (uprn 200004784741): solar PV alone earns 32.6 SAP points (likely oversized array) +- property **714227** (uprn 100061738603): solar PV alone earns 33.9 SAP points (likely oversized array) +- property **714257** (uprn 100061740105): solar PV alone earns 29.8 SAP points (likely oversized array) +- property **714511** (uprn 100061761168): solar PV alone earns 32.3 SAP points (likely oversized array) +- property **714977** (uprn 100061761946): solar PV alone earns 31.4 SAP points (likely oversized array) +- property **715563** (uprn 100061761947): solar PV alone earns 28.5 SAP points (likely oversized array) +- property **715595** (uprn 10002468656): solar PV alone earns 29.3 SAP points (likely oversized array) +- property **715740** (uprn 100061764300): solar PV alone earns 27.3 SAP points (likely oversized array) +- property **716734** (uprn 10002469029): solar PV alone earns 31.0 SAP points (likely oversized array) +- property **716740** (uprn 100061753334): solar PV alone earns 31.1 SAP points (likely oversized array) +- property **717201** (uprn 100091206047): solar PV alone earns 31.3 SAP points (likely oversized array) +- property **718323** (uprn 10002472608): solar PV alone earns 25.2 SAP points (likely oversized array) +- property **718371** (uprn 100061764075): solar PV alone earns 35.5 SAP points (likely oversized array) +- property **718890** (uprn 100062344151): solar PV alone earns 31.9 SAP points (likely oversized array) +- property **719064** (uprn 100021004675): solar PV alone earns 25.6 SAP points (likely oversized array) +- property **719080** (uprn 100091206033): solar PV alone earns 31.3 SAP points (likely oversized array) +- property **719829** (uprn 100091206052): solar PV alone earns 31.6 SAP points (likely oversized array) +- property **720039** (uprn 100061741993): solar PV alone earns 32.6 SAP points (likely oversized array) +- property **720104** (uprn 100061764078): solar PV alone earns 29.9 SAP points (likely oversized array) +- property **721044** (uprn 100061764081): solar PV alone earns 33.9 SAP points (likely oversized array) +- property **721222** (uprn 100062480566): solar PV alone earns 27.0 SAP points (likely oversized array) +- property **722986** (uprn 100061742571): solar PV alone earns 29.8 SAP points (likely oversized array) +- property **723096** (uprn 100061763625): solar PV alone earns 34.5 SAP points (likely oversized array) +- property **723516** (uprn 100061741124): solar PV alone earns 31.0 SAP points (likely oversized array) +- property **723528** (uprn 100061742003): solar PV alone earns 30.9 SAP points (likely oversized array) +- property **724609** (uprn 100061742008): solar PV alone earns 27.0 SAP points (likely oversized array) +- property **725224** (uprn 100062480573): solar PV alone earns 27.6 SAP points (likely oversized array) +- property **725266** (uprn 10034506076): solar PV alone earns 38.5 SAP points (likely oversized array) +- property **725603** (uprn 100061761161): solar PV alone earns 35.6 SAP points (likely oversized array) +- property **727422** (uprn 200001645537): solar PV alone earns 29.4 SAP points (likely oversized array) +- property **727688** (uprn 100061761162): solar PV alone earns 28.8 SAP points (likely oversized array) +- property **727699** (uprn 200004784746): solar PV alone earns 27.3 SAP points (likely oversized array) +- property **727968** (uprn 100061740165): solar PV alone earns 34.5 SAP points (likely oversized array) +- property **728344** (uprn 100061764582): solar PV alone earns 26.9 SAP points (likely oversized array) +- property **729028** (uprn 100061735428): solar PV alone earns 32.0 SAP points (likely oversized array) +- property **729329** (uprn 100061749297): solar PV alone earns 30.2 SAP points (likely oversized array) +- property **729415** (uprn 100091206037): solar PV alone earns 31.5 SAP points (likely oversized array) +- property **731052** (uprn 100021958934): solar PV alone earns 29.3 SAP points (likely oversized array) +- property **731481** (uprn 100021935669): solar PV alone earns 25.1 SAP points (likely oversized array) +- property **731812** (uprn 200001645541): solar PV alone earns 32.1 SAP points (likely oversized array) +- property **731994** (uprn 10023371767): solar PV alone earns 25.3 SAP points (likely oversized array) +- property **732041** (uprn 100061761165): solar PV alone earns 30.1 SAP points (likely oversized array) +- property **732050** (uprn 100061764295): solar PV alone earns 30.5 SAP points (likely oversized array) +- property **732860** (uprn 100061733678): solar PV alone earns 32.7 SAP points (likely oversized array) +- property **733394** (uprn 100061748753): solar PV alone earns 25.1 SAP points (likely oversized array) +- property **739556** (uprn 100061754547): solar PV alone earns 31.3 SAP points (likely oversized array) +- property **739814** (uprn 200003724253): solar PV alone earns 31.6 SAP points (likely oversized array) +- property **739998** (uprn 100090224018): solar PV alone earns 25.9 SAP points (likely oversized array) +- property **740214** (uprn 100061762820): solar PV alone earns 30.4 SAP points (likely oversized array) +- … and 1 more (see CSV) + +## low-solar-bill-savings (MEDIUM) — 83 + +- property **710178** (uprn 100020465019): solar PV bill saving only £49/yr — check self-consumption / SEG export +- property **711663** (uprn 100020481859): solar PV bill saving only £47/yr — check self-consumption / SEG export +- property **712943** (uprn 100061741910): solar PV bill saving only £47/yr — check self-consumption / SEG export +- property **713299** (uprn 100060331540): solar PV bill saving only £46/yr — check self-consumption / SEG export +- property **713607** (uprn 100090108855): solar PV bill saving only £46/yr — check self-consumption / SEG export +- property **713631** (uprn 10002470912): solar PV bill saving only £49/yr — check self-consumption / SEG export +- property **713743** (uprn 100060359906): solar PV bill saving only £36/yr — check self-consumption / SEG export +- property **715182** (uprn 100060726237): solar PV bill saving only £46/yr — check self-consumption / SEG export +- property **715701** (uprn 100021001715): solar PV bill saving only £50/yr — check self-consumption / SEG export +- property **716152** (uprn 100090108859): solar PV bill saving only £43/yr — check self-consumption / SEG export +- property **716268** (uprn 100061763778): solar PV bill saving only £36/yr — check self-consumption / SEG export +- property **716289** (uprn 200003688201): solar PV bill saving only £43/yr — check self-consumption / SEG export +- property **716659** (uprn 100061747951): solar PV bill saving only £41/yr — check self-consumption / SEG export +- property **716693** (uprn 100061733911): solar PV bill saving only £39/yr — check self-consumption / SEG export +- property **717392** (uprn 100020450723): solar PV bill saving only £45/yr — check self-consumption / SEG export +- property **717661** (uprn 6176751): solar PV bill saving only £42/yr — check self-consumption / SEG export +- property **718073** (uprn 100021017285): solar PV bill saving only £47/yr — check self-consumption / SEG export +- property **718298** (uprn 100061741870): solar PV bill saving only £43/yr — check self-consumption / SEG export +- property **718567** (uprn 100091206050): solar PV bill saving only £50/yr — check self-consumption / SEG export +- property **719647** (uprn 100020590930): solar PV bill saving only £49/yr — check self-consumption / SEG export +- property **720335** (uprn 100020594071): solar PV bill saving only £36/yr — check self-consumption / SEG export +- property **720677** (uprn 100020969500): solar PV bill saving only £30/yr — check self-consumption / SEG export +- property **721018** (uprn 202140958): solar PV bill saving only £44/yr — check self-consumption / SEG export +- property **721529** (uprn 100060696301): solar PV bill saving only £43/yr — check self-consumption / SEG export +- property **721664** (uprn 100061765249): solar PV bill saving only £46/yr — check self-consumption / SEG export +- property **721895** (uprn 100060726253): solar PV bill saving only £48/yr — check self-consumption / SEG export +- property **722155** (uprn 100061758903): solar PV bill saving only £47/yr — check self-consumption / SEG export +- property **722390** (uprn 100020944918): solar PV bill saving only £47/yr — check self-consumption / SEG export +- property **722910** (uprn 10009428749): solar PV bill saving only £41/yr — check self-consumption / SEG export +- property **723298** (uprn 202140961): solar PV bill saving only £45/yr — check self-consumption / SEG export +- property **723578** (uprn 100020986231): solar PV bill saving only £47/yr — check self-consumption / SEG export +- property **723748** (uprn 100061757556): solar PV bill saving only £48/yr — check self-consumption / SEG export +- property **723965** (uprn 100061761860): solar PV bill saving only £50/yr — check self-consumption / SEG export +- property **724311** (uprn 100061765331): solar PV bill saving only £30/yr — check self-consumption / SEG export +- property **724331** (uprn 100022008224): solar PV bill saving only £22/yr — check self-consumption / SEG export +- property **724850** (uprn 100060719545): solar PV bill saving only £50/yr — check self-consumption / SEG export +- property **725287** (uprn 100062187008): solar PV bill saving only £46/yr — check self-consumption / SEG export +- property **725585** (uprn 100061752475): solar PV bill saving only £44/yr — check self-consumption / SEG export +- property **725807** (uprn 100020996065): solar PV bill saving only £40/yr — check self-consumption / SEG export +- property **725969** (uprn 202141567): solar PV bill saving only £31/yr — check self-consumption / SEG export +- property **726419** (uprn 100061809723): solar PV bill saving only £46/yr — check self-consumption / SEG export +- property **726507** (uprn 100061752706): solar PV bill saving only £48/yr — check self-consumption / SEG export +- property **726627** (uprn 100061152652): solar PV bill saving only £42/yr — check self-consumption / SEG export +- property **726735** (uprn 100061809725): solar PV bill saving only £46/yr — check self-consumption / SEG export +- property **726998** (uprn 100061809727): solar PV bill saving only £46/yr — check self-consumption / SEG export +- property **727114** (uprn 100061809728): solar PV bill saving only £47/yr — check self-consumption / SEG export +- property **727143** (uprn 202141571): solar PV bill saving only £45/yr — check self-consumption / SEG export +- property **727820** (uprn 100061809729): solar PV bill saving only £47/yr — check self-consumption / SEG export +- property **727994** (uprn 202141572): solar PV bill saving only £45/yr — check self-consumption / SEG export +- property **728027** (uprn 10090265602): solar PV bill saving only £45/yr — check self-consumption / SEG export +- … and 33 more (see CSV) + +## zero-works-post-differs (MEDIUM) — 5624 + +- property **709772** (uprn 10093116528): £0 works but post SAP 80.3 != effective 79.0 +- property **709773** (uprn 10093116543): £0 works but post SAP 78.5 != effective 77.0 +- property **709774** (uprn 10093116529): £0 works but post SAP 76.8 != effective 75.0 +- property **709777** (uprn 10094601392): £0 works but post SAP 77.2 != effective 74.0 +- property **709778** (uprn 10023444324): £0 works but post SAP 76.9 != effective 75.0 +- property **709779** (uprn 10092970673): £0 works but post SAP 70.9 != effective 61.0 +- property **709780** (uprn 10094601287): £0 works but post SAP 77.7 != effective 75.0 +- property **709783** (uprn 10094601162): £0 works but post SAP 78.1 != effective 74.0 +- property **709784** (uprn 10090844948): £0 works but post SAP 76.4 != effective 73.0 +- property **709786** (uprn 10093114053): £0 works but post SAP 81.3 != effective 76.0 +- property **709787** (uprn 10091568921): £0 works but post SAP 79.4 != effective 77.0 +- property **709788** (uprn 10093718424): £0 works but post SAP 80.0 != effective 78.0 +- property **709790** (uprn 10023443426): £0 works but post SAP 74.2 != effective 76.0 +- property **709791** (uprn 10093412452): £0 works but post SAP 80.2 != effective 79.0 +- property **709792** (uprn 6199384): £0 works but post SAP 80.5 != effective 78.0 +- property **709793** (uprn 10014314798): £0 works but post SAP 73.8 != effective 72.0 +- property **709794** (uprn 10094601294): £0 works but post SAP 78.1 != effective 76.0 +- property **709795** (uprn 10090343335): £0 works but post SAP 84.0 != effective 82.0 +- property **709796** (uprn 10093115480): £0 works but post SAP 78.8 != effective 77.0 +- property **709798** (uprn 10094601226): £0 works but post SAP 77.3 != effective 75.0 +- property **709800** (uprn 6701369): £0 works but post SAP 82.5 != effective 78.0 +- property **709801** (uprn 202211152): £0 works but post SAP 81.8 != effective 79.0 +- property **709803** (uprn 10093394010): £0 works but post SAP 79.9 != effective 78.0 +- property **709806** (uprn 10090341811): £0 works but post SAP 80.2 != effective 78.0 +- property **709808** (uprn 10093117227): £0 works but post SAP 77.6 != effective 76.0 +- property **709809** (uprn 10023444170): £0 works but post SAP 80.3 != effective 78.0 +- property **709810** (uprn 10096028301): £0 works but post SAP 78.0 != effective 85.0 +- property **709813** (uprn 10094601280): £0 works but post SAP 77.9 != effective 75.0 +- property **709814** (uprn 10093386418): £0 works but post SAP 78.8 != effective 76.0 +- property **709817** (uprn 10094895444): £0 works but post SAP 79.6 != effective 77.0 +- property **709818** (uprn 10092973960): £0 works but post SAP 77.6 != effective 74.0 +- property **709819** (uprn 10012028763): £0 works but post SAP 83.3 != effective 82.0 +- property **709820** (uprn 10093049867): £0 works but post SAP 78.7 != effective 75.0 +- property **709821** (uprn 10093116336): £0 works but post SAP 79.9 != effective 78.0 +- property **709823** (uprn 10093116334): £0 works but post SAP 79.0 != effective 78.0 +- property **709824** (uprn 44042992): £0 works but post SAP 79.9 != effective 79.0 +- property **709825** (uprn 10014314853): £0 works but post SAP 72.0 != effective 70.0 +- property **709828** (uprn 10091636116): £0 works but post SAP 75.8 != effective 71.0 +- property **709829** (uprn 10094601381): £0 works but post SAP 78.4 != effective 74.0 +- property **709830** (uprn 10093049853): £0 works but post SAP 81.5 != effective 78.0 +- property **709831** (uprn 10093390790): £0 works but post SAP 74.9 != effective 72.0 +- property **709832** (uprn 10093116330): £0 works but post SAP 80.1 != effective 79.0 +- property **709833** (uprn 10093116326): £0 works but post SAP 80.1 != effective 79.0 +- property **709834** (uprn 10094601351): £0 works but post SAP 79.0 != effective 76.0 +- property **709835** (uprn 10090317693): £0 works but post SAP 77.5 != effective 76.0 +- property **709836** (uprn 10090034872): £0 works but post SAP 81.4 != effective 79.0 +- property **709837** (uprn 10093115985): £0 works but post SAP 79.5 != effective 77.0 +- property **709842** (uprn 202211170): £0 works but post SAP 82.1 != effective 79.0 +- property **709845** (uprn 6701311): £0 works but post SAP 81.4 != effective 78.0 +- property **709846** (uprn 10096399556): £0 works but post SAP 79.7 != effective 93.0 +- … and 5574 more (see CSV) + +## effective-lodged-divergence (LOW) — 1527 + +- property **709779** (uprn 10092970673): effective 61 vs lodged 86 (Δ-25, reason=pre_sap10) +- property **709802** (uprn 100020665611): effective 52 vs lodged 37 (Δ+15, reason=pre_sap10) +- property **709804** (uprn 10093388044): effective 33 vs lodged 93 (Δ-60, reason=pre_sap10) +- property **709815** (uprn 100090108846): effective 57 vs lodged 79 (Δ-22, reason=pre_sap10) +- property **709828** (uprn 10091636116): effective 71 vs lodged 88 (Δ-17, reason=pre_sap10) +- property **709860** (uprn 100061086424): effective 60 vs lodged 83 (Δ-23, reason=pre_sap10) +- property **709863** (uprn 10090342180): effective 39 vs lodged 78 (Δ-39, reason=pre_sap10) +- property **709874** (uprn 10093388053): effective 35 vs lodged 94 (Δ-59, reason=pre_sap10) +- property **709892** (uprn 10090343767): effective 75 vs lodged 91 (Δ-16, reason=pre_sap10) +- property **709945** (uprn 10090342181): effective 42 vs lodged 81 (Δ-39, reason=pre_sap10) +- property **709993** (uprn 15043874): effective 68 vs lodged 49 (Δ+19, reason=pre_sap10) +- property **710003** (uprn 10091636410): effective 71 vs lodged 86 (Δ-15, reason=pre_sap10) +- property **710009** (uprn 100022895379): effective 62 vs lodged 80 (Δ-18, reason=pre_sap10) +- property **710019** (uprn 10090342182): effective 44 vs lodged 80 (Δ-36, reason=pre_sap10) +- property **710025** (uprn 10093388055): effective 55 vs lodged 93 (Δ-38, reason=pre_sap10) +- property **710037** (uprn 100090108857): effective 56 vs lodged 86 (Δ-30, reason=pre_sap10) +- property **710091** (uprn 100061086427): effective 60 vs lodged 83 (Δ-23, reason=pre_sap10) +- property **710114** (uprn 10096026315): effective 74 vs lodged 89 (Δ-15, reason=pre_sap10) +- property **710134** (uprn 10096026271): effective 73 vs lodged 88 (Δ-15, reason=pre_sap10) +- property **710139** (uprn 10090342183): effective 41 vs lodged 77 (Δ-36, reason=pre_sap10) +- property **710144** (uprn 10014314832): effective 63 vs lodged 83 (Δ-20, reason=pre_sap10) +- property **710148** (uprn 10093388056): effective 57 vs lodged 94 (Δ-37, reason=pre_sap10) +- property **710191** (uprn 10090342184): effective 49 vs lodged 79 (Δ-30, reason=pre_sap10) +- property **710198** (uprn 10012138502): effective 65 vs lodged 85 (Δ-20, reason=pre_sap10) +- property **710199** (uprn 10093388057): effective 38 vs lodged 95 (Δ-57, reason=pre_sap10) +- property **710202** (uprn 10090342864): effective 68 vs lodged 84 (Δ-16, reason=pre_sap10) +- property **710244** (uprn 10010249676): effective 74 vs lodged 90 (Δ-16, reason=pre_sap10) +- property **710246** (uprn 100061086430): effective 59 vs lodged 83 (Δ-24, reason=pre_sap10) +- property **710250** (uprn 10090342185): effective 62 vs lodged 81 (Δ-19, reason=pre_sap10) +- property **710260** (uprn 10093388058): effective 47 vs lodged 95 (Δ-48, reason=pre_sap10) +- property **710261** (uprn 10090341825): effective 75 vs lodged 91 (Δ-16, reason=pre_sap10) +- property **710300** (uprn 10090341826): effective 75 vs lodged 91 (Δ-16, reason=pre_sap10) +- property **710342** (uprn 10090342187): effective 37 vs lodged 77 (Δ-40, reason=pre_sap10) +- property **710348** (uprn 10090341827): effective 76 vs lodged 91 (Δ-15, reason=pre_sap10) +- property **710429** (uprn 10093388045): effective 49 vs lodged 93 (Δ-44, reason=pre_sap10) +- property **710449** (uprn 100020993501): effective 56 vs lodged 5 (Δ+51, reason=pre_sap10) +- property **710491** (uprn 10014314835): effective 69 vs lodged 84 (Δ-15, reason=pre_sap10) +- property **710536** (uprn 100062482536): effective 38 vs lodged 53 (Δ-15, reason=pre_sap10) +- property **710640** (uprn 10014316004): effective 71 vs lodged 88 (Δ-17, reason=pre_sap10) +- property **710658** (uprn 10014316005): effective 70 vs lodged 88 (Δ-18, reason=pre_sap10) +- property **710659** (uprn 22061763): effective 50 vs lodged 70 (Δ-20, reason=pre_sap10) +- property **710664** (uprn 10096026322): effective 74 vs lodged 89 (Δ-15, reason=pre_sap10) +- property **710672** (uprn 200003378407): effective 48 vs lodged 28 (Δ+20, reason=pre_sap10) +- property **710683** (uprn 10014316006): effective 70 vs lodged 88 (Δ-18, reason=pre_sap10) +- property **710704** (uprn 10014316007): effective 71 vs lodged 88 (Δ-17, reason=pre_sap10) +- property **710745** (uprn 10093386244): effective 50 vs lodged 83 (Δ-33, reason=pre_sap10) +- property **710752** (uprn 10093388046): effective 47 vs lodged 94 (Δ-47, reason=pre_sap10) +- property **710779** (uprn 10091636118): effective 71 vs lodged 88 (Δ-17, reason=pre_sap10) +- property **710814** (uprn 10014316008): effective 71 vs lodged 88 (Δ-17, reason=pre_sap10) +- property **710837** (uprn 10014316009): effective 71 vs lodged 88 (Δ-17, reason=pre_sap10) +- … and 1477 more (see CSV) + +## negative-bill-savings (LOW) — 100 + +- property **712847** (uprn 100061737192): energy_bill_savings £-30/yr on £16110 of works +- property **713453** (uprn 100061739869): energy_bill_savings £-68/yr on £14533 of works +- property **713721** (uprn 100061753373): energy_bill_savings £-18/yr on £14554 of works +- property **713743** (uprn 100060359906): energy_bill_savings £-98/yr on £18823 of works +- property **713791** (uprn 10013528635): energy_bill_savings £-130/yr on £1029 of works +- property **713970** (uprn 10013528640): energy_bill_savings £-130/yr on £1029 of works +- property **714001** (uprn 10013528641): energy_bill_savings £-130/yr on £1029 of works +- property **714038** (uprn 10013528642): energy_bill_savings £-130/yr on £1029 of works +- property **714373** (uprn 100062185281): energy_bill_savings £-113/yr on £14800 of works +- property **714420** (uprn 10010242243): energy_bill_savings £-162/yr on £1029 of works +- property **715227** (uprn 100061753375): energy_bill_savings £-18/yr on £14554 of works +- property **715561** (uprn 100061739874): energy_bill_savings £-69/yr on £14533 of works +- property **715999** (uprn 100061737204): energy_bill_savings £-13/yr on £14533 of works +- property **716169** (uprn 100061747915): energy_bill_savings £-53/yr on £14533 of works +- property **716251** (uprn 100061753377): energy_bill_savings £-18/yr on £14554 of works +- property **716548** (uprn 100061765632): energy_bill_savings £-70/yr on £14533 of works +- property **716811** (uprn 22106963): energy_bill_savings £-3/yr on £4785 of works +- property **717209** (uprn 100061753379): energy_bill_savings £-20/yr on £14554 of works +- property **717675** (uprn 6176752): energy_bill_savings £-75/yr on £15067 of works +- property **717998** (uprn 100061749651): energy_bill_savings £-7/yr on £14533 of works +- property **719002** (uprn 10002476550): energy_bill_savings £-101/yr on £14533 of works +- property **719091** (uprn 100061753365): energy_bill_savings £-46/yr on £14533 of works +- property **719449** (uprn 100061753383): energy_bill_savings £-18/yr on £14554 of works +- property **719680** (uprn 100061741246): energy_bill_savings £-34/yr on £17072 of works +- property **719811** (uprn 100060327562): energy_bill_savings £-92/yr on £14533 of works +- property **720349** (uprn 100021812910): energy_bill_savings £-122/yr on £1029 of works +- property **720364** (uprn 100061741682): energy_bill_savings £-51/yr on £15070 of works +- property **720800** (uprn 100061753387): energy_bill_savings £-20/yr on £14554 of works +- property **720889** (uprn 10023224588): energy_bill_savings £-172/yr on £1029 of works +- property **720989** (uprn 100061764553): energy_bill_savings £-143/yr on £14533 of works +- property **721034** (uprn 100061758484): energy_bill_savings £-145/yr on £15105 of works +- property **721070** (uprn 100061749663): energy_bill_savings £-69/yr on £14800 of works +- property **722402** (uprn 100061737194): energy_bill_savings £-73/yr on £14266 of works +- property **722448** (uprn 100061739862): energy_bill_savings £-68/yr on £14533 of works +- property **722732** (uprn 100061750203): energy_bill_savings £-155/yr on £16445 of works +- property **722808** (uprn 100062186150): energy_bill_savings £-61/yr on £14533 of works +- property **723367** (uprn 100061753394): energy_bill_savings £-56/yr on £14533 of works +- property **723503** (uprn 10010242632): energy_bill_savings £-112/yr on £1029 of works +- property **723540** (uprn 100061743541): energy_bill_savings £-35/yr on £14533 of works +- property **723771** (uprn 200001645519): energy_bill_savings £-58/yr on £14533 of works +- property **723782** (uprn 100061743542): energy_bill_savings £-30/yr on £17032 of works +- property **725039** (uprn 100061748331): energy_bill_savings £-4/yr on £14800 of works +- property **725350** (uprn 100061743145): energy_bill_savings £-189/yr on £15067 of works +- property **725715** (uprn 100061736694): energy_bill_savings £-42/yr on £14533 of works +- property **725731** (uprn 100061763962): energy_bill_savings £-71/yr on £14533 of works +- property **725988** (uprn 100061749671): energy_bill_savings £-13/yr on £14533 of works +- property **726184** (uprn 100061752891): energy_bill_savings £-66/yr on £14533 of works +- property **726268** (uprn 100061741268): energy_bill_savings £-76/yr on £14266 of works +- property **726271** (uprn 100061741692): energy_bill_savings £-63/yr on £14533 of works +- property **726302** (uprn 202141568): energy_bill_savings £-73/yr on £15721 of works +- … and 50 more (see CSV) + +## unusually-high-post-sap (LOW) — 21 + +- property **714511** (uprn 100061761168): post SAP 100.0 (near band A) — confirm not over-credited +- property **716734** (uprn 10002469029): post SAP 100.0 (near band A) — confirm not over-credited +- property **716740** (uprn 100061753334): post SAP 100.0 (near band A) — confirm not over-credited +- property **717201** (uprn 100091206047): post SAP 100.0 (near band A) — confirm not over-credited +- property **718890** (uprn 100062344151): post SAP 100.0 (near band A) — confirm not over-credited +- property **719080** (uprn 100091206033): post SAP 100.0 (near band A) — confirm not over-credited +- property **719829** (uprn 100091206052): post SAP 100.0 (near band A) — confirm not over-credited +- property **721222** (uprn 100062480566): post SAP 98.6 (near band A) — confirm not over-credited +- property **723516** (uprn 100061741124): post SAP 100.0 (near band A) — confirm not over-credited +- property **725224** (uprn 100062480573): post SAP 95.3 (near band A) — confirm not over-credited +- property **725440** (uprn 100061746447): post SAP 100.0 (near band A) — confirm not over-credited +- property **725627** (uprn 10008885879): post SAP 99.6 (near band A) — confirm not over-credited +- property **726993** (uprn 100061757571): post SAP 100.0 (near band A) — confirm not over-credited +- property **727688** (uprn 100061761162): post SAP 97.6 (near band A) — confirm not over-credited +- property **727699** (uprn 200004784746): post SAP 100.0 (near band A) — confirm not over-credited +- property **729329** (uprn 100061749297): post SAP 99.2 (near band A) — confirm not over-credited +- property **729415** (uprn 100091206037): post SAP 100.0 (near band A) — confirm not over-credited +- property **732041** (uprn 100061761165): post SAP 97.9 (near band A) — confirm not over-credited +- property **732050** (uprn 100061764295): post SAP 98.1 (near band A) — confirm not over-credited +- property **740214** (uprn 100061762820): post SAP 99.4 (near band A) — confirm not over-credited +- property **740234** (uprn 100061761814): post SAP 100.0 (near band A) — confirm not over-credited diff --git a/scripts/audit/anomalies.py b/scripts/audit/anomalies.py index 420d1be9..48f16875 100644 --- a/scripts/audit/anomalies.py +++ b/scripts/audit/anomalies.py @@ -245,6 +245,7 @@ _QUERY = text( FROM property p LEFT JOIN property_baseline_performance pbp ON pbp.property_id = p.id LEFT JOIN plan pl ON pl.property_id = p.id AND pl.is_default = TRUE + AND (:scenario_id IS NULL OR pl.scenario_id = :scenario_id) LEFT JOIN scenario s ON s.id = pl.scenario_id WHERE (:portfolio_id IS NULL OR p.portfolio_id = :portfolio_id) AND (:property_id IS NULL OR p.id = :property_id) @@ -261,6 +262,7 @@ _ROLLUP_QUERY = text( COUNT(*) AS n_measures FROM recommendation r JOIN plan pl ON pl.id = r.plan_id AND pl.is_default = TRUE + AND (:scenario_id IS NULL OR pl.scenario_id = :scenario_id) JOIN property p ON p.id = r.property_id WHERE (:portfolio_id IS NULL OR p.portfolio_id = :portfolio_id) AND (:property_id IS NULL OR p.id = :property_id) @@ -269,23 +271,26 @@ _ROLLUP_QUERY = text( ) -def _load(portfolio_id: Optional[int], property_id: Optional[int]) -> list[PropertyAudit]: +def _load( + portfolio_id: Optional[int], + property_id: Optional[int], + scenario_id: Optional[int], +) -> list[PropertyAudit]: engine = build_engine() out: list[PropertyAudit] = [] + params = { + "portfolio_id": portfolio_id, + "property_id": property_id, + "scenario_id": scenario_id, + } with engine.connect() as conn: rollups: dict[int, tuple[Optional[float], Optional[float], int]] = { m["property_id"]: (m["solar_sap"], m["solar_bill"], m["n_measures"]) for m in ( - row._mapping - for row in conn.execute( - _ROLLUP_QUERY, - {"portfolio_id": portfolio_id, "property_id": property_id}, - ) + row._mapping for row in conn.execute(_ROLLUP_QUERY, params) ) } - for r in conn.execute( - _QUERY, {"portfolio_id": portfolio_id, "property_id": property_id} - ): + for r in conn.execute(_QUERY, params): m = r._mapping solar_sap, solar_bill, n_measures = rollups.get(m["id"], (None, None, 0)) out.append( @@ -361,6 +366,9 @@ def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--portfolio", type=int, default=None, help="portfolio_id to scan") parser.add_argument("--property", type=int, default=None, help="a single property_id") + parser.add_argument( + "--scenario", type=int, default=None, help="restrict to one scenario_id" + ) parser.add_argument( "--severity", choices=[s.name.lower() for s in Severity], @@ -370,7 +378,7 @@ def main() -> None: args = parser.parse_args() min_severity = Severity[args.severity.upper()] - audits = _load(args.portfolio, args.property) + audits = _load(args.portfolio, args.property, args.scenario) anomalies = run(audits, min_severity) _write_reports(anomalies, len(audits)) From 2d6b078bd81efec58db03d98b3e581b93d1e7c9e Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 26 Jun 2026 19:57:57 +0000 Subject: [PATCH 4/4] =?UTF-8?q?feat(audit):=20self-improvement=20loop=20in?= =?UTF-8?q?=20the=20skill=20+=20provenance=20convention=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Phase 6 (self-improve) to audit-ara-portfolio: when a run confirms a novel systematic problem, codify it as a check — gated on systematic (>=5 props, root-caused), not-already-covered, and /grill-me-pressure-tested. Each check records provenance (motivating cause + example properties) so the registry stays sharp and compounds every run. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/skills/audit-ara-portfolio/SKILL.md | 38 ++++++++++++++++++--- scripts/audit/anomalies.py | 7 ++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/.claude/skills/audit-ara-portfolio/SKILL.md b/.claude/skills/audit-ara-portfolio/SKILL.md index dd6c1824..6621a9f3 100644 --- a/.claude/skills/audit-ara-portfolio/SKILL.md +++ b/.claude/skills/audit-ara-portfolio/SKILL.md @@ -76,10 +76,40 @@ A triage report. Per check group: - existing-PR/ADR coverage, - recommended action: **fix** / **tune threshold** / **accept (expected)** / **new ticket**. -Then propose any **new deterministic checks** worth adding to -`scripts/audit/anomalies.py` (one decorated function each) so the next run catches -the pattern automatically — the check registry is the durable output of every -audit. +Then run Phase 6 to make the audit permanently better. + +## Phase 6 — Self-improve (the compounding loop) + +When the review confirms a **novel, systematic** problem, codify it so every +future run catches it automatically. This is what makes the audit get better each +time it runs. Apply the gates below — they keep the registry sharp, not noisy. + +**Gates (all must hold before adding a check):** +1. **Systematic** — reproduced on **≥ 5** properties and root-caused, not a + one-off. (A single weird property is a ticket, not a check.) +2. **Not already covered** — no existing check fires on it, and no open/merged PR + or ADR already addresses the cause (you checked in Phase 5). +3. **Pressure-tested** — for any non-trivial check (a threshold, a heuristic), + run `/grill-me` on the proposed check first: what's the false-positive rate on + this portfolio? is the threshold defensible against the real distribution? does + it overlap an existing check? Tune from the answers before committing. + +**What to change, smallest first:** +- **A check** — add one decorated `(PropertyAudit) -> Optional[str]` function to + `scripts/audit/anomalies.py`. Its docstring MUST record **provenance**: the + motivating property ids and the one-line root cause, so the check is traceable + and re-verifiable later. If it needs a field not on `PropertyAudit`, extend the + bundle + query. +- **The skill** — if the review revealed a new *expectation* (a pattern that is + expected-not-a-bug, or a new deep-dive technique), add it to this file's Notes + / phases so the next reviewer starts ahead. +- **Docs** — if the cause is a load-bearing modelling decision, an ADR may be + warranted (rare; only when hard-to-reverse + surprising + a real trade-off). + +Commit each codified check on its own with the motivating run referenced, then +**re-run Phase 1** to confirm the new check fires on the cases that motivated it +and nothing else surprising. The check registry — with provenance — is the +durable, compounding output of every audit. ## Notes diff --git a/scripts/audit/anomalies.py b/scripts/audit/anomalies.py index 48f16875..7b1ac62e 100644 --- a/scripts/audit/anomalies.py +++ b/scripts/audit/anomalies.py @@ -16,6 +16,13 @@ runner discovers it, runs it over every Property, and reports the reasons. Keep each check small and single-purpose; lean on the shared `PropertyAudit` bundle rather than re-querying. +This registry is meant to **compound**: each audit that confirms a new +systematic problem should leave behind a check (see the `audit-ara-portfolio` +skill's self-improve phase). So every check's docstring records its +**provenance** — the motivating cause and example properties — so a future reader +can re-verify it and judge whether it still earns its place. A threshold should +be justified against the real distribution, not guessed. + Read-only: this script never writes to the DB. """