Skip to content

convert the nine fields.out.keyType dropped-refinement rows to the bannedKeys projection arm — expected ledger +0, and the published JSON Schema would carry the constructor/prototype refusal itself #19346

Description

@os-elon-musk

Path: P3 | 那条路第 3 步「验证响亮拒绝错的」 | 已发布 JSON Schema 不带 constructor/prototype 的拒绝,只有运行时带
分诊重测与定级:2026-09-20T15:25Z

Filed by the domain:spec execution seat (seat 3, session_019srGWGCBBCBHqcDoRZpQRh) as the committed follow-up of a delegated adjudication: the maintainer handed the seat the ratchet-increment decision on PR #19147 (「棘轮增量 你决定」), the seat took A (land the +9 now) on the explicit condition that B becomes this card. ⛔ Unassigned and ungraded — grading is triage's. Readings on origin/main at 2026-09-20 12:4x UTC.

Nine dropped-refinements rows can probably become projected rows — which both shrinks the ledger by 9 AND makes the published JSON Schema actually carry the refusal. The mechanism landed on main after the ruling that produced those nine rows, so nobody has tried it here yet.

What the nine rows are

PR #19147 refuses constructor and prototype as ObjectSchema.fields keys via a .refine() on the key schema. A .refine() is a custom check, and z.toJSONSchema() has no arm for one, so it reaches the runtime and not the file — which is exactly what packages/spec/dropped-refinements.baseline.json exists to record. Result: nine new rows, one fields.out.keyType site per embedding schema (api/AssembledInstalledPackage, api/GetInstalledPackageResponse, api/InstalledPackageAtEitherStage, api/ListInstalledPackagesResponse, api/ObjectDefinitionResponse, data/Object, system/ChangeSet, system/CreateObjectOperation, system/MigrationOperation), measured identically at three different ledger anchors (553→562, 551→560, 560→569 — always +9, always the same nine).

⚠️ The __proto__ half contributes zero and is NOT in scope here: its pre-parse guard is a z.preprocess transform node, which this ratchet cannot see at all. So this card can reach two of the three names, never three.

The mechanism that may make it +0

PR #19137 (5eebc9edc, merged 2026-09-19 00:50:01 UTC) added a banned-keys arm to the closed projection list in packages/spec/src/shared/refinement-projection.ts, under the same ruling batch (#154 item 3, letter C). bannedKeys([...]) is a record-level .refine() predicate that the generator publishes as propertyNames + not, it reads own properties only, and its own docblock discusses constructor by name.

⇒ Rewriting the refusal as a record-level bannedKeys(['constructor','prototype']) inside the existing refuseRecordProtoKey(...) wrapper is expected to move those nine sites from dropped to refinementSitesThatDidProject, leaving the ledger unchanged (+0) — and, more importantly than the number, making packages/spec/json-schema/** refuse those two names by itself, which is the whole point of #18670.

That path is what the ledger's own description prescribes for a repair: 「a refinement DECLARED through the closed list … reads projected rather than dropped, and its row LEAVES this ledger in the same PR — which is why the ledger shrinks and never grows on a repair」. #19137 says the same in its own words about its remaining banned-key sites.

⛔ NOT measured — this is a lead, not a result

The +0 is the at-tier reviewer's expectation, stated by it as unmeasured (its checkout had no node_modules), and this seat has not measured it either. Two named reasons it could come out differently, both from that review:

  1. how the projection walker handles a .refine() sitting on a pipe's out edge — which is where this one would sit, because fields is wrapped in z.preprocess;
  2. packages/spec/src/data/object.test.ts:2706-2726 pins the refusal's error shape: path fields.constructor with code invalid_key. A record-level refine changes that to path fields with code custom, so those pins must move with it — and whether the located-ness that the original dispatch required ("the issue must name which slot and which key") survives the move is a real question, not a formality.

First deliverable of any round on this card is the measurement, not the edit: implement, run pnpm --filter @objectstack/spec gen:schema, and report the four measured numbers against main's. If it is not +0, that is a fine answer and the card closes with the reading.

What is NOT claimed

Dedupe words

bannedKeys projection arm · dropped to projected conversion · fields.out.keyType nine sites · propertyNames not published refusal · dropped-refinements shrink repair

Related: PR #19147 and cards #17852 / #18847 (where the nine rows come from) · #19137 (the arm) · #18670 (the projection gap this closes a piece of) · #19180 (the ledger's merge/admission discipline) · the delegation and four-axis reasoning at comment 5749885844 on #17852.

Generated by Claude Code in session session_019srGWGCBBCBHqcDoRZpQRh; attribution is prose because a footer block is stripped on issue creation.


Generated by Claude Code

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions