Commit 875e9ad
fix(objectql):
Fixes #19082
Clause-②: no
A diagnostic added to an internal, private index neither loosens an
accept set nor widens a published surface. No schema changed;
`buildSummaryIndex` is `private` and nothing about its signature, its
return shape or its resolution rule moved.
## The premise, re-taken by symbol
Triage said it had not re-taken the reading and asked the executor to.
`packages/objectql/src/engine.ts` took a lander after the card's reading
ref `221dabb72` — `a675ad4e` (#19080, the ten-residual-readers round) —
so the site was re-located by **symbol**, never by the card's line
numbers.
It still resolves by carrier equality. `buildSummaryIndex` is at
`engine.ts:9009`; the comparison the card quotes at `:9033` is
byte-identical and now sits at the same line, and the silent `continue`
at `:9039` is unchanged. #19080 routed `planCascadeAtomicity` and
`cascadeDeleteRelations` through the arbiter and left this third site
alone. **`premise_still_valid: true`.**
## The defect
The child-to-parent foreign key is resolved by scanning the child
object's `master_detail` / `lookup` fields for one whose `reference`
names the parent. That comparison read the carrier raw, so a carrier
**no reader can read** — a non-string, where `FieldSchema.reference`
declares an optional string — compared `false` against every name,
`fkField` stayed unset, and
```ts
if (!fkField) continue; // can't resolve the relationship — skip
```
dropped a **declared** `summary` field out of *both* indexes.
`recomputeSummaries()` then had nothing to do after every insert /
update / delete of the child, so the parent's stored summary value kept
whatever it held while each of those writes reported success, and
nothing anywhere said so. It is the second way this one function invents
*"nothing to recompute"*; the first, its registry read, was closed as
#9154.
## The boundary this card asked to reopen — and where it now stands
PR #18503 recorded this site in its **C2** list and the #18550 round
left it there deliberately. **That boundary stands: the resolution rule
is untouched.** Loosening the comparison would trade a silent stall for
a **mis-matched foreign key**, which is more expensive — a roll-up
quietly aggregating the wrong children reads exactly like a correct one,
while a roll-up that stopped moving is at least visible to anyone who
looks at the value. What ends here is only the **silence**, which triage
named as the half available today:
- the carrier is read through the one arbiter, `referenceCarrierOf` —
the accessor #19080 routed the two cascade seams through;
- its refusal is **caught** rather than propagated, because this is a
*scan* looking for the FK across every relation field: a propagating
refusal on one unreadable field would hide a readable sibling that
really is the foreign key, turning a roll-up that works today into a
hard failure of every write to that child. Pinned (§5 of the new test);
- the skip reports itself at **`error`**, once per index build. A
persisted summary that silently stops tracking its children while every
write keeps reporting success is the durability class by AGENTS.md's own
question, and the line carries both halves it owes: the consequence
(which field will not recompute, and that the system keeps looking
healthy) and the fix (spell the carrier as the target object's name, or
name the FK with `summaryOperations.relationshipField`);
- **absence is untouched.** `undefined`, `null` and `''` mean "this
field names no target", which is legal; they skip silently exactly as
before. Every readable carrier resolves exactly as before.
The decision and its reasoning are recorded on the card and in the
function's own docblock, so the next reader of the skip branch finds
them instead of re-filing.
## Reachability — measured, and deliberately not inflated
The card recorded this as **not established**, and it is now measured on
this tree rather than argued. One probe, three doors, each with a
readable-carrier control that passes:
| door | shape `{ object: 'bad' }` on a `master_detail` | control
`reference: 'bad'` |
|---|---|---|
| `ObjectSchema.safeParse` (the contract door) | **REFUSED** —
`fields.bad.reference: invalid_type` | accepted |
| `getMetadataTypeSchema('object')` — what `saveMetaItem` resolves for a
stored `/meta` write | **REFUSED** | accepted |
| `registry.registerObject` — the choke point every metadata door
funnels through | **ACCEPTED**, carrier stored verbatim as
`{"object":"bad"}`; `referenceCarrierOf` on the registered field throws
| accepted |
So: **not a live outage** — the live authoring and stored-write doors
refuse this shape today — and **not unreachable either**. The registry
takes it raw, which is the population `engine.ts`'s own #9689 note
already names for the sibling seam: "a raw `registerObject`, or a
stored/artifact row written before the tightening — the two populations
parse-time rejection measurably cannot catch, since the engine registers
raw objects and never re-parses". Graded exactly there, and ⛔ not
escalated: no stored `summary` field was measured to have never
recomputed, which is this card's only escalation condition.
One honest qualifier, measured in the same probe: registration **does**
already emit an ADR-0078 completeness warning for this field
(`field/relationship-without-reference` fires on `typeof def.reference
!== 'string'`). That is a one-shot, console-carried note about the
**child field** at registration; it does not name the **parent's**
declared `summary` field, does not say the roll-up was dropped, and this
package's own vitest config quiets `[Registry]` output to `warn`. It is
a neighbouring signal, not this one.
## Tests
`packages/objectql/src/engine-summary-index-unreadable-carrier.test.ts`,
7 cases, both directions — because without the second, a change that
simply stopped resolving anything would be indistinguishable from a fix:
- **§1 control** — a normal `reference` still resolves `fkField`
(`inv_line` / `inv`), and the recorder stays at zero;
- **§2 the defect** — an unreadable carrier emits the skip signal, at
`error` and not `warn`, naming the field, the consequence and both
fixes;
- **§3** — both in one index build: the readable roll-up is indexed
while the unreadable one is reported;
- **§4** — absence stays silent;
- **§5** — an unreadable sibling declared *before* the real FK does not
hide it;
- **§6** — said once per index **build**: five consults report once, and
a registry mutation makes it report again (without that second leg a "1"
could equally mean "once per process").
The zeros in §1, §4 and §5 are readings rather than a dead instrument:
§2 drives the same recorder through the same handle and measures it at
1.
Every command below captured its exit code before any pipe, at HEAD
`308a3403`:
| command | result |
|---|---|
| `pnpm --filter @objectstack/objectql test` | **301 files / 5016 tests
passed** |
| `pnpm --filter @objectstack/objectql typecheck` | exit 0 — and
`check:test-typecheck` holds at 40 files / 234 errors / 65 signatures,
unmoved |
| `pnpm --filter '@objectstack/objectql^...' build` | exit 0 |
| `pnpm exec turbo run build --filter='./packages/*'
--filter='./packages/*/*'` | 72/72 successful |
| `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` then `--ran` | **62 derived, 62 run, 0 NOT-MEASURED, 0
UNRUN** — every family carries a recorded exit code and none is 3 |
| `pnpm lint` (`eslint . --no-inline-config`, whole repo — no narrowing
to declare) | exit 0 |
Five of the 62 first returned exit 2 or 3 — never a pass, nothing
measured — and each was cleared rather than reported as one:
`check-engine-split-ratio` and `check-plugin-teardown-shape --self-test`
refused on a shallow clone (deepened with `git fetch
--shallow-since=2026-06-15`, both then exit 0), and
`check:dual-build-cjs-loads`, `check:lean-entry-closure` and
`check:type-check-debt` refused for want of built output (built, then
exit 0).
## Acceptance notes
Out-of-scope observations, noted and deliberately **not** filed:
- **noted, not filed** — the `!fkField` skip is still silent in its
*other* branch: a roll-up whose child declares **no** relation field at
all is dropped with no diagnostic here. It is not this card's input, it
is loud at a different layer (the ADR-0078 completeness rule fires on
exactly that shape at registration, at `severity: 'error'`), and
widening the new diagnostic to cover it would make every
legitimately-unresolvable `summary` declaration log per index build.
Successor: whoever next reopens PR #18503's C2 boundary for this
function — the decision is now recorded in `buildSummaryIndex`'s
docblock, where they will meet it.
- **noted, not filed** — when an unreadable sibling carrier sits beside
a readable FK that does resolve, the unreadable one is passed over
silently (§5 pins that it does not break resolution). Nothing is dropped
on that path, so there is no defect to report; the carrier itself is
already reported by the ADR-0078 rule at registration. Successor: none —
no PR or reader reaches this path with a question the ADR-0078 warning
does not already answer.
Sibling card #19081 shares the same root (an unreadable `reference`
carrier) and is deliberately **not** folded in: different file,
different failure direction (it leaks the carrier onward; this one
silently drops work), different lane. Triage ruled both should be taken,
in either order.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE)_
---------
Co-authored-by: Claude <noreply@anthropic.com>buildSummaryIndex reports the skip when a roll-up's reference carrier is unreadable (#19293)1 parent 4fef271 commit 875e9ad
3 files changed
Lines changed: 421 additions & 3 deletions
File tree
- .changeset
- packages/objectql/src
Lines changed: 20 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
Lines changed: 307 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
0 commit comments