Skip to content

refactor(shared): four modules reinvent the success/not-found/error triple as ad-hoc dicts #17740

Description

@mrveiss

Problem

Four service modules have independently invented the same three-state result and expressed it as a
string-keyed dict with locally chosen key names:

autobot-backend/agents/knowledge_retrieval_agent.py
autobot-backend/api/knowledge_population.py
autobot-backend/api/vnc_manager.py
autobot-backend/services/kb_watch_ingest.py

Measured on origin/main: 8 non-test files return "status": "not_found"; 4 of those also return
"status": "error", which is the shape this issue is about. The fourth is mine, added in #17546 and
#17533 — this is a pattern I reproduced, not one I only noticed.

The triple is always the same distinction, and it is always the one that matters:

  • success — the operation did what was asked
  • not_found — the lookup ran and the thing is not there
  • error — the lookup did not run, so nothing is known

Collapsing the last two is a recurring defect class in this repo, not a style question.
search_by_metadata returns fact_ids: [] for both a genuine miss and an internal failure,
separated only by status; a caller reading the list reports "nothing to remove" for a lookup that
never happened. #17546 fixed one instance of that; nothing stops the next one.

What a shared type would give

llc/services/workspace_disposal.py already models this properly, as a typed enum plus a
NamedTuple carrying the evidence:

class DisposalVerdict(str, Enum):
    LANDED = "landed"
    NOT_LANDED = "not_landed"
    UNKNOWN = "unknown"

with a may_dispose property so both refusals refuse, and the reason they are kept apart written
where a future simplifier will read it. That shape is worth having once, not four times:

  • a caller cannot read the wrong key, because there are no string keys
  • not_found and error cannot be compared to the same falsy value
  • the "could not look" case carries its detail, so a refusal is actionable
  • adding a state is one edit, not four

Acceptance criteria

  • A shared outcome type in autobot_shared/ expressing success / not-found / error with a
    mandatory detail on the two non-success states.
  • The four sites above use it, or are documented as genuinely different (an HTTP response shape
    is not the same thing as a service return, and some of these may be the former — establish
    which before converting).
  • A test asserts a failed lookup and a clean miss are distinguishable without reading a string.
  • The test is mutation-checked: collapse the two states and it must fail.

Not in scope

The other 4 files that use "status": "not_found" without an "error" twin. They may be HTTP
response bodies, where a status string is the correct idiom. Converting them because they matched a
grep would be the same error this issue is about — acting on a match rather than on the question.

Related

Found while implementing #16818, #17546 and #17533. The DisposalVerdict reference implementation
is in PR #17725.

Activity

  1. mrveiss commented on Sep 28, 2026

    @mrveiss
    OwnerAuthor

    B10 spec — the API returning a shape that loses a distinction the caller needs

    Batch B10 = #17694 (self-contained, first) → #17740 (four modules, quieter slot). Full spec at ~/b10-spec-2026-09-28.md.

    #17694 — two defects, and the first is a familiar shape

    llc/api/sprints.py:698 — body.model_dump(exclude_none=True) drops every None, so "set this field to null" and "do not change this field" are the same request. One field, two meanings — the same shape as role_detector.py:114 (#17742) and the or 0.0 class (#17660).
    Change: exclude_unset=True, which is the distinction Pydantic already tracks and exclude_none throws away. Second defect: validate the status in the request model so FastAPI answers 422 before the handler runs, instead of a 500 from inside the session.
    Assertions: an explicit null clears · an omitted field is unchanged · an invalid status is 422 naming the field.
    Mutations: revert to exclude_none=True → the clears case reddens while unchanged stays green, which is what makes the pair necessary; remove the enum validation → 500 returns.
    Do not lose the IDOR guard at :696-697 (#10148) — a rewrite of the dump loop is exactly where it gets dropped.
    Test home: llc/tests/ already has test_sprints_idor.py and test_sprints_enrichment.py; a new test_sprints_patch_semantics_17694.py keeps both concerns together.

    #17740 — and the trap is that a shared type nobody adopts is a fifth vocabulary

    Four modules, measured in the issue itself: 8 non-test files return "status": "not_found", 4 of those also return "status": "error" — the second set is the subject. Credit where the issue gives it: one of the four is its author's own, from #17546/#17533 — "a pattern I reproduced, not one I only noticed."

    Change: define the triple once in autobot_shared/ as a typed result, error carrying its reason, and convert all four call sites. Convert all four, or convert one and record why the others are not migrated — adding a type and hoping produces a fifth vocabulary, which is #17693's warning.
    Mutation: collapse not-found into error in the shared type → the distinguishability tests redden; revert one module to its dict → only that module's test reddens, which proves the conversion is asserted per call site rather than in aggregate.
    Scope boundary to state in the PR: the other 4 of the 8 not_found returners are not in scope — they do not also return "error", so they are not instances. Say so, or the next sweep counts 8 and reads the PR as half-finished.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions