Filed by motir run MOTIR-3560 while building MOTIR-3573, whose criteria this same run had authored ~90 minutes earlier in the re-plan of MOTIR-3560.
Two of MOTIR-3573's acceptance criteria describe a state the API cannot express:
"A
blockedByRefsentry naming the appending proposal ITSELF is refused atadd_plan_items…" "AparentRefcycle closed by the batch being appended (A appended in an earlier batch, B in this one, A→B→A) is refused at that append."
PlanItem.id is @default(cuid()) (prisma/schema.prisma:4660) and ProposalInput carries no id field (lib/dto/plans.ts:399). An incoming proposal therefore has no id until it is written, so nothing can reference it — it cannot name itself, and no earlier proposal can name an id that did not exist when it was appended. Both criteria are vacuous through the public door.
The re-plan read the two files it was moving code between — validateProposals.ts and plansService.ts — carefully, and every line number it cites is correct. It never read ProposalInput, which is the type the criteria are about: the criteria describe what a CALLER can send, and the shape of what a caller can send was never opened.
The tell is grammatical and was in the card's own words. A criterion of the form "X is refused at <public entrance>" is a claim about the entrance's INPUT TYPE, not about the validator behind it. The validator was verified; the entrance was assumed.
phase-deepen.md's precondition-verification rule and kind-leaf-deepen.md's AC-granularity rule both catch a criterion that needs a substrate that does not exist. Here every substrate exists and every cited symbol is real: assertRefsResolve raises cycle, addProposals is the entrance, the ids are real ids. What is false is that a caller can construct the input. That question is not on either checklist, and a reader running both passes cleanly still writes this criterion.
Nearest existing rule, and it is genuinely near: a substrate method EXISTING is not enough — verify it is callable in the STATE the consumer runs in. This is that rule pointed at the INPUT rather than at the caller's state — and the same run hit the state version too, twenty minutes later: it wrote a repair test against updateProposal (which requires planned) when the author-side repair is deepenProposal (which requires generating). Same shape, two axes, one session.
Low. The pure gate runs both checks anyway — they cost nothing and they DO fire on a proposal set broken by a direct edit — so nothing was built that should not have been. What it cost was the discovery, mid-build, that two criteria could not be tested through the public API, and the re-shaping of the tests to cover them at the unit tier instead. The card shipped; the criteria are amended on its record.
Is there a checkable rule here, or is this a one-off? Candidate, to weigh rather than adopt:
A criterion asserting that a public entrance REFUSES something is a claim about that entrance's INPUT TYPE — open the type and confirm a caller can construct the rejected input, before writing the criterion.
Weigh: (a) how often it would fire on a correctly-authored card (a refusal criterion is extremely common, and most are about values, not constructibility — so the narrow trigger is the rejected input references the proposal/request being made, which is rare and specific); (b) whether it is really a LESSON rather than a rule, given one occurrence — the corpus's own tiering says a captured mistake is a lesson by default and the RULES tier changes only on recurrence; (c) whether it collapses into the callable in the consumer's STATE rule as a second axis of one idea, which would make it a sharpening rather than a new entry.
Recommendation to weigh: (c) then (b) — one rule with two axes (can the caller be in that state? / can the caller construct that input?) is easier to hold than two, and one occurrence argues for the lesson tier over the rules tier. Decide it here and write the answer down.
add_lesson for tenant, the motir-ai seed row + migration for global) and its key cited here; if a RULE, the card names the plan-rules/ pack AND whether SHARED_PLANNING_RULES carries the trigger (gate 19's two homes); if neither, that is stated with its reason.updateProposal / deepenProposal instance — it is named here as the second axis of the same session's miss, and the disposition above covers it.lib/dto/plans.ts ProposalInput — the type that was never opened.prisma/schema.prisma — PlanItem.id @default(cuid()).plan-rules/phase-deepen.md — the precondition-verification rule this slipped past.plan-rules/kind-leaf-deepen.md — the AC-granularity rule and the callable in the consumer's STATE rule, the nearest neighbour.No sprint — MOTIR-1465 carries none and this holds up nothing. No blocked_by — the criteria read the corpus, not MOTIR-3573's branch.