mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(gateway): carry intake technical depth down the chain + widen review coherence scope (#491)
Two structural issues flagged by the CEO:
1. Task technical-depth dilution — intake's rich analysis (file:line
targets, code examples, rationale) was getting lost as it traveled
umbrella -> root-subtask -> cell -> dev. The detail IS preserved in
Task.description; the dilution was in delegation (PMs re-authoring)
and the intake prompt not demanding depth.
Fixes:
- evidence_repo: ancestor_context_for_task walks the parent chain
(cycle-guarded, depth-capped 16, desc-clipped 1500) and surfaces it
as parent_context in the evidence payload, so a leaf dev finally
sees the upstream intake analysis instead of a bare title.
- evidence_builder: Task.description now rides in the payload;
EvidencePayload gains description + parent_context (omit-when-empty
so no null noise).
- orchestrator: _description_body (capped 4000) injects the
description into the dev spawn prompt + SessionStart briefing.
- role prompts (main_pm/cell_pm/developer/prompter): teach pass-the-
torch, don't-dim-it; prompter now demands file:line/code-examples
in the_work/notes (reconciled with the no-code-level-ACs-on-roots
rule). main_pm's brief-not-a-spec scoped: not-a-spec applies to the
solution only, facts forward verbatim.
2. PR-review/QA scope too narrow — they only checked the AC checklist,
not whether the change is coherent with project structure/intent.
Fixes:
- qa.md + pr_reviewer.md: Coherence & intent rule (intent via
description+parent_context, coherence with project patterns,
standards). Criterion-less major findings allowed for intent drift
(Finding.criterion is optional).
- parent_context + description wired into the gate/QA/inbound-PR
evidence builders (fail-open, logged).
Skipped per YAGNI: a technical_spec JSONB column (detail is already in
description) and a criterion_kind enum (criterion is already optional).
All gates green: ruff, mypy (1152), pytest (12883 passed, 94.82% cov),
xenon, vulture, bandit, pip-audit, deptry, alembic, import-linter,
foundation-check.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
@@ -13,7 +13,25 @@ from unittest.mock import AsyncMock, MagicMock
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from roboco.services.gateway.evidence_repo import EvidenceRepo
|
||||
from roboco.services.gateway.evidence_repo import (
|
||||
_ANCESTOR_DESC_CAP,
|
||||
_HIERARCHY_DEPTH_CAP,
|
||||
EvidenceRepo,
|
||||
)
|
||||
|
||||
|
||||
def _scalar_result(value: object) -> MagicMock:
|
||||
"""A query result whose ``scalar_one_or_none()`` returns ``value``."""
|
||||
r = MagicMock()
|
||||
r.scalar_one_or_none.return_value = value
|
||||
return r
|
||||
|
||||
|
||||
def _rows_result(rows: list[object]) -> MagicMock:
|
||||
"""A query result whose ``all()`` returns ``rows``."""
|
||||
r = MagicMock()
|
||||
r.all.return_value = rows
|
||||
return r
|
||||
|
||||
|
||||
def _empty_repo() -> EvidenceRepo:
|
||||
@@ -233,3 +251,130 @@ async def test_journal_highlights_for_task_maps_rows_with_author() -> None:
|
||||
"timestamp": ts.isoformat(),
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parent-chain walk + ancestor context (the intake-analysis torch carrier).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ancestor_task_ids_walks_parent_to_root() -> None:
|
||||
leaf, parent, grand = uuid4(), uuid4(), uuid4()
|
||||
db = MagicMock()
|
||||
db.execute = AsyncMock(
|
||||
side_effect=[
|
||||
_scalar_result(parent),
|
||||
_scalar_result(grand),
|
||||
_scalar_result(None), # grand is a root
|
||||
]
|
||||
)
|
||||
repo = EvidenceRepo(db)
|
||||
assert await repo._ancestor_task_ids(leaf) == [parent, grand]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ancestor_task_ids_cycle_guard_stops() -> None:
|
||||
leaf, parent = uuid4(), uuid4()
|
||||
db = MagicMock()
|
||||
# leaf -> parent -> leaf (cycles back to the start, already in `seen`).
|
||||
db.execute = AsyncMock(side_effect=[_scalar_result(parent), _scalar_result(leaf)])
|
||||
repo = EvidenceRepo(db)
|
||||
assert await repo._ancestor_task_ids(leaf) == [parent]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ancestor_task_ids_missing_row_returns_empty() -> None:
|
||||
db = MagicMock()
|
||||
db.execute = AsyncMock(side_effect=[_scalar_result(None)])
|
||||
repo = EvidenceRepo(db)
|
||||
assert await repo._ancestor_task_ids(uuid4()) == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ancestor_task_ids_depth_capped() -> None:
|
||||
start = uuid4()
|
||||
chain = [uuid4() for _ in range(_HIERARCHY_DEPTH_CAP + 5)]
|
||||
db = MagicMock()
|
||||
db.execute = AsyncMock(side_effect=[_scalar_result(p) for p in chain])
|
||||
repo = EvidenceRepo(db)
|
||||
result = await repo._ancestor_task_ids(start)
|
||||
assert len(result) == _HIERARCHY_DEPTH_CAP
|
||||
assert result == chain[:_HIERARCHY_DEPTH_CAP]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ancestor_context_for_task_parentless_returns_empty() -> None:
|
||||
db = MagicMock()
|
||||
db.execute = AsyncMock(side_effect=[_scalar_result(None)])
|
||||
repo = EvidenceRepo(db)
|
||||
assert await repo.ancestor_context_for_task(uuid4()) == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ancestor_context_for_task_maps_chain_with_depth() -> None:
|
||||
leaf, parent, grand = uuid4(), uuid4(), uuid4()
|
||||
row_p = SimpleNamespace(id=parent, title="Cell PM slice", description="p-desc")
|
||||
row_g = SimpleNamespace(id=grand, title="Root", description="g-desc")
|
||||
db = MagicMock()
|
||||
db.execute = AsyncMock(
|
||||
side_effect=[
|
||||
_scalar_result(parent),
|
||||
_scalar_result(grand),
|
||||
_scalar_result(None), # end of _ancestor_task_ids
|
||||
_rows_result([row_p, row_g]), # the batch fetch
|
||||
]
|
||||
)
|
||||
repo = EvidenceRepo(db)
|
||||
assert await repo.ancestor_context_for_task(leaf) == [
|
||||
{
|
||||
"task_id": str(parent),
|
||||
"depth": 1,
|
||||
"title": "Cell PM slice",
|
||||
"description": "p-desc",
|
||||
},
|
||||
{"task_id": str(grand), "depth": 2, "title": "Root", "description": "g-desc"},
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ancestor_context_for_task_skips_missing_ancestor_row() -> None:
|
||||
leaf, parent, grand = uuid4(), uuid4(), uuid4()
|
||||
row_p = SimpleNamespace(id=parent, title="Cell PM slice", description="p-desc")
|
||||
db = MagicMock()
|
||||
db.execute = AsyncMock(
|
||||
side_effect=[
|
||||
_scalar_result(parent),
|
||||
_scalar_result(grand),
|
||||
_scalar_result(None),
|
||||
_rows_result([row_p]), # grand's row is missing from the batch
|
||||
]
|
||||
)
|
||||
repo = EvidenceRepo(db)
|
||||
assert await repo.ancestor_context_for_task(leaf) == [
|
||||
{
|
||||
"task_id": str(parent),
|
||||
"depth": 1,
|
||||
"title": "Cell PM slice",
|
||||
"description": "p-desc",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ancestor_context_for_task_clips_long_description() -> None:
|
||||
leaf, parent = uuid4(), uuid4()
|
||||
row_p = SimpleNamespace(
|
||||
id=parent, title="P", description="x" * (_ANCESTOR_DESC_CAP + 500)
|
||||
)
|
||||
db = MagicMock()
|
||||
db.execute = AsyncMock(
|
||||
side_effect=[
|
||||
_scalar_result(parent),
|
||||
_scalar_result(None),
|
||||
_rows_result([row_p]),
|
||||
]
|
||||
)
|
||||
repo = EvidenceRepo(db)
|
||||
ctx = await repo.ancestor_context_for_task(leaf)
|
||||
assert len(ctx[0]["description"]) == _ANCESTOR_DESC_CAP
|
||||
|
||||
Reference in New Issue
Block a user