Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
393 changes: 335 additions & 58 deletions docs/factors/d5_c5_audit_report.md

Large diffs are not rendered by default.

269 changes: 269 additions & 0 deletions docs/factors/d5_runner_difference_catalogue.md

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions qt/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -632,13 +632,26 @@ def _cmd_run_factor_eval_reconcile(args: argparse.Namespace) -> int:
f"frozen={result.rows_frozen} new={result.rows_new} "
f"equal={result.equal} within_tol={result.within_tolerance} "
f"warmup={len(result.by_class('warmup_left_extension'))} "
f"sparse_tail={len(result.by_class('warmup_sparse_valid_day_tail'))} "
f"float_tail={len(result.by_class('float_reordering_tail'))} "
f"threshold_flip={len(result.by_class('threshold_flip_tail'))} "
f"flip_contamination={len(result.by_class('threshold_flip_contamination'))} "
f"nan_footprint={result.nan_footprint_rows} "
f"unclassified={len(unclassified)} "
f"max_rel_diff={result.max_rel_diff:.3e}"
)
# The headline max is taken before any bucketing, so print it per
# class too: a big number that sits entirely inside a registered
# class must not read as an ungated tolerance (review NIT-1).
per_class = result.max_rel_by_class()
print(
" max_rel by class: "
+ (
", ".join(f"{k}={v:.3e}" for k, v in sorted(per_class.items()))
if per_class
else "(no differing cells)"
)
)
if result.warmup_by_direction:
print(f" warmup by direction: {result.warmup_by_direction}")
if result.warmup_by_month:
Expand Down
20 changes: 16 additions & 4 deletions qt/exec_basis_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ def run_exec_basis_evaluation(
stem: str,
force_rebuild: bool = False,
book_view: str = View.CLOSE.value,
with_book_suffix: str = "",
extra_sections: Sequence[SectionLike] | None = None,
) -> ExecBasisEvaluation:
"""Build the exec-to-exec returns, sanity-check them, evaluate twice, report.
Expand All @@ -296,6 +297,18 @@ def run_exec_basis_evaluation(
``decision`` explicitly. Only the caller knows this, so it is a parameter
rather than something guessed here.

``with_book_suffix`` (default ``""`` -> the legacy paths, byte-for-byte)
is appended to the WITH-BOOK artifact stem only, so a caller that runs the
same factor under two book views writes them to two different files IN ONE
STEP. It is a write-time parameter and never a post-hoc move: the D5 C5 F5
defect was exactly that — the close-book run wrote the shared
``{stem}_exec_with_book.*`` names first and renamed them afterwards, so the
decision-book artifacts written by an earlier run were destroyed between
the write and the rename, and the reconcile's reports leg then failed on a
file that had existed minutes before. The no-book artifacts carry no book
at all and the sanity report is book-independent, so both keep the shared
stem regardless of this suffix.

``extra_sections`` (default None -> the legacy behavior, byte-for-byte) are
add-Sections appended to BOTH exec reports before they are written — the
contract's §3.6 extension point ("may ADD sections but never drop a
Expand Down Expand Up @@ -425,15 +438,14 @@ def run_exec_basis_evaluation(
report_no_book = _with_extra_sections(report_no_book, extra_sections)
report_with_book = _with_extra_sections(report_with_book, extra_sections)

with_book_stem = f"{stem}_exec_with_book{with_book_suffix}"
nb_md, nb_json = _write_report(report_no_book, report_dir, f"{stem}_exec_no_book")
wb_md, wb_json = _write_report(
report_with_book, report_dir, f"{stem}_exec_with_book"
)
wb_md, wb_json = _write_report(report_with_book, report_dir, with_book_stem)
nb_png = render_factor_dashboard(
report_no_book, ir_no_book, report_dir / f"{stem}_exec_no_book_dashboard.png"
)
wb_png = render_factor_dashboard(
report_with_book, ir_with_book, report_dir / f"{stem}_exec_with_book_dashboard.png"
report_with_book, ir_with_book, report_dir / f"{with_book_stem}_dashboard.png"
)
return ExecBasisEvaluation(
spec=exec_spec,
Expand Down
14 changes: 13 additions & 1 deletion qt/factor_eval_disclosures.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,17 @@ def summarize_neutralization(
# --------------------------------------------------------------------------- #
# The add-Section bridge (contract §3.6: may ADD, never drop a mandatory one)
# --------------------------------------------------------------------------- #
#: Payload keys that are computed PROPERTIES rather than dataclass fields, so
#: ``asdict`` alone does not produce them. Author-once: the reconcile harness
#: derives an add-Section's Markdown line prefixes from this same tuple, and a
#: property listed in only one of the two places would silently unregister a
#: rendered line (measured: three such lines failed the reports leg).
DERIVED_PAYLOAD_PROPERTIES: tuple[str, ...] = (
"validity_rate",
"return_guard_attrition",
)


def to_section(name: str, coverage) -> Section:
"""Pack a coverage disclosure dataclass into an add-Section.

Expand All @@ -590,7 +601,7 @@ def to_section(name: str, coverage) -> Section:
section added this way cannot move a verdict — pinned by test.
"""
payload: dict[str, object] = dict(asdict(coverage))
for prop in ("validity_rate", "return_guard_attrition"):
for prop in DERIVED_PAYLOAD_PROPERTIES:
if hasattr(coverage, prop):
payload[prop] = getattr(coverage, prop)
return Section(name=name, payload=payload, note=coverage.render())
Expand Down Expand Up @@ -652,6 +663,7 @@ def publishes_neutralization_disclosure(factor) -> bool:


__all__ = [
"DERIVED_PAYLOAD_PROPERTIES",
"DisclosureBinding",
"NEUTRALIZATION_SECTION_NAME",
"NeutralizationCoverage",
Expand Down
Loading