P1-fix5c: State both need_evidence forms in the findings output contract

Owner-forwarded Codex audit, finding 3. The rubric inserted into the plan
review prompt invites a question to the AUTHOR as a need_evidence whose breaks
names the spec id with no locator, and plan_spec.validate_findings accepts
exactly that shape, while the Output contract in the same prompt still read
"locator: REQUIRED for need_evidence". A reviewer had to guess between two
halves of one prompt, or invent a file to satisfy the stricter half.

The one contract line now states the two forms: the locator spelling for a
document request, and no locator for the question to the author, which names
the spec id in breaks. Behaviour and the validator are unchanged.

Test: tests/test_plan_spec_reviewer_questions.py
test_the_output_contract_states_both_need_evidence_forms, failing before this
commit, pins the assembled prompt (no absolute locator requirement, both forms
present) beside the validator result the auditor reproduced.

(cherry picked from commit 2a6e19ee1fb6baccdd036f09c6967cb8baad4728)
This commit is contained in:
Ouroboros 2026-09-12 12:34:52 +03:00
parent c5cd1e4479
commit fadd3f7c36
2 changed files with 24 additions and 1 deletions

View file

@ -570,7 +570,7 @@ _PLAN_FINDING_ELEMENT_SCHEMA = """\
"id": "<short local id, e.g. f1>",
"class": "blocking" | "note" | "need_evidence",
"breaks": "<spec id — REQUIRED for blocking: goal | claim_N | invariant_N | decision_N | deferred_N>",
"locator": "<REQUIRED for need_evidence: an absolute path, or one relative to the subject workspace root; add ::lines=A-B, ::bytes=A-B, ::tail=N or ::symbol=Name (.py only) for one range; task:<id> = a prior task's result; a URL may be named; the host never fetches it>",
"locator": "<for a need_evidence DOCUMENT request: an absolute path, or one relative to the subject workspace root; add ::lines=A-B, ::bytes=A-B, ::tail=N or ::symbol=Name (.py only) for one range; task:<id> = a prior task's result; a URL may be named; the host never fetches it. Leave it out for the other form: a need_evidence that asks the AUTHOR a question names the spec id in `breaks` and needs no locator>",
"summary": "<what is wrong or missing, concretely>",
"recommendation": "<the smallest change to the SPEC that resolves it>"
}"""

View file

@ -57,3 +57,26 @@ def test_a_question_only_wave_is_review_required_and_never_earns_a_paid_delta_cy
# question wave and a paid panel bought by rejecting the question; pinned here.
assert plan_spec.blocking_fully_rejected(
agg["findings"], [{"finding_id": "s1:q1", "decision": "reject", "rationale": "not needed"}]) is False
def test_the_output_contract_states_both_need_evidence_forms():
"""Owner-forwarded audit, finding 3: the rubric invites a question to the author with
the spec id in ``breaks`` and no locator, and the validator accepts exactly that, while
the Output contract inserted into the same prompt still demanded a locator for every
``need_evidence``. The contract must state the two forms instead of making the reviewer
guess between its own halves or invent a file."""
from ouroboros.tools.plan_packet import build_plan_review_system_prompt
prompt = build_plan_review_system_prompt(
checklist_section="", constitutional=False, bible_text=None,
cycle_index=1, enforcement="blocking")
assert "no locator needed" in prompt # the rubric's question-to-the-author form
assert "REQUIRED for need_evidence" not in prompt
assert "for a need_evidence DOCUMENT request" in plan_spec.PLAN_FINDINGS_ARRAY_CONTRACT
assert ("a need_evidence that asks the AUTHOR a question names the spec id in `breaks` "
"and needs no locator") in plan_spec.PLAN_FINDINGS_ARRAY_CONTRACT
assert plan_spec.PLAN_FINDINGS_ARRAY_CONTRACT in prompt
# The validator already accepts the question form; the contract now says so.
normalized, disclosures, _seen = _validate(
[{"id": "q1", "class": "need_evidence", "breaks": "goal", "summary": "Which goal matters more?"}])
assert normalized[0]["class"] == "need_evidence" and not disclosures