Skip to content
Closed
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
34 changes: 34 additions & 0 deletions .github/workflows/paper-contracts.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Paper contracts

on:
pull_request:
paths:
- "templates/default/ui/v2_*.py"
- "templates/default/schemas/**"
- "templates/default/research_trajectory/**/*.example.json"
- "tests/paper_contracts/**"
- ".github/workflows/paper-contracts.yml"
push:
branches: [main]
paths:
- "templates/default/ui/v2_*.py"
- "templates/default/schemas/**"
- "templates/default/research_trajectory/**/*.example.json"
- "tests/paper_contracts/**"
- ".github/workflows/paper-contracts.yml"
workflow_dispatch:

permissions:
contents: read

jobs:
contracts:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Run paper-facing contract tests
run: python -m unittest discover -s tests/paper_contracts -p 'test_*.py' -v
63 changes: 55 additions & 8 deletions templates/default/ui/v2_merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,31 @@ def _decoded_projection(projection: Mapping[str, Any]) -> dict[str, Any]:
}


def _canonical_card_status_sets(projection: Mapping[str, Any]) -> dict[str, set[str]]:
"""Return service-canonical card identities by effective findings status.

Merely mentioning a card ID elsewhere in canonical state (for example in a
material exclusion) does not make that card accepted evidence.
"""

decoded = _decoded_projection(projection)
findings = next(
(
value
for value in _projection_json_values(decoded)
if value.get("artifact_type") == "current_findings"
),
None,
)
if not findings:
return {"accepted": set(), "qualified": set(), "superseded": set()}
return {
"accepted": set(findings.get("accepted_card_ids", ())),
"qualified": set(findings.get("qualified_card_ids", ())),
"superseded": set(findings.get("superseded_card_ids", ())),
}


