Commit aae4388
authored
docs: capture the key-value storage contract and the index smell test (#330)
## Summary
### Why?
The extensions section of CLAUDE.md already says store interfaces must
be designed for the technology space (per-key reads, no server-side
filters), but review of a recent storage change showed the rule is not
stated in the form that catches violations mechanically: a
ListByBatch(batchID) method plus a new KEY idx_batch slipped through
even though a plain key-value backend cannot satisfy it without a
secondary index. The missing pieces were the concrete smell test and the
prescribed alternative.
### What?
CLAUDE.md's over-constraints list gains a query-by-attribute /
secondary-index bullet: a schema diff that adds a `KEY idx_*` to make a
store method viable means the contract has left get/put-by-key
territory. The storage extension README gains a "Key-value contract"
section spelling out the rules where store PRs are written: stores
expose get/put/conditional-update by primary key only; the derived-key
pattern (encode the relationship in a deterministic primary key like
`{parentID}/{hash(child identity)}`, giving idempotent creation and
at-most-one-row-per-identity by construction) replaces
query-by-attribute; and domain state is often already the index — an
aggregate that references its parts by ID enumerates their keys for
free, so a database index duplicating it is a second source of truth.
The README also spells out the case the two avoidance patterns don't
cover — a true reverse lookup where the caller arrives holding only the
attribute. In the KV space the only mechanism for that is making the
attribute a primary key somewhere, so the doc sanctions it as a
first-class **mapping store** (idempotent puts, eventually consistent
with the source, rebuildable as a projection;
`ChangeStore`/`ChangeRecord` is the in-repo example) rather than leaving
it implied as "faking it". An ordered decision path (derive → enumerate
→ map) makes the choice mechanical, with guards on both sides: escalate
out of an aggregate that would grow unbounded or take contended appends,
allow one mapping per hot-path access need (never per attribute, never
for ops/debug queries), and don't contort keys or aggregates to dodge a
legitimate mapping store.1 parent 2c6dc1f commit aae4388
2 files changed
Lines changed: 23 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
141 | 141 | | |
142 | 142 | | |
143 | 143 | | |
| 144 | + | |
144 | 145 | | |
145 | 146 | | |
146 | 147 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
0 commit comments