> ## Documentation Index
> Fetch the complete documentation index at: https://prism.ntecdev.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# SPEC-024 v1.0: Typed-artifact verb bugs — status parsing, body parity, duplicate prevention, projection retirement

> Typed-artifact verb bugs — status parsing, body parity, duplicate prevention, projection retirement

<Info>**Status:** `accepted` · **Version** `1.0` · Filed 2026-04-21</Info>

# 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 column                              | Applies to                             |
| ---------------------------------------------- | ----------------------------------------------- | -------------------------------------- |
| `# Title: <text>` or `## Title\n<text>`        | `title`                                         | all                                    |
| `## Status\n<value>`                           | `status` (trailing `Status:` overrides per 5.1) | specs, decisions, plans, retros, todos |
| `## Version\n<text>` or frontmatter `version:` | `version`                                       | specs, plans                           |
| `## Decision\n<text>`                          | `decision`                                      | decisions                              |
| `## Rationale\n<text>`                         | `rationale`                                     | decisions                              |
| `## Alternatives\n<text>`                      | `alternatives`                                  | decisions                              |
| `## Supersedes\n<spec_id>`                     | parsed; side-effect deferred to PR3             | specs, decisions, plans                |
| all other content                              | stored verbatim in `body`                       | all                                    |

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

| Verb            | Body-mode?      | Status column? | Uniqueness?      | Action taken               |
| --------------- | --------------- | -------------- | ---------------- | -------------------------- |
| `prism_decide`  | yes (reference) | `status`       | `(pid, number)`  | confirmed correct          |
| `prism_spec`    | yes             | `status`       | `(pid, spec_id)` | 5.1 + 5.2 + 5.5 applied    |
| `prism_plan`    | yes             | `status`       | `(pid, number)`  | 5.1 + 5.2 applied          |
| `prism_retro`   | yes             | `status`       | `(pid, number)`  | 5.1 + 5.2 applied          |
| `prism_todo`    | structured      | `status`       | `(pid, number)`  | 5.1 applied (no body-mode) |
| `prism_journal` | yes             | —              | `(pid, date)`    | 5.2 applied                |
| `prism_note`    | yes             | —              | ad-hoc           | 5.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: accepted` → `record.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)

| Phase | PR       | Status             | Scope                                                                                                                                                                       |
| ----- | -------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1     | PR1      | SHIPPED (b162f1b)  | §5.1 body-mode status parsing across prism\_spec / prism\_plan / prism\_decide                                                                                              |
| 2     | PR2      | SHIPPED (b1bca10)  | §5.2 body-mode parity parser (Title / Version / Decision / Rationale / Alternatives / Supersedes)                                                                           |
| 3     | PR2.5    | SHIPPED            | §5.3 body-column plumbing: migration 012 + schemas + service + router + client + verb + embedding + smoke                                                                   |
| 4     | PR3      | SHIPPED            | §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 |
| 5     | SPEC-025 | SHIPPED 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
