mirror of
https://github.com/Imbad0202/academic-research-skills.git
synced 2026-09-14 13:51:17 +08:00
734dd23e03
* feat(v3.6.6): Track A+B — Schema 13.1 file + SC-* mode-gating + writer/evaluator tests
Track A — Schema 13.1 file upgrade (shared/sprint_contract.schema.json):
- Title: "ARS Sprint Contract (Schema 13)" → "ARS Sprint Contract (Schema 13.1)"
- mode enum gains writer_full + evaluator_full (5 → 7 values per §3.2)
- Top-level required list drops measurement_procedure + panel_size (lifted to
reviewer-conditional gates per §3.3.1 / §3.3.5)
- New top-level optional fields: pre_commitment_artifacts (writer-only per
§3.4.1) + disagreement_handling (evaluator-only per §3.4.2)
- failure_conditions[].action base enum lifted; mode-conditional enums in
allOf branches 4 / 5 / 6 (reviewer / writer / evaluator) per §3.3.3
- failure_conditions inline contains-F0 lifted; mode-conditional contains in
allOf branches 7 / 8 / 9 per §3.3.4
- 12 allOf branches total (existing 2 from Schema 13 + 10 new per §3.5):
branch 1 cross_reviewer_quantifier reviewer-conditional (existing);
branch 2 override_ladder three-round shape (existing);
branch 3 reviewer requires measurement_procedure (new §3.3.1);
branches 4/5/6 mode-conditional action enum triplet (new §3.3.3);
branches 7/8/9 mode-conditional F0 contains triplet (new §3.3.4);
branch 10 reviewer requires panel_size (new §3.3.5);
branch 11 writer_full requires pre_commitment_artifacts (new §3.5);
branch 12 evaluator_full requires disagreement_handling (new §3.5).
check_sprint_contract.py print "Schema 13" → "Schema 13.1".
Track B — SC-* mode-gating audit (per §7.1 implementation requirement):
The validator's warn_suspicious() body emits SC-1 through SC-11 advisory
warnings. Audit per §7.1: reviewer-mode-specific warnings must be mode-gated
to fire only when mode startsWith reviewer_; mode-agnostic warnings continue
to fire universally. Writer / evaluator templates intentionally omit
reviewer-only fields per §3.3.1 / §3.3.5 — without mode-gating, every clean
generator template validation would noise SC-5 / SC-11 (and SC-9 would crash
attempting to read the absent measurement_procedure path).
Mode-agnostic warnings (unchanged): SC-1 baseline lag / SC-2 single
dimension / SC-3 no mandatory / SC-4 orphan dim ref / SC-7 conflicting
actions at same severity / SC-10 unreferenced mandatory/high dim.
Reviewer-only warnings (now gated):
- SC-5 measurement_procedure.reviewer_must_output_before_paper canonical
outputs check — gated to mode.startswith("reviewer_") only.
- SC-11 panel_size sanity (panel=1 collapse / mode-expected panel size
mismatch) — gated to reviewer-only; writer / evaluator have no panel.
SC-9 paraphrase_minimum_dimensions (count exceeds dim count) — extended
across all three mode families. Each mode reads its own field per §3.3.1 /
§3.4.1 / §3.4.2:
- reviewer reads measurement_procedure.paraphrase_minimum_dimensions
- writer_full reads pre_commitment_artifacts.acceptance_criteria_paraphrase.minimum_dimensions
- evaluator_full reads disagreement_handling.paraphrase_minimum_dimensions
The lint rule (paraphrase count must not exceed dim count) is identical
across all three; only the source field name + Phase label change.
Test coverage (54 → 71 tests, +17 new):
- TestSchema131WriterEvaluatorPositive (4): shipped writer/evaluator
templates validate cleanly with zero warnings under Schema 13.1.
- TestSchema131NegativeBranches (5): branches 4 / 5 / 6 / 11 / 12
hard-fail tests. Branch 5 writer mis-pinning to editorial enum;
branch 6 evaluator mis-pinning to writer enum; branch 4 reviewer
mis-pinning to generator enum (completes the cross-mode triplet);
branch 11 writer missing pre_commitment_artifacts; branch 12
evaluator missing disagreement_handling. Cross-mode field leakage
is intentionally NOT tested as a hard-fail per §7.1 R1 settled
decision (v3.7.x not-clause hardening covers that direction).
- TestSchema131ReviewerZeroTouch (2): the §3.6 promised regression
tests test_existing_reviewer_contracts_still_valid_under_13_1
and test_byte_equivalent_validation_for_reviewer_contracts. Loads
both shipped reviewer templates against Schema 13.1; asserts
validate() returns empty + warn_suspicious() returns empty.
- TestSC5SC9SC11ModeGating (6): SC-5 / SC-11 do not fire on writer
or evaluator templates; SC-9 reads the right per-mode field path
for writer (pre_commitment_artifacts) and evaluator
(disagreement_handling).
All 4 shipped templates validate cleanly with zero soft warnings under
Schema 13.1 (verified against shared/contracts/{reviewer,writer,evaluator}/*.json).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(v3.6.6): Track C+D — academic-paper v3.6.6 contract block + sprint_contract_protocol cross-ref
Track C — academic-paper SKILL.md + agent files v3.6.6 contract block:
academic-paper/SKILL.md adds a new ## v3.6.6 Generator-Evaluator Contract
Protocol section (101 lines) right after the v3.4.0 compliance note,
mirroring sprint_contract_protocol.md §2 reviewer pattern verbatim:
- Overview + four-call structure (Phase 4a / 4b / 6a / 6b) with system
prompt sources, user content composition, output sections, and lint
check counts.
- System prompt vs user content discipline (system carries invariant
policy text only; user content carries contract JSON re-injected per
call + runtime delimiters + upstream artefacts).
- Schema field name vs runtime emission distinction (pre_commitment_artifacts
schema field vs writer Phase 4a runtime emission; same for
disagreement_handling vs evaluator Phase 6a emission).
- Phase 4a / 6a output lint (writer 3 / evaluator 5) + Phase 4b / 6b
output lint (writer 4 / evaluator 5) + lint count summary table
including the v3.6.2 reviewer 5+6 zero-touch baseline.
- Single-agent generator unusable handling (GENERATOR-PHASE-ABORTED tag
+ per-mode regression target + 5%/3-month operational monitor + Stage 3
entry path explicit non-membership).
- Cross-session resume scope (in-session atomic unit; v3.6.7+
pre_commitment_history forward note).
academic-paper/SKILL.md also adds a new ## Known limitations section
(per design doc §5.6 implementation-PR scope) carrying three forward
notes: no graceful-degradation fallback in v3.6.6 (v3.6.7 candidate);
no cross-session resume mid-round (v3.6.7+ pre_commitment_history
candidate); in-pair Phase 6 evaluator vs external academic-paper-reviewer
remain documented as known tech debt deferred to v3.7.x.
academic-paper/agents/draft_writer_agent.md adds a new
## v3.6.6 Generator-Evaluator Contract Protocol section (47 lines) at
end of file, containing the verbatim system-prompt sub-sections for
Phase 4a (writer paper-blind pre-commitment) + Phase 4b (writer
paper-visible drafting + self-scoring). Each sub-section names exactly
what user content the orchestrator injects, exactly what required
output sections + ordering the writer must produce, exactly what lint
constraints apply, and the abort tag for second-failure mark-unusable.
academic-paper/agents/peer_reviewer_agent.md adds a parallel new
## v3.6.6 Generator-Evaluator Contract Protocol section (57 lines) at
end of file, with verbatim system-prompt sub-sections for Phase 6a
(evaluator paper-blind pre-commitment with the writer's <phase4a_output>
visible per pre_commitment_check_protocol.check_writer_artifact) +
Phase 6b (evaluator paper-visible scoring + decision with both
<phase4a_output> and <phase6a_output> + writer Phase 4b draft visible).
Sub-sections explicitly distinguish the in-pair Phase 6 evaluator from
the standalone academic-paper-reviewer skill, name the two valid Stage 3
entry paths (standard F0/F4 + exceptional F5), and exclude
GENERATOR-PHASE-ABORTED from Stage 3 entry per design doc §5.1.
Track D — sprint_contract_protocol.md cross-ref:
Adds a single one-line cross-reference paragraph in the head matter of
academic-paper-reviewer/references/sprint_contract_protocol.md noting
that the reviewer protocol is byte-equivalent across v3.6.2 → v3.6.6
(zero-touch promise per §3.6) and pointing readers at academic-paper
SKILL.md + design doc §5 for the parallel generator-evaluator protocol.
Also notes Schema 13.1 since v3.6.6 in the schema reference line.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(v3.6.6): Track H — manifest CI lint script + workflow writer/evaluator loops
Track H — Manifest CI lint script + spec-consistency.yml workflow extension:
scripts/check_v3_6_6_ab_manifest.py (new) implements §7.5 manifest CI lint:
- Three rule families per spec body §7.5:
- Schema-shape checks per §6.2 (top-level required fields with declared
types; per-paper paper_id slug + role enum + paper_type non-empty +
topic_label non-empty + input_artefacts at least one populated +
baseline_output two required sub-fields with string paths;
treatment_output object shape with four required sub-fields when
present; paper_id uniqueness; aggregate role counts 6+1; paper-A
paper_type families exactly 3 each appearing twice; paper-A
judge_output_baseline required; paper-C must-have known_failure_mode
+ failure_evidence; paper-C must-not-have judge / metrics fields).
- Path-existence checks (declared-paths-exist subsumes mode-required;
every populated optional path also checked).
- Reverse-scan (every git-tracked file under tests/fixtures/v3.6.6-ab/
must be referenced from manifest, excluding manifest.yaml itself).
- Mode-conditional rules read manifest_lint_mode value: under
spec_branch mode, treatment_output / judge_output_treatment /
metrics_output / top-level summary_output are optional; under
implementation_pr mode, all four become required.
- Behaviour on malformed YAML / non-mapping top-level: exit 1 with
parse-error message identifying the file (mirrors
check_sprint_contract.py exit-1-on-file-error convention).
- CLI: --root <path> (default cwd), exit 0 on pass, exit 1 on any
rule violation. Violation messages list the offending rule + offending
path / value.
.github/workflows/spec-consistency.yml extension:
- The "Validate sprint contract templates" step now iterates writer +
evaluator template directories alongside the existing reviewer loop
(per §7.4 v3.6.6 wiring extension). Three for-loops each calling
check_sprint_contract.py against shared/contracts/{reviewer,writer,evaluator}/*.json
with --ars-version "v${ARS_VERSION}" extracted from CHANGELOG heading.
- New "Validate v3.6.6 A/B fixture manifest" step runs
scripts/check_v3_6_6_ab_manifest.py as an additional step inside the
existing spec-consistency job (chosen placement among the three §7.4
options — additional step in spec-consistency.yml, additional job in
same workflow, or new dedicated workflow file).
Verification (manual + against current stub):
- Manifest CI lint passes against the spec-PR-shipped stub state
(manifest_lint_mode=spec_branch, fixture_version 0.1.0, 30 git-tracked
files = 29 declared paths + manifest.yaml itself, no orphans).
- Negative tests run inline:
- Adding judge_output_baseline to paper-C entry → caught by
paper-C must-not-have rule + declared-path-not-exist.
- Adding extra_orphan_file.md under fixture dir → caught by
reverse-scan.
- All shipped contract templates validate cleanly under Schema 13.1
through the workflow's extended writer + evaluator loops.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(v3.6.6): Track I — version bump sweep to suite v3.6.8 + CHANGELOG entry
Version naming note: this release ships the v3.6.6 generator-evaluator
contract spec + implementation. The v3.6.6 work landed AFTER v3.6.7
(downstream-agent pattern protection) due to project sequencing — so
to keep CHANGELOG monotonic, the suite release is tagged v3.6.8 even
though the design doc internal naming retains v3.6.6 for the contract
gate version (writer_full / evaluator_full mode, Schema 13.1,
pre_commitment_artifacts + disagreement_handling schema fields).
Per feedback_version_bump_sweep_checklist.md 7 类地点:
1. CHANGELOG.md — new ## [3.6.8] - 2026-05-03 entry above ## [3.6.7]
with Added (12 categories) + Changed (suite version + pipeline
skill version) + Deferred (real fixture data populate + treatment
runs + manifest_lint_mode flip + ROADMAP §3.6.4 correction goes to
private ROADMAP.md not this repo).
2. .claude/CLAUDE.md — pipeline version cell v3.6.7 → v3.6.8 in
Skills Overview table; new ## v3.6.8 Key Additions section above
v3.6.7 with the same naming-note framing + 9 bullets summarising
Schema 13.1 + templates + orchestration + SKILL/agent block + SC-*
mode-gating + tests + manifest CI lint + workflow extension +
fixture stub + sprint_contract_protocol cross-ref; Suite version
3.6.7 → 3.6.8; Last Updated 2026-04-30 → 2026-05-03.
3. README.md — version badge v3.6.7 → v3.6.8; new
### v3.6.8 (2026-05-03) Changelog section above v3.6.7 with
naming-note framing + 7 bullets.
4. README.zh-TW.md — same updates as README.md, in zh-TW.
5. MODE_REGISTRY.md — Last updated v3.6.7 → v3.6.8 / 2026-04-30 →
2026-05-03.
6. academic-pipeline/SKILL.md — metadata.version 3.6.7 → 3.6.8;
metadata.last_updated 2026-04-30 → 2026-05-03.
7. scripts/check_spec_consistency.py — 8 hardcoded version anchors
updated to v3.6.8 + 2026-05-03; v3.6.7 anchor RE-ADDED in both
English and zh-TW README anchor lists since v3.6.7 README entry
stays present alongside the new v3.6.8 entry.
8. shared/contracts/README.md — "Spec-branch status (v3.6.6 work in
progress)" block REMOVED since the spec branch was merged in PR #58
(commit 61fd9be). The "Shipped templates (v3.6.2)" section
reorganised into a single "Shipped templates" section listing
v3.6.2 (reviewer family) + v3.6.6 / suite v3.6.8 (generator-evaluator
family with full writer + evaluator template enumeration including
dimension counts + F-condition counts + Schema 13.1 allOf branch
references).
ROADMAP §3.6.4 description correction (per design doc §9.3) is the
8th item in feedback_version_bump_sweep_checklist.md but ROADMAP.md
is gitignored (lives in private claude-memory-sync repo). Recorded in
the Deferred section of the CHANGELOG entry above; out-of-scope for
this repo PR.
All 4 lint scripts green at this commit:
- check_spec_consistency.py PASS
- check_version_consistency.py PASS
- check_v3_6_6_ab_manifest.py PASS
- 71 sprint contract unit tests PASS
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
365 lines
15 KiB
Python
Executable File
365 lines
15 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Enforce the v3.6.6 A/B evidence fixture manifest contract per §6.2 schema +
|
|
§6.5 git-tracked invariants per design doc 2026-04-27-ars-v3.6.6-generator-evaluator-contract-design.md.
|
|
|
|
Three rule families per spec body §7.5:
|
|
|
|
1. Schema-shape checks: top-level fields exist with declared types;
|
|
per-paper required fields present; paper-A vs paper-C asymmetric rules.
|
|
|
|
2. Path-existence checks (mode-conditional + populated-optional):
|
|
- Required-paths-exist: every required-under-current-mode path declared in
|
|
the manifest must exist git-tracked.
|
|
- Declared-paths-exist: any populated optional path must also exist.
|
|
- Reverse-scan: every git-tracked file under tests/fixtures/v3.6.6-ab/
|
|
must be referenced from the manifest (no orphans).
|
|
|
|
3. Behaviour on malformed input: exit 1 with parse-error message
|
|
identifying the file (mirrors check_sprint_contract.py convention).
|
|
|
|
Exit code: 0 on pass, 1 on any rule violation. CLI: `--root <path>` (default `.`).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
FIXTURE_DIR = "tests/fixtures/v3.6.6-ab"
|
|
MANIFEST_RELPATH = "manifest.yaml"
|
|
SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+$")
|
|
LINT_MODES = {"spec_branch", "implementation_pr"}
|
|
PAPER_ROLES = {"paper-A", "paper-C"}
|
|
INPUT_ARTEFACT_FIELDS = {
|
|
"paper_configuration_record",
|
|
"paper_outline",
|
|
"argument_blueprint",
|
|
"annotated_bibliography",
|
|
"style_profile",
|
|
"knowledge_isolation_directive",
|
|
}
|
|
TREATMENT_SUBFIELDS = {
|
|
"phase4a_output",
|
|
"phase4b_output",
|
|
"phase6a_output",
|
|
"phase6b_output",
|
|
}
|
|
|
|
|
|
def _load_manifest(manifest_path: Path) -> dict | None:
|
|
"""Load YAML manifest. Return parsed dict, or None on parse failure
|
|
(caller emits the parse error)."""
|
|
try:
|
|
with manifest_path.open("r", encoding="utf-8") as f:
|
|
data = yaml.safe_load(f)
|
|
except (FileNotFoundError, yaml.YAMLError) as exc:
|
|
print(
|
|
f"ERROR: failed to load manifest at {manifest_path}: {exc}",
|
|
file=sys.stderr,
|
|
)
|
|
return None
|
|
if not isinstance(data, dict):
|
|
print(
|
|
f"ERROR: manifest at {manifest_path} is not a top-level YAML mapping "
|
|
f"(got {type(data).__name__})",
|
|
file=sys.stderr,
|
|
)
|
|
return None
|
|
return data
|
|
|
|
|
|
def _check_schema_shape(manifest: dict) -> list[str]:
|
|
"""Schema-shape checks per §6.2 + §7.5. Returns list of error messages."""
|
|
errors: list[str] = []
|
|
|
|
# Top-level required fields
|
|
fixture_version = manifest.get("fixture_version")
|
|
if not isinstance(fixture_version, str) or not SEMVER_RE.match(fixture_version):
|
|
errors.append(
|
|
f"fixture_version must match semver regex {SEMVER_RE.pattern}; "
|
|
f"got {fixture_version!r}"
|
|
)
|
|
mode = manifest.get("manifest_lint_mode")
|
|
if mode not in LINT_MODES:
|
|
errors.append(
|
|
f"manifest_lint_mode must be one of {sorted(LINT_MODES)}; got {mode!r}"
|
|
)
|
|
docs = manifest.get("documentation_paths")
|
|
if not isinstance(docs, list) or len(docs) == 0:
|
|
errors.append(
|
|
"documentation_paths must be a non-empty array (must contain at least the spec-PR-shipped README.md)"
|
|
)
|
|
elif not all(isinstance(p, str) for p in docs):
|
|
errors.append("documentation_paths entries must all be strings (relative paths)")
|
|
papers = manifest.get("papers")
|
|
if not isinstance(papers, list):
|
|
errors.append("papers must be an array")
|
|
return errors # cannot continue per-paper checks
|
|
if len(papers) != 7:
|
|
errors.append(f"papers must contain exactly 7 entries; got {len(papers)}")
|
|
|
|
# summary_output type check (mode-conditional requirement handled in path-existence)
|
|
summary_output = manifest.get("summary_output")
|
|
if summary_output is not None and not isinstance(summary_output, str):
|
|
errors.append(
|
|
f"summary_output (when present) must be a string relative path; got {type(summary_output).__name__}"
|
|
)
|
|
if mode == "implementation_pr" and summary_output is None:
|
|
errors.append("summary_output is required under manifest_lint_mode=implementation_pr")
|
|
|
|
# Per-paper checks
|
|
paper_a_count = 0
|
|
paper_c_count = 0
|
|
paper_a_types: dict[str, int] = {}
|
|
seen_paper_ids: set[str] = set()
|
|
for i, p in enumerate(papers):
|
|
if not isinstance(p, dict):
|
|
errors.append(f"papers[{i}] must be an object")
|
|
continue
|
|
ctx = f"papers[{i}]"
|
|
# paper_id
|
|
pid = p.get("paper_id")
|
|
if not isinstance(pid, str) or not pid:
|
|
errors.append(f"{ctx}.paper_id must be a non-empty string slug")
|
|
else:
|
|
if pid in seen_paper_ids:
|
|
errors.append(f"{ctx}.paper_id={pid!r} is not unique across papers[]")
|
|
seen_paper_ids.add(pid)
|
|
ctx = f"papers[{pid}]"
|
|
# role
|
|
role = p.get("role")
|
|
if role not in PAPER_ROLES:
|
|
errors.append(f"{ctx}.role must be one of {sorted(PAPER_ROLES)}; got {role!r}")
|
|
elif role == "paper-A":
|
|
paper_a_count += 1
|
|
elif role == "paper-C":
|
|
paper_c_count += 1
|
|
# paper_type + topic_label required strings
|
|
for field in ("paper_type", "topic_label"):
|
|
val = p.get(field)
|
|
if not isinstance(val, str) or not val:
|
|
errors.append(f"{ctx}.{field} must be a non-empty string")
|
|
if role == "paper-A":
|
|
pt = p.get("paper_type")
|
|
if isinstance(pt, str):
|
|
paper_a_types[pt] = paper_a_types.get(pt, 0) + 1
|
|
# input_artefacts
|
|
ia = p.get("input_artefacts")
|
|
if not isinstance(ia, dict) or not any(
|
|
ia.get(f) for f in INPUT_ARTEFACT_FIELDS
|
|
):
|
|
errors.append(
|
|
f"{ctx}.input_artefacts must be an object with at least one populated sub-field "
|
|
f"from {sorted(INPUT_ARTEFACT_FIELDS)}"
|
|
)
|
|
elif not all(
|
|
isinstance(v, str) and v for k, v in ia.items() if k in INPUT_ARTEFACT_FIELDS
|
|
):
|
|
errors.append(
|
|
f"{ctx}.input_artefacts populated sub-fields must be non-empty string paths"
|
|
)
|
|
# baseline_output (object with two required sub-fields, both string paths)
|
|
bo = p.get("baseline_output")
|
|
if not isinstance(bo, dict):
|
|
errors.append(f"{ctx}.baseline_output must be an object")
|
|
else:
|
|
for sub in ("writer_draft", "evaluator_review"):
|
|
v = bo.get(sub)
|
|
if not isinstance(v, str) or not v:
|
|
errors.append(
|
|
f"{ctx}.baseline_output.{sub} must be a non-empty string path"
|
|
)
|
|
# treatment_output (object with four sub-fields when present)
|
|
to = p.get("treatment_output")
|
|
if to is not None:
|
|
if not isinstance(to, dict):
|
|
errors.append(f"{ctx}.treatment_output (when present) must be an object")
|
|
else:
|
|
for sub in TREATMENT_SUBFIELDS:
|
|
v = to.get(sub)
|
|
if v is not None and (not isinstance(v, str) or not v):
|
|
errors.append(
|
|
f"{ctx}.treatment_output.{sub} (when present) must be a non-empty string path"
|
|
)
|
|
# implementation_pr mode: all four sub-fields required
|
|
if mode == "implementation_pr":
|
|
for sub in TREATMENT_SUBFIELDS:
|
|
if to.get(sub) is None:
|
|
errors.append(
|
|
f"{ctx}.treatment_output.{sub} is required under manifest_lint_mode=implementation_pr"
|
|
)
|
|
elif mode == "implementation_pr":
|
|
errors.append(
|
|
f"{ctx}.treatment_output (object with phase4a/4b/6a/6b sub-fields) is required under manifest_lint_mode=implementation_pr"
|
|
)
|
|
# role-conditional fields
|
|
if role == "paper-A":
|
|
judge_baseline = p.get("judge_output_baseline")
|
|
if not isinstance(judge_baseline, str) or not judge_baseline:
|
|
errors.append(
|
|
f"{ctx}.judge_output_baseline is required for paper-A entries (non-empty string path)"
|
|
)
|
|
judge_treatment = p.get("judge_output_treatment")
|
|
if mode == "implementation_pr":
|
|
if not isinstance(judge_treatment, str) or not judge_treatment:
|
|
errors.append(
|
|
f"{ctx}.judge_output_treatment is required for paper-A entries under manifest_lint_mode=implementation_pr"
|
|
)
|
|
elif judge_treatment is not None and (
|
|
not isinstance(judge_treatment, str) or not judge_treatment
|
|
):
|
|
errors.append(
|
|
f"{ctx}.judge_output_treatment (when present) must be a non-empty string path"
|
|
)
|
|
metrics_output = p.get("metrics_output")
|
|
if mode == "implementation_pr":
|
|
if not isinstance(metrics_output, str) or not metrics_output:
|
|
errors.append(
|
|
f"{ctx}.metrics_output is required for paper-A entries under manifest_lint_mode=implementation_pr"
|
|
)
|
|
elif metrics_output is not None and (
|
|
not isinstance(metrics_output, str) or not metrics_output
|
|
):
|
|
errors.append(
|
|
f"{ctx}.metrics_output (when present) must be a non-empty string path"
|
|
)
|
|
elif role == "paper-C":
|
|
# paper-C must-not-have rules
|
|
for forbidden in ("judge_output_baseline", "judge_output_treatment", "metrics_output"):
|
|
if forbidden in p:
|
|
errors.append(
|
|
f"{ctx}.{forbidden} must NOT be present on paper-C entries (paper-C is categorical and exempt from codex H1/H3 judge + per-paper metrics)"
|
|
)
|
|
# paper-C must-have rules
|
|
kfm = p.get("known_failure_mode")
|
|
if not isinstance(kfm, str) or not kfm:
|
|
errors.append(f"{ctx}.known_failure_mode is required for paper-C entries (non-empty string)")
|
|
fe = p.get("failure_evidence")
|
|
if not isinstance(fe, str) or not fe:
|
|
errors.append(f"{ctx}.failure_evidence is required for paper-C entries (non-empty string path)")
|
|
|
|
# Aggregate role + paper-type counts
|
|
if paper_a_count != 6:
|
|
errors.append(f"len([p for p in papers if p.role == 'paper-A']) must == 6; got {paper_a_count}")
|
|
if paper_c_count != 1:
|
|
errors.append(f"len([p for p in papers if p.role == 'paper-C']) must == 1; got {paper_c_count}")
|
|
if len(paper_a_types) != 3:
|
|
errors.append(
|
|
f"paper-A entries must span exactly 3 paper_type families; got {sorted(paper_a_types)}"
|
|
)
|
|
for pt, count in paper_a_types.items():
|
|
if count != 2:
|
|
errors.append(
|
|
f"paper-A paper_type={pt!r} must appear exactly twice; got {count}"
|
|
)
|
|
|
|
return errors
|
|
|
|
|
|
def _collect_declared_paths(manifest: dict) -> set[str]:
|
|
"""All paths declared in the manifest. Used for both required + reverse-scan."""
|
|
declared: set[str] = set()
|
|
for p in manifest.get("documentation_paths", []) or []:
|
|
if isinstance(p, str):
|
|
declared.add(p)
|
|
summary = manifest.get("summary_output")
|
|
if isinstance(summary, str):
|
|
declared.add(summary)
|
|
for paper in manifest.get("papers", []) or []:
|
|
if not isinstance(paper, dict):
|
|
continue
|
|
ia = paper.get("input_artefacts") or {}
|
|
if isinstance(ia, dict):
|
|
for k, v in ia.items():
|
|
if k in INPUT_ARTEFACT_FIELDS and isinstance(v, str):
|
|
declared.add(v)
|
|
bo = paper.get("baseline_output") or {}
|
|
if isinstance(bo, dict):
|
|
for sub in ("writer_draft", "evaluator_review"):
|
|
v = bo.get(sub)
|
|
if isinstance(v, str):
|
|
declared.add(v)
|
|
to = paper.get("treatment_output") or {}
|
|
if isinstance(to, dict):
|
|
for sub in TREATMENT_SUBFIELDS:
|
|
v = to.get(sub)
|
|
if isinstance(v, str):
|
|
declared.add(v)
|
|
for field in ("judge_output_baseline", "judge_output_treatment", "metrics_output", "failure_evidence"):
|
|
v = paper.get(field)
|
|
if isinstance(v, str):
|
|
declared.add(v)
|
|
return declared
|
|
|
|
|
|
def _check_path_existence(manifest: dict, fixture_root: Path) -> list[str]:
|
|
"""Path-existence checks: every declared path must exist git-tracked.
|
|
Schema-shape already enforced mode-conditional required-vs-optional;
|
|
here we just check declared-paths-exist (which subsumes required-paths-exist
|
|
because all required paths are declared)."""
|
|
errors: list[str] = []
|
|
declared = _collect_declared_paths(manifest)
|
|
for rel in sorted(declared):
|
|
full = fixture_root / rel
|
|
if not full.exists():
|
|
errors.append(f"declared path does not exist git-tracked: {rel}")
|
|
return errors
|
|
|
|
|
|
def _check_reverse_scan(manifest: dict, fixture_root: Path) -> list[str]:
|
|
"""Reverse-scan: every file under fixture_root (except manifest.yaml itself)
|
|
must be referenced from the manifest. Orphans fail lint."""
|
|
errors: list[str] = []
|
|
declared = _collect_declared_paths(manifest)
|
|
for path in sorted(fixture_root.rglob("*")):
|
|
if not path.is_file():
|
|
continue
|
|
rel = str(path.relative_to(fixture_root))
|
|
if rel == MANIFEST_RELPATH:
|
|
continue
|
|
if rel not in declared:
|
|
errors.append(
|
|
f"fixture-orphan: {rel} is git-tracked under {fixture_root} "
|
|
f"but not referenced from manifest.yaml"
|
|
)
|
|
return errors
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument(
|
|
"--root",
|
|
type=Path,
|
|
default=Path("."),
|
|
help="Repo root (default: cwd)",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
fixture_root = args.root / FIXTURE_DIR
|
|
manifest_path = fixture_root / MANIFEST_RELPATH
|
|
|
|
manifest = _load_manifest(manifest_path)
|
|
if manifest is None:
|
|
return 1
|
|
|
|
errors: list[str] = []
|
|
errors.extend(_check_schema_shape(manifest))
|
|
errors.extend(_check_path_existence(manifest, fixture_root))
|
|
errors.extend(_check_reverse_scan(manifest, fixture_root))
|
|
|
|
if errors:
|
|
print("v3.6.6 A/B fixture manifest lint FAILED:", file=sys.stderr)
|
|
for e in errors:
|
|
print(f" - {e}", file=sys.stderr)
|
|
return 1
|
|
|
|
print(f"OK: {manifest_path} passes v3.6.6 A/B fixture manifest lint")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|