fix: one field-resolution law across aggregation hooks, source.where, removeMany, and find() spellings

Four fixes from a consumer conformance report, one root disease — two
field-resolution regimes where there must be one:

- The delete/update aggregation hooks fed the engine a partial entity
  view (type/service/data/metadata only), so a reserved-field groupBy
  (subtype, visibility, ...) resolved to a nonexistent group on the way
  down: counts drifted upward forever after deletes, and updates moving
  an entity between reserved-field groups double-counted. The hooks now
  pass the full-fidelity view via entityForAggFromRawRecord (every
  reserved field top-level, mirroring the add path); the update sites
  pass the full get() view instead of a hand-rolled subset.
- Aggregation source.where resolved fields only against the custom
  metadata bag, so where on a reserved field silently matched nothing.
  The matcher now resolves each filtered field through
  resolveEntityField — the same single source of truth groupBy uses.
- removeMany() with no usable selector (bare array passed positionally,
  empty params, ids: []) resolved successfully having deleted nothing.
  All three now throw; the two legacy tests that pinned the silent
  no-op as 'graceful' now pin the refusal.
- find() where keys accept both spellings: a metadata.-prefixed key
  falls back to its flattened spelling when the prefixed one is not
  indexed (metadata is flattened at index time). A literal nested custom
  key named metadata still wins when indexed as spelled.

Five regression pins in aggregate-reserved-fields.test.ts (4 of 5 vary
red on the unfixed code).
This commit is contained in:
David Snelling 2026-07-19 10:54:36 -07:00
parent 42037d0cd0
commit 945d92d29e
7 changed files with 303 additions and 45 deletions

View file

@ -10,6 +10,32 @@ Full auto-generated changelog: `CHANGELOG.md` · Releases: https://github.com/so
---
## v8.8.2 — 2026-07-19 (one field-resolution law: reserved-field aggregates stop drifting)
Four fixes from a consumer conformance audit, all rooted in the same disease — two field-resolution
regimes where there must be one:
- **Aggregates grouped by a RESERVED field (`subtype`, `visibility`, …) now decrement on
delete.** The delete/update hooks fed the aggregation engine a partial entity view (type,
service, data, metadata only), so a reserved-field `groupBy` resolved to a nonexistent group
on the way DOWN — counts drifted upward forever after any delete, and updates that moved an
entity between reserved-field groups double-counted it. The hooks now pass the full-fidelity
entity view (every reserved field top-level, the same shape the add path uses). If your
deployment derives stats from reserved-field aggregates, re-define those aggregates once
after upgrading (a changed definition triggers one rescan) or run them fresh — the drifted
persisted counts do not self-heal retroactively.
- **Aggregation `source.where` on reserved fields now filters** instead of silently matching
nothing: the matcher resolves fields through the same resolver `groupBy` uses (top-level
standard fields + custom metadata), so `where: { subtype: 'note' }` means what it says.
- **`removeMany()` refuses empty/invalid selectors loudly.** A bare array passed positionally
(`removeMany([id])` instead of `removeMany({ ids: [id] })`), an empty params object, or
`ids: []` used to resolve successfully having deleted nothing. All three now throw.
- **`find()` accepts both where-key spellings.** Metadata is flattened at index time
(`metadata.entry.title` indexes as `entry.title`); a `metadata.`-prefixed where key now
falls back to its flattened spelling when the prefixed one isn't indexed — the
"unindexed field(s), returning []" confusion for storage-shaped spellings is gone. (A
literal nested custom key named `metadata` still wins when indexed as spelled.)
## v8.8.1 — 2026-07-18 (flush no longer walks the whole generation history + the import dedup off-switch is now honest)
### The flush-storm fix (production incident, reported by a long-running deployment)