Skip to content

chore(objectui): bump the console pin to 31971ff1e28f (one zod instance in the vendored Console), add a single-zod canary to build-console.sh, and key the release console cache on the spec zod range - #21149

Merged
objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/issue-21108-objectui-pin-single-zod
Oct 1, 2026

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #21108

Clause-②: no

Moves the console pin past objectstack-ai/objectui#11353, so the vendored Console bundles one zod instance again. Adds a single-zod canary to scripts/build-console.sh, and keys release.yml's Console dist cache on the spec's declared zod range.

Target: 31971ff1e28f, the minimal containing commit

.objectui-sha moves e420df310f5b -> 31971ff1e28f89cfc45f0c19bc5b05e443f28b79. That is the merge commit of objectui#11353 itself, which closed objectui#11327. objectui main was 2124d0411175, 2 commits further on, when I chose the target.

Why not main:

  • The two later commits are objectui#11293 (standalone object-kanban / object-calendar honour page navigation through the host's record navigator) and objectui#11165 (a docs example and its test). Neither is needed by this card.
  • objectui#11293 rewrites plugin-kanban/src/index.tsx and plugin-calendar/src/index.tsx. Nine asserting pin-citation anchors in packages/spec/src read those two files. Taking it would turn this pin's citation re-measure into a navigation-claim change outside this card.
  • The procedure (docs/releases-maintenance.md) says the target is decided in the objectstack issue. The card's unlock criterion is "a commit that contains objectui#11353". The minimal containing commit meets it and drags in the least.

Containment was measured on a private full clone of objectui (rev-parse --is-shallow-repository = false):

question command exit
pin is the objectui#11353 merge gh api …/pulls/11353 answers merged: true, merge_commit_sha: 31971ff1e28f… —
pin is on objectui main merge-base --is-ancestor 31971ff1e28f origin/main 0
old pin is inside the new range --is-ancestor e420df310f5b 31971ff1e28f 0
negative: new pin is not inside the old one --is-ancestor 31971ff1e28f e420df310f5b 1
control for that negative (deeper commit, same target) --is-ancestor db11afd4967c e420df310f5b 0
objectui#11293 is NOT in this pin --is-ancestor 2124d0411175 31971ff1e28f 1
control for that negative --is-ancestor e420df310f5b 31971ff1e28f 0

The range e420df310f5b..31971ff1e28f is 44 commits.

What changed

  • .objectui-sha and .changeset/console-31971ff1e28f.md. Written by scripts/bump-objectui.sh 31971ff1e28f --no-commit against the private clone, so the shared shallow ../objectui was never deepened. The range has 56 releasing changesets, 12 declared breaking, and 3 commits carry no changeset.
  • sdui.manifest.json + scripts/sdui-manifest.record.json. Produced by node scripts/gen-sdui-manifest-node.mjs over the tree build-console.sh built at the pin: 107 components, @object-ui/core 17.6.0. Eleven components changed their published inputs: object-form, object-kanban, object-calendar, object-gantt, object-timeline, object-map, record:quick_actions, element:text, element:definition-list, element:repeater and action:button.
  • packages/sdui-parser/objectui-lockstep.json. Re-recorded with pnpm gen:sdui-lockstep against the pin's own checkout: 214 grammar lines, 25 codes, containment predicate 76c18fb95d1f. check:sdui-lockstep exits 0 with no port owed.
  • scripts/build-console.sh: the single-zod canary. Details in the next section.
  • .github/workflows/release.yml: the Console dist key. Details below.
  • The 49 asserting pin citations in packages/spec/src. Re-measured at the new pin (section below). This also regenerates packages/spec/src/migrations/registry.ts (gen:migration-registry) and content/docs/references/ui/view.mdx (check:generated --fix, the one artifact it proved stale), and adds .changeset/objectui-pin-citations-31971ff1e28f.md (@objectstack/spec patch). The FormField.span describe and six migration-entry descriptions name the pin, so the text change publishes.
  • origin/main merged twice.

The single-zod canary

