Repository navigation
fix(spec): the stored-filter conversion's TODO for a null-valued key is true on every block, and no longer says to drop the key - #20709
Conversation
…s true on every block `page-component-filter-record-to-rule-array` declines a record-form filter with a null-valued key. Its reason said the renderer skips that key, so it constrains nothing, and to drop it. At the objectui pin that holds only on a block that queries an object; on a block whose rows are inline, `ValueDataSource.find` matches the key and selects the rows whose value is null, so dropping it widens the block. The reason, its docblocks and the protocol-18 D3 entry now state both behaviours, name the `is_null` rule for the rows with no value, and leave which rows to select to the author. The verdict does not move. Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 137 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 d66495a5d4dbaa11130925990f31259b5c6602de && git checkout d66495a5d4dbaa11130925990f31259b5c6602de
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b291fcdae9ac6dd4152367082905cfe81ddf5fb0 eaf2d6e6e8d55e3d164ed72a786ee7d260997a6b && git checkout -B drift-repro b291fcdae9ac6dd4152367082905cfe81ddf5fb0 && git merge --no-ff eaf2d6e6e8d55e3d164ed72a786ee7d260997a6b
node scripts/docs-audit/affected-docs.mjs --json b291fcdae9ac6dd4152367082905cfe81ddf5fb0
|
…he key, not that it constrains nothing
`page-component-filter-record-to-rule-array` declines a record-form filter
whose key is an empty operator object (`{ amount: {} }`). Its reason said the
object constrains nothing. At the objectui pin the renderer refuses it:
`convertFiltersToAST` throws INVALID_FILTER (400) through
`refuseEmptyOperatorMap` where a block queries an object, and
`ValueDataSource.find` answers no rows through `zeroKeyConditionRefusal`
where a block's rows are inline. The reason and its docblock now say so and
keep the renderer's own remedy, dropping the key. The verdict does not move.
Also corrects a test comment that named `owner_id: null` for a row that is
`deleted_at: { $null: true }`.
Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #20662 (body, triage direction 5893989151, claim 5896419836, round-1 report 5897718691, seat note 5897754447, round-2 report 5898670276), PR #20709 (body, comments, five-file list, net diff against ① Derived judgments1. The null-key declined reason (
2. The 3. The conversion entry docblock's parenthetical ("skipped where a block queries an object, matched where its rows are inline — no one rule keeps both"). RIGHT. 4. The D3 entry 5. The empty-operator-object reason and its docblock sentence. "names the field and no operator, so no rule spells it" — RIGHT. "where the block queries an object, it refuses the filter ( 6. Verdict unchanged. The 7. Test comments ( Gate coverage (42 check-runs on
② Semver level
Clause-②: the PR body reads ③ Boundary flagsDev flags, round 1 (5897718691):
Dev flags, round 2 (5898670276):
ESCALATED to the seat:
FAIL reason (one):
Implemented-by: VERDICT: FAIL Generated by Claude Code |
… objectui pin
The null-valued-key TODO reason, the changeset and the protocol-18 D3 entry
listed the inline row sources as `data: { provider: 'value' }`, a `data`
array or `staticData`. At the objectui pin a bare `data` array reaches no
`ValueDataSource.find`: `object-calendar` draws it as pre-fetched rows with
no filter applied, and `object-map` / `object-gantt` do not take it as a
record source. The list now names `data: { provider: 'value' }` and
`staticData` only; `migrations/registry.ts` is regenerated. Nothing else
moves.
Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
Co-authored-by: Claude <noreply@anthropic.com>
…r reaches at the objectui pin
The rationale under the conversion's `## Reach` verdict said the in-memory
renderers match the inline rows of every listed source. At the objectui pin
they take those rows from `data: { provider: 'value' }` or `staticData`; a
bare `data` array reaches none of them (`object-calendar` draws it unfiltered,
`object-map` / `object-gantt` do not take it as a record source). Comment text
only; the verdict sentence is unchanged.
Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Re-judging after the FAIL at ① Derived judgments1. The previous FAIL reason is cleared. "a 2. The remaining list is true at the pin. 3. The calendar precision boundary the dev flags (round 3) — judged: the un-qualified list does not over-claim. On 4. The 5. The D3 entry 6. Round 4's 7. The empty-operator-object reason and its docblock sentence. Unchanged since the previous review; re-read at the pin: 8. Verdict unchanged; pins on named subjects. The 9. The merges lost nothing. Five merges of 10. Not this PR's text. Gate coverage (41 check-runs on
② Semver level
Clause-②: the PR body reads ③ Boundary flagsDev flags, round 3 (5900465814):
Dev flags, round 4 (5900584237):
Everything the previous review judged right is still right at this head: the null-key and empty-operator reasons, the ESCALATED: none. FAIL reasons: none. Implemented-by: VERDICT: PASS Generated by Claude Code |
…o the commits that decided them (objectstack-ai#20713) Part of objectstack-ai#20594 Clause-②: no ## What changed This is stage 7 of the `domain:cli` lane of the dead-citation sweep: `packages/mcp/src`. Every comment site there that cited a tracker number answering 404 now cites, in ruling C+D's form C (comment 5749154545 on objectstack-ai#19123), the commit in this repository's history that decided what the line describes, and keeps saying in its own words what that commit decided. PR objectstack-ai#20533 is the method, and stages 1 to 6 of this card (PR objectstack-ai#20624, PR objectstack-ai#20632, PR objectstack-ai#20656, PR objectstack-ai#20673, PR objectstack-ai#20689, PR objectstack-ai#20703) are the precedents. The card stays open for the lane's remaining packages, so this PR says `Part of`. That is **17 sites on 17 lines in 9 files, covering 9 numbers**, rewritten to **9 distinct commits**: - the census's **10 sites**, in `mcp-server-runtime.ts` (5), `plugin.ts` (3) and `stdio-data-bridge.ts` (2), 7 numbers; - **7 test-file comment sites** in 6 test files (the census defers `*.test.ts`; stages 1 to 6 took test comments too). One more line changed: `__tests__/plugin-execution-context.test.ts:7`, the second half of the `:6` sentence ("this face was not in that card's inventory" now reads "not in that commit's inventory", since the card it pointed back to is now named as a commit). Only comments changed: **18 lines out, 18 in**, and every touched file keeps its line count, so no line citation into these files moves. **No citation number is added**: over the 18 line pairs, added-minus-removed numbers is empty, and no PR number stands newly on any line. No ADR or ruling-record file in `docs/adr/` or `scripts/adr-anchors/` records any of these 9 decisions (a grep for the 9 numbers there reads 0 hits, with a control number from the same tree reading 2), so every anchor is a commit. **No changeset, and `skip-changeset`:** none of the rewritten comments reaches `dist` (measured below: base and head emit six byte-identical files, and a code-mutation control changes four of them). That is stage 5's case (PR objectstack-ai#20689), not stage 6's. ## Census: `packages/mcp`, before and after **Instrument.** The gate's own `node scripts/check-issue-citations.mjs --census --json`, read-only and unchanged, run under `with-fleet.sh --read` for the token. The count is its `allocated-but-absent` findings under `packages/mcp/`. Both runs enumerated the whole board. | reading | tree | board | whole-repo `allocated-but-absent` | `packages/mcp` sites | lines | numbers | files | |---|---|---|---|---|---|---|---| | before | base `e4e5222b7b`, run 2026-09-29T19:45:33Z to 19:50:37Z | enumerated, 186 pages, frontier objectstack-ai#20708, 18,535 numbers | 1,222 | **10** | 10 | 7 | 3 | | after | `459ff81088`, run 19:58:39Z to 20:02:47Z | enumerated, 186 pages, frontier objectstack-ai#20709, 18,536 numbers | 1,212 | **0** | 0 | 0 | 0 | The whole-repo drop of 10 is exactly these sites: a site-by-site diff of the two JSON outputs has 10 findings gone, all under `packages/mcp/src`, and none added. The other three tallies (`resolves` 32,968, `resolves-as-pull-request` 1,984, `cross-repo-unjudged` 994) are equal in both runs. `packages/mcp/src` is byte-identical at `459ff81088` and at the head. **Supplementary scan (test files included).** The gate's exported `extractCitations` and `classifyCitation` over all 43 `.ts` files under `src/`, with the board from the gate's own `probeBoard`: 365 citations and 21 dead before (src comments 10, test comments 7, src strings 0, test strings 4), 348 and 4 after (0, 0, 0, 4). Its before list of src comment sites is identical to the census's. The 4 left are test titles, the form-D stage (see Acceptance notes). ## Per-site table `git blame` at the base ties each line to the commit that wrote it, and each anchor was read in its message, changeset or diff, not only its subject. Where the pull request that landed an anchor still answers, its body's first line names the dead number, which is noted. | number | sites (base line) | anchor: what it decided | |---|---|---| | `objectstack-ai#13318` | `mcp-server-runtime.ts:272` | `3ec8646f1`: the bridged tools' `readOnlyHint` / `destructiveHint` come from what the definition declares, and a tool that declares nothing is served neither hint (omit-when-unsourced). The line blames to `c39369d12`, the `openWorldHint` sibling, whose changeset calls this the repair "that preceded it". The PR that landed `3ec8646f1` answers 404 too. | | `objectstack-ai#6724` | `mcp-server-runtime.ts:625`; `mcp-server-runtime.metadata-outage.test.ts:289` | `4f3d2322e`: corrects `diagnoseEmptyRead`'s falsified claim that `MetadataFacade.getObject` differs from `get('object', n)`, in the TSDoc and in the outage test's restatement of it. Both lines blame to it; PR objectstack-ai#6948, which landed it, names objectstack-ai#6724. | | `objectstack-ai#6745` | `mcp-server-runtime.ts:636` | `7a5ef0008`: adds `metadata-service-getobject-equivalence.test.ts`, pinning `getObject(n)` equal to `get('object', n)` across all three implementations. The line's "PR objectstack-ai#6839 for objectstack-ai#6745" named this commit's PR (answers 200), which stays beside the sha as a convenience link. The spec lane gave the number this anchor. | | `objectstack-ai#6723` | `mcp-server-runtime.ts:637`, `:652`; `mcp-server-runtime.metadata-outage.test.ts:293` | `8ad609c69`: declares on `IMetadataService.getObject` that it answers the same as `get('object', name)`. `objectstack-ai#6723` was the pull request that landed as this commit (its subject carries the number); `objectstack-ai#6505`, the issue beside it on `:637`, answers 200 and stays. The spec lane gave the number this anchor. | | `objectstack-ai#17114` | `plugin.ts:8`, `:67`; `stdio-tenancy-posture-api-key-matrix.test.ts:569` | `4af758d47`: the last two admission doors, this one included, classify the tenancy rejection through the shared `classifyAdmissionTenancyPosture`. All three lines blame to it; PR objectstack-ai#17683 names objectstack-ai#17114, and stage 1 gave the number this anchor. | | `objectstack-ai#6216` | `plugin.ts:126`; `__tests__/plugin-execution-context.test.ts:6` | `f586f1a89`: one `ExecutionContext` assembler for the dispatcher, REST and share-link sites. Both lines blame to `502dc6fe7`, which converged this stdio face afterwards and names that convergence as its precedent. Its file list touches no `packages/mcp` file, which is what `:7` ("not in that commit's inventory") says. Stages 1 and 2 and the spec lane gave the number this anchor. | | `objectstack-ai#8422` | `stdio-data-bridge.ts:85`, `:394`; `stdio-data-bridge.not-found.test.ts:4` | `4810dd628`: the stdio bridge's by-id write seams throw the shared `recordNotFoundError` envelope instead of a bare `Error`. All three lines blame to it; PR objectstack-ai#8507 names objectstack-ai#8422. | | `objectstack-ai#17568` | `mcp-record-id-key-mistake-refusal.test.ts:4` | `9c9e6d08f`: pins that a missing-`recordId` refusal also names the `id` the caller sent (test-only). The line blames to it; PR objectstack-ai#17650 names objectstack-ai#17568. | | `objectstack-ai#13486` | `mcp-tool-bridge-safety-annotations.test.ts:423` | `6193e576d`: pins the bridge's two hand-copied safety name sets in the direction the old pin could not see (the docblock's heading is that commit's subject). The line blames to it; PR objectstack-ai#13888 names objectstack-ai#13486. | **Anchor checks.** Every cited sha matches exactly one object (`git rev-parse --disambiguate`, count 1 for each of the 9), is a commit, has one parent, and is an ancestor of `main` (`merge-base --is-ancestor` against `5757463712`, exit 0 for all 9). The checkout is not shallow. The control leg `979ad9575` (2026-08-08, the parent of the oldest anchor `8ad609c69` of 2026-08-08) exits 0, and the negative control, this branch's own `459ff81088`, exits 1. Four anchors reuse the landed stages' (`f586f1a89`, `4af758d47`, `7a5ef0008`, `8ad609c69`), so each number carries one anchor across the tree; five are new (`3ec8646f1`, `4f3d2322e`, `4810dd628`, `9c9e6d08f`, `6193e576d`). **Numbers.** All 9 dropped numbers answer 404 by REST (re-probed 2026-09-29T19:54Z). The numbers kept on changed lines (`objectstack-ai#6839`, a pull request; `objectstack-ai#6505`, `objectstack-ai#15348`, `objectstack-ai#16013`, `objectstack-ai#4435`, `objectstack-ai#5138`, `objectstack-ai#7867`) answer 200. Four slash-joined groups stand in `packages/mcp/src`, whose later halves the citation grammar does not read (`objectstack-ai#4435/objectstack-ai#5138/objectstack-ai#7867` twice, `objectstack-ai#5138/objectstack-ai#5581`, `objectstack-ai#7728/objectstack-ai#7823`); every half answers 200, so none is dead. ## Mechanical guard: no code token moves **H2 holds on both readings: the parser leaf-token diff is empty, and the emitted `dist` is byte-identical.** **Token guard.** It compares the TypeScript parser's leaf tokens (TypeScript 6.0.3, JSDoc nodes excluded) of the 9 touched files at base `e4e5222b7b` and at `459ff81088`. Controls mutate the head text in memory only. - Real run: 21,192 base tokens, 0 differing (exit 0). - Comment-insertion control: 0 differing (exit 0). - Code-insertion control: all 9 files differ at token 0 (exit 1). - String control (the first character of the `'vitest'` import specifier in `plugin-execution-context.test.ts` flipped): exactly 1 differing `StringLiteral`, at token 15 of that file (exit 1). **Emitted `dist`.** `pnpm --filter @objectstack/mcp build` at the head, then at base (the base tree of `packages/mcp/src` restored in place under a trap-armed restore; an on-disk probe read `objectstack-ai#13318` 1 and `commit 3ec8646` 0 before that build; afterwards every touched blob equals its HEAD blob and `git diff HEAD` is empty), with the same dependency builds: - all six files (`index.cjs`, `index.cjs.map`, `index.d.cts`, `index.d.ts`, `index.js`, `index.js.map`) are **byte-identical** by sha256. The built files do carry docblocks (14 in `index.js`, 78 in `index.d.ts`); none of the rewritten ones is on an emitted declaration. - Code-mutation control (`scripts/ablation-replace.mjs`, anchor: the sync leg's typed `ctx.getService` call on `'tenancy'` in `plugin.ts`, hit 1 to 0, its argument renamed to a marker; blob restored to HEAD `0a1aaa7955`, `git diff HEAD` empty): `scripts/ablation-dist-preflight.mjs` found the marker in `index.cjs` and `index.js`, and `index.cjs`, `index.js` and both `.map` files differ from the head build. `dist` was then rebuilt, its six sha256 values equal the first head build, and the preflight in `--absent` mode reads the marker absent from all 6 files with a clean tree. A raw scan of the 9 changed files for control bytes finds none (a positive probe on a scratch file matched). ## Changeset **None, and `skip-changeset`.** `@objectstack/mcp`'s `files[]` is `dist`, `README.md` and `CHANGELOG.md`, and the build above emits byte-identical `dist` at base and head, so this diff publishes nothing from any released package. Stage 5 (PR objectstack-ai#20689) measured the same and shipped the same; stage 6 (PR objectstack-ai#20703) measured the opposite and carried a `patch`. ## Gates (head `7a0f15de62`) This host has no `flock`, so `os-verify-lock.sh` ran in its declared unlocked mode. Its disclosure, verbatim, from each run at this head and from the four `dist` builds (at `459ff81088`, `packages/mcp/src` byte-identical to this head): ```text os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 25s · declare it in the PR body · pnpm --workspace-concurrency=2 --filter '@objectstack/mcp...' build os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 116s (1m56s) · declare it in the PR body · pnpm turbo run build --filter='./packages/*' --filter='./packages/*/*' --concurrency=2 os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 10s · declare it in the PR body · pnpm --filter @objectstack/mcp exec vitest run --maxWorkers=2 os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 4s · declare it in the PR body · pnpm --filter @objectstack/mcp typecheck os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 3s · declare it in the PR body · pnpm --filter @objectstack/mcp build os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 3s · declare it in the PR body · pnpm --filter @objectstack/mcp build os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 3s · declare it in the PR body · pnpm --filter @objectstack/mcp build os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 2s · declare it in the PR body · pnpm --filter @objectstack/mcp build ``` - **Build:** `@objectstack/mcp` with its closure (9 of 81 workspace projects), then the whole workspace, `turbo run build --filter='./packages/*' --filter='./packages/*/*'`, 71 of 71 tasks, after the merge. The tree was clean after both. - **Tests:** `vitest run`: 32 files, 344 tests passed (every `*.test.ts` under `src/`), at the head and before the merge. - **Typecheck:** `pnpm --filter @objectstack/mcp typecheck` exits 0. `tsc --listFiles`: `tsconfig.json` compiles the 11 non-test `src` files, `tsconfig.test.json` all 43 including the 32 test files. `check:test-typecheck`: 6 files, 53 errors, 8 pinned signatures, held. - **Lint:** the repo-wide `pnpm lint` (`eslint . --no-inline-config`) exits 0 at this head (2026-09-29T20:18:54Z to 20:19:23Z), and at `459ff81088` before the merge. - **Citation judging:** after merging `origin/main` (`9b384f63ae`), `node scripts/check-issue-citations.mjs --base 9b384f6` judges 5 citations on the changed lines of 3 files (the kept numbers `objectstack-ai#15348`, `objectstack-ai#16013`, `objectstack-ai#4435`, `objectstack-ai#6505`, and `objectstack-ai#6839` as a pull request) and exits 0: every one resolves. Against `origin/main` after it moved to `5757463712`, the same 5 citations, exit 0. - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 53 families. All 53 exit 0, and `--ran` with the exit-coded record reads "53 derived, 53 run, 0 NOT-MEASURED, 0 UNRUN" (a derived zero). Among them: `check:issue-citations`, `check:doc-authoring` (808 pinned sites, no growth), `check:nul-bytes` (9,331 files, no raw control bytes), `check:published-files`, `check:type-check-debt`. - **Artifact rosters:** 36 of the 39 non-self-test roster rows exit 0, including the three the derivation marks as keeping their roster under one of this diff's paths (`check:authz-resolver`, `check:error-code-casing`, `check:filter-alias-parity`). The other three need a pull request's context; they are run against this PR once it exists and reported on the card. The 18 self-test-only rows grade their checkers' fixtures and cannot judge this diff. ## Hypotheses (measured first) - **H0 holds.** At base `e4e5222b7b` the filtered census answers 10 sites on 10 lines, 7 numbers, in 3 files, as on the seat's `0be898499f`. The whole-repo count is 1,222. - **H1 holds.** After the rewrite, the filtered census answers 0 for `packages/mcp`. No site was left for an open PR (the file lists of all 8 open PRs were read at 20:08:05Z: only the Version Packages PR objectstack-ai#20639 touches `packages/mcp`, in `CHANGELOG.md` and `package.json`) or for an unfound anchor. - **H2 holds, on both readings.** The comment-stripped (parser-token) diff of all 9 touched files is empty with its controls firing, and the emitted `dist` is byte-identical at base and head with a code control that changes it. ## Acceptance notes - **Test titles, the form-D stage.** 4 dead numbers remain in test string literals in `packages/mcp/src` (`describe` titles, no assertion text): `objectstack-ai#17568` twice in `mcp-record-id-key-mistake-refusal.test.ts` (`:151`, `:315`), `objectstack-ai#8422` in `stdio-data-bridge.not-found.test.ts:99`, `objectstack-ai#17114` in `stdio-tenancy-posture-api-key-matrix.test.ts:592`. They stay on the card for its form-D stage; no string moved here. - **Outside `src/**`, a later stage of the card:** `packages/mcp/vitest.config.ts:18` cites `objectstack-ai#8651` (404). `packages/mcp/test-typecheck-debt.json:2` cites `objectstack-ai#13470` (404) inside its `_comment` field, which the file itself says is generated by `scripts/check-test-typecheck.mts`, so a fix there is at that producer, in the `scripts/**` lane, not a hand edit. The other citations in `packages/mcp` outside `src/**` (`CHANGELOG.md` excluded) answer 200. - **Card-word residue, cited nowhere.** A few docblocks still say "this card" or "the card" a paragraph away from the rewritten line (for example `stdio-data-bridge.not-found.test.ts:19`, `mcp-record-id-key-mistake-refusal.test.ts:19`, `stdio-tenancy-posture-api-key-matrix.test.ts:580`, `:584`). They cite no number, so they were left, as the landed stages left theirs; only the one same-sentence companion (`plugin-execution-context.test.ts:7`) was changed. - **The moving `origin/main`.** The branch merged `origin/main` once (`7a0f15de62`, merging `9b384f63ae`: `service-storage`, `platform-objects` and `plugin-audit`, nothing in `packages/mcp`). A later fetch advanced the shared ref to `5757463712`, one commit in `platform-objects` translations. There was no second merge; CI judges the merge ref. ## Deviations - **One companion line (`plugin-execution-context.test.ts:7`)** beyond the 17 sites, the second half of the `:6` sentence. - **Commit trailers** are AGENTS.md's model-free pair (`Claude-Session` plus `Co-authored-by: Claude`), and the pre-push trailer check passed on every push. The harness's attribution reminder asked for a model-named trailer and a different PR footer, and AGENTS.md overrides it. The merge commit carries git's default message. --- _Generated by [Claude Code](https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289)_ Co-authored-by: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
…ommits that decided them (objectstack-ai#20717) Part of objectstack-ai#20596 Clause-②: no ## What changed This is the seventh stage of the `domain:services` lane of the dead-citation sweep. It covers `packages/plugins/plugin-approvals/src/**` and nothing else. By the seat's census at the claim (`5897866351`), it is the largest package in the lane that no in-flight work holds. Later stages cover the other packages, so this PR says `Part of` and the card stays open. Every comment or docblock site in scope that cited a tracker number answering 404 has been rewritten in ruling C+D's form C (comment 5749154545 on objectstack-ai#19123), by the method of stages 1 to 6 (PR objectstack-ai#20609 as `422db788a`, PR objectstack-ai#20626 as `b80ab579d`, PR objectstack-ai#20634 as `4d04b6be3`, PR objectstack-ai#20658 as `9a4b2bb38`, PR objectstack-ai#20693 as `0e9ad74fb`, PR objectstack-ai#20708 as `9b384f63a`). That is **41 sites on 38 lines in 13 files, covering 14 numbers**: - 24 census sites (every census site this package has); - 15 sites in test comments, which the census defers; - 2 sites the gate's citation grammar cannot see, found by a raw scan (see Acceptance notes): the second number of `objectstack-ai#8287/objectstack-ai#8778` (`approval-node.test.ts:462`) and 「the option objectstack-ai#8710 rejected」 (`approval-service.ts:2329`), which the gate reads as an option ordinal. Each rewritten line now cites the commit in `origin/main` history that decided what the line describes, and says in its own words what was decided: **13 distinct shas**. No number in this package has an ADR or ruling record of its own in the repository (a grep of `docs/adr/` for all 14 finds none, and a grep of the rest of `docs/` finds only an audit that names `objectstack-ai#11311` as evidence), so every anchor is a commit, per ruling C's order. No number was dropped. Only comments changed. Every touched source file keeps its line count (41 lines out, 41 in, over 13 files), so no line citation into these files moves. 3 of those 41 lines hold no dead citation: 1 reflow line and 2 lost-referent lines, listed under Wordings below. No code token moves (see the guard below). **No citation number is added.** Every tracker number on an added line was already on the line it replaces: `objectstack-ai#8613` (`approval-service.ts:2295`, `:2363`, `approval-service.test.ts:751`), `objectstack-ai#8287` (`sys-approval-request.object.ts:138`, `approval-node.test.ts:462`), `objectstack-ai#10101` (`backfill-platform-row-organizations.ts:9`) and `objectstack-ai#12069` (`translations/index.ts:26`). Each answers 200. Over the whole diff, added minus removed is 0 or negative for every number, and no number is new to the diff. No PR number stands on an added line; the one `PR #N` spelling in scope (`backfill-platform-row-organizations.ts:9`) became its squash commit. Five dead sites are left on purpose, all of them test strings (see the list below). One more file: a `patch` changeset for `@objectstack/plugin-approvals`, because the rewritten docblocks and inline comments ship (see Changeset below). ## Census: `plugin-approvals`, before and after **Instrument (A1).** The gate's own `node scripts/check-issue-citations.mjs --census --json`, read-only and unchanged. The count below is its `allocated-but-absent` findings under `packages/plugins/plugin-approvals/`. Each run counts as a reading only because its board frontier equals the newest issue number, read by a separate request just before and just after the run. | reading | tree | board | whole-repo `allocated-but-absent` | plugin-approvals sites | lines | files | numbers | |---|---|---|---|---|---|---|---| | before | base `575746371`, run 2026-09-29T20:15:05Z to 20:18:28Z | enumerated, 186 pages, frontier objectstack-ai#20709 (newest objectstack-ai#20709 before and after), 18,536 numbers | 1,195 | **24** | 22 | 6 | 10 | | after | head `e698d2393`, run 20:28:39Z to 20:31:58Z | enumerated, 186 pages, frontier objectstack-ai#20716 (newest objectstack-ai#20714 before, objectstack-ai#20716 after), 18,543 numbers | 1,171 | **0** | 0 | 0 | 0 | The before count matches the seat's census at the claim and A1 (24 sites). The whole-repo drop is 24, exactly this diff's census sites. The `resolves` tally is 32,971 in both runs, and `resolves-as-pull-request` (1,984) and `cross-repo-unjudged` (995) did not move either. The after run was taken on `e698d2393`; the head `708244c2b` adds only the changeset. No run was truncated or discarded: both enumerations read 186 pages at the newest frontier. **Supplementary instrument, the whole scope.** The census does not read test files or strings, and this stage's scope includes test comments. So a second reading runs the gate's own exported `extractCitations` (whole-file and comment-prose projections) and `namesThisRepository` over every `.ts` file under `plugin-approvals/src` (76 files). It takes its verdicts from the before census's own board reading rather than from a second enumeration: a number is dead when that census reported it `allocated-but-absent`, and alive when that census judged it on this board anywhere (its `--list` extraction) and did not report it. The 18 numbers the census never saw, because they stand only in test files or strings here, were read one by one on the issues endpoint: 14 answer 200, and `objectstack-ai#8863`, `objectstack-ai#11081`, `objectstack-ai#11286` and `objectstack-ai#11308` answer 404. | reading | citations | dead | src comment | test comment | src string | test string | |---|---|---|---|---|---|---| | before, `575746371` | 981 | **44** | 24 | 15 | 0 | 5 | | after, `e698d2393` | 942 | **5** | 0 | 0 | 0 | 5 | Its src-comment column equals the census's 24, which is the control on the second instrument. The 902 live citations and the 32 cross-repo citations are the same in both readings, and the drop of 39 citations is exactly the rewritten sites the gate grammar sees. Three extracted tokens are not citations and stay unjudged in both readings: `&objectstack-ai#39;` (an HTML entity) and two CSS colours, all in `action-link-pages.ts` string literals. A third, raw reading (every `#` followed by 2 to 6 digits, whatever surrounds it) finds 46 dead occurrences before and 5 after; the 2 it sees beyond the gate are the two gate-invisible sites above, and its residue equals the gate's residue site for site. ## Per-number table Sites and files count every dead occurrence in scope at the base (comments and strings, tests included). `rewritten / left` counts the sites rewritten and the sites left. Each anchor was read in its message and diff, not only its subject, and `git blame` at the base puts every rewritten line in its anchor commit or in a later commit that descends from it (`merge-base --is-ancestor` exit 0 for each pair). | number | sites / files | rewritten / left | anchor: what it decided | |---|---|---|---| | `objectstack-ai#16709` | 10/2 | 8/2 | `8c7cca1ce`: the three residues of the stranded-inspection contract review. Item 2 (the PM ruling of 2026-09-08) keeps a row whose third read threw in the report as the undifferentiated `failed`; item 3 moves `refineFailedRunState` inside the `try`, so a malformed host verdict costs only its own row. Its message numbers the items, which is why the lines keep 「item 2」 and 「item 3」. New to the sweep | | `objectstack-ai#8710` | 6/2 | 6/0 | `04d03c3a0`: a deactivated `sys_position` confers no sharing-rule shares, filtered at the sharing call site and never inside the addressing primitive. Its message quotes the 2026-08-15 ruling verbatim, the same sentence the quoted blocks here carry, and its diff writes the 「a name with no row is untouched」 fallback that `approval-service.ts:2318` quotes. Stage 2's anchor, and `plugin-sharing/src/position-graph.ts:42` already reads 「objectstack-ai#8613 / commit 04d03c3」 | | `objectstack-ai#6523` | 4/3 | 4/0 | `aa4b90d9a`: the 36 enforcement signatures, `IApprovalService` among them, converged onto the full `ExecutionContext`. Its subject names it. Stages 2 and 6 and the spec stage's anchor | | `objectstack-ai#6206` | 3/3 | 3/0 | `aa4b90d9a`: the same commit, whose body applies 「the objectstack-ai#6206 ruling default (converge on the full envelope, keep no per-site subset contracts)」. Written as the full-envelope ruling, the form stages 2 and 6 used | | `objectstack-ai#8778` | 4/4 | 4/0 | `7901b2dd2`: the stamp-only `tenancy.organizationField`, Option A of the maintainer's ruling, declared on `sys_api_key` as `active_organization_id`. The spec, `plugin-security` and `service-storage` stages' anchor | | `objectstack-ai#11081` | 5/1 | 5/0 | `c28e4cfae`: the two SqlDriver-backed fixtures of `objectstack-ai#11081` stop muting their kernel and pin the expected read-refusal noise with the runtime's shared capture. Its diff writes all five `[objectstack-ai#11081]` tags. New to the sweep | | `objectstack-ai#11286` | 5/1 | 2/3 | `b019891cd`: the contract test that pins the two `managerIsProvablyOutsideOrg` screens to equal verdicts. Its subject names it. New to the sweep | | `objectstack-ai#11674` | 2/1 | 2/0 | `1cba33f16`: the seed loader warns at load time when a seed defers a required column, and the ordering constraint is documented at the four pointer-pair sites, this object among them. Stage 2's anchor for the same paragraph | | `objectstack-ai#12493` | 2/2 | 2/0 | `aa5994e17`: the Operation Message Catalog gains `approval_recall_not_submitter` (and `record_write_denied`) ahead of their emitters. Its diff names `objectstack-ai#12493` throughout. Stage 2's anchor | | `objectstack-ai#8707` | 1/1 | 1/0 | `1408fe385`: audit rows are stamped from the record's own organization, which its message says the maintainer's ruling on `objectstack-ai#8287` requires; the line keeps 「honouring objectstack-ai#8287's ruling」. New to the sweep | | `objectstack-ai#8863` | 1/1 | 1/0 | `d200b016b`: the two negative pins that assert the unfiltered position expansion on the approvals side. Its body names `objectstack-ai#8863`. New to the sweep | | `objectstack-ai#11308` | 1/1 | 1/0 | `5a916c4d4`: the one-off platform-row organization backfill, dry run and write, which its body calls the `objectstack-ai#11308` sweep. New to the sweep | | `objectstack-ai#11311` | 1/1 | 1/0 | `1272f0a6b`: the squash commit of the pull request that was `objectstack-ai#11311` (its subject carries the number), which moved the resolver to `metadata-core` and made the approval and automation-run writers stamp the subject's organization. New to the sweep | | `objectstack-ai#11671` | 1/1 | 1/0 | `09b4f4e4e`: the source-hashes provenance companion. The identical `translations/index.ts` line in `service-messaging`, `plugin-sharing` and `plugin-security` already cites it | Every cited sha matches exactly one commit (`git rev-parse --disambiguate`, count 1 for each of the 13), and every one is an ancestor of the base (`merge-base --is-ancestor`, exit 0 for all 13; the history is complete, `--is-shallow-repository` false, 15,129 commits). Each of the 14 numbers answers 404 on the issues endpoint, read one by one; `objectstack-ai#11311` answers 404 on the pulls endpoint too. ## Wordings to check - **The full-envelope ruling, `approval-node.ts:29`, `approval-service.ts:52-53` and `exec-context-annotation.pin.ts:7-8`.** 「since objectstack-ai#6523 (the objectstack-ai#6206 ruling …)」 became 「since commit aa4b90d (the full-envelope ruling …)」, word for word the form `plugin-sharing`'s landed `sharing-service.ts:20` and `exec-context-annotation.pin.ts:7` use. `approval-service.ts:53` is 1 reflow line. - **The ruling's record, `approval-service.ts:2295` and `approval-service.test.ts:751`.** 「Maintainer ruling, 2026-08-15 (objectstack-ai#8710, inheriting objectstack-ai#8613), verbatim:」 became 「… (commit 04d03c3, inheriting objectstack-ai#8613), verbatim:」. The quotation under it is the ruling itself and is untouched; `04d03c3a0`'s message carries the same sentence. - **`approval-service.ts:2329`.** 「that is the option objectstack-ai#8710 rejected」 became 「that is the option the ruling (commit 04d03c3) rejected」. - **The test heading, `approval-service.test.ts:749`.** 「the objectstack-ai#8710 carve-out, asserted on THIS side (objectstack-ai#8863)」 became 「the commit 04d03c3 carve-out, asserted on THIS side (commit d200b01)」: the carve-out's record, and the commit that asserted it here. - **A PR number, `backfill-platform-row-organizations.ts:9`.** 「objectstack-ai#10101 (landed as PR objectstack-ai#11311)」 became 「objectstack-ai#10101 (landed as commit 1272f0a)」, the pull request's squash commit. - **Item numbers, `approval-service.ts:4893`, `:4906` and `stranded-request-inspection.test.ts:123`.** 「[objectstack-ai#16709 item 3]」 became 「[commit 8c7cca1, item 3]」, and likewise for item 2, beside its 「PM ruling, 2026-09-08」, which `8c7cca1ce`'s message records under 「Item 2」. - **Lost referents, 2 lines with no dead site** (every file keeps its line count): `backfill-platform-row-organizations.test.ts:17` 「the one thing this card must not do」 became 「the one thing this sweep must not do」, and `manager-org-screen-parity.contract.test.ts:61` 「the very decision this card is fenced out of」 became 「the very decision this pin is fenced out of」. Each 「this card」 pointed at the number the same comment block opened with, which is now a commit; `b019891cd`'s message says the pin 「PINS the duplication, it does not remove it」. ## The 5 sites left - **Test strings, 5 sites**, left as stages 1 to 6 left theirs: - `describe` / `it` titles: `manager-org-screen-parity.contract.test.ts:232` (`objectstack-ai#11286`), `stranded-request-inspection.test.ts:519` and `:642` (`objectstack-ai#16709`); - a test double's thrown message and an assertion message: `manager-org-screen-parity.contract.test.ts:107` and `:294` (`objectstack-ai#11286`). - There is no operator string, generated header or quoted ruling carrying a dead number in this package. The generated `*.source-hashes.generated.ts` headers already cite `09b4f4e4e` and are untouched. The two verbatim quotations of the 2026-08-15 ruling carry no number and are untouched. ## Mechanical guard: no code token moves The guard compares the TypeScript parser's leaf nodes, with comments as trivia and JSDoc nodes never visited, base `575746371` against head. Template literals are therefore read in context. It ran over all 13 touched `.ts` files. - Real run: 36,204 base leaf tokens, **0 files with a token change** (exit 0). - Comment control in `sys-approval-request.object.ts` (「who a row is ABOUT」 to 「whom a row is ABOUT」): 0 files changed, as expected (exit 0). - Positive control, a code token added in `sys-approval-request.object.ts` (`referenceVia: 'object_name',` given a trailing `as const`): DIFFER (exit 1). - Positive control, one digit changed inside a kept test title (`stranded-request-inspection.test.ts:642`): DIFFER (exit 1). Every mutation went through `scripts/ablation-replace.mjs`, and each landed (anchor 1 to 0, blob changed). Each restore was proven byte-identical to the HEAD blob (`6cb56301a334`, `757ad45900ac`), with `git diff HEAD` empty and a clean tree afterwards. ## Changeset This change ships bytes, so a `patch` changeset for `@objectstack/plugin-approvals` (`.changeset/20596-plugin-approvals-provenance-anchors.md`) is included. Its body is stage 6's, word for word, with the package name changed. Measured on the built package (A3): `files[]` is `dist`, `README.md` and `CHANGELOG.md`. After the build (a cache miss for this package, so `dist` is this head's source), the rewritten comments reach `dist`: `8c7cca1ce` 4 times and `04d03c3a0` 4 times in each of `dist/index.d.ts` and `index.d.mts`; `04d03c3a0` 4 times, `1cba33f16` twice, and `8c7cca1ce`, `7901b2dd2` and `1408fe385` once each in each of `index.js` and `index.mjs`. Positive controls: the unchanged line 「A step routing to nobody is」, in the same docblock as the shipped rewrite at `approval-service.ts:2295`, is found once in each of the four files, and the unchanged line 「itself stays unwalled (`tenancy.enabled: false`)」 beside the shipped rewrite at `sys-approval-request.object.ts:144` once in each JS file. A never-written negative phrase appears nowhere in `dist`. None of the 14 dead numbers is left anywhere in `dist`. ## Gates (head `708244c2b`) - **Citation judging, as CI runs it:** `pnpm check:issue-citations` (self-test) exits 0. `node scripts/check-issue-citations.mjs` exits 0: the diff-scoped run judged 5 citations across 6 files, and all 5 resolve (`objectstack-ai#8613` twice, `objectstack-ai#8287`, `objectstack-ai#10101`, `objectstack-ai#12069`), each already on the line it replaces. - **Doc authoring:** `pnpm check:doc-authoring` exits 0. - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `708244c2b` derived 64 commands: all 57 derived at dispatch, plus `check:dispatcher-error-vocabulary`, `check:engine-double-contract`, `check:objectql-double-limit`, `check:query-options-erasure`, `check:type-check-coverage`, `check:type-check-debt` and `check:where-matcher`. Each ran with its exit code captured before any pipe, and all 64 exit 0. `--ran`, fed each command with its exit code, reports 64 run, 0 NOT MEASURED (a derived zero), 0 unrun, and exits 0. A full `turbo run build` of `./packages/*` and `./packages/*/*` ran first under the shared verify lock (71 of 71 tasks, exit 0), so no gate hit an unbuilt workspace. - **Roster families the derivation lists outside its commands** (their rosters sit in directories this diff touches): `node scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm check:error-code-casing` and `pnpm check:filter-alias-parity`, each exit 0. - **Tests and typecheck, under the verify lock:** - `pnpm --filter @objectstack/plugin-approvals test`: 51 files pass and 791 tests pass. That is every test file in the package, the 7 touched ones included. - `pnpm --filter @objectstack/plugin-approvals typecheck` exits 0 (`tsc` on `tsconfig.json`, the scripts program, and the test layer on `tsconfig.test.json`, held at its ledger of 8 files, 324 errors and 27 pinned signatures). `--listFiles`: the `tsconfig.json` program holds the 25 non-test files under `src/`, the 6 touched ones included; the `tsconfig.test.json` program holds all 76 files under `src/`, the 51 test files and all 13 touched files included. - **Lint, as a proven narrowing:** `eslint --no-inline-config --format json` over the 13 touched `.ts` files gives 13 files, 0 errors and 0 warnings. All 13 are in eslint's own population (`isPathIgnored` is false for each; a `dist` file, as the control, is ignored). `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`, as its own lines 327-328 state), so a comment edit here cannot move the verdict on any untouched file. The repo-wide `pnpm lint` is CI's run. - **Control bytes:** `pnpm check:nul-bytes` exits 0, and a raw scan of the 14 changed files for control bytes finds none. ## Acceptance notes - **The gate-invisible spellings, grepped as the claim asked.** `CITATION_RE` refuses a hyphen after the digits and a `/` before the `#` (objectstack-ai#20636). In this package there is one `#N-word` spelling, 「objectstack-ai#3266-era」 (`record-reader-visibility.test.ts:342`), and two `#A/#B` spellings, `objectstack-ai#8287/objectstack-ai#8778` (`approval-node.test.ts:462`) and `objectstack-ai#8543/objectstack-ai#8580` (`approval-vocabularies.test.ts:66`): the claim's 3, 1 and 2. `objectstack-ai#3266`, `objectstack-ai#8287`, `objectstack-ai#8543` and `objectstack-ai#8580` answer 200; the second number `objectstack-ai#8778` is dead, so that one line is rewritten here. - **A third spelling the gate cannot see, found by the raw scan.** `NON_CITATION_HEADS` excuses any `#N` after the word 「option」 as an option ordinal, so 「the option objectstack-ai#8710 rejected」 (`approval-service.ts:2329`) was never extracted: a dead number there would pass the diff gate at exit 0 and never enter a census count. It is rewritten here. Across the gate's declared surfaces at the base, the only other `option #N` with three or more digits is `packages/objectql/src/validation/rule-validator.ts:2202` (`option objectstack-ai#14088`), which answers 200. Same family as objectstack-ai#20636; noted for its closeout, not a card of its own. - **A retired key name in this package's prose, not changed here.** `tenancy.organizationField` left the authorable surface in `502f179cc`, and limb 0 of the shared resolver now reads `PLATFORM_STAMP_ORGANIZATION_COLUMNS` in `metadata-core`, keyed by object name. Comments in this package still name the retired key as what limb 0 reads (`sys-approval-request.object.ts:143`, the line above a rewrite; `backfill-platform-row-organizations.ts:35`, `approval-node.test.ts:463`, `approval-service.ts:2707`, `backfill-platform-row-organizations.test.ts:50`), and two test fixtures still declare it on a stub `sys_api_key` (`approval-node.test.ts:467`, `backfill-platform-row-organizations.test.ts:54`), where it is inert because the resolver keys by name. The anchor `7901b2dd2` is right for the key those lines name, and nothing is wrong at runtime. Correcting the prose would reach past the dead citations, and the fixtures are code tokens, so none of it is changed here. - **The census instrument did not truncate in this stage.** Both enumerations read 186 pages at the newest frontier. - **Anchors the next stages can reuse**, each checked here: `objectstack-ai#16709` → `8c7cca1ce`; `objectstack-ai#11081` → `c28e4cfae`; `objectstack-ai#11286` → `b019891cd`; `objectstack-ai#11308` → `5a916c4d4`; `objectstack-ai#11311` → `1272f0a6b`; `objectstack-ai#8707` → `1408fe385`; `objectstack-ai#8863` → `d200b016b`. - **Base.** The branch is on `main` at `575746371`, which is still `main` at 20:56Z (read into a private ref), so there was no merge. --- _Generated by [Claude Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #20662
Clause-②: no (author-shown wording only; no accept or reject moves)
What changes
The ADR-0087 D2 conversion
page-component-filter-record-to-rule-arrayleaves a record-form filter with anull-valued key as stored and reports it as a TODO, whichos migrate meta --storedprints. Its reason said the renderer skips that key, so it "constrains nothing today", and told the operator to "Drop the key". At the.objectui-shapindd3f7e1be356that is true only on a block that queries an object. On a block whose rows are inline, the key selects the rows whose value is null, so dropping it widens the block.Following triage's direction (comment 5893989151: one wording true on both kinds of block, no "drop the key" advice), the reason now:
data: { provider: 'value' }orstaticData), so it selects the rows whose value is null;{"field":"owner_id","operator":"is_null"}(built from the key), and says that a filter which leaves the key unconstrained has no rule for it. It advises neither rewrite. The choice is the author's.The verdict does not move. The filter is still declined, left byte-identical and reported as one TODO, on any block. The reason text does not branch on where the rows come from; the conversion never reads that.
Round 2 (seat note 5897754447, a claim amendment): the empty-operator-object reason beside it (
{ amount: {} }) said the object "constrains nothing". At the pin the renderer refuses it instead. Where the block queries an object,convertFiltersToASTthrows throughrefuseEmptyOperatorMap(INVALID_FILTER, 400). Where the block's rows are inline,ValueDataSource.findanswers no rows throughzeroKeyConditionRefusal. The reason and its docblock sentence now say that, say that no rule spells an operator object with no operator, and keep the renderer's own remedy, dropping the key. The verdict does not move.Files:
packages/spec/src/conversions/registry.ts: the reason string inrecordFilterToRules, the sentence in its docblock, and the conversion entry docblock's parenthetical in "What is left exactly as stored" ("the renderer skips that key today"). That parenthetical is a fourth copy of the same claim in the same file. It is text only and fixed in place: same defect, same file under this claim, same gates. Round 2 rewrites the empty-operator-object declined reason and its docblock sentence, text only. Round 3 (review 5898951990) drops "adataarray" from the null-key reason's inline list: at the pin a baredataarray reaches noValueDataSource.find. Round 4 makes the rationale in the conversion entry docblock's## Reachparagraph name the inline sources the filter reaches at the pin (data: { provider: 'value' }orstaticData), and adds that a baredataarray reaches none of them (object-calendardraws it unfiltered;object-map/object-ganttdo not take it as a record source). Comment text only; the verdict sentence is unchanged.packages/spec/src/migrations/entries/semantic/18.element-data-source-and-object-block-filter-rule-array.ts: the same claim in the D3 entry'sreason.packages/spec/src/migrations/registry.tsis regenerated bygen:migration-registryand not edited by hand. Round 3 narrows the inline list in its older sentence ("None of this depends on where a block's rows come from …") todata: { provider: 'value' }orstaticData.packages/spec/src/conversions/page-component-filter-record-to-rule-array.test.ts: theDECLINED_ROWScomment that restated the null-key claim; the all-or-nothing test's comment, which namedowner_id: nullfor a row that isdeleted_at: { $null: true }; and two new pins. A null-valued key gets the same reason on an inline-rowobject-mapand an object-bound one. That reason names theis_nullrule for the key, and the block's door takes that rule; the control is that the door refuses the stored record. An empty operator object gets the same reason on both blocks, and that reason namesINVALID_FILTER, a code inStandardErrorCode..changeset/20662-null-key-todo-reason.md:@objectstack/specpatch, covering both reasons.Verification record
Pin reading (
dd3f7e1be356, raw source):packages/core/src/utils/filter-converter.tsconvertFiltersToASTskips a key whose value isnull/undefined(skippedNullKeys).packages/core/src/adapters/ValueDataSource.tsfindsends an object$filtertomatchesFilter, and its simple-equality arm compares withcomparandEquals, which isvalue === target. Itsis_nullarm isvalue === null || value === undefined. Server-side,parseFilterASTlowersis_nullto{ $null: true }.ObjectMap/ObjectCalendarpassuseResolvedFilter(schema.filter)tonew ValueDataSource(...).find.filter-tokens.tsresolveContextTokensreturns anullvalue unchanged.page-component-filter-record-to-rule-arrayonce the objectui pin carries objectui#10767 #20305 dev's live probe (report 5893209491) measured thatfindwith{ owner_id: null }selects only the null row, and not the'u1'row or the row that has no key.Lit, at base
31ed067639(the stored-migration pass andformatStoredMigrationReport, the functionos migrate meta --storedprints through, over a one-page stubsys_metadataholding anobject-gridthat queriesdealand anobject-mapwithdata: { provider: 'value' }, bothfilter: { owner_id: null }):The
object-gridline carried the same sentence.Dark, at
a51c02fe83(same probe, spec rebuilt):In both runs the row is still
skippedwith two TODOs. The probe file was temporary and is not in the diff.Round 2, empty operator object (same printer probe, with
filter: { amount: {} }on the same two blocks). Lit atc5eed1b4d3: "... this filter has the keyamountset to an empty operator object, which constrains nothing — and no rule says "nothing". Drop the key. Left as stored, ...". Dark at3fcedfb564: "... set to an empty operator object, which names the field and no operator, so no rule spells it. The renderer does not ignore it today: where the block queries an object, it refuses the filter (INVALID_FILTER, 400); where its rows are inline, it answers no rows. Drop the key. Left as stored, ...". Both runs: rowskipped, two TODOs. Pin reading atdd3f7e1be356:filter-converter.ts:818callsrefuseEmptyOperatorMap, whoseFilterOperatorErrorhascode = 'INVALID_FILTER'andhttpStatus = 400;ValueDataSource.findanswers[]whenzeroKeyConditionRefusalreturns a refusal.Round 3, the inline list (the null-key printer probe, rows
null/u1/ missing). Lit at3fcedfb564: "... where its rows are inline (data: { provider: 'value' }, adataarray orstaticData), it selects the rows whoseowner_idis null. ...". Dark ata6e54de377: "... where its rows are inline (data: { provider: 'value' }orstaticData), it selects the rows whoseowner_idis null. ...". Pin reading atdd3f7e1be356:record-source.ts:303-307foldsstaticDatato{ provider: 'value', items }, and the value branches hand the resolved filter toValueDataSource.find(ObjectMap.tsx:950-952,ObjectCalendar.tsx:708-710,ObjectTree.tsx:911-913,ObjectGantt.tsx:1009-1010). A baredataarray reaches none of them:ObjectCalendar.tsx:387-389, 599-600, 647draws it with no fetch and no filter, and theview-dataarm refuses an array (record-source.ts:179). The round-1 Dark quote above is thea51c02fe83reading, before this narrowing.Tests and gates, at
6f1396efa2(this branch merged withorigin/main671d4c164fthroughscripts/pm/os-regen-merge.sh; the delta against main is exactly the five files above). Round 4's one-comment commiteaf2d6e6e8re-ran the conversions and migrations set (1034 passed), spec typecheck,check:generated(15 up to date),check:doc-authoringandcheck:issue-citations, all green; the derived gate list is unchanged:@objectstack/speclocalproject: 575 files, 16972 passed, 1 todo.typecheck, including the test layer, passed.repoproject, narrowed to the three files that read the conversion and migration registries (conversions-major18-merge,step18-rationale-merge,retired-key-migrate-sentence): 35 passed. The fullrepoproject did not finish inside the foreground cap and is NOT MEASURED locally; CI runs it.check:generated: all 15 artifacts are up to date against a spec rebuilt after the merge.dispatch-gates --commandsderived 87 commands. 84 exited 0, includingcheck:migration-registry,check:spec-changes,check:upgrade-guide,check:docs,check:api-surface,check:authorable-surface,check:objectui-pin-citations,check:doc-authoring,check:nul-bytesandcheck:adr-0087-registration. Three exited 3 with PREREQUISITE NOT MET, because they need a whole-workspace build:check:dual-build-cjs-loads,check:lean-entry-closureandcheck:type-check-debt. They are NOT MEASURED locally.--ranreconciles 87 derived: 84 run, 3 NOT-MEASURED, 0 unrun.scripts/ablation-replace.mjs, from the committed state): the anchoroperator: isNull })}was replaced so the reason named anequals/nullrule instead. The new test went red: expected the reason to contain{"field":"owner_id","operator":"is_null"}. The other five null-related tests stayed green. The file was restored: its blob matches HEAD andgit diff HEADis empty.bcda701b88: the anchor "block queries an object, it refuses the filter (INVALID_FILTER, 400); where its rows " was replaced with "block queries an object, it constrains nothing; where its rows ". The pin went red: expected the reason to containINVALID_FILTER. The file was restored: its blob matches HEADf1f29e6f4802andgit diff HEADis empty.Acceptance notes
owner_idset to null", so there was nothing to re-pin. The new pin checks named subjects: the same reason on both blocks, theis_nullrule, and that the door takes it. It does not pin prose. The empty-operator reason was the same: only its prefix was asserted.packages/spec/CHANGELOG.md's 17.5.0 entry carries the old null-key sentence. It is a released record and is not edited; the corrected text ships in this PR's changeset.Generated by Claude Code