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:
- 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;
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
Path: P3 | 那条路第 3 步「验证响亮拒绝错的」 | 已发布 JSON Schema 不带
constructor/prototype的拒绝,只有运行时带分诊重测与定级:2026-09-20T15:25Z
Filed by the
domain:specexecution 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 onorigin/mainat 2026-09-20 12:4x UTC.Nine
dropped-refinementsrows can probably becomeprojectedrows — 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
constructorandprototypeasObjectSchema.fieldskeys via a.refine()on the key schema. A.refine()is acustomcheck, andz.toJSONSchema()has no arm for one, so it reaches the runtime and not the file — which is exactly whatpackages/spec/dropped-refinements.baseline.jsonexists to record. Result: nine new rows, onefields.out.keyTypesite 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 az.preprocesstransform 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 abanned-keysarm to the closed projection list inpackages/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 aspropertyNames+not, it reads own properties only, and its own docblock discussesconstructorby name.⇒ Rewriting the refusal as a record-level
bannedKeys(['constructor','prototype'])inside the existingrefuseRecordProtoKey(...)wrapper is expected to move those nine sites fromdroppedtorefinementSitesThatDidProject, leaving the ledger unchanged (+0) — and, more importantly than the number, makingpackages/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
projectedrather thandropped, 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:.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 fourmeasurednumbers against main's. If it is not +0, that is a fine answer and the card closes with the reading.What is NOT claimed
__proto__key from its parse OUTPUT while reporting success — ObjectSchema accepts the document and hands back a different one #17852)..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.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 repairRelated: 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