Repository navigation
fix(spec): keep lazySchema proxies identity-compatible with z.toJSONSchema (objectui#2561) - #3021
Merged
Merged
Conversation
…chema (objectui#2561) zod's toJSONSchema keys its seen-map on the traversed node (the lazySchema Proxy at z.lazy recursion getters and conversion roots), while its wrapper-type processors (pipe/lazy/optional/default/...) resolve ctx.seen.get(inst) via the REAL instance captured at construction — the identity mismatch crashes with "Cannot set properties of undefined (setting 'ref')". Latent while lazy-referenced schemas were plain objects; ADR-0089 D3a made PageComponentSchema/FormFieldSchema .strict().transform() pipes, breaking ObjectUI Studio's spec-derived Page/View inspector JSONSchema derivation under spec 15. The proxy now serves a memoised _zod facade that prototype-delegates to the real internals and wraps only processJSONSchema to alias the proxy's seen entry onto the real instance before delegating. Parse behavior unchanged; OS_EAGER_SCHEMAS=1 remains the bypass. Regression tests: the D3a pipe shape, recursion through z.lazy(() => proxy), mixed proxy+real traversal, and the PageSchema/ViewSchema Studio derivation paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 97 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
2 tasks done
7 of 68 tasks
veigajoao
pushed a commit
to veigajoao/objectstack
that referenced
this pull request
Sep 29, 2026
…mes a known repository, so pre-#N / post-#N are judged and framework#N is this repository (objectstack-ai#20554) Fixes objectstack-ai#20330 Clause-②: no ## What changed `scripts/check-issue-citations.mjs` read ANY `word#N` as a repository reference, so `pre-objectstack-ai#12248`, `post-objectstack-ai#6640`, `Pre-#N`, `POST-#N` and `Framework#N` were classed cross-repo and never judged. Its qualifier is now a **closed set**. A token joined to `#N` names a repository only when it is an `owner/repo` form or a name in the new `KNOWN_REPOSITORIES` table, matched case-insensitively. Any other prefix is prose, and the number after it is this repository's and is judged. One recogniser, `repositoryOf`, is asked in all three places: by extraction, by the board's probe set (`boardWanted`) and by the classifier (`namesThisRepository`). The three can no longer disagree about a token. - `KNOWN_REPOSITORIES` rows: `objectstack` and `framework` (both THIS repository), `objectui`, `ui` (an objectui alias, joined form only), `cloud`, `hotcrm`, `hotcrm-heimao`, `os-tianshun-mtc` and `os-project-titanwind-ehr`. Each row carries the measurement that put it in. - **Prose form.** `objectui PR objectstack-ai#10264`, `objectui objectstack-ai#2670`, `cloud objectstack-ai#2937` and `framework objectstack-ai#2679` read as their repository. The form is a known repository name, whitespace, an optional `PR` or `issue`, then `#N`. The alias `ui` is excluded from it, because `UI #N` is ordinary English. - **No carry across a pair.** In `objectui#6110 + objectstack-ai#6111` the second number stays this repository's. The convention is to qualify each number, and the one live site in the claim's surface is respelled: `packages/spec/src/data/field.zod.ts:370` now reads `objectui#6110 + objectui#6111`. This is comment-only, with a `patch` changeset. - **Ordinal heads.** Closing the set exposes ordinals that were hidden behind a fake qualifier. `NON_CITATION_HEADS` gains `OQ` (`ADR-0076 OQ#10`, 10 sites) and `PKCS` (`PKCS#11`, 1 site). A hyphen joining a head to its `#` is now read as the same head (`Prime-Directive-objectstack-ai#12`, 2 sites). `PD#12` (8 sites) was already covered by the existing `pd` head once its candidate is refused. - **Refusal text.** The `REMEDY` text now states the grammar that judged the author. ### A false red in the same seam, fixed because this change would have widened it `buildBoard` probed only UNQUALIFIED numbers, so a diff adding `objectstack#N` was classified against a board that never asked about N. Reproduced on unmodified `288611e3e5`: I appended `objectstack#20330` (this live card) to a swept file and ran `node scripts/check-issue-citations.mjs`. It exited **2**, reading `board: probed (0 citations)` and `[allocated-but-absent] ... objectstack#20330`. Reading `framework#N` as this repository would have inherited that false red on every site. The probe set is now `boardWanted`, meaning every citation judged here. The self-test pins it through `probeBoard` over a stub. The blocking rule is unchanged: findings still exit 2. ## Measurements the design rests on - **`framework` names this repository.** `git ls-remote https://github.com/objectstack-ai/framework` answered HEAD `288611e3e5`, identical to `objectstack-ai/objectstack`. The controls diverged: a nonexistent name under the same owner exited 128, and `objectstack-ai/objectui` answered its own HEAD `0eb9f36aca`. The REST and web routes to `framework` answered 403 from this session's proxy (bound to configured repositories), so git's rename redirect was the readable instrument. `framework#N` / `Framework#N` therefore read as THIS repository. - **Qualifier census on `288611e3e5`, over the declared surfaces.** There were 27 distinct candidates behind 1,655 sites: - This repository: `framework` 255, `objectstack` 111, `objectstack-ai/objectstack` 9, `Framework` 2. - Siblings: `objectui` 672, `cloud` 213, `hotcrm` 24, `objectstack-ai/objectui` 12, `ui` 11, `objectstack-ai/cloud` 9, `better-auth/better-auth` 3, `os-tianshun-mtc` 2, and 1 each of `hotcrm-heimao`, `os-project-titanwind-ehr`, `objectstack-ai/objectos`, `objectstack-ai/ats` and `objectstack-ai/hotcrm`. - Prose: `pre-` 284, `post-` 12, `Pre-` 4, `Post-` 4, `PRE-` 1 and `POST-` 1. - Ordinals: `OQ` 10, `PD` 8, `Prime-Directive-` 2, `PKCS` 1. - **`ui` is objectui.** `objectstack-ai/ui` does not exist, and `ui#6837`, `ui#6206` and `ui#6207` are objectui's records on its board (`objectstack#6206` answers 404, so it would have been a false death). - **Prose form, 27 sites.** For every objectui number I read objectui's board and this repository's. The objectui record is the one each sentence describes: `objectui objectstack-ai#2670` is "Flow designer: render loop / parallel / try_catch as nested", cited from `loop-node.ts`, and `objectui PR objectstack-ai#4264` diagnoses a path on the right side of `==`, cited beside `PATH_SHAPED_LITERAL`. This repository's same number is unrelated on every site. The `cloud` sites could not be read (private) and follow their context. There were zero false positives. - **Pair carry, 48 sites. The measurement refuses a carry rule.** - `,` and `and`: every cross-repo pair I could judge names THIS repository's second number. `cloud#1013 and objectstack-ai#10645` is this repository's cli `serve` issue (4 sites), `cloud#1020, objectstack-ai#5233` its org gate issue (6 sites), `objectui#2561, objectstack-ai#3021` its lazySchema PR, and `objectui#3136 and objectstack-ai#14492` answers 404 on objectui. - `/`: mostly carries, but not always. `objectui#3226 / objectstack-ai#4827` is this repository's objectstack-ai#4827, a conversion entry handed over from objectui and cited from `conversions/registry.ts`. - `+`: exactly one distinct pair exists in the corpus, which is too thin to establish a convention. ## Census of the newly judged spellings Taken with the gate's own `--census --json` at `a3c14755f8` against an enumerated board (184 pages, frontier objectstack-ai#20551). The per-site transition comes from the gate's `--list` before and after the change. Dead means `allocated-but-absent`. Four of the numbers (14657, 12998, 10194 and 8692) were re-probed directly and answered 404. | spelling | sites now judged | dead | |---|---:|---:| | `pre-#N` | 284 | 26 | | `framework#N` | 255 | 0 | | `post-#N` | 12 | 0 | | `Pre-#N` | 4 | 1 | | `Post-#N` | 4 | 0 | | `Framework#N` | 2 | 0 | | `PRE-#N` | 1 | 0 | | `POST-#N` | 1 | 0 | | **total** | **563** | **27** | Other readings, same run: - **Newly deferred (25 sites):** the 24 prose-form sites plus the respelled `field.zod.ts:370`. Four of them were base census deaths that were never deaths: `objectui PR objectstack-ai#8758` three times, and the respelled `objectstack-ai#6111`. - **No longer extracted (21 ordinals):** `OQ#10` ×10, `PD#12`/`PD#10` ×8, `Prime-Directive-objectstack-ai#10`/`objectstack-ai#12` ×2 and `PKCS#11` ×1. - **Whole-census tally:** - Base `288611e3e5`: 38,109 judged. resolves 32,202 · resolves-as-pull 1,863 · cross-repo-unjudged 1,535 · allocated-but-absent 2,509. - Branch `a3c14755f8`: 38,088 judged. resolves 32,689 · resolves-as-pull 1,891 · cross-repo-unjudged 976 · allocated-but-absent 2,532. - The board moved between the two runs, so the per-site transition above is the reading, not the tally difference. The cross-repo count reconciles exactly: 1,535 − 563 − 21 + 25 = 976. ## Verification All gates below ran at `a3c14755f8`, the branch head. - **Self-test.** `node scripts/check-issue-citations.mjs --self-test` exits 0 with 114 cases across 8 batteries (base: 73 cases across 7). The new battery `qualifier` (floor 40) pins every spelling both ways: lit on a live number and a FINDING on a dead one for `pre-` `post-` `Pre-` `Post-` `PRE-` `POST-`, and for `framework` `Framework` `objectstack-ai/framework` `objectstack`. It also pins cross-repo even when dead for `objectui` `OBJECTUI` `ui` `cloud` `hotcrm` `objectstack-ai/objectui` `better-auth/better-auth`, and covers: - an unknown word prefix read as prose; - the four ordinal heads; - the prose form, lit and dead, including `PR #N` and `UI #N` NOT being the prose form; - the pair, no carry (dead second number red) and qualified number by number (both deferred); - `boardWanted` and a probed-board resolution of `objectstack#20330`; - registry hygiene. `live-corpus` gains a pin that every qualifier the live corpus keeps names a repository. - **Ablations.** Six mutations went through `scripts/ablation-replace.mjs`, each landing on disk with the anchor count 1 → 0 and the blob changed, each red on its own case, and each restored with blob equal to HEAD `8b6cf12653dd` and `git diff HEAD` empty: - M1: the recogniser returns any bare candidate. The self-test reds on "`pre-` is prose". - M2: the probe set reverts to unqualified-only. It reds on "the board's probe set". - M3: the `framework` row is renamed. It reds on "`Framework` is THIS repository". - M4: the hyphen head is off. It reds on "`Prime-Directive-objectstack-ai#12`". - M5: the prose form is off. It reds on "`objectui PR #N` names objectui". - M6: the `OQ` head is removed. It reds on "`ADR-0076 OQ#10`". - **Diff-scoped verdict** (`node scripts/check-issue-citations.mjs`, as CI runs it): exit 0 on this branch. It judged the respelled line's 2 citations, both `cross-repo-unjudged`. - **One-time end-to-end proof** (no permanent test; injected uncommitted and restored with blob equal to HEAD and `git diff HEAD` empty). I appended `pre-objectstack-ai#12248` (dead) and `framework#20330` (live) to `packages/cli/src/commands/generate.ts` and ran the diff verdict twice: - The branch gate exits **2**. `pre-objectstack-ai#12248` is `allocated-but-absent`, `framework#20330` resolves on a board `probed (2 citations)`, and the respelled pair stays cross-repo. - The `288611e3e5` gate, from a temporary copy, exits **0** with all 4 `cross-repo-unjudged`. That is the hole this closes. - **Census** (`--census --json`): exit 0. It is report-only and never fails. - **Derived gates.** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 97 commands, and all 97 ran. 96 exit 0, including `pnpm check:pm-dispatch-gates`, whose `dispatch-gates.mjs --self-test` passes 1,976 cases. That self-test pins this file's `:204 local-env` declaration line, and every edit here stays below it or is line-neutral. `--ran` reconciliation reads "97 derived famil(ies) accounted for — 96 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)". - **NOT MEASURED: `check:dual-build-cjs-loads`.** Reason: it loads every package's built CJS entry, and this box holds no dist for 80+ packages. The diff changes no emitted code. `@objectstack/spec` was rebuilt, and its entry gates (`check:browser-reachable-entries`, `check:entry-nameability`) exit 0. CI's Lint and Repo Gates owns this one. - **Spec package.** `pnpm --filter @objectstack/spec typecheck` returned VERDICT command-exit 0. See the report comment for the test run. ## Acceptance notes - **Dead sites the census hands over, not rewritten here.** One is under `packages/spec/src/**` and is input for the staged sweep on objectstack-ai#20234: `packages/spec/src/meta-spelling/manifest-collection-spelling.ts:71` `pre-objectstack-ai#10194`. The other 26 are outside `packages/spec/src/**`, and no card names them: - `packages/cli/src/commands/generate.ts:1842` `pre-objectstack-ai#14657` - `packages/cli/src/utils/storage-driver.ts:206` `pre-objectstack-ai#6345` - `packages/drivers/driver-sql/src/schema-drift.ts:2458`, `:2474` `pre-objectstack-ai#12998` - `packages/drivers/driver-sql/src/sql-driver.ts:3872`, `:16545` `pre-objectstack-ai#17590` - `packages/drivers/driver-sql/src/sql-driver.ts:18285`, `:18324` `pre-objectstack-ai#12998` - `packages/drivers/driver-sql/src/sql-driver.ts:20095` `Pre-objectstack-ai#12380` - `packages/drivers/driver-turso/src/remote-transport.ts:2624` `pre-objectstack-ai#12380` - `packages/lint/src/validate-searchable-fields.ts:312` `pre-objectstack-ai#8404` - `packages/metadata-protocol/src/protocol.ts:2793` `pre-objectstack-ai#10888` - `packages/metadata-protocol/src/seed-loader.ts:1947` `pre-objectstack-ai#11674` - `packages/objectql/src/action-governance.ts:339` `pre-objectstack-ai#14423` - `packages/plugins/plugin-auth/src/auth-manager.ts:5629` `pre-objectstack-ai#14762` - `packages/plugins/plugin-security/src/bootstrap-platform-admin.ts:266`, `:630` `pre-objectstack-ai#8692` - `packages/plugins/plugin-security/src/per-organization-catalog.ts:314` `pre-objectstack-ai#8692` - `packages/plugins/plugin-security/src/permission-set-projection.ts:482` `pre-objectstack-ai#6483` - `packages/plugins/plugin-sharing/src/backfill-sys-record-share-organizations.ts:5` `pre-objectstack-ai#14484` - `packages/runtime/src/domains/mcp.ts:364` `pre-objectstack-ai#8726` - `packages/runtime/src/sandbox/body-runner.ts:548`, `:735` `pre-objectstack-ai#14758` - `packages/runtime/src/sandbox/script-runner.ts:440` `pre-objectstack-ai#14758` - `packages/types/src/driver-error-classification.ts:608` `pre-objectstack-ai#13324` - `packages/types/src/node.ts:1428` `pre-objectstack-ai#10943` - **The same `objectui#6110 + objectstack-ai#6111` pair outside the claim's surface.** It still reads `objectstack-ai#6111` as this repository's (404 here), so these are existing census deaths: `packages/spec/src/ui/view.zod.ts:3634` (for the objectstack-ai#20234 sweep), `packages/metadata-core/src/form-predicate-root-policy.ts:14`, `:120` and `:205`, and `packages/metadata/src/plugin.ts:910`. Each respells to `objectui#6110 + objectui#6111`. - **Pairs that read silently wrong, not dead.** Several `REPO#N / #M` pairs name the qualifier's own second number, which resolves here as an unrelated record. Examples: `hotcrm-heimao#35/objectstack-ai#40/objectstack-ai#59`, `objectui#2715/objectstack-ai#2717`, `objectui#2711/objectstack-ai#2722`, `objectui#4648/objectstack-ai#4901`, `objectui#5018 / objectstack-ai#6469`, `cloud#957 / objectstack-ai#962` and `cloud#930/objectstack-ai#944`. No gate can see these, because they resolve. The convention in the refusal text (qualify each number) is the remedy when someone next touches the line. - **Seat 4's `objectui PR objectstack-ai#10264` specimen.** `packages/spec/src/api/export-job-family-retirement.test.ts:25` sits on a DEFERRED surface (`packages/**/*.test.ts`), and `surfaceFor` answers `null` for it. The census never judged that site. The prose form it names is now read correctly wherever the census does look. - **Observed once: a truncated board enumeration accepted as a reading.** My first branch `--census` read `enumerated (126 pages)` with frontier objectstack-ai#13977, against 184 pages and objectstack-ai#20551 on the re-run minutes later, and reported 9,160 `never-issued` phantoms. `enumerateBoard` stops at the first page without `rel="next"` and trusts the maximum it saw as the frontier. This diff does not touch that code. The diff-scoped verdict enumerates only past 400 distinct numbers. Recorded, not filed; the seat decides. - **Scope declaration.** `NON_CITATION_HEADS` (two rows) and `nonCitationHead` (the hyphen) are grammar next to the qualifier, not the qualifier itself. They are here because closing the qualifier made those 13 ordinal sites judged citations of this repository's objectstack-ai#10, objectstack-ai#11 and objectstack-ai#12, which is false. No gate was added, and the diff-scoped blocking rule is unchanged. --- _Generated by [Claude Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
z.toJSONSchemacrashes withTypeError: Cannot set properties of undefined (setting 'ref')on spec 15PageSchema/ViewSchema/PageComponentSchema/FormFieldSchema(default lazy mode;OS_EAGER_SCHEMAS=1converts fine). This breaks ObjectUI Studio's spec-derived Page/View inspector JSONSchema derivation (page-schema.ts/view-schema.tsin objectui app-shell) and currently blocks objectui's spec ^15 adoption (objectstack-ai/objectui#2561).Root cause
zod's
toJSONSchemakeys itsseenmap on the node object it traverses — thelazySchemaProxy, wherever a schema is referenced lazily (z.lazy(() => X)recursion getters, direct conversion roots). But zod's per-type hooks close over the real instance at construction time (inst._zod.processJSONSchema = (ctx, …) => pipeProcessor(inst, ctx, …)), and the wrapper-type processors (pipe/lazy/optional/default/…) then resolvectx.seen.get(inst)— a miss when the entry was keyed on the Proxy.Latent until now: plain-object schemas never look themselves up (
objectProcessordoesn't touchseen.get(self)), so the proxies were harmless while every lazy-referenced schema was a barez.object. ADR-0089 D3a turnedPageComponentSchema/FormFieldSchemainto.strict().transform(normalizeVisibleWhen)pipes — a wrapper type — detonating the identity mismatch. (zod 4.4.3 is current latest; no zod-side fix available.)Fix
lazySchema's proxy now serves a memoised_zodfacade: prototype-delegates every read to the real internals, wrapping onlyprocessJSONSchemato alias the proxy'sseenentry onto the real instance (alias, never clobber) before delegating. Parse hot path unchanged (facade built once per proxy);OS_EAGER_SCHEMAS=1bypass unchanged.Tests
lazy-schema.test.ts: D3a pipe shape, recursion throughz.lazy(() => proxy)(FormFieldSchema shape), mixed proxy+real traversal in both orders, facade memoisation. Verified the 3 new conversion tests fail without the fix.page.test.ts/view.test.ts: pin the exact Studio derivation calls (z.toJSONSchema(PageSchema/ViewSchema, { io: 'input', unrepresentable: 'any' })).@objectstack/specsuite: 253 files / 6863 tests green.Changeset: patch
@objectstack/spec. Once released, objectui rides it in the #2561 types-package PR (spec ^15 pin bump).Refs objectstack-ai/objectui#2561, ADR-0089 D3a.
🤖 Generated with Claude Code