Repository navigation
fix(plugin-audit): resolve a summary reference field's target through referenceTargetOf (#19264) - #19285
Conversation
… `referenceTargetOf` (#19264) `audit-writers.ts` admitted `user` as a reference type and then required the materialized `reference` carrier, so a `trackHistory`'d `{ type: 'user' }` field authored without one was planned for nobody and the timeline rendered the raw id — silently, with no refusal and no diagnostic. `IMPLICIT_REFERENCE_TARGETS` declares the opposite for exactly that type: the target is a CONSTANT OF THE TYPE, and metadata authored without it is fully specified. `referenceTargetOf` exists to supply it, and is the same arbiter `plugin-approvals` moved to in #19263. The carrier was read at FOUR sites, not two: the two read planners key the plan by target object and the two renderers look the resolved titles back up under the same key, so a plan-only repair would issue the read and still print the id. One helper answers for all four. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhQASwqJr2Z7XfGWUdvnbF
…e caller's bound `check:objectql-double-limit` graded the new counting driver's `find` as UNJUDGED: the gate lifts the double and drives it with a row stub in place of every non-function declaration it cannot supply, and `(reads.findOn[object] ?? 0) + 1` throws on that stub before the body can be judged at all. Read the counter by TYPE instead, apply the caller's `limit` by PRESENCE after the filter and before the rows are copied. Graded rather than baselined: the family now reports 393 doubles graded (was 392), 195 applying the bound (was 194), 39 proving the bound precedes a row-touching stage (was 38), and `no files added` to `scripts/objectql-double-limit.baseline.json`. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhQASwqJr2Z7XfGWUdvnbF
📓 Docs Drift Check5 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 9 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0842f11ca71fb8abbd075f420398afd5c487079c && git checkout 0842f11ca71fb8abbd075f420398afd5c487079c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 15f92842ffca0be5d24e4778698c711310a984ee 7ecf511d2263960290d625ca3a3b1ad2e5406b13 && git checkout -B drift-repro 15f92842ffca0be5d24e4778698c711310a984ee && git merge --no-ff 7ecf511d2263960290d625ca3a3b1ad2e5406b13
node scripts/docs-audit/affected-docs.mjs --json 15f92842ffca0be5d24e4778698c711310a984ee |
…ur repaired through `referenceTargetOf` (objectstack-ai#19289) (objectstack-ai#19472) Fixes objectstack-ai#19289 Clause-②: no `IMPLICIT_REFERENCE_TARGETS` (`packages/spec/src/data/field-value.zod.ts`) declares a `user` field's target "a **CONSTANT OF THE TYPE**" and metadata authored without `reference` "**fully specified, not under-specified**". Two arbiters answer two different questions — `referenceCarrierOf` what the carrier says, `referenceTargetOf` what the field points at — and for `user` only the second matches that text. objectstack-ai#18550 standardised a population of readers on the first. This is the census of that population. **The arbiters are NOT edited.** `packages/spec/src/data/field-value.zod.ts` is untouched; the diff is `packages/lint`, `packages/metadata-protocol`, `packages/rest` and their tests. ## The census was re-measured, and it had moved Taken on `8f6d831` with **no pathspec**, excluding tests and the arbiters' own module: **17 `referenceCarrierOf` vs 8 `referenceTargetOf`** — ⛔ not the card's 16 vs 7. Class (A) is **8** (card: 7); class (B) is **9** (unchanged). Both deltas landed *after* the card's census: | delta | commit | landed | |---|---|---| | `objectql/src/engine.ts:9077` — a NEW class-(A) carrier site (`buildSummaryIndex`) | `875e9ad` (objectstack-ai#19293) | 2026-09-20T10:37Z | | `plugin-audit/src/audit-writers.ts:429` — raw read → target, so the target count rose | `5636641` (objectstack-ai#19264 / PR objectstack-ai#19285) | 2026-09-20T10:09Z | The `objectql/engine.ts` line numbers also drifted (13113 → 13204, 13568 → 13659). The card's instrument trap reproduces on this tree, with `REFERENCE_FIELD_TYPES` as the known-present needle: bare `packages/**/src/**/*.ts` → **0 files**, `:(glob)…` → **2**, no pathspec → **2**. ## Per-site verdict — all 17 The deciding question is each site's **own type gate**: where it excludes `user`, the carrier *is* the target for the question that site asks, and the site stays on the carrier. ### Class (A) — handed a real field definition (8) | # | site | own type gate | verdict | |---|---|---|---| | A1 | `metadata-protocol/src/seed-loader.ts:712` | `lookup \| master_detail \| user` — **admits** | 🔴 **DEFECT ① (silent)** — repaired | | A2 | `rest/src/rest-server.ts:10914` | **none** — any field the picker names | 🔴 **DEFECT ② (loud, 500)** — repaired | | A3 | `cli/src/commands/doctor.ts:726` | `lookup` only | ✅ not a defect — type-gate exclusion | | A4 | `cli/src/commands/doctor.ts:930` | `lookup` only | ✅ not a defect — type-gate exclusion | | A5 | `objectql/src/engine.ts:9077` *(new)* | `master_detail \| lookup` | ✅ not a defect — type-gate exclusion | | A6 | `objectql/src/engine.ts:13204` | `master_detail \| lookup` | ✅ not a defect — type-gate exclusion | | A7 | `objectql/src/engine.ts:13659` | `master_detail \| lookup` | ✅ not a defect — type-gate exclusion | | A8 | `services/service-analytics/src/plugin.ts:748` | `lookup \| master_detail` | ✅ not a defect — pre-judged on the card | ### Class (B) — synthesize `{ reference: … }`, discarding `type` (9) | # | site | own type gate | verdict | |---|---|---|---| | B1 | `lint/src/data-model-rules.ts:250` (`refOf`) | 4 callers: `RELATIONSHIP_TYPES` = `{lookup, master_detail}`, `OPTION_FIELD_TYPES`, `summary` | ✅ not a defect | | B2 | `lint/src/object-graph.ts:242` (`graphFieldOf`) | **none**; consumer `RELATIONSHIP_FIELD_TYPES` **admits `user`** | 🔴 **DEFECT ③ (silent, widest)** — repaired | | B3 | `lint/src/validate-expressions.ts:395` | `master_detail` only | ✅ not a defect | | B4 | `lint/src/validate-field-consumers.ts:560` | **none** | 🔴 **DEFECT ④ (silent)** — repaired | | B5 | `lint/src/validate-object-references.ts:306` | `RELATIONSHIP_TARGET_FIELD_TYPES` **admits `user`** | 🟡 latent misread — aligned, no output change | | B6 | `lint/src/validate-object-references.ts:329` (action param) | `ActionParamSchema.type` is **optional** | ✅ not a defect — **measured**, see below | | B7 | `lint/src/validate-security-posture.ts:292` (`refOf`) | `CBP_TIERS` = master_detail / lookup | ✅ not a defect | | B8 | `lint/src/validate-sharing-rule-enforceability.ts:267` | `master_detail` only | ✅ not a defect | | B9 | `verify/src/derive.ts:148` | `RELATIONAL` = lookup / master_detail / tree — **excludes `user`** | ✅ not a defect | ⛔ **Class (B) was smaller than the card's framing, not larger.** Seven of the nine are type-gated away from `user`; only B2 and B4 needed the pass-the-field-through repair. Three of the nine (B3, B7, B9) additionally keep their synthesized `{ reference: x.reference }` literal because the **objectstack-ai#5017 receiver meta-test reads their source** to prove they read `reference` and never an alias — folding that read into a helper call would disarm that scan silently. Those three are untouched. ## B6 — the site the tests refused, and why it is in the table as judged I initially swapped the action-param site too. `reference-integrity-suite.test.ts` went red: `object-reference-unknown` vanished from the suite's findings entirely. The cause is that a param is **not** a field definition — `ActionParamSchema.type` is optional, because a field-backed param inherits its type at runtime, "not visible at parse time" per that schema's own refinement comment. So `referenceTargetOf` answered `undefined` for **every param that declares no type**, and the corpus param `{ name: 'owner', reference: 'user' }` (`user` being the classic miss for `sys_user`) stopped being checked. The swap deleted a live check. Nothing was owed there in the other direction either: a carrier-less `user` param already produced no finding, because `check` returns early on absence. Reverted, verdict recorded as not-a-defect, with a regression guard kept beside it. ## The four repairs 1. **`metadata-protocol` seed-loader — SILENT, and it stored a wrong value.** A `{type:'user'}` field with no `reference` contributed no `dependsOn` edge and never reached `references`, so its natural key was written **verbatim** into a column holding a record id. 2. **`rest` public-form picker — LOUD.** A `publicPicker` on a spec-complete `{type:'user'}` field answered `500 LOOKUP_TARGET_MISSING`. Now `200` over `sys_user`. 3. **`lint` `object-graph.graphFieldOf` — SILENT and widest.** The slice feeds `resolveFieldPath`, whose `RELATIONSHIP_FIELD_TYPES` admits `user`; a carrier-less one answered `hop-untargeted`, which `isUnjudgeable` treats as "the graph could not answer" and `describeFieldPathVerdict` renders as *nothing*. Every rule in the package that resolves a field path therefore **stopped judging** any path through such a field — the failure mode `isUnjudgeable`'s own docblock says this family exists to end ("a missed report is silence"). 4. **`lint` `validate-field-consumers.walkObject` — SILENT.** The `displayField` consumer edge onto `sys_user` was never recorded, so a field that column displays was reported consumed by nobody.⚠️ Materiality stated honestly: recordable only where `sys_user` is compiled into the linted stack. **Nothing widens.** `user` is the only member of `IMPLICIT_REFERENCE_TARGETS`; `lookup` / `master_detail` / `tree` with an absent carrier still name nothing, pinned at every repaired site. **The unreadable-carrier refusal is unchanged** — `referenceTargetOf` reads the carrier through `referenceCarrierOf` *before* it judges the type, so objectstack-ai#13053/objectstack-ai#18550's `TypeError` still fires everywhere it fired. ⛔ **Not a re-widening of objectstack-ai#12920.** A control pins it: a `user` field spelling `referenceTo: 'zzz_aliased_object'` resolves `sys_user` from the type and is never asked for the aliased name; a `lookup` spelling the same alias still resolves nothing and still answers `500`. ## Evidence - `@objectstack/lint` — **106 files / 4019 tests passed** - `@objectstack/metadata-protocol` — **184 passed, 3 skipped / 2627 passed, 19 skipped** - `@objectstack/rest` — **194 files / 3254 passed, 1 skipped** - `typecheck` green on all three (incl. `check:test-typecheck` for lint and rest) - Re-run in full **after** merging `origin/main`; the closure was rebuilt first because `packages/spec` moved on main's side - **Gates:** `dispatch-gates --ran` reconciles **63 derived / 60 run / 3 NOT MEASURED / 0 UNRUN**. The three are `PREREQUISITE NOT MET` (exit 3, ⛔ not a pass): `check:dual-build-cjs-loads` and `check:type-check-debt` need a whole-repo build; `check-plugin-teardown-shape --self-test` cannot reach a commit-pinned fixture on a shallow clone. - **`pnpm lint`, narrowed and declared:** ① the population is read from `eslint.config.mjs`, which states in its own comment that this repo "never enables type-aware linting (no `parserOptions.project`, no typed `@typescript-eslint` rules) for ANY file" ⇒ this diff cannot move the verdict on any file it does not touch; ② `--format json` reports **37 files linted, 0 errors, 0 warnings** (a superset — the three-dot set includes what the merge brought); ③ measured at `97b689b`. - Control-character self-scan over the 12 changed files: 0 hits. ## Attribution Authored by Claude Code session `session_01UDXER3sdqfeVYpEWZs5mZx`. Recorded here in prose deliberately: this body was edited on the raw REST edit side, which appends the BARE footer form carrying no session id (pm-dispatch `references/platform-readings.md`:350). ## Acceptance notes - **Noted, not filed:** `referenceTargetOf` takes no `reader` label, while `referenceCarrierOf` does precisely so "the message says who could not read it". Every site moved onto the target arbiter therefore loses its own name from the refusal, and four existing pins were retargeted from the site label to `/referenceTargetOf/` here. The error class, the offending shape and the prescription stay asserted, and this matches the two landed repairs (objectstack-ai#19198, objectstack-ai#19264). It is a diagnostic-fidelity question about the arbiter's signature, ⛔ not a defect — fixing it would edit `packages/spec/src/data/field-value.zod.ts` and change this card's landing path. **Successor: the next PR that moves a consumer onto `referenceTargetOf`.** - **Noted, not filed:** `graphFieldOf` no longer populates `reference` for a **non-relationship** field carrying a stray `reference` (e.g. `{type:'text', reference:'foo'}`), because `referenceTargetOf` returns `undefined` outside `REFERENCE_VALUE_TYPES`.⚠️ **Corrected by the at-tier contract review (`5754774179`), re-measured on this head by the seat:** `resolveFieldPath`'s `RELATIONSHIP_FIELD_TYPES` gate is on the intermediate HOPS (`object-graph.ts:405`, `hop-untargeted`), ⛔ not on the leaf — the leaf comes back `ok` carrying its `meta` whatever its type (`object-graph.ts:413`) — and `validate-preset-comparands.ts:450` DOES read it (`verdict?.kind === 'ok' ? strName(verdict.meta?.reference) : undefined`). So «no other module reads `GraphField.reference`» is **false on the tree**, and the sentence is withdrawn. What actually follows: a `user` picker's filter rows now bind to `sys_user`, which is the object the route queries — correct; a `text`-plus-stray-carrier picker's filter rows bind to nothing where they bound to the stray object. The same shape reaches the REST picker, which has no field-type gate before `referenceTargetOf` (`rest-server.ts:10935`) and whose `FieldSchema.reference` carries no non-reference-type refusal, so `{type:'text', reference:'foo'}` behind a `publicPicker` now answers 500 `LOOKUP_TARGET_MISSING` where it used to search `foo`. Both are pull-backs to the declared contract — the `reference` describe text scopes it to lookup/master_detail fields, and `forms.mdx:238` scopes pickers to lookup / master-detail / `user` — on a shape outside the documented surface, so ⛔ neither is a widening and `Clause-②: no` is unaffected. Strictly more correct: a `text` field's stray carrier is not a target. ## 维护者速读(草稿) **改了什么** — `user` 字段的目标由类型常量决定(`sys_user`),不必作者手写 `reference`。本 PR 普查了全部 17 个读「目标」的调用点,逐个判定该问哪个仲裁器,修了其中 4 个真缺陷,其余 13 个判为「读载体本来就对」并留档。 **为什么改** — 契约白纸黑字写着这种元数据「已完整声明」,但四处消费者把它当作「没写目标」。后果:公开表单上点开「负责人」选择器直接 500 错误页;种子数据把人名原样写进本该存记录 id 的列;lint 静默放弃对这类字段路径的全部校验。 **风险与代价(含回滚)** — 风险低。没有放宽任何拼写:别名仍旧拒收,不可读的载体仍旧抛错,`lookup`/`master_detail` 缺目标仍旧当作没目标。回滚即 revert 本 PR,无数据迁移、无存量数据改写。 **席位意见** — *(留空,待席位定稿)* **你要做的** — 确认一件事即可:**本 PR 没有改动 `packages/spec` 的两个仲裁器**,所以不触发合流闸的路径腿。其余按常规复核。 --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #19264
Clause-②: no
The defect
packages/plugins/plugin-audit/src/audit-writers.tsadmitsuseras a reference type (REFERENCE_FIELD_TYPES) and then required the materializedreferencecarrier, so atrackHistory'd{ type: 'user' }field authored without an explicitreferencewas planned for nobody and the timeline rendered the raw id.The contract says the opposite for exactly that type.
IMPLICIT_REFERENCE_TARGETS(packages/spec/src/data/field-value.zod.ts), re-read verbatim onorigin/mainat215840f43before this branch was cut:So the one spelling the contract calls complete was the one the reader refused, and it refused it silently: no refusal, no diagnostic, the field simply absent from the read plan.
referenceTargetOflives in that same module to supply the constant, and is the arbiterplugin-approvalsmoved to in PR #19263 /841a71e51.FOUR sites, not two — the card's site count is falsified
The card and the dispatch order both name two carrier-gated sites, the two read planners. Re-measured on this branch's own base rather than trusted, the carrier was read at four:
215840f43:472planTrackedLookupReads:527renderTrackedChangeSummary:629planMilestoneTokenReads:693renderMilestoneSummaryThe target is the KEY of the
objectName -> (id -> title)map, so it has two ends. Repairing only the two planners issues the read and then looks the result up under a key the plan never used — the timeline still shows the id, and now pays for a read it cannot use. That is measured below, not argued: the renderer-only ablation leaves every read-count assertion green and every rendered-string assertion red.Everything else the card states holds as given: the line ranges
:471-473and:628-630, and the reading that:434indisplayFieldValuedoes not gate on the carrier and is the consumer where the symptom surfaces rather than a defect site.The repair
One module-private helper,
referenceTargetForSummary, asksreferenceTargetOf, and all four sites use it.REFERENCE_FIELD_TYPESis untouched, so the admitted types staylookup/master_detail/user.treeis deliberately still out: it carries an author-chosen target, so admitting it would widen what the timeline tracks rather than repair what it silently dropped. Alookup/master_detailwhose author-chosen target is absent still names nothing and still issues no read.trim()and the renderers did not, soreference: ' crm_account 'produced two keys and no title. One helper trims once, for both ends.referenceTargetOfreads the carrier throughreferenceCarrierOf, which throws for an object- or array-valuedreference. All four call sites run insidewriteAudit's summary composition, which is not inside thetrythat guards the audit row write — measured: betweenwriteAudit's first line and its end there is exactly onetry, and it wrapspersistAuditTrailRowalone. An escapingTypeErrorwould therefore leave the hook, turning a display-enrichment miss into a failure on the audited write's own path. Such a carrier is left out of the plan exactly astypeof field.reference === 'string'already left it out: byte-identical behaviour for that class, not a new silence.reference: 'sys_user'resolves to the same target it always did.Tests — silence is the defect, so the read and the rendering are pinned separately
New
packages/plugins/plugin-audit/src/implicit-reference-target-summary.test.ts, 10 cases against the counting driver the sibling summary suites already use (reads counted, rows copied).Every implicit-target case asserts both halves: the
sys_userread delta (the half a happy-value assertion cannot see — before this change that read was never issued at all) and the rendered summary string (the half a read count cannot see — a plan-only repair pays for the read and still prints the id). The target is asserted throughreferenceTargetOf({ type: 'user' }), never a hand-copied'sys_user', so a second de-facto constant in this package would not satisfy it.Both planner sites are covered: the tracked-change branch through a tracked
userfield with noreference, and the milestone branch through a milestone token on an untrackeduserfield with noreference(that branch is deliberately not gated ontrackHistory).Four narrowness controls, so "the implicit target resolves" cannot be read as "everything resolves now": a tracked targetless
lookup(as a diff row and as a milestone token) stays unresolved and costs nothing; a trackedtreewith a perfectly readable target stays out of the type gate and renders its raw id; and a write that moves no reference field, on a row that HOLDS one, still pays zero reads.The second commit is that file's
finddouble, not the subject: see Gates below.Reverse verification — two ablations, both through
scripts/ablation-replace.mjsRe-run from the final committed state at
7ecf511d2, after the test file's last edit. The subject is resolved from SOURCE, not fromdist: the test imports./audit-writers.js, a relative specifier inside the package, so vitest transforms the mutated file directly and no build sits between the mutation and the run.Leg 1 — revert BOTH planner sites (anchor hit 2 times as declared, blob
9123759bc8e2tof6b9e89ac1a7):The three implicit-target cases go red on the READ:
expected +0 to be 1, three times. The plan is empty again, exactly as before the repair.Leg 2 — revert BOTH renderer sites, leaving both planners repaired (anchor hit 2 times as declared, blob
9123759bc8e2to90516141da24). This is the leg that measures the card's two-site reading:Every read-count assertion stays green and every rendered string goes red —
expected 'Owner: ∅ → usr_1' to be 'Owner: ∅ → Grace Hopper'andexpected 'Deal won by usr_1' to be 'Deal won by Grace Hopper'. The reads are issued and thrown away. That is the reported symptom, unrepaired, at a higher read cost.Both legs restored and proven restored by the tool: blob back to
9123759bc8e2, identical to HEAD,git diff HEADempty.The census triage asked for
Corpus: every
referenceCarrierOfandreferenceTargetOfcall site in the repo's package sources, plus every rawfield.referenceread, at7ecf511d2. Positive control on the same corpus:referenceTargetOfis a real, importable symbol with call sites inmetadata-protocol/src/protocol.ts,objectql/src/engine.ts,objectql/src/integrity/dangling-reference-audit.ts,objectql/src/record-title.ts,plugin-approvals/src/approval-service.tsandservice-analytics/src/dimension-labels.ts— so a zero below is a reading.A methodology note that is itself a finding. The first pass of this sweep used the pathspec
packages/**/src/**/*.ts, which under git's default (non-:(glob)) matching does not reachpackages/plugins/plugin-audit/src/audit-writers.ts— the very file this card is about. It returned a clean five hits. What caught it was a control: grepping for a string the defect file is known to contain and finding the file absent from the result. Every count below was re-taken without that pathspec.One third site, same silent shape — reported, deliberately not fixed (outside this card's file surface):
packages/metadata-protocol/src/seed-loader.ts:712,SeedLoader.buildDependencyGraph. The type gate islookup || master_detail || user—useradmitted — and the next line reads the carrier andcontinues when it is absent. A seed row whose{ type: 'user' }field carries noreferencetherefore contributes nodependsOnedge and is never pushed ontoreferences, so its natural key is written verbatim instead of being resolved to a record id. That consequence is named in this same function's own docblock, for the case it does handle: "every lookup / master_detail seed value was written verbatim … Dangling references break parent joins." Silent, user-visible, same misread contract. Dedupe words:seed-loader user implicit reference target·buildDependencyGraph referenceCarrierOf user·seed natural key written verbatim user field·metadata-protocol implicit target census.One near-site of a different shape — LOUD, reported for judgment:
packages/rest/src/rest-server.ts:10914, the public-form lookup picker. It has no type gate at all (any field the form opts in withpublicPicker), reads the carrier, and answers500 LOOKUP_TARGET_MISSINGwhen it is absent — so apublicPickeron a{ type: 'user' }field authored withoutreferenceis refused rather than silently dropped. Same misreading, opposite failure mode.PUBLIC_FORM_SERVER_MANAGED_FIELDSalready refuses server-managed anchors on this surface, so the reachable case is a hand-authored user field. Dedupe words:public form lookup picker user implicit target·LOOKUP_TARGET_MISSING user field·rest picker referenceCarrierOf implicit.The card's two unverified analytics readings — both CONFIRMED:
service-analytics/src/dimension-labels.ts:142already delegates toreferenceTargetOf. Correct today; no action.service-analytics/src/plugin.ts:748(relationshipResolver) gates onlookup || master_detail, sousernever enters the gate at all — materialized carrier or not — and falls through toreturn engine ? undefined : relationshipName. With an engine present thatundefinedmakes the dataset compiler reject the relationship. LOUD, and a different shape: a type-gate exclusion, not an implicit-target misread. No action.Cleared, each with its reason — every other carrier consumer in the corpus:
user, so the implicit target cannot arise:cli/src/commands/doctor.ts:726and:930(lookuponly),objectql/src/engine.ts:13113and:13568(cascade;master_detail/lookup),lint/src/validate-expressions.ts:395andlint/src/validate-sharing-rule-enforceability.ts:267(master_detailonly),verify/src/derive.ts:148(itsRELATIONALset is thelookup/master_detailspellings plustree), andspec/src/kernel/functional-completeness.ts:177— whoseRELATIONSHIP_TYPESislookup/master_detail, sofield/relationship-without-referencedoes not fire on a spec-completeuserfield.data-model-rules.ts:250,object-graph.ts:242,validate-field-consumers.ts:560,validate-object-references.ts:306,validate-security-posture.ts:292).Gates
Every exit code below was captured before any pipe. The derived set was re-derived at the final head
7ecf511d2and is byte-identical to the one derived atdce04112d.pnpm --filter @objectstack/plugin-audit test— exit 0, 25 files / 363 tests passed, at the final head.pnpm --filter @objectstack/plugin-audit typecheck— exit 0. The leg count was read from the package, not assumed: itstypecheckscript istsc --noEmit && tsc --noEmit -p tsconfig.scripts.json && pnpm check:test-typecheck, three legs, all run. The test-layer ledger is unmoved —0 file(s) / 0 error(s) / 0 pinned signature(s)— and this package carries notest-typecheck-debt.jsonof its own, so nothing was touched there.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 61 families from a change set of 3 paths (three-dot semantics, merge base215840f43), 542 changed lines, under the 5,000 human-merge threshold. All 61 were run.pnpm check:objectql-double-limitexit 1: the new test file's countingfinddouble was UNJUDGED, because the gate lifts the double and drives it with a row stub in place of every non-function declaration, and(reads.findOn[object] ?? 0) + 1throws on that stub before the body can be judged. The double now reads its counter by TYPE and applies the caller'slimitby PRESENCE, after the filter and before the rows are copied. Re-run: exit 0, and the census moved the right way — 393 graded (was 392), 195 applying the bound (was 194), 39 proving the bound precedes a row-touching stage (was 38), 55 unjudged (was 56), withbaseline key set verified against 215840f: no files added.check:dual-build-cjs-loads,check:i18nandcheck:type-check-debt. Each named the same prerequisite ("this gate reads built output, and some package has no dist/ … Runpnpm buildfirst. ⛔ This is NOT a pass: nothing was measured"). The prerequisite was cleared with a whole-treepnpm build(exit 0, 73/73 tasks) and each re-ran to exit 0. None is reported as a zero.check:objectql-double-limit,check:type-check-debt,check:type-check-coverage,check:test-source-alias,check:cross-package-test-inputs,check:nul-bytes,check:tier-file-adoption,check:engine-double-contract,check:driver-memory-census,check:where-matcher,check:published-files,check-empty-changeset,check-changeset-no-major,check-adr-0087-registration,check-closing-keyword-parity— all exit 0.pnpm lint(eslint . --no-inline-config, the repo's only style authority) — exit 0 over the whole tree, so no narrowing is being claimed and no narrowing evidence is owed.grep os-regen .gitattributes, read on the spot: every merge-driver-managed path is underpackages/spec/,docs/,content/docs/,skills/orscripts/platform-object-tenancy-census.json. No path in this diff is driver-managed.check:nul-bytes, over all three changed files:grep -naPfor the C0 range plus DEL — no hits.packages/plugins/plugin-audit/package.jsoncarries noprivatekey and shipsfiles: ["dist", "README.md", "CHANGELOG.md"], so a fix here publishes. Apatchchangeset is included.Verdict lines transcribed verbatim, because those logs die with the worktree
From
pnpm check:engine-double-contract(exit 0):From
node scripts/check-changeset-no-major.mjs --base origin/main(exit 0):That second one is a local-run artefact by construction: on this PR the axis has its input, and the declaration it will read is the
Clause-②: noline at the top of this body.Not this PR
packages/specis read-only on this lane:referenceTargetOfis imported and nothing in that package is edited. No release-notes page is touched.plugin-approvalswas repaired in PR #19263 and is out of scope here. The two census findings above are reported for the seat to file and deliberately not fixed here — that is the difference between two bugs and one systemic gap.Acceptance notes
noted, not filed— the sibling summary suites (audit-lookup-summary.test.ts,audit-milestone-summary.test.ts,audit-option-label-summary.test.ts,audit-bound-previous.test.ts) each hold the same counting driver in its pre-repair, UNJUDGED shape and are grandfathered inscripts/objectql-double-limit.baseline.json. The one-line change made here would shrink that ledger by four entries. Not done in this PR: it is unrelated to the card and would put four files this card has no business in on the diff. Carrier: any later PR editing those suites.noted, not filed—renderTrackedChangeSummaryandrenderMilestoneSummaryboth build anoptionLabelForclosure per field per write. Ordinary allocation on a hot path; no defect, no repro, no contract text. Carrier: none — no open PR or queued card touches those two functions.noted, not filed—referenceTargetOfanswers a whitespace-only carrier (reference: ' ') with that whitespace rather than falling through to the type's constant, becausereferenceCarrierOftreats only the empty string as absence. The helper added here trims afterwards, so the audit path's behaviour is unchanged in both directions; making the arbiter itself trim is apackages/specquestion and this lane is read-only there. Carrier: none observed.Generated by Claude Code