Status:
accepted · Version 1.0 · Filed 2026-04-21SPEC-024 v1.0 — Typed-artifact verb bugs (accepted)
Consolidated spec covering four bugs in Prism’s typed-artifact verbs, the fix series that shipped across PR1 → PR3, and the projection-retirement model that replaced the originally drafted hard-delete approach. Shipped end-to-end against server1 2026-04-21.1. Summary
Four bugs in Prism’s typed-artifact verbs (prism_spec, prism_decide,
prism_plan, prism_retro, prism_todo, prism_journal, prism_note)
surfaced during the 2026-04-21 AM retroactive-filing session for SPEC-020.
All four related to how the verbs parse, normalize, and deduplicate content
between the body-mode (single markdown string) filing path and the
structured-param path. These were implementation defects in accepted
architecture — no new ADR. The fix series completed the intent of
prism_spec / prism_decide / the typed-artifact surface as originally
designed.
2. Origin
While closing the SPEC-020 governance gap by filing it formally throughprism_spec(action=create, body=..., spec_id="SPEC-020", title="..."),
two anomalies were observed in the returned payload and one more after a
confirming action=list call. Frank’s response: “Write the spec for the
bugs we found for Donna to fix.”
3. The bugs
Bug #1 — prism_spec(action=create) did not parse Status: from body
Calling prism_spec(action=create, body="...\nStatus: accepted", ...)
returned a record with status: "draft". The body contained the literal
text Status: accepted (in a ## Status section and again as a trailing
line), yet the status column was not promoted.
Repro evidence: spec filing fbb9cb51-e535-43b1-903d-af56f4e9aac2 (SPEC-020,
filed 2026-04-21 10:53:18Z). Body included ## Status\naccepted at line 4
and trailing Status: accepted at end; returned record showed
status: "draft".
Impact: specs filed as accepted/superseded came back as draft, making
action=list and status filters misleading.
Bug #2 — Body-mode parity across typed-artifact verbs was incomplete
prism_decide had an explicit, documented body-mode convention (title,
decision, rationale, alternatives, status all parse from structured markdown
sections). prism_spec accepted a body param but did not parse known
structured sections — ## Title, ## Version, ## Supersedes were
ignored. Agents calling through MCP clients with field-collapse (notably
Claude Desktop, which collapses named params into XML-tagged blobs) had no
reliable workaround for specs, plans, retros, TODOs, journals, or notes —
only decisions.
Bug #3 — prism_spec(action=create) with existing spec_id silently created a duplicate
prism_spec(action=list, pid=PID-PGR01) on 2026-04-21 returned two records
both with spec_id: "SPEC-020":
82e4f2b9-...created 2026-04-21T10:34:18Z (Donna session)fbb9cb51-...created 2026-04-21T10:53:18Z (Lola session)
status: "draft", version: "1.0", the same title. Export
pipelines (Mintlify) had no way to choose “the” SPEC-020;
semantic_recall("SPEC-020") returned both.
Bug #4 — prism_plan / prism_retro / prism_todo / prism_journal / prism_note likely had the same three bugs
Bugs #1, #2, #3 surfaced onprism_spec. The sibling typed-artifact verbs
use the same shape (action=create with structured params or a body string),
so the same failure modes were expected to be pervasive. Donna’s audit
confirmed.
4. Out of scope
- Changes to
prism_decidebody-mode (it is the reference implementation; this spec extended it, didn’t replace it). - Status lifecycle automation (auto-advance superseded on version bump) — follow-up if desired.
- Tri-graph representation of supersession — SPEC-020 territory; the
tri-graph has
:SUPERSEDED_BYedges already. - Fixing the existing duplicate SPEC-020 records — one-off data cleanup folded into migration 013 (see §5.5).
5. Fix series
5.1 Status parsing (Bug #1) — shipped in PR1 (b162f1b)
Every typed-artifact verb that accepts abody param parses status via the
convention used by prism_decide:
- Case-insensitive match on
Status:prefix at start of a line. - Last occurrence in body wins (supports trailing status lines).
- If both
## Status\n<value>and trailingStatus: <value>exist, trailing line wins (canonical “summary” location). - Parsed value overrides any explicit
statusparameter. - Allowed values per artifact:
- specs:
draft,accepted,superseded,rejected - decisions:
proposed,accepted,superseded,rejected - plans:
active,superseded,complete,abandoned - retros:
open,closed - todos:
open,in_progress,done,abandoned
- specs:
- Invalid values fall back to the verb’s default with a warning in
warnings[]in the return payload.
5.2 Body-mode parity (Bug #2) — shipped in PR2 (b1bca10)
Every typed-artifact verb with abody param supports the same
structured-section parsing:
Parsing is tolerant: unknown sections left in body as-is; missing sections
fall through to explicit params. Explicit params always lose to body-mode
when both present (body-mode is the MCP field-collapse escape hatch).
5.3 Body-column plumbing (PR2.5, promoted from deferred list)
Body-mode parsing in 5.2 is useless if the body column isn’t plumbed end to end through schemas, service, router, client, and verb layers — plus the embedding call that feedssemantic_recall. PR2.5 landed this narrow
5-layer slice across ADRs + retros + specs with a local Docker Compose
round-trip smoke.
5.4 Cross-artifact audit (Bug #4) — shipped across PR1, PR2, PR2.5
5.5 Projection retirement (Bug #3) — shipped in PR3 (superseded the original §5.3 hard-delete + UNIQUE approach)
Change from v0.1 draft. v0.1 proposed a hardUNIQUE (tenant_id, project_id, spec_id) constraint plus a structured error on duplicates.
v0.2 replaced this with projection retirement because append-never-delete
is the governance invariant — a hard UNIQUE would collide with retired rows
and lose history.
Shipped model:
retired_at TIMESTAMPTZ NULLcolumn onspecs,adrs,retros.- Partial UNIQUE index
WHERE retired_at IS NULL— retired rows do not participate in the constraint and can coexist with a live row of the same (project_id, spec_id). - Migration 013 includes
op.executecalls that soft-retire the known SPEC-020 duplicate (82e4f2b9-0eaa-4def-a2cb-3c192220c41d) — the older row keeps its content on disk but is excluded fromaction=list, from recall, and from the partial UNIQUE. action=createon an existing live spec_id returns a structured 409 withexisting_id,existing_record, andremediation.action=listhas aninclude_retiredflag (default false) for admin / audit callers that need the historical view.
5.6 Tri-graph typed-artifact backfill gap — scoped to SPEC-025
Verified 2026-04-21: typed-artifact creates (create_spec, create_adr,
create_retro) did not populate the tri-graph. Only SPEC-016, SPEC-017,
SPEC-019 existed as :Entity; SPEC-020 through SPEC-024 were orphan
Postgres rows. SPEC-020’s “tri-graph is source of truth for history”
narrative was not delivered for typed artifacts. Tri-graph auto-entity-
on-create was scoped as SPEC-025 (accepted; Waves PR1–PR3 shipped).
SPEC-024’s append-never-delete invariants stand on Postgres semantics
regardless of tri-graph wiring.
6. Verification / smoke tests
Smoke tests ship alongside each PR. Per-verb coverage matrix: Status parsing (5.1):action=createwith body trailingStatus: accepted→record.status == "accepted".action=createwith## Status\nacceptedsection only →record.status == "accepted".- Both present → trailing wins.
- Invalid
Status: yellow→ default status + warning inwarnings[]. status="accepted"param AND bodyStatus: draft→ body wins.
# Title: <X>overrides emptytitle=""param.## Version\n1.2promotes to version column (specs, plans).## Decision/## Rationale/## Alternativespromote (decisions).- Unknown
## Notes\n<X>preserved in body verbatim.
- Fresh
spec_id→ success, live row created. - Second
action=createwith samespec_idwhile first is live → 409 withexisting_id+existing_record; no new row. - After
retired_atis set on the first row → secondaction=createwith samespec_idsucceeds; partial UNIQUE permits it. action=listdefault → only live rows.include_retired=true→ live + retired.
mcp/smoke_spec024_pr1.py, pr2.py, pr2_5_integration.py, pr3.py.
7. Open questions (resolved)
- Q1 Migration of existing duplicates. Resolved in §5.5: migration 013
soft-retires the SPEC-020 loser via
op.execute. Same pattern migration 011 used for wrap-prefix backfill. - Q2 Version column on specs / plans. Resolved: body-mode parse. Specs
already carry
## Versionsections in practice. - Q3 Warnings field. Resolved:
warnings[]added uniformly to return shapes. Silent drops of invalid values were exactly the failure pattern this spec closed. - Q4 Body-mode for prism_todo / prism_journal / prism_note. Resolved: journals + notes yes; TODOs stay structured-param only.
- Q5
action=updatesurface. Partial resolution:action=updatewas out of scope for SPEC-024 (bugs #1–#4 were create-path bugs). A full PATCH path for specs was added 2026-04-22 in the course of closing this acceptance — seeprism_spec(action="update")andPATCH /api/v1/specs/{spec_id}.
8. Relationships
- Supersedes: nothing; extended accepted typed-artifact architecture.
- Related:
prism_decidebody-mode convention is the reference; SPEC-024 generalized it. - Related: SPEC-023 (wrap discipline + summary quality) — independent but share the theme of making governance surfaces rigorous.
- Informs: future “draft-arc closure” check
(e.g.,
prism_wrapsurfacing “you drafted a SPEC in deltas but never filed it”) depends on 5.5’s duplicate-prevention to distinguish “drafted-only” from “filed formally.” - Scopes-out-to: SPEC-025 (tri-graph auto-entity-on-create + historical backfill), shipped and accepted.
9. Implementation sequencing (shipped)
10. Acceptance / exit criteria (met)
- ✅ All four bugs fixed across the seven typed-artifact verbs.
- ✅ Smoke tests in
mcp/smoke_spec024_pr*.pycover every applicable verb × bug combination. - ✅
prism_spec(action=list, pid=PID-PGR01)returns exactly one live SPEC-020 record after migration 013’s soft-retire of the duplicate. - ✅ Body-mode filings with
Status: <value>promote to the status column. - ✅ Duplicate-create attempts return structured 409, not a second live row.
- ✅ A follow-up filing of SPEC-024 itself with
Status: acceptedtrailing the body promotes correctly (this record’s existence + status confirms §5.1 shipped).
11. Install / deploy impact
Zero install script changes.backend/docker-entrypoint.sh runs
alembic upgrade head on container boot; migrations 012 and 013 apply
automatically on next backend restart. No env var additions. No compose file
changes.
12. Change log
- v0.1 (2026-04-21 AM, Lola). Initial draft; 4 bugs identified; proposed §5.3 hard-delete + UNIQUE for duplicate prevention.
- v0.2 (2026-04-21 PM, Donna). §5.3 replaced by §5.5 projection retirement (append-never-delete); PR2.5 promoted from deferred list; §5.6 added documenting the tri-graph backfill gap (scoped to SPEC-025).
- v1.0 (2026-04-21, accepted). PR1, PR2, PR2.5, PR3 all shipped
end-to-end against server1; status transitions from draft → accepted.
Consolidated on 2026-04-22 to be self-contained (v0.1 substantive content
- v0.2 model changes + shipped-status reconciliation in one body).

