Generalizes the lesson from the iter_image_url_values fail-open regression (code_reviews/pr-3-capability-gate-followups-review.md): a plan that proposes collapsing N implementations into one shared helper has to specify what it yields on every input shape, including the "well-typed but unparseable" case that the N originals never had to agree on explicitly. Gives a contract-table template for future plans to use, and a watch list of dispatcher.py traversals that aren't currently merged/broken but would need the same treatment if they ever are. Starts code_plans/ as where forward-looking spec/plan reports go, alongside code_reviews/ for after-the-fact review reports. No code changes.
141 lines
7.1 KiB
Markdown
141 lines
7.1 KiB
Markdown
# Spec + implementation plan: pin down what a shared helper yields on well-typed-but-unparseable input
|
|
|
|
**Origin.** PR #3 collapsed four independent `messages` → `image_url` part
|
|
traversals into one generator, `capabilities.iter_image_url_values`
|
|
(`code_reviews/capability-gate-followups.md`, item #6). The plan specified
|
|
the two shapes the four originals agreed on (`image_url` as a dict with
|
|
`url`, or as a bare string) but not the case where a part is typed
|
|
`image_url` and carries neither — well-typed, but nothing to extract. The
|
|
merged helper silently skipped that case instead of yielding a placeholder,
|
|
which dropped a degenerate part out of two fail-closed checks at once (see
|
|
`code_reviews/pr-3-capability-gate-followups-review.md` for the live
|
|
repro and the fix). The follow-up opencode session that implemented the fix
|
|
named the general lesson explicitly, which is what this document is
|
|
formalizing: **a plan that proposes collapsing N implementations into one
|
|
shared helper has to specify the helper's contract on every input shape,
|
|
not only the ones the N originals happened to agree on.**
|
|
|
|
This is a process document, not a bug report — no code changes accompany
|
|
it. It exists so the next plan that proposes a shared helper pins this down
|
|
up front instead of it being rediscovered in review.
|
|
|
|
---
|
|
|
|
## Spec
|
|
|
|
### The problem, generalized
|
|
|
|
When several independent implementations of "walk this structure and pull
|
|
out X" get unified into one shared helper, each original's handling of its
|
|
*own* edge cases has to be re-derived by the person writing the unification
|
|
— nothing forces them to enumerate it, because the edge case was never the
|
|
reason any of the N implementations were written in the first place. Each
|
|
one just happened to fall into some fallback path. The happy path is easy
|
|
to unify because all N agree on it by construction (they all worked, on
|
|
real traffic, for the common shape). The edge cases are exactly where they
|
|
are most likely to *quietly disagree*, and disagreement there is invisible
|
|
unless a plan makes each implementation's edge-case behavior explicit
|
|
before proposing the replacement.
|
|
|
|
The specific edge case that bit PR #3 — matches the coarse type tag but the
|
|
payload is missing or empty — deserves its own name because it is a
|
|
distinct category from "wrong type" or "absent": it is a **well-typed but
|
|
unparseable** input. A traversal that filters on `part.get("type") ==
|
|
"image_url"` before doing anything else will always let this shape through
|
|
the filter; what happens next is the part that has to be decided
|
|
deliberately.
|
|
|
|
### The rule
|
|
|
|
Any implementation plan that proposes extracting or unifying a shared
|
|
traversal, parser, or extractor helper from multiple existing call sites
|
|
must include an explicit contract: for every input shape the helper will
|
|
see, state what it yields or returns. At minimum, enumerate:
|
|
|
|
1. **The common/valid shape** — the one all N originals handle the same way.
|
|
2. **Every alternate valid shape** any existing call site already handles
|
|
(there may be more than one — PR #3 had three: `image_url` as
|
|
`{"url": ...}`, `image_url` as a bare string, and a bare `url` key on
|
|
the part itself).
|
|
3. **The well-typed-but-unparseable shape** — passes the coarse type/shape
|
|
check but the value cannot actually be extracted. State the yielded
|
|
value explicitly (a sentinel like `""`, not "nothing" / "skip") unless
|
|
skipping is a deliberate, stated decision.
|
|
4. **The wrong-typed or absent shape** — fails the coarse check. Usually
|
|
"skip", but say so, so it's a decision and not an assumption.
|
|
|
|
And critically: **for category 3, list what each of the N originals
|
|
currently does**, not just what the new helper will do. That list is what
|
|
surfaces disagreement before the merge, rather than after. In PR #3, all
|
|
four originals agreed by accident (each one's "nothing extracted" path
|
|
happened to either count the part or fail closed) — but nothing in the
|
|
plan recorded that agreement, so the merge didn't have to preserve it and
|
|
didn't.
|
|
|
|
### Why "skip silently" is the default failure mode to watch for
|
|
|
|
A `for`/`if`/`continue` traversal skips by construction unless a branch
|
|
explicitly appends/yields on every path. Converting four such loops into
|
|
one `yield`-per-match generator is a natural, idiomatic simplification —
|
|
and it's exactly the transformation that turns "always contributes
|
|
*something*, even a fail-closed placeholder" into "contributes nothing,
|
|
silently, for a whole category of input." This isn't specific to
|
|
generators — the same risk applies to a shared parser that returns `None`
|
|
for the unparseable case where callers previously each had their own
|
|
`None`-handling that didn't all agree — but generators make it easy to
|
|
introduce, because `continue`-without-yielding reads as the most natural
|
|
thing to write.
|
|
|
|
---
|
|
|
|
## Implementation plan
|
|
|
|
This is a discipline to apply going forward, not a code change. Concretely:
|
|
|
|
### 1. Template for future plans
|
|
|
|
When a `code_plans/` (or `code_reviews/`) entry proposes extracting a
|
|
shared helper from multiple call sites, include a table shaped like this
|
|
alongside the proposed code:
|
|
|
|
| Input shape | Original A | Original B | Original C | ... | New shared helper |
|
|
|---|---|---|---|---|---|
|
|
| common/valid | ... | ... | ... | | ... |
|
|
| alternate valid #1 | ... | ... | ... | | ... |
|
|
| well-typed, unparseable | ... | ... | ... | | ... (state the sentinel) |
|
|
| wrong-typed / absent | skip | skip | skip | | skip |
|
|
|
|
If any two originals disagree in a row, that disagreement is the thing the
|
|
plan has to resolve on purpose (pick one, document why) rather than let the
|
|
merge resolve by accident.
|
|
|
|
### 2. No retroactive audit needed right now, but a watch list for next time
|
|
|
|
Checked the rest of `dispatcher.py` for other traversals of a message's
|
|
multimodal `content` list, since that's the same shape that bit PR #3.
|
|
None of these are currently merged/unified helpers, so none are broken —
|
|
they're independent, single-purpose traversals, not N-collapsed-into-1. But
|
|
if any of them are ever unified (with each other, or with
|
|
`iter_image_url_values`), this spec applies:
|
|
|
|
- `session_fingerprint` (dispatcher.py:1302) — joins text parts to fingerprint a session.
|
|
- `session_directory` (dispatcher.py:1321) — joins text parts to find a working directory.
|
|
- `estimate_prompt_tokens` (dispatcher.py:1364) — sums text-part lengths for the context estimate.
|
|
- `_last_user_text` (dispatcher.py:1391) — joins the last user turn's text parts.
|
|
|
|
All four already treat a non-dict or non-string part as "contributes
|
|
nothing" consistently with each other (each filters with
|
|
`isinstance(p, dict)` / `isinstance(p.get("text"), str)` before including
|
|
it), so there's no live disagreement to flag today — this is a watch list,
|
|
not a finding.
|
|
|
|
### 3. Where this lives
|
|
|
|
Keep this document as the reference the next relevant plan cites, rather
|
|
than duplicating the rule inline each time. A future plan proposing a
|
|
shared-helper extraction should link back to this file
|
|
(`code_plans/shared-helper-unparseable-input-contract.md`) and fill in the
|
|
contract table from §1 as part of the plan, the same way
|
|
`code_reviews/capability-gate-followups.md` gave concrete before/after code
|
|
for each finding rather than describing it in prose only.
|