Skip to main content
Status: accepted · Version 1.0 · Filed 2026-04-21

SPEC-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 through prism_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)
Both carried 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 on prism_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_decide body-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_BY edges 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 a body 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 trailing Status: <value> exist, trailing line wins (canonical “summary” location).
  • Parsed value overrides any explicit status parameter.
  • 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
  • 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 a body param supports the same structured-section parsing:
Section (markdown)Promotes to columnApplies to
# Title: <text> or ## Title\n<text>titleall
## Status\n<value>status (trailing Status: overrides per 5.1)specs, decisions, plans, retros, todos
## Version\n<text> or frontmatter version:versionspecs, plans
## Decision\n<text>decisiondecisions
## Rationale\n<text>rationaledecisions
## Alternatives\n<text>alternativesdecisions
## Supersedes\n<spec_id>parsed; side-effect deferred to PR3specs, decisions, plans
all other contentstored verbatim in bodyall
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 feeds semantic_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

VerbBody-mode?Status column?Uniqueness?Action taken
prism_decideyes (reference)status(pid, number)confirmed correct
prism_specyesstatus(pid, spec_id)5.1 + 5.2 + 5.5 applied
prism_planyesstatus(pid, number)5.1 + 5.2 applied
prism_retroyesstatus(pid, number)5.1 + 5.2 applied
prism_todostructuredstatus(pid, number)5.1 applied (no body-mode)
prism_journalyes(pid, date)5.2 applied
prism_noteyesad-hoc5.2 applied

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 hard UNIQUE (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 NULL column on specs, 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.execute calls that soft-retire the known SPEC-020 duplicate (82e4f2b9-0eaa-4def-a2cb-3c192220c41d) — the older row keeps its content on disk but is excluded from action=list, from recall, and from the partial UNIQUE.
  • action=create on an existing live spec_id returns a structured 409 with existing_id, existing_record, and remediation.
  • action=list has an include_retired flag (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):
  1. action=create with body trailing Status: acceptedrecord.status == "accepted".
  2. action=create with ## Status\naccepted section only → record.status == "accepted".
  3. Both present → trailing wins.
  4. Invalid Status: yellow → default status + warning in warnings[].
  5. status="accepted" param AND body Status: draft → body wins.
Body-mode parity (5.2):
  1. # Title: <X> overrides empty title="" param.
  2. ## Version\n1.2 promotes to version column (specs, plans).
  3. ## Decision / ## Rationale / ## Alternatives promote (decisions).
  4. Unknown ## Notes\n<X> preserved in body verbatim.
Duplicate prevention / projection retirement (5.5):
  1. Fresh spec_id → success, live row created.
  2. Second action=create with same spec_id while first is live → 409 with existing_id + existing_record; no new row.
  3. After retired_at is set on the first row → second action=create with same spec_id succeeds; partial UNIQUE permits it.
  4. action=list default → only live rows. include_retired=true → live + retired.
Shipped smoke scripts: 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 ## Version sections 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=update surface. Partial resolution: action=update was 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 — see prism_spec(action="update") and PATCH /api/v1/specs/{spec_id}.

8. Relationships

  • Supersedes: nothing; extended accepted typed-artifact architecture.
  • Related: prism_decide body-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_wrap surfacing “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)

PhasePRStatusScope
1PR1SHIPPED (b162f1b)§5.1 body-mode status parsing across prism_spec / prism_plan / prism_decide
2PR2SHIPPED (b1bca10)§5.2 body-mode parity parser (Title / Version / Decision / Rationale / Alternatives / Supersedes)
3PR2.5SHIPPED§5.3 body-column plumbing: migration 012 + schemas + service + router + client + verb + embedding + smoke
4PR3SHIPPED§5.5 projection retirement: migration 013 (retired_at on adrs/specs/retros + SPEC-020 soft-retire + partial UNIQUE), duplicate pre-flight guard, verb-layer structured 409
5SPEC-025SHIPPED separately§5.6 tri-graph auto-entity-on-create + historical backfill

10. Acceptance / exit criteria (met)

  1. ✅ All four bugs fixed across the seven typed-artifact verbs.
  2. ✅ Smoke tests in mcp/smoke_spec024_pr*.py cover every applicable verb × bug combination.
  3. prism_spec(action=list, pid=PID-PGR01) returns exactly one live SPEC-020 record after migration 013’s soft-retire of the duplicate.
  4. ✅ Body-mode filings with Status: <value> promote to the status column.
  5. ✅ Duplicate-create attempts return structured 409, not a second live row.
  6. ✅ A follow-up filing of SPEC-024 itself with Status: accepted trailing 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).
Status: accepted
Last modified on April 22, 2026