Planning bug: an acceptance criterion demanded evidence from a substrate on the OTHER side of a service boundary — "asserted against a real database" for an HTTP seam
Found while running MOTIR-3374 (motir-ai#280). The card's second acceptance criterion read:
If it is wired: a pass that calls
validate_planmid-generation gets a real verdict, asserted against a real database, and the pre-markPlannedvalidateCandidatePlancall is stated to be either redundant or complementary.
The run could not meet it as written, and the reason is structural rather than a matter of cost.
The mistake
The criterion demanded evidence from a substrate that lives on the OTHER side of a service boundary from the card's targetRepo. The card is pinned motir-ai. validate_plan is an HTTP call to motir-core's POST /api/internal/ai/validate-plan, and the tree it projects lives in motir-core's database. There is no motir-core process, schema, migration or fixture in the motir-ai checkout, so no motir-ai suite can assert against that database at all — the criterion is unavailable, not expensive.
Why it was written, and why the trap is general
It was written by analogy with the card's own predecessor. MOTIR-3374 was filed off MOTIR-3368, whose fix — the capture pair log_planning_mistake / log_planning_bug — writes to motir-ai's OWN Postgres via lessonService, and whose suite (tests/replanCaptureIntegration.test.ts) genuinely does assert against a real database. Both cards are "an optional GenerationSink method is unimplemented", so the evidence bar was carried across with the defect shape.
The two sinks differ in exactly the way the bar depends on. logPlanningMistake terminates in a local repository write; validatePlan terminates in a fetch to another service. That difference is invisible at the altitude the criterion was written at ("wire the sink, assert it works"), which is what makes this worth a rule rather than a one-off correction: the sibling card's evidence bar is exactly as portable as its SINK is, and a defect-shape match is not a substrate match.
The general rule this suggests
A criterion naming a test SUBSTRATE — a database, a live tenant, a deployed surface, a registry — is subject to the same repo-pinning discipline gate 1 already applies to a criterion naming a PATH. validate_work_item mechanises the path case as likely-repo-straddle; there is no equivalent for a substrate, and the check a human would run is the same one: does the thing this criterion measures live in this card's targetRepo?
Note this is NOT a repo straddle in gate 1's sense — the card ships wholly in motir-ai and its criteria name only motir-ai paths. The straddle is in the evidence, which is why the shipped detector does not see it. That asymmetry is the finding.
What the run did instead
Disposed of it on the record rather than silently re-scoping (run.md, a falsified premise is REPORTED): tests/planValidationSinkWiring.test.ts uses the discipline this repo already established for this exact endpoint — the contract-seam shape of tests/planValidationSeam.test.ts (Subtask 7.28.6 / MOTIR-1390). The real executor → the real sink → the real client, driven against a faithfully-recorded fixture of motir-core's shipped WorkItemValidityDto wire shape, so a key drift on either side breaks the build. Nothing about the sink is stubbed. Amended on MOTIR-3374's own record, and in motir-ai#280's body.
Acceptance criteria
plan-rules/phase-deepen.md(the gate-14 AC-axes limb, beside the existing precondition and NEGATIVE/ENUMERATION limbs) carries a SUBSTRATE limb: a criterion naming the thing it will be measured against — a database, a deployed surface, a live tenant, a registry, a platform API — owes the check that that substrate is reachable from the card'stargetRepo. Where it is not, the criterion names the evidence that IS available at that boundary instead.- The limb states the discriminator plainly, because the shipped
likely-repo-straddledetector does NOT cover this: gate 1 checks the PATHS a criterion names; this checks the SUBSTRATE it measures against, and a card can pass the first while failing the second (MOTIR-3374 did). - It names the carrier: a criterion inherited from a sibling card by defect-shape analogy. The sibling's evidence bar travels only as far as its sink does — a local write and a cross-service call are the same shape of defect and different shapes of proof.
- The MOTIR-3374 fixture is cited by key, with both sides:
logPlanningMistake→ motir-ai's own Postgres (assertable) vsvalidatePlan→ an HTTP call into motir-core (not assertable from here). - A
notes.htmllesson is added for the same finding — the corpus's LESSON half, perplan-rules/CORPUS-MAINTENANCE.md; this card is the RULE half.
Context refs
- MOTIR-3374 · motir-ai#280 — the run, the amendment comment, and
tests/planValidationSinkWiring.test.ts - MOTIR-3368 · motir-ai#277 — the sibling the bar was carried from, and
tests/replanCaptureIntegration.test.ts, which does assert against a real database motir-aitests/planValidationSeam.test.ts— the contract-seam discipline used insteadplan-rules/phase-deepen.md— the NEGATIVE and ENUMERATION limbs this parallelsplan-rules/core.mdgate 1 — the repo column, criterion by criterion, and what it does and does not reach