mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
* feat(lifecycle): revision findings ledger — structured QA/PR/PM/CEO failure feedback, persisted and delivered down the chain Every bounce used to survive only as flattened prose: rounds overwrote each other in notes_structured, request_changes persisted nothing, two raw dev_notes appends were silently destroyed by the next handoff note, and the dev prompt pointed at fields (qa_notes via evidence(), pm_notes) the API never delivered. Agents re-interpreted and re-discovered every failure before they could start fixing it. - task_review_findings (migration 071, append-only): file/line/severity/ criterion(AC-id-validated)/expected/actual/fix/evidence per finding, with origin (qa|pr_gate|pm|ceo), round, and an open->addressed->verified lifecycle (waived reserved); new tasks.pm_notes + PmReviewContent give request_changes a structured home - producers: fail_review/pr_fail/request_changes take findings=[...] (prose issues shimmed+merged for one release, deprecation-logged); ceo_reject validates its reason (no 500), lands an origin=ceo finding, and bumps round+audit on branchless coordination roots; guardrails at the verb chokepoint (nudge >5, hard reject >10, field caps, traversal-safe file); the dev_notes data-loss appends are removed; new task.request_changes + task.ceo_reject audit events close rework attribution - delivery: qa_notes/pr_reviewer_notes/pm_notes carry the deterministic [F-id8] rendering; claim briefings, evidence(), the REVISION_REQUIRED spawn prompt, PM triage bounced-blocks, and A2A bodies deliver open findings; round-N+1 QA and gate reviewers get the full prior ledger; panel Findings tab + bounced-xN chip; metrics pm_rejects/ceo_rejects + findings counts; vault task notes render a Findings section (fail-open) - resolution closes for every origin: i_am_done and submit_up/submit_root take resolved_findings gated by FINDINGS_ADDRESSED (owner-gated so a stale non-owner PM can never mutate the ledger); pass_review/pr_pass/ complete verify-stamp same-transaction; ceo_approve stamps best-effort - 24 real-DB integration tests drive the full loop through the real choreographer; full suite 12856 green * docs: revision findings ledger sweep — CLAUDE.md, map, RAG corpus - CLAUDE.md: new ledger section + corrected request_changes row - docs/map/review-findings.md (new subsystem map) + surgical updates to task-service/pr-gate-review/metrics-observability/vault/panel maps - docs/rag: producers' findings contract across qa/pr-reviewer/developer/ cell-pm/main-pm/ceo role docs (the PM docs were missing request_changes entirely), verb references, and a new architecture/review-findings.md disambiguating ledger findings from convention findings * test(e2e): resubmit resolves the pr_fail finding per the ledger contract The scripted pr_fail revision loop resubmitted submit_up without resolved_findings — correctly rejected now that FINDINGS_ADDRESSED gates the PM resubmit verbs (green locally, red only in CI since the e2e suite skips without ROBOCO_E2E_SMOKE=1). The scripted PM now reads the open ledger row pr_fail persisted (new open_finding_ids arc helper) and resolves it on resubmit, asserting the open set drains — exercising the coordinator half of the new contract end to end. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
237 lines
7.9 KiB
Python
237 lines
7.9 KiB
Python
"""Tier 1 — tracing Requirement enum + check_requirements scaffolding."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
from roboco.foundation.policy import tracing
|
|
|
|
|
|
def test_requirement_enum_has_canonical_values() -> None:
|
|
"""Required-set vocabulary mirrors the pre-Phase-2 tracing_gate.py
|
|
PLUS the new pre-gateway parity additions."""
|
|
expected = {
|
|
"plan",
|
|
"commits>=1",
|
|
"pr_open",
|
|
"progress>=1",
|
|
"journal:reflect",
|
|
"journal:decision",
|
|
"journal:learning",
|
|
"journal:struggle",
|
|
"journal:note_at_claim",
|
|
"journal:decision_at_claim",
|
|
"journal:during_work>=1",
|
|
"acceptance_criteria_addressed",
|
|
"qa_notes>=min",
|
|
"qa_evidence_inspected",
|
|
"docs_notes>=min",
|
|
"docs_files_non_empty",
|
|
"self_verified",
|
|
"notes>=min",
|
|
"subtasks_terminal",
|
|
"dev_notes>=min",
|
|
"pr_reviewer_notes>=min",
|
|
"quick_context>=min",
|
|
"findings_addressed",
|
|
}
|
|
actual = {r.value for r in tracing.Requirement}
|
|
assert actual == expected, f"Requirement drift: {actual ^ expected}"
|
|
|
|
|
|
def test_gate_context_has_journal_presence_flags() -> None:
|
|
ctx = tracing.GateContext()
|
|
assert ctx.journal_reflect_present is False
|
|
assert ctx.journal_decision_present is False
|
|
assert ctx.journal_learning_present is False
|
|
assert ctx.journal_struggle_present is False
|
|
assert ctx.journal_note_at_claim_present is False
|
|
assert ctx.journal_during_work_count == 0
|
|
|
|
|
|
def test_gate_result_has_passed_and_missing() -> None:
|
|
result = tracing.GateResult(passed=True)
|
|
assert result.passed is True
|
|
assert result.missing == []
|
|
|
|
|
|
def test_docs_notes_checker_reads_doc_notes_not_dev_notes() -> None:
|
|
"""Regression: _check_docs_notes_min_chars must read ``doc_notes`` (the
|
|
documenter's section), not ``dev_notes`` (the developer's)."""
|
|
ctx = tracing.GateContext(docs_notes_min_chars=20)
|
|
# A long dev_notes must NOT satisfy the docs requirement.
|
|
only_dev = SimpleNamespace(dev_notes="x" * 50, doc_notes="")
|
|
assert tracing._check_docs_notes_min_chars(only_dev, ctx) == ["docs_notes>=min"]
|
|
# A long doc_notes satisfies it.
|
|
has_doc = SimpleNamespace(dev_notes="", doc_notes="y" * 25)
|
|
assert tracing._check_docs_notes_min_chars(has_doc, ctx) == []
|
|
|
|
|
|
def test_note_section_checkers_read_their_own_fields() -> None:
|
|
ctx = tracing.GateContext() # defaults: dev 40, pr_reviewer 40, quick_context 30
|
|
assert (
|
|
tracing._check_dev_notes_min_chars(SimpleNamespace(dev_notes="z" * 40), ctx)
|
|
== []
|
|
)
|
|
assert tracing._check_dev_notes_min_chars(
|
|
SimpleNamespace(dev_notes="z" * 39), ctx
|
|
) == ["dev_notes>=min"]
|
|
assert (
|
|
tracing._check_pr_reviewer_notes_min_chars(
|
|
SimpleNamespace(pr_reviewer_notes="z" * 40), ctx
|
|
)
|
|
== []
|
|
)
|
|
assert tracing._check_quick_context_min_chars(
|
|
SimpleNamespace(quick_context="z" * 29), ctx
|
|
) == ["quick_context>=min"]
|
|
|
|
|
|
def test_note_section_obligations_wired_to_verbs() -> None:
|
|
assert tracing.Requirement.DEV_NOTES_MIN_CHARS in tracing.requirements_for(
|
|
"i_am_done"
|
|
)
|
|
assert tracing.Requirement.QUICK_CONTEXT_MIN_CHARS in tracing.requirements_for(
|
|
"delegate"
|
|
)
|
|
for verb in ("pr_pass", "pr_fail", "post_pr_review"):
|
|
assert tracing.Requirement.PR_REVIEWER_NOTES_MIN_CHARS in (
|
|
tracing.requirements_for(verb)
|
|
)
|
|
|
|
|
|
def test_check_requirements_passes_when_all_satisfied() -> None:
|
|
task = SimpleNamespace(
|
|
plan={"x": 1},
|
|
commits=[{"sha": "abc"}],
|
|
pr_number=42,
|
|
progress_updates=[{"message": "x"}],
|
|
acceptance_criteria=[],
|
|
acceptance_criteria_status=[],
|
|
)
|
|
ctx = tracing.GateContext(journal_reflect_present=True)
|
|
result = tracing.check_requirements(
|
|
task=task,
|
|
requirements=[
|
|
tracing.Requirement.PLAN,
|
|
tracing.Requirement.COMMITS_AT_LEAST_ONE,
|
|
tracing.Requirement.PR_OPEN,
|
|
tracing.Requirement.JOURNAL_REFLECT,
|
|
],
|
|
ctx=ctx,
|
|
)
|
|
assert result.passed is True
|
|
|
|
|
|
def test_check_requirements_returns_missing_keys_on_failure() -> None:
|
|
task = SimpleNamespace(
|
|
plan=None,
|
|
commits=[],
|
|
pr_number=None,
|
|
progress_updates=[],
|
|
acceptance_criteria=[],
|
|
acceptance_criteria_status=[],
|
|
)
|
|
result = tracing.check_requirements(
|
|
task=task,
|
|
requirements=[
|
|
tracing.Requirement.PLAN,
|
|
tracing.Requirement.COMMITS_AT_LEAST_ONE,
|
|
tracing.Requirement.PR_OPEN,
|
|
tracing.Requirement.JOURNAL_REFLECT,
|
|
],
|
|
)
|
|
assert result.passed is False
|
|
assert "plan" in result.missing
|
|
assert "commits>=1" in result.missing
|
|
assert "pr_open" in result.missing
|
|
assert "journal:reflect" in result.missing
|
|
|
|
|
|
def test_acceptance_criteria_check_treats_reflect_note_as_addressing_artifact() -> None:
|
|
"""Spec §9 item 1: reflect-note clears the criteria gate."""
|
|
task = SimpleNamespace(
|
|
acceptance_criteria=["AC1", "AC2"],
|
|
acceptance_criteria_status=[],
|
|
)
|
|
ctx = tracing.GateContext(journal_reflect_present=True)
|
|
result = tracing.check_requirements(
|
|
task=task,
|
|
requirements=[tracing.Requirement.ACCEPTANCE_CRITERIA_ADDRESSED],
|
|
ctx=ctx,
|
|
)
|
|
assert result.passed is True
|
|
|
|
|
|
def test_during_work_count_satisfies_requirement() -> None:
|
|
task = SimpleNamespace()
|
|
ctx = tracing.GateContext(journal_during_work_count=1)
|
|
result = tracing.check_requirements(
|
|
task=task,
|
|
requirements=[tracing.Requirement.JOURNAL_DURING_WORK_AT_LEAST_ONE],
|
|
ctx=ctx,
|
|
)
|
|
assert result.passed is True
|
|
|
|
|
|
def test_verb_requirements_covers_pm_decision_chain() -> None:
|
|
"""The 6 inline journal:decision callsites' verbs all require it."""
|
|
for verb in (
|
|
"submit_up",
|
|
"complete",
|
|
"unblock",
|
|
"escalate_up",
|
|
"escalate_to_ceo",
|
|
"delegate",
|
|
):
|
|
reqs = tracing.requirements_for(verb)
|
|
assert tracing.Requirement.JOURNAL_DECISION in reqs, (
|
|
f"{verb} should require journal:decision per spec §11"
|
|
)
|
|
|
|
|
|
def test_verb_requirements_covers_dev_completion_chain() -> None:
|
|
reqs = tracing.requirements_for("i_am_done")
|
|
assert tracing.Requirement.COMMITS_AT_LEAST_ONE in reqs
|
|
assert tracing.Requirement.PR_OPEN in reqs
|
|
assert tracing.Requirement.PROGRESS_AT_LEAST_ONE in reqs
|
|
assert tracing.Requirement.JOURNAL_REFLECT in reqs
|
|
assert tracing.Requirement.JOURNAL_DURING_WORK_AT_LEAST_ONE in reqs
|
|
assert tracing.Requirement.ACCEPTANCE_CRITERIA_ADDRESSED in reqs
|
|
|
|
|
|
def test_verb_requirements_includes_pre_gateway_parity_at_claim() -> None:
|
|
assert tracing.Requirement.JOURNAL_NOTE_AT_CLAIM in tracing.requirements_for(
|
|
"i_will_work_on"
|
|
)
|
|
assert tracing.Requirement.JOURNAL_DECISION_AT_CLAIM in tracing.requirements_for(
|
|
"i_will_plan"
|
|
)
|
|
|
|
|
|
def test_pm_complete_requires_both_decision_and_reflect() -> None:
|
|
"""Pre-gateway parity P4: PMs wrote both before complete."""
|
|
reqs = tracing.requirements_for("complete")
|
|
assert tracing.Requirement.JOURNAL_DECISION in reqs
|
|
assert tracing.Requirement.JOURNAL_REFLECT in reqs
|
|
|
|
|
|
def test_qa_pass_review_requires_learning() -> None:
|
|
reqs = tracing.requirements_for("pass_review")
|
|
assert tracing.Requirement.QA_NOTES_MIN_CHARS in reqs
|
|
assert tracing.Requirement.QA_EVIDENCE_INSPECTED in reqs
|
|
assert tracing.Requirement.JOURNAL_LEARNING in reqs
|
|
|
|
|
|
def test_i_am_blocked_requires_struggle_journal() -> None:
|
|
"""Lifts JOURNAL_STRUGGLE out of dangling-enum status."""
|
|
assert tracing.Requirement.JOURNAL_STRUGGLE in tracing.requirements_for(
|
|
"i_am_blocked"
|
|
)
|
|
|
|
|
|
def test_requirements_for_unknown_verb_raises_key_error() -> None:
|
|
with pytest.raises(KeyError):
|
|
tracing.requirements_for("not_a_real_verb")
|