def _projection_errors(
before: Mapping[str, Any],
after: Mapping[str, Any],
Expand Down Expand Up @@ -332,7 +357,9 @@ def _projection_errors(
if removed := old_evidence - new_evidence:
errors.append(f"negative/conflicting evidence was removed: {sorted(removed)}")

old_known = _all_card_ids(before)
old_status = _canonical_card_status_sets(before)
old_effective = old_status["accepted"] | old_status["qualified"]
old_history = old_effective | old_status["superseded"]
accepted_now = {
str(item["card_id"])
for item in card_decisions
Expand All @@ -341,19 +368,19 @@ def _projection_errors(
cards_by_id = {
str(card.get("id")): card for card in _result_card_list(result_cards)
}
known = old_known | accepted_now
known_active = old_effective | accepted_now
referenced = _field_card_ids(
after, _CARD_REFERENCE_FIELDS - {"superseded_card_ids"}
)
if unknown := referenced - known:
if unknown := referenced - known_active:
errors.append(
f"effective projection references unknown/unaccepted cards: {sorted(unknown)}"
)

superseded = _field_card_ids(after, {"superseded_card_ids"})
# Recorded historical proposals may be corrected without promoting them
# to active evidence. Only the service supplies these verified identities.
if unknown := superseded - old_known - set(cards_by_id) - set(prior_card_ids):
# Receipt-verified deferred cards may be named only as supersession targets;
# an arbitrary historical mention does not establish a valid identity.
if unknown := superseded - old_history - set(cards_by_id) - set(prior_card_ids):
errors.append(
f"effective projection references unknown superseded cards: {sorted(unknown)}"
)
Expand All @@ -362,7 +389,8 @@ def _projection_errors(
for card_id in accepted_now
for prior_id in cards_by_id.get(card_id, {}).get("supersedes", ())
}
if unjustified := superseded - old_known - declared_superseded:
newly_superseded = superseded - old_status["superseded"]
if unjustified := newly_superseded - declared_superseded:
errors.append(
"effective projection marks cards superseded without an accepted "
f"superseding card: {sorted(unjustified)}"
Expand All @@ -381,6 +409,19 @@ def _projection_errors(
None,
)
if old_findings and findings:
old_effective_findings = (
set(old_findings.get("accepted_card_ids", ()))
| set(old_findings.get("qualified_card_ids", ()))
)
new_effective_findings = (
set(findings.get("accepted_card_ids", ()))
| set(findings.get("qualified_card_ids", ()))
)
if unauthorized := (new_effective_findings - old_effective_findings) - accepted_now:
errors.append(
"canonical findings promote cards without an accepted current decision: "
f"{sorted(unauthorized)}"
)
old_canonical = (
set(old_findings.get("accepted_card_ids", ()))
| set(old_findings.get("qualified_card_ids", ()))
Expand Down Expand Up @@ -544,7 +585,13 @@ def prepare_card_decisions(*, merge_request, result_cards, operations, candidate
f"Merge Request repeats card decisions: {sorted(duplicate_requests)}"
)
try:
old_card_ids = _all_card_ids(_decoded_projection(base_projection)) | set(prior_card_ids)
old_status = _canonical_card_status_sets(base_projection)
old_card_ids = (
old_status["accepted"]
| old_status["qualified"]
| old_status["superseded"]
| set(prior_card_ids)
)
except (TypeError, ValueError) as exc:
old_card_ids = set()
errors.append(str(exc))
Expand Down
36 changes: 36 additions & 0 deletions templates/default/ui/v2_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -2202,6 +2202,41 @@ def _preexecution_output_paths(self, trial_id: str, stage_id: str) -> list[str]:
)
return sorted(set(outputs))

def _validate_plan_critical_path_target(
self, plan: Mapping[str, Any]
) -> None:
"""Bind the Plan to one current service-derived Critical Path item.

The JSON schema can only require a non-empty identifier. This runtime
check supplies the relational invariant: the target must name an item
in the currently published project state for the current canonical
revision. Scientific move selection remains agent reasoning; this
check validates only the Plan-to-control-state reference.
"""

state = self._load_artifact(
"research_trajectory/STATE.json", "project_state"
)
if state.get("project_id") != self.project_id:
raise V2RuntimeError("published project state belongs to another project")
current_revision = self.transactions.current_revision()
if state.get("canonical_revision") != current_revision:
raise V2RuntimeError(
"published project state revision differs from the current canonical revision"
)
target = str(plan.get("critical_path_target") or "").strip()
current = {
str(item.get("id")): item
for item in state.get("critical_path", ())
if isinstance(item, Mapping) and item.get("status") != "complete"
}
if target not in current:
available = sorted(current)
raise V2RuntimeError(
"PLAN critical_path_target does not reference a current actionable "
f"Critical Path item: {target or '<empty>'}; available={available}"
)

def _plan_context(
self,
trial_id: str,
Expand All @@ -2216,6 +2251,7 @@ def _plan_context(
route_relative = f"{trial_root}/EXPERT_ROUTE.json"
review_relative = f"{trial_root}/reviews/PLAN_REVIEW.json"
plan = self._load_artifact(plan_relative, "plan", paired=True)
self._validate_plan_critical_path_target(plan)
route = self._load_artifact(route_relative, "expert_route", paired=True)
self._validate_expert_route(route, plan)
review = self._load_artifact(
Expand Down
20 changes: 20 additions & 0 deletions tests/paper_contracts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Paper contract tests

These tests expose a small, public set of software invariants that correspond to
claims in the CoAutoResearch system paper. They use synthetic artifacts and
synthetic passing reviewer outputs. They do **not** evaluate scientific quality,
autonomous-agent performance, or real reviewer judgments.

Run from the repository root:

```bash
python -m unittest discover -s tests/paper_contracts -p 'test_*.py' -v
```

The suite currently checks:

- historical mentions do not become accepted evidence merely by appearing in canonical state;
- superseded evidence cannot silently return as active support;
- one accepted Result Card may be composed into multiple Research Lines;
- an execution-approved Plan must target a current actionable Critical Path item;
- a publication smoke path binds Publish Receipt, transaction, and canonical revision.
Loading
Loading