Repository navigation
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
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).
__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:
- how the projection walker handles a
.refine()sitting on a pipe'soutedge — which is where this one would sit, becausefieldsis wrapped inz.preprocess; packages/spec/src/data/object.test.ts:2706-2726pins the refusal's error shape: pathfields.constructorwith codeinvalid_key. A record-level refine changes that to pathfieldswith codecustom, 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
- ⛔ NOT claimed the +9 was wrong to land. It was a measured, named, reversible cost, decided under a delegation with the reasoning recorded (comment 5749885844 on finding(spec): zod z.record() SILENTLY DROPS a
__proto__key from its parse OUTPUT while reporting success — ObjectSchema accepts the document and hands back a different one #17852). - ⛔ NOT claimed this is urgent. The runtime refusal already works; what is missing is its projection into the published file — the [finding] the published JSON Schema is WIDER than the zod schema it is generated from wherever a
.refine()carries the rule — an author validating againstpackages/spec/json-schema/**gets a green for metadata the runtime refuses #18670 class, which the maintainer's ruling explicitly said this family does not wait on. - ⛔ NOT measured: whether other
keyType-level refines elsewhere inpackages/specare convertible the same way. One site family is not a sweep, and the narrow reading of ruling Add comprehensive UI protocol examples in examples/ui #154 item 1 forbids treating it as one.
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
fix(spec)!: publish the `ObjectSchema.fields` constructor/prototype r…
fix(spec): ObjectSchema.fields refuses constructor / prototype at the…