What
POST /v1/composition/works/{work_id}/outline/nodes advertises four node kinds:
# services/composition-service/app/db/models.py:39
NodeKind = Literal["arc", "chapter", "scene", "beat"]
The table permits two:
-- loreweave_composition, constraint outline_node_kind_check
CHECK ((kind = ANY (ARRAY['chapter'::text, 'scene'::text])))
So arc and beat pass request validation, reach the INSERT, and die on a Postgres
check constraint. The caller gets a 400 carrying the raw database error:
400 {"detail":{"code":"CONSTRAINT","detail":"new row for relation \"outline_node\"
violates check constraint \"outline_node_kind_check\"\nDETAIL: Failing row contains
(01a09ad8-2de9-..., 01a09a04-ce63-..., ..., beat, r, Beat, null, {}, , null, empty, ...)."}}
Why it matters
A documented, typed part of the API cannot be used. A client written against the schema —
or generated from it — will construct a beat node, receive a 400, and have no way to know
the kind was never storable. Nothing in the contract says two of the four are unusable.
Either the migration is missing (beat/arc were meant to be storable) or the Literal is
stale (they were removed and the type was not). The two halves disagree and only whoever
owns the outline model knows which is correct.
Secondary: the error body is a raw Postgres message
The 400 returns the constraint text and the entire failing row. It is the caller's own
row, so this is not an exposure of anyone else's data, but it does hand out column order
and internal schema, and it is not an error a client can act on. A typed
OUTLINE_NODE_KIND_UNSUPPORTED naming the accepted kinds would be both safer and usable.
How it was found
composition-telemetry.spec.ts B3.3 asserts scene_committed fires on mark-done and NOT
for a non-scene node. To test "not for a non-scene node" it creates a beat — which is why
this surfaced.
Re-measured deliberately: this spec asserts through helpers/db.ts, which until today
defaulted to the BASE stack's Postgres regardless of the browser target, so the original
observation could not be trusted. It reproduces on the isolated stack after that fix.
Not fixed here
Which side moves is a data-model decision, and guessing at it inside a test-repair change
would be the wrong place to make it.
What
POST /v1/composition/works/{work_id}/outline/nodesadvertises four node kinds:The table permits two:
So
arcandbeatpass request validation, reach the INSERT, and die on a Postgrescheck constraint. The caller gets a 400 carrying the raw database error:
Why it matters
A documented, typed part of the API cannot be used. A client written against the schema —
or generated from it — will construct a
beatnode, receive a 400, and have no way to knowthe kind was never storable. Nothing in the contract says two of the four are unusable.
Either the migration is missing (
beat/arcwere meant to be storable) or the Literal isstale (they were removed and the type was not). The two halves disagree and only whoever
owns the outline model knows which is correct.
Secondary: the error body is a raw Postgres message
The 400 returns the constraint text and the entire failing row. It is the caller's own
row, so this is not an exposure of anyone else's data, but it does hand out column order
and internal schema, and it is not an error a client can act on. A typed
OUTLINE_NODE_KIND_UNSUPPORTEDnaming the accepted kinds would be both safer and usable.How it was found
composition-telemetry.spec.tsB3.3 assertsscene_committedfires on mark-done and NOTfor a non-scene node. To test "not for a non-scene node" it creates a
beat— which is whythis surfaced.
Re-measured deliberately: this spec asserts through
helpers/db.ts, which until todaydefaulted to the BASE stack's Postgres regardless of the browser target, so the original
observation could not be trusted. It reproduces on the isolated stack after that fix.
Not fixed here
Which side moves is a data-model decision, and guessing at it inside a test-repair change
would be the wrong place to make it.