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 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):
action=create with body trailing Status: accepted → record.status == "accepted".
action=create with ## Status\naccepted section only → record.status == "accepted".
- Both present → trailing wins.
- Invalid
Status: yellow → default status + warning in warnings[].
status="accepted" param AND body Status: draft → body wins.
Body-mode parity (5.2):
# Title: <X> overrides empty title="" param.
## Version\n1.2 promotes to version column (specs, plans).
## Decision / ## Rationale / ## Alternatives promote (decisions).
- Unknown
## Notes\n<X> preserved in body verbatim.
Duplicate prevention / projection retirement (5.5):
- Fresh
spec_id → success, live row created.
- Second
action=create with same spec_id while first is live → 409 with
existing_id + existing_record; no new row.
- After
retired_at is set on the first row → second action=create with
same spec_id succeeds; partial UNIQUE permits it.
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)
- ✅ All four bugs fixed across the seven typed-artifact verbs.
- ✅ Smoke tests in
mcp/smoke_spec024_pr*.py cover 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: 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