NC-METH-001 v1.1 — Annex I: pytest Acceptance Test Suite Specification¶
18 Tests across 5 Categories · Closes AUDIT-031 + AUDIT-034¶
12 May 2026 · Internal · Author: AI-assisted, S. Rubinyi review · Implementation per WP3
I.0 Overview¶
Annex I closes AUDIT-031 (anomaly detection / QA framework) and AUDIT-034 (no acceptance test suite) by specifying a pytest-based acceptance test suite for the methodology.
The suite is the methodology's continuous-integration safety net. Every change to the methodology, the LC model, or downstream segment registers triggers the suite; failures block the version lock per Part F § F.2.
I.0.1 What this annex is¶
A specification — natural-language description of each test, with pseudocode and pass/fail criteria. The actual Python implementation (pytest files in methodology-package/tests/) is the WP3 deliverable per Part F § F.2.5.
This split is deliberate. Specifying tests today lets Marc and Steven review the test logic before code is written; the implementation in WP3 then converts the specification to executable Python.
I.0.2 What this annex is NOT¶
- Not the implementation. Python code is [PENDING WP3 — Weeks 5–8]
- Not the test runner CI. GitHub Actions or equivalent CI is v1.2 deliverable
- Not test coverage beyond the v1.1.0 lock scope. Additional tests (yield model unit tests, OPEX model unit tests) deferred to v1.2
I.0.3 Test suite categories (per Part F § F.2.2)¶
| Category | Test count | Closes |
|---|---|---|
| Replication tests | 5 | AUDIT-031 (anomaly detection on actual outputs) |
| Cross-estate ranking | 2 | AUDIT-031 |
| High-materiality findings | 4 | AUDIT-034 (verifies the 3 ACTIVE-IRR + Annex H gating) |
| Governance | 5 | AUDIT-034 |
| Cross-reference integrity | 2 | AUDIT-031 |
| Total | 18 tests |
I.0.4 Pass/fail criteria per version bump (per Part F § F.2.4)¶
| Version type | Test suite requirement |
|---|---|
| PATCH (v1.1.0 → v1.1.1) | Advisory pass; failures documented but not blocking |
| MINOR (v1.1.0 → v1.2.0) | All tests pass; blocking for lock |
| MAJOR (v1.x.y → v2.0.0) | All tests pass + re-validation of all estate IC papers; blocking |
I.1 Test framework architecture¶
I.1.1 Language and framework¶
- Language: Python 3.11+
- Framework: pytest (industry standard; rich fixtures, parametrisation, reporting)
- Dependencies: pandas (segment register reads), openpyxl (LC model reads), pyyaml (methodology canonical YAML), pathlib (file resolution)
I.1.2 Repository location¶
nc-methodology/tests/
├── conftest.py # shared fixtures
├── test_replication.py # 5 tests (Section I.3)
├── test_cross_estate.py # 2 tests (Section I.4)
├── test_audit_findings.py # 4 tests (Section I.5)
├── test_governance.py # 5 tests (Section I.6)
├── test_xref.py # 2 tests (Section I.7)
└── fixtures/
├── lc_v1_register.csv
├── methodology_canonical.yaml
├── changelog_v1_1.md
└── deprecated_params.yaml
I.1.3 Execution¶
| Trigger | Action |
|---|---|
| Local development | pytest tests/ from repo root |
| Pre-commit hook | All tests run before any commit to main (WP3 sets up via pre-commit Python package) |
| CI (v1.2 deliverable) | GitHub Actions on push to any branch |
I.1.4 Test naming convention¶
test_{category}_{specific_assertion}.py::test_{specific_function}
Examples: test_replication.py::test_lc_irr_within_methodology, test_governance.py::test_semver_tag_present.
I.2 Test inventory¶
The 18 tests at v1.1.0:
| # | Test | Category | Closes | Status |
|---|---|---|---|---|
| 1 | test_lc_irr_within_methodology | Replication | AUDIT-031 | [PENDING WP3] |
| 2 | test_lc_dscr_p90_compliant | Replication | AUDIT-031 | [PENDING WP3] |
| 3 | test_bp_irr_within_methodology | Replication | AUDIT-031 | [PENDING WP3] |
| 4 | test_lc_envelope_capacity_matches_register | Replication | AUDIT-031 | [PENDING WP3] |
| 5 | test_lc_capex_matches_register | Replication | AUDIT-031 | [PENDING WP3] |
| 6 | test_estate_ranking_stable_across_cost_bands | Cross-estate | AUDIT-031 | [PENDING WP3] |
| 7 | test_top5_estates_stable_p90 | Cross-estate | AUDIT-031 | [PENDING WP3] |
| 8 | test_audit_001_yield_convention_documented | High-materiality | AUDIT-034 | [PENDING WP3] |
| 9 | test_audit_014_debt_cost_aligned | High-materiality | AUDIT-034 | [PENDING WP3] |
| 10 | test_audit_016_opex_aligned | High-materiality | AUDIT-034 | [PENDING WP3] |
| 11 | test_audit_040_methodology_canonical_present | High-materiality | AUDIT-034 | [PENDING WP3] |
| 12 | test_semver_tag_present | Governance | AUDIT-034 | [PENDING WP3] |
| 13 | test_changelog_entry_for_current_version | Governance | AUDIT-034 | [PENDING WP3] |
| 14 | test_deprecated_parameters_absent | Governance | AUDIT-034 | [PENDING WP3] |
| 15 | test_v3_segment_ids_absent | Governance | AUDIT-034 | [PENDING WP3] |
| 16 | test_methodology_package_2_retired | Governance | AUDIT-034 | [PENDING WP3] |
| 17 | test_part_b_to_c_references_resolve | Cross-reference | AUDIT-031 | [PENDING WP3] |
| 18 | test_part_d_to_c_references_resolve | Cross-reference | AUDIT-031 | [PENDING WP3] |
I.3 Replication tests (5 tests)¶
Replication tests verify that running the methodology end-to-end against known inputs produces known outputs. They are the highest-confidence test category because they exercise the full pipeline.
I.3.1 test_lc_irr_within_methodology¶
What it asserts: LC sponsor IRR at 52% LTV produced by methodology-aligned inputs falls within ±50 bps of the IC paper canonical 12.2%.
Pseudocode:
def test_lc_irr_within_methodology():
"""LC sponsor IRR at 52% LTV should match IC canonical ±50 bps."""
# Load LC v1.0 register
register = load_segment_register("fixtures/lc_v1_register.csv")
# Apply methodology canonical parameters
inputs = {
"yield_kwh_per_kwp_per_yr": 1485, # methodology RES-001; or 1380 after AUDIT-001 reconciliation
"tariff_thb_per_kwh": 3.85, # locked
"tariff_escalation": 0.0, # LC flat, no escalation
"fx_thb_per_usd": 35, # main program
"debt_cost": 0.0575, # EXIM 5.75%
"debt_tenor_yrs": 12,
"ltv": 0.52,
"dscr_target": 1.05,
"opex_per_typology": load_opex_canonical(),
"contract_tenor_yrs": 25,
}
# Solve for lender-sized debt
debt = solve_debt_size(inputs, register)
# Compute sponsor IRR
irr = compute_sponsor_irr(register, inputs, debt)
# Assert within ±50 bps of canonical
assert 11.7 <= irr <= 12.7, (
f"LC IRR {irr:.2%} outside ±50 bps of canonical 12.2%. "
f"Check yield convention (AUDIT-001) or O&M build (AUDIT-016)."
)
Pass/fail: - PASS: 11.7% ≤ IRR ≤ 12.7% - FAIL: IRR outside band → investigate AUDIT-001 / AUDIT-014 / AUDIT-016 closures
Closes: AUDIT-031 (detects methodology output drift)
I.3.2 test_lc_dscr_p90_compliant¶
What it asserts: LC minimum DSCR over the 12-year debt tenor at P90 yield stays above the lender threshold 1.05×.
Pseudocode:
def test_lc_dscr_p90_compliant():
"""LC minimum DSCR at P90 yield must be ≥1.05×."""
register = load_segment_register("fixtures/lc_v1_register.csv")
inputs = methodology_canonical_inputs("LC")
inputs["yield_kwh_per_kwp_per_yr"] = 1380 * 0.92 # P90 (8% degradation of P50)
debt = solve_debt_size(inputs, register)
dscr_curve = compute_dscr_curve(register, inputs, debt, years=12)
min_dscr = min(dscr_curve)
assert min_dscr >= 1.05, (
f"LC min DSCR at P90 = {min_dscr:.2f}× — below lender threshold. "
f"Possible debt sizing error or yield convention mismatch."
)
Pass/fail: - PASS: min DSCR ≥ 1.05× - FAIL: triggers re-examination of debt sizing logic or P90 derivation
Closes: AUDIT-031
I.3.3 test_bp_irr_within_methodology¶
What it asserts: Bangpoo (BP) sponsor IRR at methodology-canonical inputs falls within expected range (per BP investment thesis, ±50 bps).
Pseudocode:
def test_bp_irr_within_methodology():
"""BP sponsor IRR at methodology canonical inputs must match BP IC ±50 bps."""
register = load_segment_register("fixtures/bp_v6_3_register.csv") # placeholder
inputs = methodology_canonical_inputs("BP") # includes BESS, carbon
debt = solve_debt_size(inputs, register)
irr = compute_sponsor_irr(register, inputs, debt)
# BP target IRR [PENDING — BP IC not yet locked at v1.1.0 lock]
# Use ±100 bps band until BP IC locks
target_low, target_high = (11.5, 13.5)
assert target_low <= irr <= target_high, (
f"BP IRR {irr:.2%} outside acceptable band [{target_low}%, {target_high}%]"
)
Pass/fail:
- PASS: IRR within band
- FAIL: BP methodology application diverges; investigate
- Note: BP register fixtures/bp_v6_3_register.csv is [PENDING WP3]; test stub at v1.1.0 lock; full assertion when BP IC locks
Closes: AUDIT-031
I.3.4 test_lc_envelope_capacity_matches_register¶
What it asserts: LC envelope total capacity (MWp) computed from segment register matches v1.0 canonical 34.59 MWp ±0.5%.
Pseudocode:
def test_lc_envelope_capacity_matches_register():
"""LC envelope MWp from register must match canonical 34.59 ±0.5%."""
register = load_segment_register("fixtures/lc_v1_register.csv")
total_mwp = register["mwp"].sum()
assert abs(total_mwp - 34.59) / 34.59 < 0.005, (
f"LC envelope from register = {total_mwp:.2f} MWp; canonical = 34.59 MWp. "
f"Mismatch > 0.5% indicates register tampering or segment ID drift."
)
Pass/fail: - PASS: |total − 34.59| / 34.59 < 0.005 - FAIL: register or canonical broken
Closes: AUDIT-031
I.3.5 test_lc_capex_matches_register¶
What it asserts: LC total CAPEX (USD) computed from segment register matches v1.0 canonical $25.04M EPC ±0.5%.
Pseudocode:
def test_lc_capex_matches_register():
"""LC total EPC from register must match canonical $25.04M ±0.5%."""
register = load_segment_register("fixtures/lc_v1_register.csv")
total_epc_usd = register["epc_usd"].sum()
assert abs(total_epc_usd - 25.04e6) / 25.04e6 < 0.005, (
f"LC EPC from register = ${total_epc_usd/1e6:.2f}M; canonical = $25.04M. "
f"Mismatch > 0.5% indicates per-segment cost mutation or sum error."
)
Pass/fail: - PASS: |total − $25.04M| / $25.04M < 0.005 - FAIL: register cost build broken
Closes: AUDIT-031
I.4 Cross-estate ranking tests (2 tests)¶
Cross-estate tests verify that the methodology produces stable estate rankings across reasonable parameter uncertainty bands.
I.4.1 test_estate_ranking_stable_across_cost_bands¶
What it asserts: Top-5 estates by post-mitigation leveraged IRR are stable across the methodology cost band uncertainty ($700–1,100/kWp range for solar).
Pseudocode:
def test_estate_ranking_stable_across_cost_bands():
"""Top-5 estates by leveraged IRR must be stable across cost band uncertainty."""
estates = ["LC", "BP", "MTP", "Lat_Krabang", "MTP_Port", "Smart_Park",
"Map_Ta_Phut", "Lamphun", "Phichit", "Songkhla",
"Bang_Chan", "Nakhon_Luang", "Kaeng_Khoi"]
# Three cost scenarios per estate
rankings = {}
for cost_scenario in ["low_700", "mid_900", "high_1100"]:
rankings[cost_scenario] = rank_estates_by_irr(estates, cost_scenario)
# Take top-5 from each scenario
top5_low = set(rankings["low_700"][:5])
top5_mid = set(rankings["mid_900"][:5])
top5_high = set(rankings["high_1100"][:5])
# Overlap must be ≥ 4 of 5 across all three scenarios
common = top5_low & top5_mid & top5_high
assert len(common) >= 4, (
f"Top-5 estate ranking unstable across cost bands: {common}. "
f"Methodology may be producing rank-flips under reasonable uncertainty."
)
Pass/fail: - PASS: ≥4 of top-5 are stable across low/mid/high cost scenarios - FAIL: ranking is fragile; methodology produces rank-flips under cost band uncertainty
Closes: AUDIT-031 (anomaly detection on cross-estate ranking)
I.4.2 test_top5_estates_stable_p90¶
What it asserts: Top-5 estates by P50 IRR are similar to top-5 by P90 IRR (cohort stability under yield risk).
Pseudocode:
def test_top5_estates_stable_p90():
"""Top-5 estates by P50 IRR vs P90 IRR — overlap must be ≥4 of 5."""
estates = [...] # 13 IEAT estates
top5_p50 = set(rank_estates_by_irr(estates, "p50")[:5])
top5_p90 = set(rank_estates_by_irr(estates, "p90")[:5])
common = top5_p50 & top5_p90
assert len(common) >= 4, (
f"Top-5 estate ranking unstable from P50 → P90: {common}. "
f"Yield risk is creating cohort instability — investigate methodology yield treatment."
)
Pass/fail: - PASS: ≥4 of 5 estates remain in top-5 under P90 stress - FAIL: methodology yield treatment creates excessive cross-estate noise
Closes: AUDIT-031
I.5 High-materiality findings tests (4 tests)¶
These tests verify that the methodology v1.1.0 lock correctly reflects the closure of the 3 ACTIVE-IRR findings + the Annex H gating event.
I.5.1 test_audit_001_yield_convention_documented¶
What it asserts: The methodology canonical yields document the P50 convention used (AC delivery vs DC generation; pre/post soiling; pre/post Y1 degradation).
Pseudocode:
def test_audit_001_yield_convention_documented():
"""AUDIT-001 closure: yield convention must be documented in methodology canonical YAML."""
canonical = load_methodology_canonical("fixtures/methodology_canonical.yaml")
yield_record = canonical["yield"]
required_keys = ["p50_kwh_per_kwp_per_yr", "convention_basis",
"convention_soiling", "convention_degradation_y1"]
for key in required_keys:
assert key in yield_record, (
f"Yield convention key '{key}' missing. AUDIT-001 not closed."
)
# Convention basis must be explicit
valid_bases = ["AC_delivery", "DC_generation_pre_inverter"]
assert yield_record["convention_basis"] in valid_bases
Pass/fail: - PASS: all yield convention keys present and valid - FAIL: AUDIT-001 not closed; yield convention not documented
Closes: AUDIT-034 (verifies AUDIT-001 has tangible closure artefact)
I.5.2 test_audit_014_debt_cost_aligned¶
What it asserts: LC model debt cost matches methodology canonical 5.75% EXIM (or documents why blended 6.0% applies).
Pseudocode:
def test_audit_014_debt_cost_aligned():
"""AUDIT-014 closure: LC model debt cost must align with methodology canonical."""
canonical_debt_cost = load_methodology_canonical("fixtures/methodology_canonical.yaml")["debt"]["cost_pct"]
lc_model_debt_cost = read_lc_model_inputs("fixtures/lc_model_v1.xlsx")["debt_cost"]
# Allow either exact alignment OR documented exception
if not (canonical_debt_cost - 0.001 <= lc_model_debt_cost <= canonical_debt_cost + 0.001):
# Documented exception required
exception_doc = read_lc_model_assumptions_tab("fixtures/lc_model_v1.xlsx")
assert "debt_cost_blended_rationale" in exception_doc, (
f"LC model debt cost {lc_model_debt_cost:.2%} ≠ canonical {canonical_debt_cost:.2%}. "
f"AUDIT-014 not closed; either align or document blended rationale."
)
Pass/fail: - PASS: aligned, OR documented exception - FAIL: misaligned and no documentation
Closes: AUDIT-034 (verifies AUDIT-014 closure)
I.5.3 test_audit_016_opex_aligned¶
What it asserts: LC model OPEX aligns with methodology canonical per-typology O&M build (per AUDIT-016 closure).
Pseudocode:
def test_audit_016_opex_aligned():
"""AUDIT-016 closure: LC model OPEX must use per-typology methodology canonical."""
canonical = load_methodology_canonical("fixtures/methodology_canonical.yaml")["opex"]
lc_model = read_lc_model_opex_tab("fixtures/lc_model_v1.xlsx")
# Verify model uses per-typology values, not flat
typologies = ["T1", "T2", "T4A", "T4B_DC", "T6W"]
for typology in typologies:
assert typology in lc_model, (
f"LC model OPEX missing typology '{typology}'. AUDIT-016 not closed (still flat OPEX)."
)
canonical_value = canonical[typology]["o_and_m_usd_per_kwp_per_yr"]
model_value = lc_model[typology]["o_and_m_usd_per_kwp_per_yr"]
assert abs(canonical_value - model_value) < 0.01 * canonical_value, (
f"LC model OPEX for {typology} = {model_value} vs canonical {canonical_value}. "
f"AUDIT-016 closure incomplete."
)
Pass/fail: - PASS: model uses per-typology, values match canonical - FAIL: model still has flat $20/kWp/yr or per-typology values misaligned
Closes: AUDIT-034 (verifies AUDIT-016 closure)
I.5.4 test_audit_040_methodology_canonical_present¶
What it asserts: Methodology canonical CAPEX per typology (Annex H closure) exists in the canonical YAML and has WP2-validated values.
Pseudocode:
def test_audit_040_methodology_canonical_present():
"""AUDIT-040 closure: Annex H methodology canonical CAPEX per typology must exist."""
canonical = load_methodology_canonical("fixtures/methodology_canonical.yaml")
capex_canonical = canonical.get("capex", {})
typologies = ["T1", "T2", "T4A", "T4B_DC", "T6W"]
for typology in typologies:
assert typology in capex_canonical, (
f"Methodology canonical CAPEX missing for '{typology}'. "
f"Annex H / AUDIT-040 not closed."
)
# WP2 validation flag must be present
record = capex_canonical[typology]
assert record.get("wp2_validated", False), (
f"Typology '{typology}' methodology canonical not WP2-validated. "
f"Annex H closure incomplete."
)
# Confidence interval must be specified
assert "confidence_interval_usd_per_kwp" in record
Pass/fail: - PASS: all 5 typologies have WP2-validated canonical values with confidence intervals - FAIL: Annex H closure incomplete (gating event for v1.1.0 lock)
Closes: AUDIT-034 (verifies AUDIT-040 / Annex H closure — gating)
I.6 Governance tests (5 tests)¶
Governance tests verify that Part F change-control discipline is being followed.
I.6.1 test_semver_tag_present¶
What it asserts: Git repo has a valid semver tag matching the methodology canonical YAML version field.
Pseudocode:
def test_semver_tag_present():
"""Methodology canonical version must match a Git tag."""
canonical = load_methodology_canonical("fixtures/methodology_canonical.yaml")
version = canonical["meta"]["version"]
# Verify version is valid semver
import re
assert re.match(r"^\d+\.\d+\.\d+(-[a-zA-Z0-9]+)?$", version), (
f"Version '{version}' is not valid semver."
)
# Verify Git tag exists
git_tags = get_git_tags() # subprocess call to `git tag -l`
expected_tag = f"v{version}"
assert expected_tag in git_tags, (
f"Methodology version '{version}' has no matching Git tag '{expected_tag}'."
)
Pass/fail: - PASS: version is valid semver AND Git tag exists - FAIL: missing or mismatched
Closes: AUDIT-034 (verifies semver discipline per Part F § F.1)
I.6.2 test_changelog_entry_for_current_version¶
What it asserts: CHANGELOG.md has an entry for the current methodology version per Part F § F.1.3.
Pseudocode:
def test_changelog_entry_for_current_version():
"""CHANGELOG.md must have an entry for the current methodology version."""
canonical = load_methodology_canonical("fixtures/methodology_canonical.yaml")
version = canonical["meta"]["version"]
changelog_content = open("fixtures/CHANGELOG.md").read()
expected_heading = f"## v{version}"
assert expected_heading in changelog_content, (
f"CHANGELOG.md missing entry '{expected_heading}'. "
f"Per Part F § F.1.3, every version must have a change-log entry."
)
# Entry must have minimum required fields per Part F § F.1.3
entry = extract_changelog_entry(changelog_content, version)
required_fields = ["Date locked", "Approver(s)", "Changes", "IRR impact summary", "Closed register items"]
for field in required_fields:
assert field in entry, (
f"Changelog entry for v{version} missing '{field}'."
)
Pass/fail: - PASS: entry exists with all required fields - FAIL: missing entry or incomplete
Closes: AUDIT-034 (verifies Part F § F.1.3 compliance)
I.6.3 test_deprecated_parameters_absent¶
What it asserts: Deprecated parameters (per Part F § F.6.3) are not used in active artefacts.
Pseudocode:
def test_deprecated_parameters_absent():
"""Deprecated parameters must not be referenced in active artefacts."""
deprecated = load_deprecated_params("fixtures/deprecated_params.yaml")
# Check LC model
lc_model_assumptions = read_lc_model_assumptions_tab("fixtures/lc_model_v1.xlsx")
for param, deprecation_record in deprecated.items():
if deprecation_record["status"] == "retired":
# Hard error if retired param appears
assert param not in lc_model_assumptions, (
f"Retired parameter '{param}' still in LC model. "
f"Per Part F § F.6.4, retired params must be removed."
)
elif deprecation_record["status"] == "deprecated":
# Warning logged but not failing
if param in lc_model_assumptions:
pytest.warns(DeprecationWarning, match=param)
Pass/fail: - PASS: no retired parameters in active artefacts; deprecated parameters generate warnings - FAIL: retired parameter found in active artefact (e.g., FX 32 unqualified, BESS $400, ITMO solar pricing)
Closes: AUDIT-034 (enforces Part F § F.6 deprecation discipline)
I.6.4 test_v3_segment_ids_absent¶
What it asserts: V3 segment IDs (deprecated per Part F § F.5.1) do not appear in active artefacts.
Pseudocode:
def test_v3_segment_ids_absent():
"""V3 segment IDs (LC-S-V3-XXXX format) must not appear in active artefacts."""
import re
v3_pattern = re.compile(r"[A-Z]{2,4}-S-V3-\d{4}")
# Check LC v1.0 register
register_content = open("fixtures/lc_v1_register.csv").read()
v3_matches = v3_pattern.findall(register_content)
assert len(v3_matches) == 0, (
f"V3 segment IDs found in LC v1.0 register: {v3_matches}. "
f"Per Part F § F.5.1, V3 IDs are retired in v1.1; v1.0 register IDs are canonical."
)
# Check methodology canonical YAML
canonical_content = open("fixtures/methodology_canonical.yaml").read()
assert len(v3_pattern.findall(canonical_content)) == 0, "V3 IDs in methodology canonical"
Pass/fail: - PASS: no V3 IDs anywhere - FAIL: V3 ID found → segment ID consolidation (AUDIT-044) regression
Closes: AUDIT-034 (enforces AUDIT-044 closure)
I.6.5 test_methodology_package_2_retired¶
What it asserts: The deprecated methodology-package 2 Drive folder is retired (not referenced in any active artefact).
Pseudocode:
def test_methodology_package_2_retired():
"""The deprecated `methodology-package 2` folder must not be referenced in active artefacts."""
forbidden_reference = "methodology-package 2"
# Scan all .md files in repo
repo_root = "."
md_files = find_files_by_extension(repo_root, ".md")
violations = []
for md_file in md_files:
content = open(md_file).read()
if forbidden_reference in content:
# Allow reference in deprecation notice / Part F § F.6.3 / Part F § F.7.5
allowed_contexts = ["DEPRECATED", "F.6.3", "F.7.5", "RETIRED"]
line_with_ref = next(line for line in content.split("\n") if forbidden_reference in line)
if not any(ctx in line_with_ref for ctx in allowed_contexts):
violations.append(f"{md_file}: {line_with_ref}")
assert len(violations) == 0, (
f"Active references to deprecated 'methodology-package 2': {violations}. "
f"Per Part F § F.7.5, this folder is retired."
)
Pass/fail: - PASS: no active references; only deprecation notices reference the folder - FAIL: active reference found → AUDIT-032 regression
Closes: AUDIT-034 (enforces AUDIT-032 closure)
I.7 Cross-reference integrity tests (2 tests)¶
Cross-reference tests verify that methodology document cross-references resolve correctly.
I.7.1 test_part_b_to_c_references_resolve¶
What it asserts: All references from Part B (pipeline) to Part C (typologies) resolve to actual sections in Part C.
Pseudocode:
def test_part_b_to_c_references_resolve():
"""All Part B references to Part C must resolve."""
import re
part_b_content = open("parts/B_pipeline.md").read()
part_c_content = open("parts/C_typologies.md").read()
# Extract all Part C references from Part B
pattern = re.compile(r"Part C § (C\.\d+(?:\.\d+)*)")
references = set(pattern.findall(part_b_content))
# For each reference, verify section heading exists in Part C
for ref in references:
heading_pattern = re.compile(rf"^#+ {re.escape(ref)}", re.MULTILINE)
assert heading_pattern.search(part_c_content), (
f"Part B references Part C § {ref}, but section not found in Part C."
)
Pass/fail: - PASS: all references resolve - FAIL: dangling reference → cross-reference drift
Closes: AUDIT-031
I.7.2 test_part_d_to_c_references_resolve¶
What it asserts: All references from Part D (cross-cutting) to Part C resolve.
Pseudocode:
def test_part_d_to_c_references_resolve():
"""All Part D references to Part C must resolve."""
# Same logic as I.7.1 but Part D → Part C
import re
part_d_content = open("parts/D_crosscutting.md").read()
part_c_content = open("parts/C_typologies.md").read()
pattern = re.compile(r"Part C § (C\.\d+(?:\.\d+)*)")
references = set(pattern.findall(part_d_content))
for ref in references:
heading_pattern = re.compile(rf"^#+ {re.escape(ref)}", re.MULTILINE)
assert heading_pattern.search(part_c_content), (
f"Part D references Part C § {ref}, but section not found in Part C."
)
Pass/fail: - PASS: all references resolve - FAIL: dangling reference
Closes: AUDIT-031
I.8 Fixtures¶
The test suite depends on several fixtures providing canonical inputs:
I.8.1 fixtures/lc_v1_register.csv¶
LC v1.0 investment segment register (NC-IS-LC-001). Read-only fixture; tests assert against canonical values.
[PENDING WP3] — must be exported from canonical Drive file 1JhPpY3z_JlbHnIxYsOJQ-AzAVwgnk1ow and committed to test fixtures.
I.8.2 fixtures/methodology_canonical.yaml¶
Methodology canonical YAML format:
meta:
version: "1.1.0"
date_locked: "2026-07-22"
approvers: ["M. Forni", "S. Rubinyi"]
yield:
p50_kwh_per_kwp_per_yr: 1485
convention_basis: "AC_delivery"
convention_soiling: "post-soiling"
convention_degradation_y1: "pre-Y1"
debt:
cost_pct: 0.0575
tenor_yrs: 12
opex:
T1:
o_and_m_usd_per_kwp_per_yr: 28
cleaning_frequency: "quarterly"
T6W:
o_and_m_usd_per_kwp_per_yr: 45
cleaning_frequency: "monthly"
# ... other typologies
capex:
T1:
usd_per_kwp: [PENDING WP2]
confidence_interval_usd_per_kwp: [PENDING WP2]
wp2_validated: false # toggles true after WP2
# ... other typologies
[PENDING WP2 + WP3] — values gated by WP2 engineering review.
I.8.3 fixtures/CHANGELOG.md¶
CHANGELOG mirror with v1.0 and v1.1.0 entries.
I.8.4 fixtures/deprecated_params.yaml¶
Deprecated parameters per Part F § F.6.3:
FX_32_THB_USD_unqualified:
status: deprecated
replaced_by: "FX 35 main / 32 IET-only"
retire_after: "v1.2"
BESS_CAPEX_400_per_kWh:
status: deprecated
replaced_by: "$175/kWh + thermal premium"
retire_after: "v1.2"
# ... 9 deprecations total
I.8.5 fixtures/lc_model_v1.xlsx¶
LC v1.0 financial model export. Required for tests I.3.1, I.3.2, I.5.2, I.5.3, I.6.3.
[PENDING WP3] — export from canonical model with PII redacted.
I.9 CI / execution discipline¶
I.9.1 Local execution (v1.1.0 lock)¶
Expected output at v1.1.0 lock: all 18 tests PASS.
I.9.2 Pre-commit hook¶
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: methodology-tests
name: Methodology acceptance tests
entry: pytest tests/
language: system
pass_filenames: false
always_run: true
I.9.3 CI pipeline (v1.2 deliverable)¶
GitHub Actions equivalent. Per Part F § F.7.2:
# .github/workflows/test.yml
name: Methodology Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.11"
- run: pip install -r requirements.txt
- run: pytest tests/ --junit-xml=test_report.xml
- uses: actions/upload-artifact@v3
with:
name: test-report
path: test_report.xml
[PENDING v1.2] — CI infrastructure setup.
I.10 Test artefacts¶
Per Part F § F.2.5, the test suite produces three artefacts:
I.10.1 test_report.html¶
Full pytest results with timestamps, pass/fail status, error tracebacks. Committed to Git in tests/_reports/ on each test run; archived per version lock.
I.10.2 test_coverage.json¶
Coverage by methodology section (which Parts/Annexes are exercised by tests):
{
"version": "1.1.0",
"coverage": {
"Part_A_framing": 0.85,
"Part_B_pipeline": 0.90,
"Part_C_typologies": 0.75,
"Part_D_crosscutting": 0.80,
"Part_E_runbook": 0.60,
"Part_F_governance": 0.95,
"Annex_G_credit": 0.50,
"Annex_H_cost_drivers": 0.65,
"Annex_I_pytest": 1.00,
"Annex_J_audit_register": 0.40
}
}
Methodology sections under 0.50 coverage flagged for v1.2 test expansion.
I.10.3 regression_log.csv¶
Historical pass/fail tracking across versions:
version,test_name,status,timestamp,error_message
1.1.0,test_lc_irr_within_methodology,PASS,2026-07-22T10:00:00Z,
1.1.0,test_audit_040_methodology_canonical_present,PASS,2026-07-22T10:00:01Z,
...
Regression log allows comparison across versions: if a test passes at v1.1.0 but fails at v1.1.1, the diff is immediate.
I.11 Acknowledged Annex I gaps¶
- Python implementation pending WP3 — Annex I specifies the test logic; actual code is WP3 deliverable (Weeks 5–8). All 18 tests show [PENDING WP3 implementation].
- Fixtures pending WP3 —
lc_v1_register.csv,methodology_canonical.yaml,CHANGELOG.md,deprecated_params.yaml,lc_model_v1.xlsxmust be created and committed in WP3. - CI pipeline deferred to v1.2 — local + pre-commit only at v1.1.0 lock.
- Test coverage targets aspirational — coverage percentages in I.10.2 are estimates; actual coverage measured post-implementation.
- BP replication test stub —
test_bp_irr_within_methodologyuses placeholder band ±100 bps until BP IC paper locks; tighter band when BP IC locks. - Additional unit tests deferred to v1.2 — yield model unit tests, OPEX model unit tests, debt sizing logic unit tests are v1.2 expansion. Annex I covers integration/acceptance, not unit-level.
- Performance benchmarks deferred to v1.2 — test suite runtime targets and parallelisation deferred.
- Methodology canonical YAML format final — YAML structure in I.8.2 is illustrative; final format locked when first WP2 outputs land.
I.12 References¶
- NC-METH-001 v1.1.0 Part F § F.2 — acceptance test suite specification
- NC-METH-001 v1.1.0 Part F § F.7.2 — Git repository structure
- NC-METH-001 v1.1.0 Annex J — AUDIT-031 + AUDIT-034 closure tracking
- NC-METH-001 v1.1.0 Annex H — methodology canonical CAPEX (gating Annex I closure)
- NC-IC-LC-001 IC Paper v1.1 — LC canonical IRR 12.2% (replication test target)
- NC-FM-LC-001 LC Financial Model v1.0 — model inputs source
- NC-IS-LC-001 LC Investment Segment Register v1.0 — register source for replication tests
- NC-MN-001-R3 v0.3 audit register — AUDIT-031 and AUDIT-034 origin
- pytest documentation: https://docs.pytest.org/
End of Annex I v1.1.0 specification.
Implementation pending: WP3 closure (Weeks 5–8). All 18 tests must PASS for v1.1.0 lock per Part F § F.2.4.