(motir-core) A `deploy` card can reach `done` with its artifact nonexistent — the one card type exempted from the authoring gate has no close-out gate either
Repo: motir-core. One PR. A type: deploy card's deliverable is an artifact somewhere outside this repository. Nothing checks that the artifact exists when the card is closed, and cards have been closed when it did not.
⚠️ This is deliberately NOT a planning rule, and the corpus says why. The obvious remedy — a gate in
plan-rules.mdmirrored intoSHARED_PLANNING_RULES— is the wrong tier.CORPUS-MAINTENANCE.md's third-tier decision (MOTIR-2164,notes.html#225) is explicit: "when a check's own design premise is that it needs no judgement — a phrase list, a column, a count — the remedy for it not firing is to MECHANIZE it, never to sharpen it." "Does a comment on this card name the thing that got published?" needs no judgement. Do not resolve this card by writing prose.
The gap, and why it is structural rather than careless
isOrderingCheckExempt (lib/workItems/proseVsGraph.ts:283) reads, in full:
return type === 'deploy' || executor === 'human';
So deploy is exempted from the one mechanized authoring gate — correctly, because the ordering remedy is to cut post-merge criteria onto a deploy card, so the release leg would otherwise fire on every plan. But nothing takes over at the other end. The card is exempt when written and unchecked when closed, and the two facts compose into a card type that can be done with nothing behind it.
The evidence
| card | claimed | actual |
|---|---|---|
| MOTIR-2539 | done — "Cut cli-v0.2.1 and PULL it back" | no tag on the remote, no 0.2.1 on npm, zero comments against two ACs that required recording the version and integrity here |
| two sandbox images (this epic) | done | CORPUS-MAINTENANCE.md records they "sat done for days in exactly that state" |
Three instances of one family — recurrent, which is the bar the corpus sets for leaving the lesson tier.
MOTIR-2539's own explanation predicted its fate verbatim before it happened to it: "a release lane can be correct, merged and green while nothing was ever published, because nobody cut the tag that fires it." The criteria were right. Nothing read them at the moment of closing.
Do this
Refuse — or at minimum, refuse silently-passing — the transition of a type: deploy work item into a done-category status when no comment on it carries artifact evidence.
The seam already exists and already special-cases this branch: lib/services/workItemsService.ts:1766, if (target.category === 'done'), which is where the manual-provenance stamp (MOTIR-1685) is applied for a structurally similar reason.
Held fixed:
- A phrase match, not a judgement. Artifact evidence is a semver, a
sha256:digest, or ansha512-integrity hash appearing in a comment. Follownotes.html#221's precedent — chose a string match over a judgement, in those words. cancelledis NOTdone. A cancelled release card published nothing on purpose. The existing block already distinguishes them; do not regress that.- A principled exemption, not a mute. Some
deploydeliverables genuinely have no identifier — a DNS cutover, a console toggle. Exempt them by a stated predicate the wayisOrderingCheckExemptdoes, so the firing set stays ≈ the defect set. An escape hatch that is a free-text override nobody records is the same failure with an extra step.
Blocking vs advisory is the one open call, and it should be recorded on this card before the code lands. The recommendation is to REFUSE the transition with a typed error naming what is missing: the existing advisories channel is non-blocking and computed at authoring/dispatch time, and the defect here is that nobody looked at close-out. A non-blocking advisory delivered at the moment someone is already not looking reproduces the bug faithfully.
Acceptance criteria
- A
type: deployitem cannot reach adone-category status with no artifact evidence in any comment — proven by a test that closes such a card and asserts the refusal, and by its inverse (a comment carrying a version/digest closes cleanly). cancelledis unaffected: adeploycard with no evidence still cancels, and a test says so.- The exemption is a named predicate with its own test, not an inline condition — and a card claiming exemption is identifiable afterwards, so the exemption cannot become a silent bypass.
- Retro-checked against the real incident: MOTIR-2539's state as it was closed (no comments) fails the new check; the state MOTIR-2584 was closed in (a comment carrying
0.3.0and itssha512-integrity) passes. Assert both against fixtures, so the check is calibrated on the defect that motivated it rather than on invented input. - No new prose in
plan-rules.mdorSHARED_PLANNING_RULES. If the implementer concludes a rule IS needed, that is a finding to record here, not a licence to add a fifth wording.
Out of scope
- The LESSON.
CORPUS-MAINTENANCE.mdkeeps the default: the incident is still filed innotes.html+lessons.base.ts, "named the mechanism that would have caught it" — i.e. this card. That entry is amotir-metaPR and is somebody's, but not this card's. - MOTIR-2699 — the adjacent half (the sandbox digest table nobody transcribes). Same family, different mechanism; do not fold them.
- The
deployexemption inisOrderingCheckExemptitself, which is correct and stays.
Context refs
lib/workItems/proseVsGraph.ts:283—isOrderingCheckExempt, the exemption this card is the other half of; and the repo-straddle check below it as the model for "a CONTRADICTION, not a count."lib/services/workItemsService.ts:1766— thecategory === 'done'branch to hook.lib/services/proseGraphAdvisoryService.ts— the advisory channel, if the call goes that way.motir-meta/prompts/plan-rules/CORPUS-MAINTENANCE.md— the three-tier routing and the MOTIR-2164 decision that makes this a code card.- MOTIR-2584 — a
deploycard closed correctly; its close-out comment is the positive fixture.