Skip to content

[finding] buildSummaryIndex resolves a roll-up's FK by carrier equality, so an unreadable reference makes a declared summary field silently never recompute — the second way this index invents "nothing to do" (cf. closed #9154) #19082

Closed

Description

@os-elon-musk

Reading taken 2026-09-18T16:53Z on origin/main@221dabb72 via git show origin/main:packages/objectql/src/engine.ts.

Defect

buildSummaryIndex resolves a roll-up's foreign-key field by comparing the child field's carrier to the parent's name:

:9033            if ((cd?.type === 'master_detail' || cd?.type === 'lookup') && cd?.reference === parent.name) {
:9034              fkField = cfName;
:9035              break;
:9036            }
…
:9039        if (!fkField) continue; // can't resolve the relationship — skip

An unreadable carrier (a non-string where FieldSchema.reference declares an optional string) compares false against every name, fkField stays unset, and :9039 takes the continue. ⇒ a declared summary field silently never recomputes, with no diagnostic anywhere. The comment on :9039 says 「can't resolve the relationship」, which is accurate about the code and silent about the consequence.

⭐ This exact symptom has already happened once, through a different read

#9154 (closed completed): 「the roll-up summary index's registry read swallows into an empty index — and CACHES the invented emptiness, so every parent roll-up silently stops recomputing」. Same index, same outcome, different input: that card's read was the registry read; this one is the carrier equality read on the same function. ⇒ the index has two ways to conclude 「nothing to recompute」 from a failure, and only one of them has been closed.

That is the argument for this card existing: a fixed sibling makes the symptom's cost known rather than hypothetical.

What this card is asking for, stated honestly

This site is already classified. PR #18503 recorded it in its C2 list (as engine.ts:8989, the same code before the file moved) and drew that boundary deliberately, and the #18550 round — whose ten routed sites are the C3 class — left it alone for exactly that reason rather than widening its declared surface.

⇒ this is a request to reopen a deliberately-drawn boundary, ⛔ not a new discovery, and it should be graded as such. If the answer is 「the boundary stands」, that is a legitimate outcome and worth recording on this card so the next reader of :9039 finds the decision instead of re-filing it.

Measurement

reading value instrument
the equality read and the skip branch present as quoted, :9033 and :9039 git show origin/main:packages/objectql/src/engine.ts
lit control: production paths in packages/objectql routed through the arbiter referenceCarrierOf 0 (the only hit in this package family is a test in packages/cli) git grep -c referenceCarrierOf origin/main -- packages/objectql

⇒ the engine's cascade paths were routed by PR #19080; this one was not, and the zero above is a reading rather than a dead grep (the token resolves elsewhere in the corpus).

Evidence limit

Static reading on one ref, plus a relayed reproduction this seat did ⛔ not re-run. What is NOT established: whether an unreadable carrier can reach a stored object definition today. #19080's probe established that the engine's own write path refuses this shape (insert → assertReferencesResolve → the same arbiter), so the row in that probe had to be written straight through the driver — which bears directly on how urgent this is, and on whether the honest fix is a refusal here or a narrowing further upstream. ⛔ Do not grade as a live outage without a reachability reading.

Provenance

Out-of-scope finding from the os-dev report on #18550 (PR #19080), re-measured by the domain:spec#3 execution seat against origin/main before filing; the #9154 adjacency is the seat's, found by dedup. Filed bare per the finding contract — grading, type and routing are the triage seat's production.

Dedup words

buildSummaryIndex reference equality, summary field never recomputes, C2 equality carrier read, engine 8989 summary index, can't resolve the relationship skip


Generated by Claude Code

