Skip to content

fix(objectql): drop the 135 as any that dodged registerObject's old parameter type (#5543) - #6786

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-5543-register-object-input-type
Aug 8, 2026
Merged

fix(objectql): drop the 135 as any that dodged registerObject's old parameter type (#5543)#6786
os-zhuang merged 1 commit into
mainfrom
claude/issue-5543-register-object-input-type

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #5543

Premise re-anchoring — the annotation half already landed upstream

Everything below was re-measured against origin/main at ea1d9165 (the head after PR #6697 merged at 15:21Z), not against the issue's 08-05 line anchors.

# Issue premise (2026-08-05) Status on origin/main @ ea1d9165 Evidence
1 object.zod.ts exports both ServiceObject (= z.infer, OUTPUT) and ServiceObjectInput (= z.input) EXPIRED. ServiceObject is now z.input; the parsed shape moved to a new ServiceObjectParsed. ServiceObjectInput no longer exists anywhere in the repo. packages/spec/src/data/object.zod.ts:2213-2215; ADR-0122 phase 2, #6083, @objectstack/spec 17.0.0
2 registerObject(schema: ServiceObject, …) names the OUTPUT type EXPIRED as a defect. Both doors are still annotated ServiceObject — and that alias now is the authored INPUT shape, so the annotation already says what the ruling asked it to say. engine.ts:7950, registry.ts:1045
3 The authored literal { name: 'task', label: 'Task', fields: { title: { type: 'text', label: 'Title' } } } fails with TS2740 EXPIRED. It compiles clean against both doors with no cast. probe module type-checked against packages/objectql/tsconfig.json; now pinned permanently by register-object-authored-shape.pin.ts
4 All 6 residual errors in packages/client/test-typecheck-debt.json are this family EXPIRED. That ledger is now "entries": {}#6083 graduated it, and its _comment records exactly this cause. packages/client/test-typecheck-debt.json; check:test-typecheck reports 0 file(s) / 0 error(s)
5 registry-log-level.test.ts:25-26 uses as any to dodge it — hiding real typos too STILL LIVE, and far larger than the issue knew. 135 such casts across 46 files in packages/objectql. grep -rn "registerObject(.*as any" packages/objectql/src

So the ruling's first clause (retype the parameter to the z.input shape) is a no-op today and this PR changes no annotation — registry.ts is not touched at all. Its second clause — 摘除相关 as any — was untouched, is the whole of premise 5, and is what this PR does.

The verbatim ruling, unmodified:

「裁决(2026-08-06):方案 A——registerObject 参数改 ServiceObjectInput。engine.ts 与 registry.ts 的同名方法注解一并改为 z.input 形状(参数放宽,非破坏);运行时本就接受 INPUT 形状(registry 不跑 zod parse),现状注解是纯编译期谎言。摘除相关 as any,两包 typecheck 债台账条目按 EXACT ratchet 毕业。实现注意:边界收窄(registry 负责补默认值)不引入 boot 抛错的 parse——registry 现行 warn-never-throw 哲学(registry.ts:966-968)。量级 S。」

What changed

Runtime behaviour is unchanged in both directions. No runtime step is added, removed, or reordered; registerObject still runs no parse, still fills no defaults, still warns rather than throws.

  1. 137 as any removed, 0 added — 135 at registerObject argument positions across 46 files, plus the 2 adjacent (objDef as any).name = name mutation casts in engine.ts. Each of the 135 existed only to get an authored literal past the pre-ADR-0122 第二期:major 窗口翻转裸名为解析态语义 + XInput 去留 #6083 parameter type.
  2. Four real defects the casts were hiding, fixed. A blanket as any suppresses every error at the call, not one:
    • save-meta-response-conformance.test.ts declared primaryKey: true on a field. There is no such Field key in the spec — it exists only on external-catalog remote columns (packages/spec/src/data/external-catalog.zod.ts:22). Inert metadata nothing ever read.
    • the same fixture typed a field 'longtext'; the spec spells it 'textarea'.
    • two validation-rule fixtures in registry.test.ts omitted the required name and message (BaseValidationRuleShape).
  3. Two casts in engine.ts were load-bearing for an unrelated reason and are fixed properly rather than restored: registerApp(manifest: any) widens its map branch to unknown. Both sites now state the contract once on the entries (as [string, ServiceObject][]), which also turns the adjacent (objDef as any).name = name into a checked objDef.name = name.
  4. register-object-authored-shape.pin.ts — a compile-time pin for both halves of the contract. It is a .pin.ts and not a *.test.ts deliberately: this package's tsconfig.json excludes **/*.test.ts, so a @ts-expect-error written in a test file here would be a phantom check (AGENTS.md / @ts-expect-error 退役 pin 在 packages/spec 里是幽灵检查:tsconfig 把 **/*.test.ts 排除出唯一的 tsc --noEmit #5286 PINS_CHECKED, which is closed to new PHANTOM_PIN_DEBT entries). The pin file is inside the program pnpm typecheck runs.
  5. register-object-authored-shape.test.ts — the runtime half: registers the objectql registerObject 的参数标成 schema 的 OUTPUT 类型(z.infer),合法的作者态字面量一律 TS2740 —— 这是 spec/client 两份测试层欠账的同一根因 #5543 literal for real, asserts no .default(...) products appear on the way out, and asserts a sparse object does not throw.

Rejection / typing pins

tsc --noEmit passing over the pin file is itself the proof of both directions — an unfired @ts-expect-error is TS2578, so each negative below is asserted to still fail. Codes captured from a directive-free probe of the same four literals:

Pin Direction Result
the #5543 literal on ObjectQL.registerObject, no cast must compile
the same literal on SchemaRegistry.registerObject, no cast must compile
{ name: 'sys_thing', fields: {} } — every defaulted key omitted must compile
{ title: { type: 'text', primaryKey: true } } — unknown Field key must fail ✅ TS2353
{ body: { type: 'longtext' } } — wrong field type must fail ✅ TS2322
{ label: 'Task', fields: {} }name missing must fail ✅ TS2345
{ fields: { title: 'text' } } — field is a bare string must fail ✅ TS2322

The loosening admits authored metadata; it does not admit garbage.

PM mechanism hypotheses

A1 — is the loosening non-breaking for every existing caller? Not applicable in the form asked, and that is the finding: this PR loosens nothing, because #6083 already did. The question it does answer is whether the post-flip parameter still accepts everything the workspace passes it — measured across the built workspace, yes. turbo run typecheck over ./packages/*, ./packages/*/*, ./apps/* is green (120/120), as are ./examples/* and @objectstack/downstream-contract, and the full check:type-check-debt re-measure reports no entry above its recorded number. No caller anywhere holds a ServiceObjectParsed and hands it to registerObject, so the output→input assignability trap never gets exercised; no counterexample to force-cast around.

A2 — where do defaults actually materialize? Nowhere on the register path. SchemaRegistry.registerObject runs no zod parse: the only ObjectSchema.parse in registry.ts is inside validate() (registry.ts:1477-1480), a separate method the register path never calls. What registerObject does run is applySystemFields, reconcileManagedApiMethods, provisionPrimary and the warn* diagnostics — injection and reconciliation, not default-filling. So the ruling's "registry 负责补默认值" narrowing has no seam to be placed at: adding one would mean introducing a parse where none exists, which is exactly the boot-throwing parse the ruling forbids and which registry.ts's warn-never-throw philosophy rules out (that anchor moved — it now reads at registry.ts:716 and :1076, not :966-968). The correct action was to place nothing, and the new runtime test asserts the seam stayed put — it checks that searchable / required / multiple / unique are all still absent after registration.

A3 — does removing the as any surface hidden errors? Yes — four, all real, all now fixed (listed above). The other 131 removals surfaced nothing new: the remaining diagnostics that appeared on a first mechanical strip were 14 literal-widening errors (const obj = { … } extracted to a variable widens type: 'text' to type: string), fixed honestly by annotating those fixtures : ServiceObject rather than re-casting — which now type-checks literals that used to be invisible.

Reverse verification — predicted, then measured

Stated before running, then measured. packages/objectql's tests are excluded from its tsconfig.json, so its real test-layer count is only visible through the TEST_DEBT re-measure; that is the number quoted.

Prediction Measured Verdict
The #5543 literal already compiles against both doors on origin/main compiles clean, 0 errors ✅ confirmed — premise expired
Stripping all 135 casts leaves packages/objectql tsc --noEmit -p tsconfig.json green 2 errors, both engine.ts, both from manifest: anynot the INPUT/OUTPUT family ⚠️ refuted as stated; cause identified and fixed properly
Stripping the casts surfaces hidden errors in the (invisible) test layer 333 → 351, i.e. +18 ✅ confirmed — the casts were hiding things
Of those 18, most are widening artefacts and a few are real defects 14 widening, 4 real (unknown key, wrong field type, 2 incomplete validation rules) ✅ confirmed
After fixing all of them the count lands at or below the 333 baseline 332 ✅ confirmed (also −1: one pre-existing TS7053 in search-companion.test.ts fell out when contact() gained its return type)
packages/client's ledger measures below its recorded number (graduation pressure) already "entries": {} — graduated by #6083, before this PR ✅ confirmed, upstream

Ledger effectpnpm check:type-check-debt after a full turbo run build --filter='!@objectstack/docs':

  • @objectstack/objectql TEST_DEBT: recorded 355, measured 332 (baseline before this PR: 333). Below ledger → informational "can be lowered", never a required bookkeeping edit. Deliberately not lowered here: that entry's own note records it as a +10 bootstrap margin precisely because the merge queue re-measures a PR onto the current queue head, and [finding] DEBT ledger counts in check-type-check-coverage.mjs drift silently — @objectstack/metadata-protocol records 28, actually reports 63 #5278 was kicked on this number three times before landing. Tightening a number this hot from a PR in flight is what the margin exists to avoid.
  • @objectstack/client: check:test-typecheck0 file(s) / 0 error(s), already empty on origin/main.
  • @objectstack/spec: check:test-typecheck58 file(s) / 267 error(s), unchanged by this PR (⛔ no spec changes). Recorded here for the record only — the issue's "691-entry ledger" figure is itself stale.

Neither test-typecheck-debt.json was hand-edited; the EXACT ratchet graduates them in their own packages' runs.

Gates

Enumerated fresh from .github/workflows/lint.yml (not from memory) and run one by one — all 40 root check:* gates, all 12 @objectstack/spec gates, @objectstack/lint check:doc-formula-expressions, pnpm lint, the workspace/examples/downstream-contract typechecks. All pass. packages/objectql: 150 test files / 2578 tests passed.

Scope

packages/objectql only (137 cast removals, 2 engine.ts seams, 4 fixture fixes, 1 pin module, 1 test) plus one .changeset/. No packages/spec change, no registry.ts change, no content/docs/releases/ edit.

… parameter type (#5543)

`registerObject(schema: ServiceObject, …)` used to name the POST-parse object
shape, so an authored literal failed with TS2740 demanding ~9 zod
`.default(...)` products the registry never materializes. ADR-0122 phase 2
(#6083) fixed the annotation upstream by making the bare `ServiceObject` alias
mean the authored (`z.input`) shape; this removes the workaround it made
obsolete — 135 `as any` casts across 46 files in this package, written only to
get authored literals past the old type.

A blanket `as any` suppresses every error at the call, not one, so the casts
were hiding real mistakes too. Removing them surfaced four, now fixed: a
`primaryKey: true` that is not a spec Field key at all, a field typed
`'longtext'` where the spec spells it `'textarea'`, and two validation-rule
fixtures missing the required `name`/`message`.

Two casts in engine.ts were load-bearing for an unrelated reason —
`registerApp(manifest: any)` widens its map branch to `unknown` — and are
replaced by stating the contract once on the entries, which also makes the
adjacent `(objDef as any).name = name` a checked assignment.

`register-object-authored-shape.pin.ts` pins both halves so a re-flip cannot
land quietly: the #5543 literal compiles with no cast, and an unknown key, a
wrong field type, a missing `name`, and a bare-string field each still fail. It
is a `.pin.ts` rather than a test because this package's tsconfig excludes its
tests, which would make the directives phantom checks (#5286 PINS_CHECKED). A
companion test registers the same literal for real and asserts the path still
fills no defaults and still does not throw.

Runtime behaviour is unchanged in both directions. Measured objectql TEST_DEBT:
333 before → 332 after (ledger records 355).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4y4NTZYxr2sRRzBb5AQxw
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 8, 2026 5:37pm

Request Review

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql.

14 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/kernel/runtime-services/examples.mdx (via packages/objectql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/plugins/index.mdx (via @objectstack/objectql)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants