Files
pad/internal/items
xarmian 6cc2c7eb95 fix(items): create refuses reserved metadata keys; convention metadata goes through a typed create member; a full fields update may only carry them (BUG-3163, ToolSurfaceVersion 0.48) (#1470)
Item CREATE was the last door that stored a hand-written reserved metadata
key (implementation_notes, decision_log, github_pr, convention). A `--field`
or MCP `field` value landed as a string no extractor reads. For notes and
decisions, that also made `note` / `decide` refuse on the item until the row
was repaired.

Create's `fields` now refuses all four keys with 400 validation_error. The
check runs on the caller's map. Library activation, the only system writer that
used this door, moves to a new typed `convention` create member, which is
validated (an empty object is refused), normalized and lowered into the blob.
Every convention writer switches to it: the CLI and remote MCP library
activate, the web library activate, the web Conventions page create, and the
e2e seed helper. The CLI checks the STORED `convention` key to confirm the
member landed. It cannot use item.Convention for that, because that value is
derived from the sibling keys and is non-nil even when an older server has
dropped the member.

A FULL `fields` update may CARRY stored reserved values unchanged. A differing
value, or an OMITTED stored key (which the whole-blob replace would delete),
is refused with 400. The comparison uses decoded JSON, never bytes, and runs
in the update transaction against the row re-read under the write lock
(composed into the precheck, like the BUG-3133 guard). A note appended
concurrently therefore cannot be erased by a stale blob. A stored blob that is
not a JSON object has no readable reserved metadata, so a full write can
still repair it.

Artifact import shares createItemChecked but needs no gate, because
artifact.Decode fills Fields from a fixed frontmatter key list. Existing test
fixtures that put reserved keys in through the two closed doors now seed at
the store level. The stdio refusal texts are pinned to the classifier's
validation_failed table.

Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N
2026-09-23 16:23:06 -04:00
..