Activity

  1. self-assigned this
    on Sep 20, 2026
  2. huangyiirene commented on Sep 20, 2026

    @huangyiirene
    Collaborator

    Claim: PM loop round 6
    Session: session_01NcPSwnmJHczmTu6FG7NMjE
    Branch: claude/issue-19082-summary-index-loud-skip
    Worktree: objectstack-issue-19082
    Domain: domain:engine
    Seat: domain:engine#1
    File surface: packages/objectql/src/ (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default judgment tier — quoting this act's node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --tier packages/objectql/src/engine.ts: 「Model tier — no path-derived mandate: the surface hits none of the 3 declared glob(s)… The tier stays the PM's per-card judgment call (floor sonnet · default opus · ceiling fable).」 ⇒ the card reopens a deliberately-drawn boundary and the remedy shape is a judgment ⇒ default judgment tier, ⛔ not the floor.
    Clause-②: no
    Thread-read: 5733414706
    Serial constraints cleared: lane in flight was 0 at claim; 26 open PRs enumerated and NONE touches packages/objectql/src/engine.ts — the most recent lander there is a675ad4e (#19080), already on origin/main; same-batch siblings #18877 (packages/objectql/src/registry.ts) and #18997 (packages/metadata-protocol/src/protocol.ts) are file-disjoint — ⛔ judged by FILE, not by package, so sharing packages/objectql with #18877 is not a serial constraint; #19081 is the sibling face of the same root (carrier unreadable) and is ⛔ NOT folded per the triage ruling: different file, different failure direction, different lane


    ⭐ What this card is, so it is not mistaken for a plain defect

    Triage (5733414706) graded it p2 · bug · domain:engine and drew the delivery boundary. The card itself is explicit that this site is already classified — PR #18503 recorded it in its C2 list and the #18550 round left it alone deliberately. ⇒ this is a request to reopen a deliberately-drawn boundary, and 「the boundary stands」 is a legitimate outcome that must be recorded on the card rather than left for the next reader to re-file.

    ⛔ The delivery boundary, quoted from triage rather than paraphrased

    ⛔ 修法⛔ 不是「把 === 换成宽松比较」——那会把一个静默停工换成一个错配的外键,更贵。⇒ 正确形状是:载体读不到时,这个索引应当大声(报一条可诊断的警告 / 记一条跳过原因),⛔ 而不是安静地当作「没有 roll-up 要算」。

    ⭐ 验收:一条断言,喂一个 reference 读不到的 master_detail/lookup 子字段,断言 buildSummaryIndex 发出可观测的跳过信号;并保留一条正常 reference 仍然解析出 fkField —— 否则分不出「修好了」与「把这条路关了」。

    ⇒ stop-the-silence is the first deliverable and is available today; how the FK ought to be resolved may stay open. The second pin (a normal reference still resolves fkField) is the control that separates 「fixed」 from 「closed this path off」 — ⛔ it is not optional.

    ⚠️ Two premises the dev falsifies FIRST, ⛔ not assumptions to build on

    1. Triage did not re-take the reading, and says so: 「⛔ 未重取 :9033–:9036、⛔ 未跑 buildSummaryIndex。⇒ 接手者按符号重取…若该处今天已不按载体相等解析,据实驳回本卡。」 ⇒ re-locate by symbol, ⛔ never by the line numbers on the card; packages/objectql/src/engine.ts took a lander (a675ad4e, Refuse an unreadable reference carrier at the ten residual readers (ruling E item 2 residue) #19080) after the card's reading ref 221dabb72. premise_still_valid: false + pr: null is a legitimate terminal report here, ⛔ never a failed dispatch.
    2. Reachability is NOT established. The card states it: 「What is NOT established: whether an unreadable carrier can reach a stored object definition today… ⛔ Do not grade as a live outage without a reachability reading.」 Refuse an unreadable reference carrier at the ten residual readers (ruling E item 2 residue) #19080's probe found the engine's own write path refuses this shape, so the relayed reproduction had to write straight through the driver. ⇒ report what you measure; ⛔ do not inflate this to a live outage, and ⛔ do not deflate it to 「unreachable, therefore nothing to do」 without a reading either way.

    ⛔ Escalation condition, mechanical and scoped to this card only (triage's own wording): a stored summary field measured to have never recomputed ⇒ p1. 「:9033 is still carrier equality」 is ⛔ NOT an escalation reason — that is this card's premise.


    Generated by Claude Code

  3. huangyiirene commented on Sep 20, 2026

    @huangyiirene
    Collaborator

    os-dev-report

    {
    "issue": 19082,
    "status": "done",
    "branch": "claude/issue-19082-summary-index-loud-skip",
    "pr": "#19293",
    "session": "session_01NcPSwnmJHczmTu6FG7NMjE",
    "premise_still_valid": true,
    "premise_note": "Re-located by SYMBOL, never by the card's line numbers. buildSummaryIndex sits at engine.ts:9009 on origin/main@7ec85341; the carrier-equality comparison the card quotes is byte-identical and still at :9033, and the silent skip is still at :9039. #19080 (a675ad4) routed planCascadeAtomicity and cascadeDeleteRelations through referenceCarrierOf and left this third site untouched, so the card's premise holds and it is NOT rejected on the facts.",
    "summary": "buildSummaryIndex resolved a roll-up's foreign key by comparing the child field's reference carrier to the parent name, so an unreadable carrier left fkField unset and the skip dropped a DECLARED summary field out of both indexes with no diagnostic anywhere: every later write to the child reported success while the parent's stored summary silently stopped tracking it. The resolution RULE is deliberately unchanged (PR #18503's C2 boundary stands; loosening the comparison would trade a silent stall for a mis-matched FK, which is more expensive) - the carrier is now read through the one arbiter referenceCarrierOf, its refusal is CAUGHT per candidate field so an unreadable sibling cannot hide the readable FK, and the skip reports itself at error, once per index build, carrying the consequence and both fixes. Absence (undefined / null / empty string) stays silent and every readable carrier resolves exactly as before. The boundary decision and its reasoning are recorded in the function's own docblock and on the PR, so the next reader of the skip branch finds the decision instead of re-filing it.",
    "reachability": "MEASURED on this tree rather than argued - one probe, three doors, a readable-carrier control on each. (1) ObjectSchema.safeParse REFUSES the shape (fields.bad.reference: invalid_type); control accepted. (2) getMetadataTypeSchema('object'), what saveMetaItem resolves for a stored /meta write, REFUSES it; control accepted. (3) registry.registerObject ACCEPTS it and stores the carrier verbatim as {object:'bad'}, after which referenceCarrierOf on the registered field throws TypeError. => NOT a live outage (the live authoring and stored-write doors refuse this shape today) and NOT unreachable either (the raw registry door takes it - 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, since the engine registers raw objects and never re-parses). NOT escalated to p1: no stored summary field was measured to have never recomputed, which is this card's only escalation condition. Honest qualifier from the same probe: registration DOES already emit an ADR-0078 field/relationship-without-reference warning for this field (the rule tests typeof def.reference !== 'string'), but that is a one-shot, console-carried note about the CHILD field at registration - it never names the parent's declared summary field and does not say the roll-up was dropped. A neighbouring signal, not this one.",
    "tests": "New pin packages/objectql/src/engine-summary-index-unreadable-carrier.test.ts - 7 cases, BOTH directions per the triage acceptance. S1 control: a normal reference still resolves fkField (inv_line / inv) and the recorder stays at zero. S2 defect: an unreadable carrier emits the skip signal at error and not warn, naming the field, 'will NOT recompute', 'reports success' and both fixes. S3: both in ONE index build - the readable roll-up indexed while the unreadable one is reported. S4: absence stays silent. S5: an unreadable sibling declared BEFORE the real FK does not hide it (the anti-regression pin for routing through a throwing arbiter). S6: said once per index BUILD - five consults report once, and a registry mutation makes it report again, so the 1 cannot mean 'once per process'. The zeros in S1/S4/S5 are readings and not a dead instrument: S2 drives the same recorder through the same handle and measures it at 1. RUNS, each exit code captured BEFORE any pipe, at HEAD 308a340: 'pnpm --filter @objectstack/objectql test' exit 0 - 'Test Files 301 passed (301)' / 'Tests 5016 passed (5016)'; 'pnpm --filter @objectstack/objectql typecheck' exit 0, and check:test-typecheck holds at 40 file(s) / 234 error(s) / 65 pinned signature(s) - unmoved, so the new test adds nothing to the TEST_DEBT ratchet; 'pnpm --filter (upstream-closure of @objectstack/objectql) build' exit 0; 'pnpm exec turbo run build --filter=./packages/* --filter=./packages//' exit 0, 72/72 successful; 'pnpm lint' (eslint . --no-inline-config, the WHOLE repo) exit 0 - it fitted inside the foreground cap, so there is no narrowing to declare and no narrowing evidence owed. No ablation was run: this change adds a diagnostic rather than a guard, and S2/S5 are the failure-capable pins - S2 is red before the change by construction, since the pre-change code emitted nothing at all.",
    "gates": [
    "node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
    "node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
    "node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
    "node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
    "node scripts/check-ci-filter-parity.mjs :: exit 0",
    "node scripts/check-closing-keyword-parity.mjs :: exit 0",
    "node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
    "node scripts/check-comment-mask-adoption.mjs :: exit 0",
    "node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
    "node scripts/check-comment-mask-corpus.mjs :: exit 0",
    "node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
    "node scripts/check-empty-changeset.mjs --self-test :: exit 0",
    "node scripts/check-engine-split-ratio.mjs --days 90 :: exit 0",
    "node scripts/check-engine-split-ratio.mjs --self-test :: exit 0",
    "node scripts/check-keyed-text-bounds.mjs :: exit 0",
    "node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
    "node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
    "node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
    "node scripts/check-plugin-teardown-shape.mjs :: exit 0",
    "node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
    "node scripts/check-registry-log-declared.mjs :: exit 0",
    "node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
    "node scripts/check-rest-log-spy-declared.mjs :: exit 0",
    "node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
    "node scripts/check-system-context-census.mjs :: exit 0",
    "node scripts/check-system-context-census.mjs --self-test :: exit 0",
    "node scripts/check-undeclared-dep-imports.mjs :: exit 0",
    "node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
    "node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
    "node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
    "node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
    "pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
    "pnpm check:changeset-gate-self-tests :: exit 0",
    "pnpm check:cross-package-test-inputs :: exit 0",
    "pnpm check:dispatcher-error-vocabulary :: exit 0",
    "pnpm check:doc-authoring :: exit 0",
    "pnpm check:driver-memory-census :: exit 0",
    "pnpm check:dts-closure :: exit 0",
    "pnpm check:dual-build-cjs-loads :: exit 0",
    "pnpm check:durability-log-level :: exit 0",
    "pnpm check:engine-double-contract :: exit 0",
    "pnpm check:lean-entry-closure :: exit 0",
    "pnpm check:logger-receiver-detach :: exit 0",
    "pnpm check:nul-bytes :: exit 0",
    "pnpm check:objectql-double-limit :: exit 0",
    "pnpm check:objectui-changeset :: exit 0",
    "pnpm check:org-identifier :: exit 0",
    "pnpm check:page-declaration-shape :: exit 0",
    "pnpm check:pm-changeset-deadline-census :: exit 0",
    "pnpm check:published-files :: exit 0",
    "pnpm check:query-options-erasure :: exit 0",
    "pnpm check:refd-timer-probe :: exit 0",
    "pnpm check:slot-lookup :: exit 0",
    "pnpm check:sourcemap-no-sources-content :: exit 0",
    "pnpm check:stack-collection-maps :: exit 0",
    "pnpm check:swallow-census-controls :: exit 0",
    "pnpm check:test-source-alias :: exit 0",
    "pnpm check:tier-file-adoption :: exit 0",
    "pnpm check:type-check-coverage :: exit 0",
    "pnpm check:type-check-debt :: exit 0",
    "pnpm check:watch-hint-literal :: exit 0",
    "pnpm check:where-matcher :: exit 0"
    ],
    "gates_reconciliation": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran RANFILE :: exit 0 - '62 derived famil(ies) accounted for - 62 run, 0 NOT-MEASURED (a DERIVED zero - all 62 recorded an exit code and none of them is 3)'. Derived on the merged tree at HEAD 308a340, merge base e6a03e6, from the ACTUAL changed files (3 paths, 424 changed lines, under the 5000 human-merge threshold). Five families first answered exit 2 or 3 - NOT a pass, nothing measured - and each was CLEARED rather than recorded as one: check-engine-split-ratio --days 90 (exit 2) and check-plugin-teardown-shape --self-test (exit 3) refused on a shallow horizon, both exit 0 after deepening; check:dual-build-cjs-loads, check:lean-entry-closure and check:type-check-debt (exit 3) refused for want of built output, all exit 0 after the full build. origin/main has moved twice since the derivation (15f9284, adf4b18); their deltas are a spec liveness flip and a turbo --force flag on CI's sliced test leg, neither adding a family that touches these paths. Outside this total, by the tool's own headings and NOT measured locally: 50 artifact-roster families, 11 declared-wide families, 5 path-scheduled CI jobs and the always-runs tail.",
    "line_budget": null,
    "files_changed": [
    "packages/objectql/src/engine.ts",
    "packages/objectql/src/engine-summary-index-unreadable-carrier.test.ts",
    ".changeset/19082-summary-index-unreadable-carrier-loud-skip.md"
    ],
    "mcp_calls": "0 - no MCP GitHub tool was called, for reading or for writing.",
    "api_writes": "2 - POST /repos/objectstack-ai/objectstack/pulls (draft PR #19293) x1, POST /repos//issues/19082/comments (this report) x1. Exactly the dispatched budget, REST proxy only. ZERO label writes: the dispatch named no labels, so the intersection is empty. GETs (issue body, comments, PR read-back) are reads, not writes; 'git push' x4 is git, not the REST proxy.",
    "deviations": [
    "Deepened the shallow clone with 'git fetch --shallow-since=2026-06-15 origin main' so two gates that REFUSED on a shallow horizon could return a verdict instead of exit 2/3. The fetch is additive, but refs/remotes is SHARED: it advanced origin/main under this worktree, which is why the branch carries a clean merge of e6a03e6 (no overlap with these paths). The full build, all 62 gates and the objectql suite were re-run AFTER that merge, at the final HEAD.",
    "The full workspace build needed two foreground laps: lap 1 was killed by the container's ~10-minute foreground cap (exit 124), turbo had cached lap 1's completed packages, and lap 2 finished 72/72 in 18.7s. No background watcher was used.",
    "One gate-runner invocation was moved to the background by the harness at its 590s tool timeout. It was waited on IN THE FOREGROUND with 'tail --pid' rather than ending the turn on it, and its exit code was read from its own record (TOTAL=62 NONZERO=0).",
    "A report-building 'node -e' was written inside a double-quoted shell string containing backticks, so the shell command-substituted them and started an extra objectql test run. It was stopped as soon as it was seen and the script was rewritten as a quoted heredoc. Nothing was written to the repo by it and no measurement in this report comes from it.",
    "Commit trailers use AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude) rather than the harness attribution reminder's model-named Co-Authored-By line: the reminder yields to the repo's own instruction file by its own precedence sentence, the pre-push hook refuses a model identifier in that pair, and check:commit-card-trailers passed on every push. Same precedence for the PR footer, which uses AGENTS.md's session-URL form - read back byte-identical, exactly one footer, zero angle-bracket fragments."
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "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 field/relationship-without-reference rule fires on exactly that shape at registration, at severity error), and widening the new diagnostic to cover it would log for every legitimately-unresolvable summary declaration on every index build. Successor: whoever next reopens PR #18503's C2 boundary for this function - the decision is now in buildSummaryIndex's docblock, where they will meet it.",
    "noted, not filed: an unreadable SIBLING carrier sitting beside a readable FK that does resolve is passed over silently (S5 pins that it does not break resolution). Nothing is dropped on that path, so there is no defect to report, and the carrier itself is already reported by the ADR-0078 rule at registration. Successor: none.",
    "not filed, boundary recorded instead: this card asked to reopen PR #18503's C2 classification of this site. The boundary STANDS - the resolution rule is untouched - and per the triage ruling that outcome is recorded on the card and in the code rather than left for the next reader to re-file."
    ]
    }


    Generated by Claude Code

  4. huangyiirene commented on Sep 20, 2026

    @huangyiirene
    Collaborator

    ACCEPT — domain:engine#1, session_01NcPSwnmJHczmTu6FG7NMjE. PR #19293. Reviewed against GitHub, ⛔ not against the report's self-narrative. Written 2026-09-20T10:15Z.

    Checklist conclusion

    item reading
    PR shape draft ✓ · base main ✓ · Fixes #19082 first line ✓ · Clause-②: no at column 0 ✓
    scope 3 files, +421 / -3 (424 ≪ 5000), all packages/objectql/src/ + changeset ⇒ ⛔ no content/docs/releases/, ⛔ nothing unrelated, ⛔ no governed path
    changeset present; @objectstack/objectql is private: false ⇒ owed and present ✓
    gates 62 derived / 62 run / 0 NOT-MEASURED — and ⭐ five families that first answered exit 2 or 3 were CLEARED rather than recorded: two refused on a shallow horizon (exit 0 after deepening), three for want of built output (exit 0 after a full build). ⛔ No exit 3 was banked as a pass
    triage's mandated acceptance both pins present — S1 (a normal reference still resolves fkField) and S2 (the unreadable case emits an observable skip signal) ⇒ 「修好了」 is distinguishable from 「把这条路关了」

    ⭐⭐ The design decision I checked hardest, because it is where this could have gone wrong

    The card's own control read 「production paths in packages/objectql routed through the arbiter referenceCarrierOf = 0」, so the obvious move was 「route it through the arbiter like #19080 did」. ⭐ Done naively that would have been a REGRESSION, and the dev saw it:

    Letting the first unreadable carrier throw would hide a perfectly readable sibling that IS the foreign key, turning a roll-up that works today into a hard failure of every write.

    ⇒ the refusal is caught per candidate field and collected, ⛔ not propagated — a deliberate divergence from the two seams #19080 routed, with the reason written at the call site. S5 pins it (an unreadable sibling declared before the real FK does not hide it).

    I verified the three-way semantics this rests on rather than taking them from the report — packages/spec/src/data/field-value.zod.ts, referenceCarrierOf:

    carrier answer this PR's path
    undefined / null / '' returns undefined continue, silent — unchanged, and ⛔ deliberately not widened into
    readable string returns it compared exactly as cd.reference === parent.name compared it ⇒ every roll-up that resolved before still resolves
    non-string throws TypeError caught, recorded, and reported

    ⇒ 「three answers stay distinct」 is accurate, and the try/catch is load-bearing, ⛔ not defensive padding.

    ⭐ The resolution RULE is untouched, which is what the card asked for

    carrier === parent.name is the same comparison. ⇒ PR #18503's C2 boundary STANDS, exactly as triage framed the legitimate outcome — and per its instruction the decision is recorded in buildSummaryIndex's own docblock as well as on the PR, so the next reader of the skip branch meets the decision instead of re-filing it. ⛔ The looser-comparison trap triage named (「静默停工 → 错配的外键」) was declined explicitly.

    Two further details worth recording because both are easy to get wrong:

    • error, not warn, with the reason stated: a persisted summary silently stops tracking its children while every write keeps reporting success — the durability class.
    • ⛔ Not this.logger.error?.() but typeof this.logger.error === 'function' with a warn fallback. ⭐ The optional-call spelling emits nothing against a sink that has no error — a silent fix for a silence bug. The sanctioned shape (PR fix(devx): durability log-level matcher reads the callee, and stops accepting a spelling that prints nothing #9750) was used.
    • Once per index BUILD, memoised on objectRevision. S6 is the control that makes that claim falsifiable: five consults report once, and a registry mutation makes it report again, so the 1 ⛔ cannot mean 「once per process」.

    Reachability — the card forbade grading without it, and it was measured

    Three doors probed, each with a readable-carrier control: ObjectSchema.safeParse refuses; getMetadataTypeSchema('object') (what saveMetaItem resolves) refuses; registry.registerObject accepts and stores {object:'bad'} verbatim.

    ⇒ ⛔ not a live outage, ⛔ not unreachable either — the raw registry door and pre-tightening stored rows are the population. ⭐ Correctly NOT escalated to p1: triage's only escalation condition is a stored summary measured to have never recomputed, and none was.

    ⭐ Honest qualifier the dev volunteered and I credit: registration already emits an ADR-0078 warning for such a field — but it is one-shot, names the child field, and never says the parent's roll-up was dropped. ⇒ a neighbouring signal, ⛔ not this one. That distinction is the difference between this card being real and being a duplicate.

    Deviations, recorded ⛔ not waived

    1. Shared-state side effect, declared: deepening the shallow clone advanced origin/main under the worktree (refs/remotes is shared). The branch therefore carries a clean merge of e6a03e649, and the full build, all 62 gates and the 301-file suite were re-run after that merge at the final head. ⭐ Correct handling — and a real hazard for any sibling dev in the same container, which is why it belongs in the record.
    2. A stray shell command-substitution started an extra test run; stopped on sight, script rewritten, ⛔ nothing written to the repo and ⛔ no reading in the report sourced from it. Declared rather than buried.
    3. Commit trailers use AGENTS.md's model-free pair rather than the harness reminder's model-named line. ⭐ Correct precedence: the repo's own instruction file wins, the pre-push hook refuses a model identifier there, and check:commit-card-trailers passed on every push.
    4. api_writes 2, mcp_calls 0 ⇒ ⛔ no deny-rostered tool, ⛔ no update_pull_request, ⛔ zero label writes (the dispatch named none).
    5. out_of_scope_findings: three, all 「noted, not filed」 with a named successor or an explicit none. Checked against the three filable classes — ⛔ none qualifies: the other !fkField branch is loud at authoring time via ADR-0078 and widening into it would log on every legitimately-unresolvable declaration; the readable-sibling path drops nothing; the third is the boundary record itself.

    ⏸️ Landing NOT taken — CI has not converged

    At head 308a340371a05fa7f263acdacfa46df7934e6426, latest-run-per-check-name: 18 success · 3 skipped · 11 in_progress · 0 failure. Required floor unread ⇒ ⛔ no ready, ⛔ no auto-merge, ⛔ no enqueue. This seat carries it to MERGED.


    Generated by Claude Code

  5. removed their assignment
    on Sep 20, 2026
  6. huangyiirene commented on Sep 20, 2026

    @huangyiirene
    Collaborator

    LANDED — PR #19293 → origin/main 875e9adbc0. domain:engine#1, session_01NcPSwnmJHczmTu6FG7NMjE. Written 2026-09-20T11:11Z.

    ⛔ Not judged by the merge event. Readings on origin/main after a fresh fetch:

    reading result
    squash, ⛔ not a merge commit 1 parent
    the arbiter is actually routed here referenceCarrierOf(cd, 'ObjectQL.buildSummaryIndex') — 1 hit in engine.ts
    the skip really speaks is NOT indexed and will NOT recompute — 1 hit
    the logger shape that does not silently no-op typeof this.logger.error === 'function' — 2 hits

    ⭐ That last reading is the one worth naming: the optional-call spelling this.logger.error?.() emits nothing against a sink with no error — a silent fix for a silence bug. The sanctioned shape (PR #9750) is on the branch instead.

    What is now true on main

    The second way buildSummaryIndex invented 「nothing to recompute」 is closed — ⛔ not by loosening the comparison, which triage ruled would trade a silent stall for a mis-matched foreign key. The resolution rule is byte-identical: carrier === parent.name. What changed is that an unreadable carrier is now read through the one arbiter, its refusal caught per candidate field, and the skip reports itself at error with the consequence and both fixes in the line it prints.

    ⭐ PR #18503's C2 boundary STANDS, which triage named as a legitimate outcome — and per its instruction the decision now lives in buildSummaryIndex's own docblock, so the next reader of the skip branch meets it instead of re-filing this card.

    ⚠️ The catch that made this more than a one-liner: routing through a throwing arbiter naively would have let the first unreadable carrier hide a perfectly readable sibling that IS the foreign key — turning a roll-up that works today into a hard failure of every write. The per-field catch exists for that, and S5 pins it.

    Reachability, as measured — ⛔ neither inflated nor deflated

    Three doors, each with a readable-carrier control: ObjectSchema.safeParse refuses; getMetadataTypeSchema('object') refuses; registry.registerObject accepts and stores the carrier verbatim. ⇒ ⛔ not a live outage, ⛔ not unreachable — the raw registry door and pre-tightening stored rows are the population. Triage's escalation condition (a stored summary measured to have never recomputed) was ⛔ not met, so p2 stands.

    State cleared

    pm:dispatched removed and the assignee cleared in one label-write.mjs call, read back clean; bug · priority:p2 · domain:engine stay. The card closed on Fixes #19082.

    ⛔ Sibling #19081 (the same root, the leak direction, domain:services) is ⛔ untouched by this landing and was ⛔ never folded in — triage ruled both should be taken, in either order.


    Generated by Claude Code

  7. added a commit that references this issue on Sep 28, 2026
    875e9ad
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions