← Files Fallow Code AnalysisARCHIVED FILE
skills/fallow-review/references/tradeoff-elicitation.md
10.2 KB · Oct 4, 2026 · 12:29 UTC
# Trade-off elicitation (taste-ownership review)
`fallow review`'s decision surface is the DETERMINISTIC slice: it surfaces only the
trade-offs fallow can prove from the module graph, a changed public-API contract
consumed outside the diff, a new boundary or coupling crossing, a new dependency.
Real architectural trade-offs are broader: abstraction level, error-handling
strategy, data-model shape, eager-vs-lazy, state ownership, extensibility-vs-YAGNI,
testability, trust boundaries. None of those are deterministically detectable from
the graph, so they need a model reading the diff.
This prompt elicits that broader set. The governing principle is TASTE OWNERSHIP:
the model makes each choice legible to the human and frames the open question; the
human decides. The model never prescribes the answer, never blocks, never
auto-applies.
A scope note on honesty: the prose framing below (the `observed` / `tradeoff` /
`question` discipline) is still enforced by the model checking its own output, not
by fallow. What fallow now DOES validate is the ANCHOR: a trade-off may cite a
`change_anchor` (a `chg:` id from the guide's `change_anchors`) and fallow
post-validates it on reentry the same way it validates a `signal_id`, rejecting an
anchor it never emitted (`unknown-change-anchor`). The accepted judgment carries
`anchor_kind: "change"` to mark it as the WEAKER, region-level anchor: it proves
only that the region changed, not that a graph finding exists there (that is
`anchor_kind: "signal"`). So the anchor is now fallow-grade; the framing prose
remains an agent-layer aid whose discipline is the prompt's.
## The honesty contract (non-negotiable)
1. **Anchor to the diff.** Every item's `anchor` must be a line PRESENT in the
provided diff: the changed line that is the LOCUS of the trade-off. If no changed
line is the locus, drop it, with ONE exception: the cross-cutting slot in rule 7.
The `tradeoff` and `question` text MAY name out-of-diff code as the affected
party, an in-diff change whose consequence reaches an untouched file is exactly
the kind of trade-off worth surfacing (anchor to the changed line, name the
untouched file in the prose). What you may NOT do is anchor an item to a line
that is not in the diff.
2. **Three layers, kept separate and neutral, per item:**
- `observed` (FACT): what the change does, readable straight from the diff. State
it neutrally. Do NOT use contrastive framing that implies a verdict ("returns
the raw error INSTEAD OF mapping it" already judges; write "returns the raw
error to the caller").
- `tradeoff` (INFERENCE): what it gains and what it costs. Your reading, not
ground truth. Name both sides; do not let the cost outweigh the gain
rhetorically.
- `question` (DECISION): the call the human owns. It must be GENUINELY OPEN. Ask
an open "how / what / under what conditions" question, never "you should...",
and never the leading form "..., or should you X?" (the "or should X" clause
smuggles your preferred answer into the question). If the only question you can
write names a specific fix, you are prescribing; reframe to the open decision
instead. Example of the trap: "..., or should this map to a domain error?" is a
prescription. The open form is "How should this surface a storage failure to
its callers?"
3. **Fence everything.** Mark every item `deterministic: false`. These are model
inferences. They never gate and never auto-post.
4. **Provenance, honestly.** Set `captured: true` ONLY if you are the same agent
that wrote this code in this session and the rationale is what you actually had
at write time. If you are reconstructing intent from a diff whose authorship you
do not own, `captured` is `false`, always. `captured` is a provenance hint, not a
trust score; do not raise it to look more authoritative. When in doubt, `false`.
The "why" is usually not in the diff; do not pretend it is.
5. **Abstain freely.** A short, high-signal surface beats a checklist. Keep at most
the top FIVE trade-offs ranked by `consequence` (impact if the call is wrong),
then by `confidence`; the rest do not exist. If nothing rises to a real decision,
return `abstained: true` with an empty `tradeoffs: []` (do not invent items to
fill the slots).
6. **Do not duplicate fallow.** Read `digest.decisions.decisions[]` from the guide
first; if fallow already framed it (public-API contract, boundary crossing, new
dependency), do not re-raise it. You add the part fallow cannot see.
7. **One cross-cutting slot.** Use this ONLY when NO single changed line is the
locus, when the trade-off emerges from the COMBINATION of several changes, or
from something the diff does NOT do, so there is genuinely nothing to anchor to.
If a changed line IS the cause and an untouched invariant is the consequence,
that is NOT this slot: anchor to the changed line (rule 1) and name the
interaction in `tradeoff`. For the truly anchorless case you MAY emit at most ONE
item with `anchor: "cross-cutting"`, `confidence: "low"`, naming the spanned
files/invariants in `observed`. If in doubt, anchor locally; this slot is rare.
## Inputs to gather
```bash
# fallow's deterministic grounding: decisions already framed, the snapshot pin,
# structural facts, and where fallow says attention belongs.
fallow review --base <ref> --walkthrough-guide --format json > /tmp/fallow-guide.json
# the raw change you are reasoning about:
git diff <ref>...HEAD # or: git diff --cached for staged work
```
Read the guide's `digest.decisions.decisions[]` (what fallow already owns) and
`digest.focus.review_here[]` (where fallow says attention is), then read the diff
itself for everything fallow cannot prove. Echo `graph_snapshot_hash` so the
surface can be checked for staleness if it is fed back.
## The lenses (where non-deterministic trade-offs hide)
Scan the diff through these. Each is a place a defensible choice was made that the
diff itself does not explain:
- **Abstraction & duplication**: extracted vs inlined; a new abstraction vs YAGNI;
a generalization built for a single caller.
- **Coupling & cohesion**: two concerns now joined; a module reaching across a
seam; new shared mutable state.
- **Data model**: type shape; optional vs required; an invariant enforced by the
type vs checked at runtime; enum vs open string.
- **Error handling**: Result vs throw/panic; propagate vs swallow; a silent
fallback; the granularity of the failure.
- **Control flow & complexity**: a new branch that hides a second responsibility;
an implicit ordering dependency between statements.
- **Performance vs simplicity**: sync vs async; eager vs lazy; a cache introduced
(and its invalidation cost); work added to a hot path.
- **Dependencies**: a new dependency vs a few lines of native code; the transitive
surface taken on.
- **Naming & API ergonomics**: a name that encodes an assumption; a boolean or
positional parameter; a leaky abstraction.
- **Compatibility & migration**: breaking vs additive; an implied data or config
migration; a deprecation path not laid.
- **State & ownership**: where state lives; lifecycle and cleanup; global vs scoped.
- **Extensibility vs simplicity**: a seam built for a future that may not arrive; a
hard-coded choice that will be costly to change later.
- **Testability**: hidden time, IO, or randomness; a seam that was not left for a
test.
- **Trust boundaries**: where input is validated; a trust assumption; secret
handling; an injection surface.
These are prompts for YOUR attention, not a checklist to fill. Most diffs touch two
or three of these meaningfully.
## Output shape
A single envelope: the echoed snapshot hash, an `abstained` flag, and the
`tradeoffs` array (empty when `abstained`). Sort `tradeoffs` by `anchor` then
`lens` so two runs are structurally diffable.
```json
{
"graph_snapshot_hash": "<echoed from the guide>",
"abstained": false,
"tradeoffs": [
{
"id": "to:src/core/api.ts:42:error-handling",
"anchor": "src/core/api.ts:42",
"lens": "error-handling",
"observed": "save() returns the raw DB error to the caller.",
"tradeoff": "Keeps the call site thin, but couples every caller to the storage layer's error shapes rather than a domain error.",
"question": "How should save() surface a storage failure to its callers?",
"consequence": "high",
"confidence": "medium",
"captured": false,
"deterministic": false
}
]
}
```
- `id`: stable per item, `to:<anchor>:<lens>`, so a consumer can dedupe across
re-runs and keep a human's dismissal sticky. Two trade-offs may legitimately share
one anchor line through different lenses (e.g. a new parameter that is both an
api-ergonomics and a compatibility question); the `lens` segment keeps their ids
distinct, and that is intended, not a duplicate to collapse.
- `anchor`: a real changed `file:line`, or the literal `cross-cutting` (rule 7 only).
- `consequence`: `low` / `medium` / `high`, how much it matters if the call is
wrong (impact). This is what you rank and cap on.
- `confidence`: `low` / `medium` / `high`, how strongly the diff itself supports your
reading (sureness). ORTHOGONAL to `consequence`. Anchors: `high` = the diff alone
shows it; `medium` = the diff plus a reasonable assumption about intent; `low` =
mostly reconstructed, or the cross-cutting slot.
- `captured`: provenance hint, see rule 4. Not a trust score.
- `abstained: true` with `tradeoffs: []` is the terminal "looked, found nothing"
state; distinguish it from a parse failure (no envelope at all).
- Render for a human as the anchor, then `observed -> trade-off -> question`, with
the question LAST so the human lands on the decision they own.
## What good looks like
- Each item names a real changed line (or the single cross-cutting slot), a concrete
cost, and a GENUINELY OPEN question the human can answer without being steered.
- The surface is short: the top five by `consequence`, or fewer, or an honest
abstain. Never padded to fill slots.
- `observed` reads as a neutral fact; the `question` names no fix. If a reader can
guess your preferred answer from the question, reframe it.
- It does not repeat fallow's deterministic decisions; it covers the part the graph
cannot see.
- It never tells the human what to choose.
SHA-256: ec06e59fdc95e393893dea6f010e157e3cc47b8fb92a933c833639817448129b