It sits beside the existing bundle canary.

  • It counts zod version literals across packages/console/dist/assets/*.js. Each zod 4 instance carries one version object (the version export of zod/v4/core/versions), minified as {major:4,minor:6,patch:5}.
  • It requires exactly one occurrence. It refuses two different versions (the shipped defect), one version found twice (two instances), and zero (a blind matcher, since the console always bundles zod).
  • The same function runs standalone as scripts/build-console.sh --check-single-zod ASSETS_DIR, which builds and writes nothing. That standalone mode is how both directions below were measured.
fixture result
the real dist this PR's build produced at 31971ff1e28f (2450 assets/*.js) exit 0: exactly one zod version literal {major:4,minor:6,patch:5} in vendor-objectstack-UrN_Az-7.js
the real db11afd4967c dist (the published two-zod split) exit 1: 2 different zod versions — vendor-objectstack-CfLtdaY1.js:{major:4,minor:6,patch:1}, vendor-zod-CHLW1iCr.js:{major:4,minor:4,patch:3}
the new dist plus one chunk carrying {major:4,minor:4,patch:3} exit 1, two different versions
the new dist plus a second {major:4,minor:6,patch:5} chunk exit 1: 2 copies of ONE zod version literal
the new dist with its one literal rewritten (major -> MAJOR; grep -o count 1 -> 0 before, 0 -> 1 after) exit 1: the blind-matcher refusal
no argument exit 2, usage

The fixtures are copies in a scratch directory. No tracked file was mutated.

release.yml: the key carries the spec's declared zod range, not the whole package.json

The card suggested hashFiles('packages/spec/package.json'). I went with a narrower input:

  • A new step reads dependencies.zod out of packages/spec/package.json. It fails rather than emitting an empty or undefined range.
  • The restore, the CONSOLE_DIST_CACHE_KEY env and the save step all spell the same key, with -zod-RANGE appended (today -zod-^4.6.5, after the merge of fbcc05f400).

Why not hash the whole file: its version field moves on every Version Packages merge, and that merge is the push that drives the publish lane. Hashing it would make every publish a cache miss and a cold console rebuild. The comment above the key records that design as deliberate. The declared range is the input the build actually judges: objectui#11353's parity assertion refuses a console zod outside it.

release.yml is not a governed surface. GOVERNED_SURFACES in scripts/pm/check-governed-merges.mjs lists docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md and docs/NORTH-STAR.md. No workflow was run or dispatched.

Pin-citation re-measure

Each record was handled the same way:

  • Its anchors were mapped through the e420df310f5b..31971ff1e28f diff of each cited file and re-read at the new pin.
  • Block hashes (git hash-object) and corpus counts (git grep -o -F over the tracked tree, 9912 files) were taken again with the method each record names. The method reproduces every e420df310 count exactly at the old pin: 9800 files, objectstack 15352, @objectstack/spec 6024, timeout 1293.
  • Earlier hops stay as history.
  • check:objectui-pin-citations --verify-anchors, run against the pin's checkout, reports 49 asserting citations matching 31971ff1e, and 7 anchor content assertions verified.

Records whose CLAIMS moved, not only their numbers. These want a human read:

  • object-calendar navigation. objectui#8652 declared the key on ObjectCalendarSchema. That removes the one as any cast objectui#8651 had left: the ledger comment 1008-1025 is replaced by a declaration note, and the read 1026 -> 1018 now says schema.navigation ?? { mode: 'drawer' }.
  • object-kanban navigation. The read is unchanged. It now compiles through a declared ObjectKanbanSchema.navigation member rather than BaseSchema's index signature.
  • object-timeline. objectui#8654 declared navigation. The read lost its cast (753-756 -> 793-796), and both registrations gained a navigation input. The schema.* read set is unchanged, because navigation was already read and is already on this row.
  • action:button. objectui#11168 slice 2 makes the size input publish the five values the row declares. The inputs range grew 414-551 -> 414-559.
  • record:quick_actions. objectui#10224 rewrote the fail-closed docblock: the bar is replaced by the notice, not hidden. The cited gate lines moved by 6 with their text byte-identical.
  • object-tree optionality count. By the record's own method, plugin-map/src/index.tsx now reads 4 ElementDataSourceGate hits, not 3. objectui#10859 batch 5 added a comment naming the gate, while the wiring is still three lines. The record now says so.
  • Wrong when written. The object-timeline data door's start/end-binding anchor 621-622 began one line early, on the rawData.map head, at e420df310 too. It is re-pointed to 657-658 and the record says why.

No key, default, enum member or export moves.

Acceptance at the new pin

Build. scripts/build-console.sh ran at 31971ff1e28f with this tree's client and spec injected, under the shared verify lock: VERDICT command-exit 0.

  • The single-zod canary passed (above). assert-console-spec-injection passed: 21 of 21 injected-only descriptions are present and all 8 published-only ones are absent.
  • The dist is 63032 KB.
  • Environment, not a product change: objectui's jsdom requires Node ^22.22.2 under engine-strict, and this container's Node is 22.22.0. A verified Node 22.22.2 was prepended to PATH for the console build only.
  • The build ran before the manifest regeneration, as the procedure orders it. So that local dist ships the previous manifest bytes, and the build itself prints the re-run step. CI's Console Pin Gate builds fresh.

Boot. pnpm dev -- --fresh --ui --no-watch -p 41633 started the showcase with its own ephemeral DB and the seeded admin. check:console-sha reads objectui@31971ff1e28f. I drove it in headless Chromium (/opt/pw-browsers/chromium) from localhost:

  • Studio "New package (writable base)" (/_console/studio): the dialog renders Display name (name), Package ID (id) and Object namespace (namespace), plus Version, Type and Description.
    • I filled Zod Probe / com.acme.zodprobe / zodprobe and clicked Create package. The request was POST /api/v1/packages -> 201.
    • The Studio opened /_console/studio/com.acme.zodprobe/data. GET /api/v1/packages/com.acme.zodprobe answers 200 with writable: true.
  • Dashboard inspector (/_console/apps/showcase_app/metadata/dashboard/showcase_ops_dashboard): the spec-derived form renders Columns, Gap, Refresh Interval Seconds, Header, Show Title, Show Description and Actions. "Spec schema unavailable" appears 0 times.
  • Report inspector (…/metadata/report/showcase_task_overview): Label, Report type, Description and Blocks render, with 0 "Spec schema unavailable".
  • Control for that absence: the string is in the shipped bundle (i18n-D40RWXZZ.js, 3 occurrences), so 0 on screen is a reading, not a missing probe.
  • Errors: 0 page errors and 0 console errors across the drive.
  • Non-2xx:
    • 401 GET /api/v1/auth/get-session before sign-in.
    • 404 GET /api/v1/usage/storage, as at the previous pin.
    • 501 GET /api/v1/ai/usage, with no AI provider configured.
    • 404 GET /api/v1/meta/{dashboard,report}/NAME?state=draft, the no-draft probe of a code-loaded item.
  • Teardown: only the 7 PIDs I recorded were stopped. The port is free, and the --fresh tempdir went with the process.

Gates and tests, at head 8435a830eb (re-run in full after the merge: 151 derived, 151 run, 0 NOT-MEASURED, 0 UNRUN)

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 151 commands, all run sequentially with exit codes captured before any pipe.
    • 150 exit 0.
    • pnpm check:pm-dispatch-gates hit my 900 s runner timeout under contention (exit 124). Re-run alone with no timeout, it passed: exit 0, dispatch-gates self-test: 1976 cases pass, battery 1096.7 s.
    • dispatch-gates --ran reconciliation: 151 derived, 151 run, 0 NOT-MEASURED, 0 UNRUN.
    • Highlights: check:objectui-pin-citations 49 matching, check-sdui-manifest intact at the live pin, check:sdui-lockstep OK, check:console-sha, check:console-injection, check-adr-0087-registration (2 declaring changesets, each with a disposition), check:generated (15 of 15 current), check:docs (227 in sync), check:migration-registry, check:bash32-floor (32 shell files), check:nul-bytes, check-ci-filter-parity and check:published-files.
  • pnpm --filter @objectstack/spec test: 591 files, 17368 passed and 1 todo, exit 0. pnpm --filter @objectstack/spec typecheck: exit 0.
  • NOT MEASURED, by CI only: the Console Pin Gate job's own steps and the workspace type-check lanes. The local equivalent of the Console Pin Gate is the build above.

Acceptance notes

  • scripts/check-console-injection.mjs's header still says release.yml's key "hashes the pin and the build script". That sentence is now incomplete, because the key also carries the spec's zod range. The file is outside this card's file surface. It is a comment, and nothing reads it.
  • objectui#11353 landed with no objectui changeset, so objectui's own CHANGELOG does not mention the zod fix. The console changeset here names it.

Generated by Claude Code

claude added 10 commits October 1, 2026 08:04
…ole changeset, lockstep record)

scripts/bump-objectui.sh 31971ff1e28f --no-commit against a private full
objectui clone, then pnpm gen:sdui-lockstep against the pin's checkout.
The ADR-0087 disposition placeholder is answered in a later commit.

Claude-Session: https://claude.ai/code/session_01MRdbfpy4sQT8bUjmMhxsN7
Co-authored-by: Claude <noreply@anthropic.com>
…ase console cache key carries the spec's zod range

build-console.sh asserts exactly one zod version literal across the
shipped assets/*.js, beside the bundle canary, and exposes the same check
as `--check-single-zod <assets-dir>` so it can be run over an existing
dist. release.yml's Console dist key carries packages/spec's declared zod
range, read out of package.json rather than hashing the file, whose
version moves on every Version Packages merge.

Claude-Session: https://claude.ai/code/session_01MRdbfpy4sQT8bUjmMhxsN7
Co-authored-by: Claude <noreply@anthropic.com>
…; re-measure pin citations (WIP, part 3)

node scripts/gen-sdui-manifest-node.mjs over the tree build-console.sh built
at the pin: 107 components, @object-ui/core 17.6.0.

Claude-Session: https://claude.ai/code/session_01MRdbfpy4sQT8bUjmMhxsN7
Co-authored-by: Claude <noreply@anthropic.com>
…answer the console changeset's ADR-0087 disposition

Every asserting pin citation in packages/spec/src re-measured at the new
pin; migrations/registry.ts regenerated (gen:migration-registry). The
console changeset's adr-0087 placeholder is answered not-required
(no-migration-prescription), worded after the previous bump's ruling.

Claude-Session: https://claude.ai/code/session_01MRdbfpy4sQT8bUjmMhxsN7
Co-authored-by: Claude <noreply@anthropic.com>
…n (check:generated --fix)

Claude-Session: https://claude.ai/code/session_01MRdbfpy4sQT8bUjmMhxsN7
Co-authored-by: Claude <noreply@anthropic.com>
…jectui-pin-single-zod

component.zod.ts conflicted in two docblocks; both intents stack: main's
#21005 (action:button / action:icon refuse `endpoint`) and #21141 (the
quickAdd tombstone prescribes "delete the key") are kept verbatim, and the
pin bump's citation updates at 31971ff1e are re-applied on top
(`inputs` `:414-559` and the slice-2 `size` sentence; `e420df310` in the
kanban-ui registration count). Regeneration follows as its own commit.

Claude-Session: https://claude.ai/code/session_01MRdbfpy4sQT8bUjmMhxsN7
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/sdui-parser, @objectstack/spec, touching 12 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/sdui-parser/objectui-lockstep.json, packages/spec/src/kernel/functional-completeness.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/analytics.mdx (via DatasetMeasureSchema (symbol, a top-level const))
  • content/docs/protocol/objectui/layout-dsl.mdx (via ComponentPropsMap (symbol, a top-level const object))

⛔ 5 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via PageTabsProps (symbol, a top-level const object))
  • content/docs/releases/v17/17-1.mdx (via ComponentPropsMap (symbol, a top-level const object))
  • content/docs/releases/v17/17-3.mdx (via ComponentPropsMap (symbol, a top-level const object))
  • content/docs/releases/v17/17-4.mdx (via ComponentPropsMap (symbol, a top-level const object))
  • content/docs/releases/v17/17-5.mdx (via ComponentPropsMap (symbol, a top-level const object))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/sdui-parser/objectui-lockstep.json, packages/spec/src/kernel/functional-completeness.ts) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 138 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 2c1cef3345738b5f2486eed5b0608687dba36f11 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from bba0bb314ae631909c98fe721003258fb1ebc2b9 — the merge of head 8435a830ebfd25edcabdc012d1190d619b7dd200 into base 2c1cef3345738b5f2486eed5b0608687dba36f11, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin bba0bb314ae631909c98fe721003258fb1ebc2b9 && git checkout bba0bb314ae631909c98fe721003258fb1ebc2b9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2c1cef3345738b5f2486eed5b0608687dba36f11 8435a830ebfd25edcabdc012d1190d619b7dd200 && git checkout -B drift-repro 2c1cef3345738b5f2486eed5b0608687dba36f11 && git merge --no-ff 8435a830ebfd25edcabdc012d1190d619b7dd200

node scripts/docs-audit/affected-docs.mjs --json 2c1cef3345738b5f2486eed5b0608687dba36f11

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 2c1cef3345738b5f2486eed5b0608687dba36f11 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…e policies — double accumulation, the PostgreSQL boolean cast and the empty-sum fold, hoisted into core (objectstack-ai#21042) (objectstack-ai#21209)

Fixes objectstack-ai#21042
Clause-②: yes (widening)

## What this changes

The analytics native-SQL strategy (`NativeSQLStrategy`, the default on a
SQL driver) skipped three aggregate policies that
`SqlDriver.aggregate()` applies. So one route answered different
numbers, or a `500`, depending on which strategy served it. This PR
follows the route ruling on objectstack-ai#21042 (comment `5925613967`): the operand
policies are hoisted into `@objectstack/core`, beside
`AGGREGATE_ANSWER_KIND`, and both faces read them from there.

- **`packages/core/src/utils/aggregate-answer.ts`**
(`@objectstack/core`, `minor`). It takes:
- `AGGREGATE_ACCUMULATION`, moved from `driver-sql` with its docblock
(the docblock and the table are byte-identical to the base, apart from
the `export` keyword);
- `aggregandColumnClass({ type, multiple })`, the one column-class
predicate (`'fractional'`, `'integral'`, `'boolean'`, or none);
- `POSTGRES_BOOLEAN_AGGREGAND_CAST`, the objectstack-ai#11635 cast, as a `Record` over
`AggregationFunction`: `sum` / `avg` / `min` / `max` are cast, the
counts never are;
- `doubleAccumulationOperand(operand, dialect)`, the double operand with
the dialect as a parameter;
- `aggregandOperandSql(func, columnClass, dialect, operand)`, the one
composition both faces emit (the cast inside, the double operand around
it).
  No `index.ts` line was added: the module is already exported.
- **`packages/drivers/driver-sql/src/sql-driver.ts`** (`patch`), in the
ruled regions only. The `AGGREGATE_ACCUMULATION` table becomes a pointer
plus an import. `isFractionalNumericType` is deleted. The two registry
fills fill `fractionalNumericFields` through the predicate.
`accumulatesInDouble` / `doubleAccumulationOperand` are replaced by
`aggregandColumnClassOf`, which maps the driver's registries onto the
predicate's classes. In `aggregate()`, the private boolean-cast
condition and the accumulation call become one
`aggregandOperandSql(funcName, class, this.dialectName, '??')`.
-
**`packages/services/service-analytics/src/strategies/native-sql-strategy.ts`**
(`patch`), in two places.
- **`resolveMeasureSql`** wraps the column it hands `AGGREGATE_SQL` /
`CONDITIONAL_AGGREGATE_SQL` in `aggregandOperandSql`. The column class
comes from the declaration the host already relays
(`declaredValueShape`), on the object the column lives on
(`columnObjectOf`, the one hop resolver). The dialect comes from
`sqlDialect`, which the strategy already reads.
- **The `execute` shaping point** that PR objectstack-ai#21040 added now folds a
`null` measure answer to `emptyGroupValueFor(measure.type)`
(`@objectstack/spec`). It does this for every measure, measure-scoped
ones included, before the number presenter, in `driver-sql`'s order. The
dataset door's `DatasetExecutor` fill stays, and it is idempotent on a
folded row.
- The one line outside those two regions is the `generateSql` call site,
which now passes `ctx` to `resolveMeasureSql`.
- No native copy of any policy, and no runtime hook asks the driver: the
rejected (C) route was not taken. `canHandle`, `buildFieldMeta`, the
hop-object sites, the filter / text-match rendering,
`analytics-service.ts` and `field-read-admission.ts` are untouched.

## The card's table, before and after

Measured through `AnalyticsService.query` (the cube door, which `POST
/api/v1/analytics/query` relays verbatim) and
`AnalyticsService.queryDataset` (the dataset door), on
`AnalyticsServicePlugin` over a real ObjectQL engine and `SqlDriver`.
**Native** is the plugin's own composition (`NativeSQLStrategy`
answered, with one raw statement and no engine aggregate). **ObjectQL**
is the same composition narrowed to `engine.aggregate`. "Before" is the
strategy file at the base `d34aa58a2a`; "after" is this branch at
`61aab5013a`. Neither merge since then touches the aggregate code paths,
and the pins below are green at `ef1f9d8484`.

Fixture:

- group `f`: `frac` (a `number` column) holds 0.1 and 0.2, and `flag`
holds true and false;
- group `i`: `stars` (a `rating` column) holds seven 1s and two 2s, and
`flag` holds 7 trues and 2 falses;
- group `n`: every aggregand is NULL in all three rows.

The measure-scoped measures filter on `tag = x`, which only group `f`
holds.

**PostgreSQL 16.13** (a private local server; the ObjectQL column is the
same before and after):

| measure | group | door | native before | native after | ObjectQL |
|:--|:--|:--|:--|:--|:--|
| `sum(frac)` | f | cube, dataset | `0.3` | `0.30000000000000004` |
`0.30000000000000004` |
| `avg(frac)` | f | cube, dataset | `0.15` | `0.15000000000000002` |
`0.15000000000000002` |
| `avg(stars)`, an integer column | i | cube, dataset |
`1.222222222222222` | `1.2222222222222223` | `1.2222222222222223` |
| `sum(flag)` | i | cube, dataset | `500 DATABASE_ERROR` | `7` | `7` |
| `avg(flag)` | i | cube, dataset | `500 DATABASE_ERROR` |
`0.7777777777777778` | `0.7777777777777778` |
| `min(flag)` / `max(flag)` | i | cube, dataset | `500 DATABASE_ERROR` |
`0` / `1` | `0` / `1` |
| `sum(frac)`, all-NULL group | n | cube | `null` | `0` | `0` |
| `sum(frac)`, all-NULL group | n | dataset | `0` (executor fill) | `0`
| `0` |
| `sum(flag)`, all-NULL group | n | cube | `500 DATABASE_ERROR` | `0` |
`0` |
| `avg(frac)`, all-NULL group | n | cube, dataset | `null` | `null` |
`null` |
| measure-scoped `sum(frac)`, no admitted row | i | cube | `null` | `0`
| `0` |
| measure-scoped `sum(frac)`, no admitted row | i | dataset | `0`
(executor fill) | `0` | `0` |
| measure-scoped `avg(frac)`, no admitted row | i | cube | `null` |
`null` | `null` |
| `count` control | n | cube, dataset | `3` | `3` | `3` |
| measure-scoped `count` control | i | cube, dataset | `0` | `0` | `0` |

**SQLite** (better-sqlite3): accumulation and the boolean answers
already agreed on every face (`0.30000000000000004`,
`0.15000000000000002`, `1.2222222222222223`, `7`, `0.7777777777777778`,
`0` / `1`). The fold is the policy that diverged there:

| measure | group | door | native before | native after | ObjectQL |
|:--|:--|:--|:--|:--|:--|
| `sum(frac)` / `sum(stars)` / `sum(flag)`, all-NULL group | n | cube |
`null` | `0` | `0` |
| `sum(frac)` / `sum(stars)` / `sum(flag)`, all-NULL group | n | dataset
| `0` (executor fill) | `0` | `0` |
| measure-scoped `sum(frac)`, no admitted row | i, n | cube | `null` |
`0` | `0` |
| measure-scoped `sum(frac)`, no admitted row | i, n | dataset | `0`
(executor fill) | `0` | `0` |

After the fix, the native and ObjectQL faces **differ in 0 of 144
cells** (2 drivers × 2 doors × 12 measures × 3 groups).

**MySQL is NOT MEASURED**: there is no MySQL server in this container.
The MySQL operand text is pinned offline: by `core`'s
`aggregate-answer.test.ts`, and by the `driver-sql` move proof for the
driver's own statements.

## The move proof

`driver-sql`'s aggregate statements were dumped at the base, before any
consumer changed. The dump covered `SqlDriver.aggregate()` for every
function (`count`, `count_distinct`, `sum`, `avg`, `min`, `max`, and
`count(*)`), aliased and unaliased, over 23 columns: every fractional,
integral and boolean type, the `float` / `integer` / `int` aliases,
multi-valued and untyped columns, and text / date / lookup / formula. It
ran on SQLite, PostgreSQL and MySQL, through both registration paths
(`registerObjectMetadata` and `registerExternalObject`), offline (knex
`toSQL()`).

The policies were then hoisted, `driver-sql` was switched to the
imports, and the same dump was run again:

- base dump: 1668 entries, 0 errors, md5
`8eee668372a28a7568f3eb1cc5a2bc9b`;
- after dump (at `bc8aa0cc2f`): 1668 entries, md5
`8eee668372a28a7568f3eb1cc5a2bc9b`. `cmp` printed nothing: the two dumps
are **byte-identical**.

`sql-driver.ts` and `aggregate-answer.ts` are unchanged between
`bc8aa0cc2f` and `61aab5013a`.

The committed move-proof pin,
`packages/drivers/driver-sql/src/sql-driver-21042-aggregate-policy-move.test.ts`,
holds the captured expressions for one column of each class, on each
dialect and through each registration path. It passed at the base
(`192fc0010b`: 54 / 54) and passes after (54 / 54).

## Pins (committed red first, then the fix)

| file | at the pins commit (`192fc0010b`, base code) | after |
|:--|:--|:--|
| `core` `aggregate-answer.test.ts` | 16 red (the exports did not exist)
| 22 / 22 |
| `service-analytics` `native-sql-aggregate-policies.test.ts` (each
measure on both faces at both doors, against the engine's arithmetic;
SQLite and live PostgreSQL cells) | SQLite: the cube-door folds red.
PostgreSQL: accumulation, boolean `500`, folds red | 49 / 49 |
| `service-analytics` `cube-measure-field-type-door.test.ts`, **the
lifted skip** | PostgreSQL native `max(boolean)` red (`500`) | 23 / 23 |
| `rest` `analytics-dataset-aggregate-policies-door.test.ts` (the route,
both strategies) | PostgreSQL: 7 red (accumulation and booleans). SQLite
green (that door already folded) | 19 / 19 |
| `driver-sql` move proof | 54 / 54 | 54 / 54 |

**The lifted skip:** `it.skipIf(cell.id === 'pg' && face === 'native')`
in `cube-measure-field-type-door.test.ts` (from PR objectstack-ai#21128) is gone. Its
comment now says why the cell runs on every cell and face. The
PostgreSQL native `max_flag` cell answers `1`.

`native-sql-measure-number-presentation.test.ts` gets a comment-only
edit: its header said the native statement does not carry objectstack-ai#20387's
accumulation, and it now points at the new pin.

## Ablations

There was one ablation per policy, each predicted in writing before it
ran. Each mutation was planted through `scripts/ablation-replace.mjs`,
which checks that the anchor hit and that the blob changed. Each
mutation was confirmed in the built `dist/`
(`ablation-dist-preflight.mjs`: marker present). Each restore ran by
absolute path (`git checkout HEAD`), and the file's blob was proven
equal to its `HEAD` blob with `git diff HEAD` empty. After each restore
the package was rebuilt, and the marker was proven absent from `dist/`
with the tree clean. Every prediction held exactly.

| ablation | mutation | predicted red | observed red |
|:--|:--|:--|:--|
| A1 accumulation | `core` `accumulatesInDouble`'s dialect gate never
admits PostgreSQL or MySQL | `core` 3; `driver-sql` move proof 24 (pg
and mysql × both fills × the six numeric / boolean columns);
`service-analytics` 10, PostgreSQL only (both doors × `sum` /
`avg(frac)`, `avg(stars)`, measure-scoped `sum` / `avg`); `rest` 3,
PostgreSQL only | the same 3 / 24 / 10 / 3; SQLite cells green;
`avg(flag)` green as predicted |
| A2 boolean cast | `core` `aggregandOperandSql` never casts | `core` 1;
move proof 4 (pg × both fills × `boolean` / `toggle`);
`service-analytics` 8, PostgreSQL only; the lifted cell, PostgreSQL
native and ObjectQL, 2; `rest` 4, PostgreSQL only | the same 1 / 4 / 8 /
2 / 4 |
| A3 fold | the native shaping point never folds | `service-analytics`
8, cube door only (SQLite and PostgreSQL × the three all-NULL `sum`s and
the measure-scoped `sum`); everything else green, the `rest`
dataset-door route included, because the executor fill folds there | the
same 8; `rest` 19 / 19 green |

A1 and A2 show one policy reaching both faces. Each one turned
`driver-sql`'s own statements red. Under A2 the ObjectQL face's
`max(boolean)` cell failed too, with the driver's refusal. Under A1 the
ObjectQL face answered the same exact decimal as the native face for the
plain measures: the failing assertion was the engine's number, while
native and ObjectQL still agreed.

A **reverse type check** also ran. Passing a dialect the new type
rejects (`'oracle'`) to `aggregandOperandSql` turned
`service-analytics`' typecheck red (`TS2345 ... not assignable to
parameter of type 'AggregandSqlDialect'`), which shows the rebuilt
`core` `.d.ts` was read. The file was restored byte-identical.

## Verification (at `ef1f9d8484`, after merging `origin/main` at
`cb45469e67`, which carries PR objectstack-ai#21170 and PR objectstack-ai#21173)

Everything below ran as one locked script, at `ef1f9d8484`, with each
exit code captured before any pipe. The live PostgreSQL 16.13 server ran
at `timezone = Asia/Shanghai`, and the `driver-sql` suite ran under
`TZ=America/New_York`, which are its own non-vacuity preconditions.

- **Refresh after the merge:** `pnpm turbo run build
--filter='!@objectstack/docs' --concurrency=1` exit 0, and `pnpm
--filter @objectstack/spec check:generated` exit 0.
- **Gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 67 commands from the real diff (10
paths). All 67 exited 0. Reconciliation with `--ran` and the recorded
exit codes printed: `Run reconciliation — 67 derived, 67 run, 0
NOT-MEASURED, 0 UNRUN.`
- **Typecheck:** `pnpm --filter … typecheck` exit 0 for
`@objectstack/core`, `@objectstack/driver-sql`,
`@objectstack/service-analytics` and `@objectstack/rest`.
- **Tests, every vitest project of every touched package** (`vitest run
--maxWorkers=2`, with `OS_TEST_POSTGRES_URL` set so the live cells ran):

| package / project | files | tests |
|:--|:--|:--|
| `core` local | 74 passed | 2120 passed |
| `core` repo | 3 passed | 48 passed |
| `driver-sql` | 216 passed, 3 skipped | 4300 passed, 96 skipped |
| `service-analytics` | 161 passed | 3765 passed |
| `rest` local | 256 passed | 5027 passed, 127 skipped |
| `rest` repo | 5 passed | 179 passed, 1 skipped |

- **The five pin files, run explicitly:** move proof 54 / 54, `core` 22
/ 22, policies 49 / 49 (24 SQLite + 24 PostgreSQL + the oracle),
field-type door 23 / 23, `rest` route 19 / 19 (9 SQLite + 9 PostgreSQL +
the oracle).
- **Lint, narrowed and proven:** `eslint --no-inline-config --format
json` over the 9 changed code files answered 9 results, 0 errors and 0
warnings. The population is read from eslint's own config:
`ESLint.isPathIgnored` answers `false` for each of the 9. The narrowing
excludes nothing that could move, because `eslint.config.mjs` never
enables type-aware linting (no `parserOptions.project`, no typed rules),
so this diff cannot change the verdict on an untouched file. The
repo-wide `pnpm lint` is CI's.
- **The `driver-sql` live preconditions:** the first gate run used a
private server at UTC, and the suite's four timezone non-vacuity cells
failed by design (`… start it with timezone=Asia/Shanghai`). With the
server at `Asia/Shanghai` and the process at `America/New_York` the
suite is green, as listed above.
- **`main` after the final merge:** `origin/main` moved 4 commits past
`cb45469e67` before this PR opened (objectstack-ai#21149, objectstack-ai#21188, objectstack-ai#21195, objectstack-ai#21192).
None of them touches `core`, `driver-sql`, `service-analytics` or the
analytics `rest` tests. The one `packages/spec` file in the analytics
area, `ui/dataset.zod.ts`, changes a comment only. They are not merged
here; CI runs on the merge ref.

## Acceptance notes

- **MySQL is NOT MEASURED** (no server in this container). The MySQL
operand text is pinned offline in `core` and in the `driver-sql` move
proof.
- **The live PostgreSQL cells are not run in CI.** No CI step sets
`OS_TEST_POSTGRES_URL` for `service-analytics` or `rest`. The cells
above ran against a private PostgreSQL 16.13 started for this run and
removed afterwards. In CI the SQLite cells run, and so do the offline
`core` / move-proof pins.
- **Phase 0's note on the dataset door, which is no divergence:** a
measure-scoped `avg` is **absent** from a row its supplementary query
reported no row for. That is `x_avg_frac` for groups `i` and `n`, on
both strategies and before and after. It is not `null`. The new service
pin holds this cell only to "both faces agree", not to a value.
Relatedly, a dataset-door selection made only of measure-scoped measures
reports only the groups their filter admits, so the pin asks each one
beside the base count.
- **Residual, as stated in the ruling:** a host that relays no field
declarations (`declaredValueShape`), or names no SQL dialect, gets no
column class or no policy. It keeps the native arithmetic it had, and a
PostgreSQL boolean `sum` there still answers `500`. The plugin's own
composition wires both.
- **How `driver-sql` reads the class:** it reads its own registries
rather than calling the predicate per column. `fractionalNumericFields`
is filled by the predicate. `booleanFields` and `numericFields` are
filled by the driver's coercion rules, whose populations equal the
predicate's `'boolean'` and `'fractional'` ∪ `'integral'` classes. The
move proof pins that equality per column class. Asking the predicate per
column through the driver's `valueShapeFields` would retire
`fractionalNumericFields`, but its declaration and shard-alias regions
are outside the ruled surface, so this PR does not do it.
- **The scan-order residual is unchanged.** On PostgreSQL and MySQL the
double sums are added without compensation (`AGGREGATE_ACCUMULATION`'s
docblock), so three or more fractions can still differ in the last place
from SQLite and the rows path. The pins use two addends.
- objectstack-ai#21129 (the presenter for a relationship-path `min` / `max`) is not
addressed here.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…nonymous lookup route (objectstack-ai#21180) (objectstack-ai#21222)

Fixes objectstack-ai#21180

Clause-②: yes (narrowing)

Retires the form field's `publicPicker` block and deletes the anonymous
lookup route `GET /forms/:slug/lookup/:field`. This is the second half
of ruling E on objectstack-ai#21079 (record `5933054144`, maintainer 「同意E」,
2026-10-01). Anonymous public forms no longer take lookup,
`master_detail` or `user` fields. The ruling reverses the objectstack-ai#7467 model
("declare `publicPicker`"). It is an immediate retirement under ADR-0087
D2: no alias window and no dual spelling. No new gate: the resolve
route's existing strip becomes unconditional.

The ADR-0061 note rides a separate docs-only PR (Tier H), not this one.

## What changed

| surface | change |
| --- | --- |
| spec | `FormFieldBaseSchema.publicPicker` is a `retiredKey()`
tombstone carrying the card's prescription (FROM → TO below).
`FormFieldPublicPickerSchema`, `FormFieldPublicPicker` and
`FormFieldPublicPickerParsed` are deleted (no other reader). |
| ADR-0087 | D2 conversion `form-field-public-picker-removed` (protocol
18, `retiredFromLoadPath`, `retiredAfter: 17.5.0`, order 53). One
`STEP18_RATIONALE` fragment. D3 semantic entry
`form-field-public-picker-retired`. `RETIRED_KEYS_BY_MAJOR[18]` gets
`ui/FormField:publicPicker` and `RETIRED_DEFS_BY_MAJOR[18]` gets
`ui/FormFieldPublicPicker`. The registry is regenerated by
`gen:migration-registry`, never hand-typed inside its markers. |
| rest | The picker handler is deleted, with its literal `guest_portal`
picker context and its picker-only helper `view-filter-rule-lowering.ts`
(no other importer, not exported). The ledger row
(`rest-route-ledger.ts`) goes too. The resolve route's strip loses its
`publicPicker` condition: lookup / `master_detail` / `user` fields are
always left off the anonymous rendering. |
| error codes | `LOOKUP_NOT_PUBLIC` and `LOOKUP_TARGET_MISSING` leave
`error-code-ledger.zod.ts`. This follows the ledger's own retirement
rule ("A row whose last EMITTER is deleted comes out with it"), so the
row is deleted, with no graded tombstone. The picker handler was the
only producer of both codes. |
| lint | The `publicPicker` claiming reader in
`validate-preset-comparands.ts` is removed, with its cases. The
now-unused `graph` parameter of `boundObjectOf` / `bindAncestors` goes
too (`noUnusedParameters`). |
| tests | Deleted: `public-form-lookup-picker.test.ts`,
`public-form-lookup-picker-queryable-key.test.ts`,
`public-form-lookup-filter-lowering.test.ts`,
`view-public-picker.test.ts`,
`public-picker-queryable-key.dogfood.test.ts`. Re-pinned:
`public-form-routes.test.ts`, `public-form-routes.stored-row.test.ts`,
`rest-server-query-number-census.test.ts`,
`rest-server-canonical-query-ast.test.ts`,
`view-union-branch-focus.test.ts`, `protocol.save-union-issues.test.ts`,
and the picker door case of `zero-set-masking.dogfood.test.ts`. New:
`form-field-public-picker-retirement.test.ts` (tombstone at four doors,
the conversion, registration, and a tree-scoped absence pin over the
radius already declared for `@objectstack/spec`). |
| docs | The picker section of `content/docs/ui/forms.mdx` is replaced
by a short statement of current behaviour. The references regenerate.
The platform checklist item `access-security.public-form-intake` moves
to revision 2: its 403 clause now asserts the route's absence. |
| generated / ledgers | Regenerated: api-surface, export-origins,
declaration-map, json-schema manifest, authorable-surface (the def's
four lines deleted deliberately, the key marked `[RETIRED]`), strictness
counts and references. `dropped-refinements.baseline.json` is corrected
as the build printed it. `engine-double-contract.pinned.json` is
regenerated with `--write`: 6 losses, all from the two deleted test
files. The `check:route-envelope` pins are banked: 43 → 39 and 58 → 54,
the four `{ code, error }` answers the deleted handler carried. |
| changeset | `.changeset/21180-retire-public-picker.md`: BREAKING,
`Clause-②: yes (narrowing)`, the card's FROM → TO, and the ADR-0087
marker `registered form-field-public-picker-removed,
form-field-public-picker-retired`. |

**FROM → TO:** delete the `publicPicker` block; an anonymous public form
no longer offers record search. Use a `select` field with static
`options`, or put the form behind sign-in.

## The PM's hypotheses, measured

- **H1, the census.** At BASE `b9087d77e9`: 280 lines. By pattern:
`publicPicker` 129, `FormFieldPublicPicker` 43, `LOOKUP_NOT_PUBLIC` 19,
`/lookup/:field` 35, `guest_portal` 65. No producer exists outside spec,
tests, docs and the REST route; no example declares one. After, at
`dafa22868`: `publicPicker` 106, `FormFieldPublicPicker` 10,
`LOOKUP_NOT_PUBLIC` 0, `/lookup/:field` 15, `guest_portal` 59. The kit
accounts for the residue: the tombstone, the conversion and registry
entries, the retirement and union pins, the H2/H3 pins, and generated
artefacts (references, authorable-surface, and the base anchor, which
only its own generator writes). Untouched history also remains: two
dated audits, `releases/v15.mdx`, the ADR-0061 sentence, and the pending
`.changeset/21062-picker-queryable-key.md`. `guest_portal` remains as
permission-set names (examples, plugin-security tests, the published
skill's resolve/submit text) and as the submit route's context. In
`rest-server.ts` the picker's literal context was 1 of the 7 lines; the
other 6 are the submit route and its docblock. All of those are outside
this card.
- **H2, the strip depended on the key.** It did: `if (t !== 'lookup' &&
t !== 'master_detail' && t !== 'user') return true; return
!!cfg?.publicPicker;`. The condition is deleted; the function now
returns the type test, with no new branch. Pinned by a stored row
carrying the old block on a lookup, a `master_detail` and a `user`
field: only the text field `subject` renders.
- **H3, gone, not refused.** On the in-process `HonoHttpServer`, with
the unmatched-request seam installed as `HonoServerPlugin.start()`
installs it, `GET /api/v1/forms/test/lookup/owner_id` answers `404` with
`error.code` `ENDPOINT_NOT_FOUND`. That body is byte-identical (path
aside) to a never-registered sibling path, and `findData` is never
called. The registered resolve route on the same harness answers 200,
the lit control.
- **H4, the ledger rule.** Deletion, per the rule's own text. The
`objectui` consumer check: zero readers at the old pin `e420df310f` and
the new pin `31971ff1e2` (only an exemption reason string in a parity
test). The `cloud` consumer check is NOT MEASURED: code search returned
zero for both the codes and a lit control (`"@objectstack/spec"`), so it
cannot see that repository.
- **H5, `os validate` on a fixture.** Before, at BASE: the fixture
authoring `publicPicker: { displayFields: ['name'], maxResults: 10 }`
gave exit 0, `valid: true`. After: exit 1, `valid: false`. The text face
prints the prescription at
`views.0.formViews.contact.sections.0.fields.1.publicPicker`. The
control fixture, identical without the key, gives exit 0 both times.
- **Pin stop condition.** objectui at `.objectui-sha` imports neither
name at `e420df310f` nor, after objectstack-ai#21149 moved it, at `31971ff1e2`: 7
string mentions and 0 import lines, with a lit control of 392 files
importing `@objectstack/spec/ui`. Its spec-parity test enumerates
`FormFieldSchema.in.shape`, and the tombstone keeps the key there. No
pin bump rides this PR.

## Reverse verification (the fix committed first, then BASE's route code
restored)

Run against `rest-server.ts` and the lowering module restored from BASE
(blob `c673773e46`, verified on disk by hash). The pins import source,
so no rebuild was needed. All three new pins went red in the expected
direction:

- strip: `expected [ 'subject', 'contact_id', …(2) ] to deeply equal [
'subject' ]`
- route registered: `expected true to be false`
- route answer: `expected 403 to be 404`

The other 20 cases in the file stayed green. Restore: back to the HEAD
blob `b52e360ec3`, `git diff HEAD` empty, `git status` clean, with a
trap on the script.

## Tests and gates (head `dafa22868`, after merging `main` at
`5e5ce48ce`)

- `@objectstack/rest`, whole package: 252 files, 4776 passed. Typecheck
green, including `check:test-typecheck`.
- `@objectstack/lint`, whole package (pre-merge, untouched by `main`):
118 files, 5486 passed. Typecheck green.
- `@objectstack/spec`: the local project ran 593 files with one failure,
the ledger's header totals (fixed: 212 → 210 schemas, 617 → 613 sites),
then 27/27. The repo project: 48 files, 849 passed. After the merge:
`src/ui`, `src/conversions`, `src/migrations`, error-code ledger,
migrate-sentence, alias-integrity and the merge-shape scripts give 120
files, 4299 passed. `check:generated`: 15/15 current. Typecheck green,
including `check:scripts-typecheck` and `check:test-typecheck`, which
compiles the new test's `@ts-expect-error`.
- `@objectstack/metadata-protocol`: `protocol.save-union-issues.test.ts`
25/25. Typecheck green.
- `@objectstack/cli`: unit tier 242 files, 3435 passed. The integration
tier is declared to CI.
- `@objectstack/dogfood`: `zero-set-masking` and
`expression-conformance`, 8/8. Typecheck green.
- Derived gates: `dispatch-gates --commands` on this head gives 138
families. All 138 ran to exit 0. `--ran` reconciliation: 138 run, 0
NOT-MEASURED, a derived zero with every exit code recorded. Three
refusals were cleared by building their prerequisites, not by skipping:
`check:skill-examples` and `check:dual-build-cjs-loads` exited 3 until
seven packages outside this diff were built. `check:pm-dispatch-gates`
and `check:type-check-debt` were re-run without the runner's 480 s cap.
- Lint, as a proven narrowing: eslint's own config lints
`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` and enables no type-aware
linting. The 21 touched code files were all linted (`--format json`): 0
errors, 0 warnings. The repo-wide `pnpm lint` is CI's.
- Changeset gates, against BASE: `check:adr-0087-registration`
(registered, both ids new here), `check:empty-changeset`,
`check-changeset-no-major` and `check:doc-authoring` all pass. The level
axis needs this PR's payload, so CI judges it.

## Serial with objectstack-ai#21079 (PR objectstack-ai#21217)

At open time PR objectstack-ai#21217 is a draft and not merged, so this PR is first of
the pair. This branch deletes both of objectstack-ai#21062's picker pins and removes
the picker door case of `zero-set-masking.dogfood.test.ts`, along with
the public form and inquiry object only that case booted, since the
fixture can no longer carry the retired key. The record-door case is
byte-identical. If objectstack-ai#21217 lands first, `main` gets merged here and these
deletions are kept. `security-plugin.ts` and `security-service.ts` are
untouched.

## Acceptance notes (observed, not filed)

- `.changeset/21062-picker-queryable-key.md` is an unreleased changeset
describing a change to the route this PR deletes. If both ship in one
release, the compiled notes will describe a route that no longer exists.
Left for the release compiler; this PR does not edit another PR's
changeset.
- `objectSchema` from `GET /forms/:slug` still publishes the definitions
of declared lookup / `master_detail` / `user` fields. The ruled strip
covers the rendered sections only, and widening it would be a new gate.
- `ISecurityService.getQueryableFields` loses its only REST reader with
the picker (`objectstack-ai#20935`). The method stays: it lives in objectstack-ai#21079's surface.
- objectui's parity test carries an exemption reason that describes the
retired route. It stays accurate until objectui moves to the spec
release that carries this; then its prose is stale (the key stays in the
shape).
- Two dated audits (`docs/audits/2026-06-*`) and the ADR-0096 table name
the route as history and are left as written.

## Deviations

- Changeset grade is `minor`, not `major`: the skill and
`check-changeset-no-major` refuse `major` in the launch window. BREAKING
is carried by the banner, the `Clause-②` line and the ADR-0087 marker.
- `LOOKUP_TARGET_MISSING` also leaves the ledger. The card names only
`LOOKUP_NOT_PUBLIC`, but the ledger's rule applies to both, and the
deleted handler was the only producer of each.
- The claim's file surface did not name `view-filter-rule-lowering.ts`
(the picker-only helper), `retired-defs/18.ui__FormFieldPublicPicker.ts`
(required by the build's manifest-deletion gate),
`dropped-refinements.baseline.json`,
`engine-double-contract.pinned.json`, `scripts/check-route-envelope.mjs`
(ratchet banking), `metadata-protocol` (a stale comment and a test that
rode the key), or the platform checklist item. Each follows mechanically
from the deletion.
- `migrations/registry.ts`: the generated regions come from
`gen:migration-registry`. The one hand edit is the `STEP18_RATIONALE`
fragment outside the markers, which the retirement skill requires.

---
_Generated by [Claude
Code](https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants