Skip to content

Commit da9fe55

Browse files
feat(paper): reviewer_mode adds Reviewer Audit section
`sp.paper(..., reviewer_mode=True)` and the `CausalQuestion.paper()` shortcut now insert a "Reviewer Audit" section before "References" that surfaces evidence a reviewer / replication agent needs without re-running the workflow: * Registry validation: estimator's `stability`, `validation_status`, validation notes, and known limitations. * Identification verdict + findings from the workflow diagnostics. * Post-estimation surface from `sp.postestimation_contract(result)`. * Result violations from `result.violations()` if implemented. * Provenance handle (run id + data hash) when one is attached. * Pipeline degradations recorded by the workflow. * A short replication checklist. The section is opt-in (default `False`) so existing paper drafts are unchanged; turning it on costs an extra registry lookup and a contract call, both of which are warning-safe. Verified: tests/test_paper_branches.py + tests/test_paper_from_question.py — 48 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4e5cb16 commit da9fe55

4 files changed

Lines changed: 175 additions & 5 deletions

File tree

src/statspai/question/question.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,8 @@ def paper(self, *, fmt: str = "markdown",
218218
output_path: Optional[str] = None,
219219
dag: Any = None,
220220
include_robustness: bool = True,
221-
cite: bool = True):
221+
cite: bool = True,
222+
reviewer_mode: bool = False):
222223
"""Build a full :class:`PaperDraft` from this declared question.
223224
224225
Convenience wrapper around :func:`statspai.paper_from_question`.
@@ -243,6 +244,7 @@ def paper(self, *, fmt: str = "markdown",
243244
include_robustness=include_robustness,
244245
cite=cite,
245246
dag=dag,
247+
reviewer_mode=reviewer_mode,
246248
)
247249

248250

src/statspai/workflow/paper.py

Lines changed: 150 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ def to_markdown(self) -> str:
213213
order = [
214214
"Question", "Data", "Identification",
215215
"Estimator", "Results", "Robustness",
216-
"Pipeline notes", "Causal DAG", "References",
216+
"Reviewer Audit", "Pipeline notes", "Causal DAG", "References",
217217
]
218218
chunks: List[str] = []
219219
for title in order:
@@ -405,7 +405,7 @@ def to_qmd(
405405
order = [
406406
"Question", "Data", "Identification",
407407
"Estimator", "Results", "Robustness",
408-
"Pipeline notes", "Causal DAG", "References",
408+
"Reviewer Audit", "Pipeline notes", "Causal DAG", "References",
409409
]
410410
# When self.dag is set, regenerate the Causal DAG body with the
411411
# Quarto-native mermaid block instead of the markdown text-art.
@@ -1021,6 +1021,124 @@ def _section_from_workflow(
10211021
return sections
10221022

10231023

1024+
def _reviewer_audit_section(
1025+
*,
1026+
workflow: Any = None,
1027+
result: Any = None,
1028+
estimator: Optional[str] = None,
1029+
degradations: Optional[List[Dict[str, Any]]] = None,
1030+
) -> str:
1031+
"""Build a reviewer-facing audit section for a fitted draft."""
1032+
lines: List[str] = []
1033+
lines.append("**Reviewer-mode audit**")
1034+
lines.append("")
1035+
1036+
# Estimator registry evidence.
1037+
est_name = estimator
1038+
if not est_name and workflow is not None:
1039+
rec = getattr(workflow, "recommendation", None)
1040+
try:
1041+
if rec is not None and rec.recommendations:
1042+
est_name = rec.recommendations[0].get("function")
1043+
except Exception:
1044+
est_name = None
1045+
if est_name:
1046+
try:
1047+
from ..registry import describe_function
1048+
spec = describe_function(est_name)
1049+
lines.append(
1050+
f"- **Registry**: `sp.{est_name}` is "
1051+
f"`stability={spec.get('stability')}`, "
1052+
f"`validation_status={spec.get('validation_status')}`."
1053+
)
1054+
notes = spec.get("validation_notes") or []
1055+
if notes:
1056+
shown = "; ".join(str(n) for n in notes[:3])
1057+
lines.append(f"- **Validation evidence**: {shown}.")
1058+
limitations = spec.get("limitations") or []
1059+
if limitations:
1060+
lines.append("- **Known implementation limitations**:")
1061+
lines.extend(f" - {lim}" for lim in limitations[:5])
1062+
except Exception as exc:
1063+
lines.append(
1064+
f"- **Registry**: lookup for `sp.{est_name}` failed "
1065+
f"({type(exc).__name__}: {exc})."
1066+
)
1067+
1068+
# Identification report.
1069+
diag = getattr(workflow, "diagnostics", None) if workflow is not None else None
1070+
if diag is not None:
1071+
verdict = getattr(diag, "verdict", None)
1072+
if verdict:
1073+
lines.append(f"- **Identification verdict**: `{verdict}`.")
1074+
findings = getattr(diag, "findings", None) or []
1075+
if findings:
1076+
lines.append("- **Identification findings to defend**:")
1077+
for finding in findings[:6]:
1078+
sev = getattr(finding, "severity", "info")
1079+
msg = getattr(finding, "message", str(finding))
1080+
lines.append(f" - [{str(sev).upper()}] {msg}")
1081+
1082+
# Post-estimation contract.
1083+
target = result or getattr(workflow, "result", None)
1084+
if target is not None:
1085+
try:
1086+
from ..postestimation import postestimation_contract
1087+
contract = postestimation_contract(target)
1088+
available = ", ".join(sorted(contract["available"].keys())[:10])
1089+
lines.append(f"- **Post-estimation surface**: {available}.")
1090+
except Exception as exc:
1091+
lines.append(
1092+
f"- **Post-estimation surface**: unavailable "
1093+
f"({type(exc).__name__}: {exc})."
1094+
)
1095+
1096+
violations = getattr(target, "violations", None)
1097+
if callable(violations):
1098+
try:
1099+
v = violations()
1100+
if v:
1101+
lines.append("- **Result violations**:")
1102+
if isinstance(v, dict):
1103+
iterable = list(v.items())[:8]
1104+
lines.extend(f" - `{k}`: {val}" for k, val in iterable)
1105+
else:
1106+
lines.append(f" - {v}")
1107+
else:
1108+
lines.append("- **Result violations**: none reported by result.")
1109+
except Exception as exc:
1110+
lines.append(
1111+
f"- **Result violations**: check failed "
1112+
f"({type(exc).__name__}: {exc})."
1113+
)
1114+
1115+
prov = get_provenance(target)
1116+
if prov is not None:
1117+
lines.append(
1118+
f"- **Provenance**: run `{prov.run_id}`, "
1119+
f"data hash `{prov.data_hash or 'not recorded'}`."
1120+
)
1121+
else:
1122+
lines.append("- **Provenance**: no attached provenance record found.")
1123+
1124+
if degradations:
1125+
lines.append("- **Pipeline degradations**:")
1126+
for item in degradations[:8]:
1127+
detail = f" ({item.get('detail')})" if item.get("detail") else ""
1128+
lines.append(
1129+
f" - {item.get('section')}: {item.get('error_type')}: "
1130+
f"{item.get('message')}{detail}"
1131+
)
1132+
1133+
lines.append("")
1134+
lines.append("**Reviewer checklist**")
1135+
lines.append("- Re-run the replication script or `sp.replication_pack()` output.")
1136+
lines.append("- Check identification assumptions against the study design, not only the code.")
1137+
lines.append("- Inspect overlap, pre-trends, weak instruments, or bandwidth sensitivity when relevant.")
1138+
lines.append("- Confirm exported tables are generated from `tidy()`/`glance()`/`sp.collect()` artifacts.")
1139+
return "\n".join(lines)
1140+
1141+
10241142
# --------------------------------------------------------------------- #
10251143
# Top-level entry point
10261144
# --------------------------------------------------------------------- #
@@ -1054,6 +1172,7 @@ def paper(
10541172
# v1.13: forwarded to sp.causal -> sp.recommend; default False
10551173
# keeps frontier MVP estimators out of auto-generated drafts.
10561174
allow_experimental: bool = False,
1175+
reviewer_mode: bool = False,
10571176
) -> PaperDraft:
10581177
"""End-to-end "data → publication-draft" pipeline.
10591178
@@ -1097,6 +1216,10 @@ def paper(
10971216
when you are explicitly drafting a paper around frontier
10981217
methods (e.g. ``causal_text`` or ``did_multiplegt_dyn``); the
10991218
Pipeline notes section records what was filtered.
1219+
reviewer_mode : bool, default False
1220+
Add a "Reviewer Audit" section summarizing registry validation
1221+
status, identification findings, post-estimation capabilities,
1222+
provenance, violations, and a replication checklist.
11001223
11011224
Returns
11021225
-------
@@ -1142,6 +1265,7 @@ def paper(
11421265
output_path=output_path,
11431266
include_robustness=include_robustness,
11441267
cite=cite, dag=dag,
1268+
reviewer_mode=reviewer_mode,
11451269
)
11461270

11471271
cols = list(data.columns)
@@ -1348,12 +1472,20 @@ def paper(
13481472
exc=exc,
13491473
detail=f"result_type={type(workflow.result).__name__}",
13501474
)
1351-
sections["References"] = (
1475+
references_body = (
13521476
"\n".join(f"- {c}" for c in citations)
13531477
if citations else "_(No explicit citations attached — see "
13541478
"`workflow.result.cite()` if available.)_"
13551479
)
13561480

1481+
if reviewer_mode:
1482+
sections["Reviewer Audit"] = _reviewer_audit_section(
1483+
workflow=workflow,
1484+
result=workflow.result,
1485+
degradations=degradations,
1486+
)
1487+
sections["References"] = references_body
1488+
13571489
pipeline_notes: List[str] = []
13581490
for error in pipeline_errors:
13591491
_record_note(pipeline_notes, error)
@@ -1402,6 +1534,7 @@ def paper_from_question(
14021534
include_robustness: bool = True,
14031535
cite: bool = True,
14041536
dag: Any = None,
1537+
reviewer_mode: bool = False,
14051538
) -> PaperDraft:
14061539
"""Build a :class:`PaperDraft` from a :class:`CausalQuestion`.
14071540
@@ -1446,6 +1579,9 @@ def paper_from_question(
14461579
Pre-built ``sp.dag`` graph. When provided, the draft's
14471580
Identification section gains a *Causal DAG* subsection (text
14481581
rendering for markdown / mermaid for qmd).
1582+
reviewer_mode : bool, default False
1583+
Add a reviewer-facing audit section with registry validation,
1584+
post-estimation capabilities, provenance, and replication checks.
14491585
14501586
Returns
14511587
-------
@@ -1621,13 +1757,23 @@ def paper_from_question(
16211757
exc=exc,
16221758
detail=f"underlying_type={type(result.underlying).__name__}",
16231759
)
1624-
sections["References"] = (
1760+
references_body = (
16251761
"\n".join(f"- {c}" for c in citations)
16261762
if citations
16271763
else "_(No explicit citations attached — see "
16281764
"`result.underlying.cite()` if available.)_"
16291765
)
16301766

1767+
if reviewer_mode:
1768+
target = result.underlying if result.underlying is not None else result
1769+
sections["Reviewer Audit"] = _reviewer_audit_section(
1770+
workflow=None,
1771+
result=target,
1772+
estimator=plan.estimator,
1773+
degradations=degradations,
1774+
)
1775+
sections["References"] = references_body
1776+
16311777
# DAG appendix when the user attached one.
16321778
if dag is not None:
16331779
try:

tests/test_paper_branches.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,20 @@ def test_paper_writes_output_path(rct_data, tmp_path):
370370
assert isinstance(d, PaperDraft)
371371

372372

373+
def test_paper_reviewer_mode_adds_audit_section(rct_data):
374+
d = paper(
375+
rct_data,
376+
question="effect of trained on wage",
377+
design="rct",
378+
reviewer_mode=True,
379+
)
380+
assert "Reviewer Audit" in d.sections
381+
body = d.sections["Reviewer Audit"]
382+
assert "Post-estimation surface" in body
383+
assert "Reviewer checklist" in body
384+
assert "Registry" in body
385+
386+
373387
# --------------------------------------------------------------------- #
374388
# PaperDraft.summary / to_dict surfaces
375389
# --------------------------------------------------------------------- #

tests/test_paper_from_question.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,14 @@ def test_dispatch_tex_format(self, panel_df):
152152
assert tex.startswith("\\documentclass")
153153
assert "Question" in tex
154154

155+
def test_dispatch_reviewer_mode(self, panel_df):
156+
q = sp.causal_question(
157+
"trained", "wage", data=panel_df, design="rct",
158+
)
159+
draft = sp.paper(q, reviewer_mode=True)
160+
assert "Reviewer Audit" in draft.sections
161+
assert "Reviewer checklist" in draft.sections["Reviewer Audit"]
162+
155163

156164
# ---------------------------------------------------------------------------
157165
# Provenance integration

0 commit comments

Comments
 (0)