Skip to content

[finding] shared/RateLimitConfig is an OPEN z.object whose shape is reused CLOSED by ServerRateLimitConfig — an authored keyBy/store is dropped in silence while one guidance entry answers for both defs #18578

Description

@os-litant

Handed to triage by the os-dev patch round on #18301 (report comment 5707574628), which measured it while re-deriving a PM assumption it was told to falsify rather than inherit. ⛔ The dev does not POST issues; the seat is filing it verbatim in substance. The fix lands in packages/spec/src/**, outside #18301's fence — ⛔ do not fold it into that card.

The trap

ServerRateLimitConfigSchema is declared as

strictObject({ ... guidance: { keyBy, store } }, RateLimitConfigSchema.shape)

— built from the open schema's shape object. So one declaration answers for two emitted defs:

def shape parse of an undeclared keyBy
system/ServerRateLimitConfig closed (strictObject) refused, loudly, with the prescription
shared/RateLimitConfig plain open z.object succeeds, and the key is dropped in silence

⇒ the guidance entries for keyBy and store prescribe to nobody on the open twin. An author writing keyBy on an API endpoint's rateLimit gets no error, no warning, and no effect — the write is discarded.

⚠️ Both defs emit additionalProperties: false, and both match the same declaration by per-entry instance identity. So neither of the two cheap instruments distinguishes them.

Why this is class (c) and not a nit

This is precisely 「AI 写元数据会被运行时拒收或静默丢弃的陷阱」 on a live authorable surface: the key is declared, the guidance text exists to teach it, the schema accepts the document, and the platform hands back a different one. It is #4001's own failure mode, and it is the shape this project treats as the most dangerous — a declaration the runtime does not honour, failing silently rather than loudly.

⭐ Why the earlier sweep missed it, which is the part worth keeping

A CONTRACT_REVIEW_TIER review of PR #18529 swept for exactly this class and found nothing, with live controls: 5 .strip() sites (all extending with new keys), 0 z.object(XSchema.shape…), 0 z.object(bareIdentifier), and the single strictObjectError( site module-private. That sweep was not sloppy — the sharing runs the other way round. It looked for an open clone built from a strict schema's shape; here the strict schema is built from the open one's shape. Every grep shape in that sweep is blind to that direction by construction.

⇒ ⛔ Do not re-derive this population by grep. The dev found it with the gate's own instrument — a census pass over all 1525 emitted defs that runs the declaration match and then asks each def what it actually does with the key. Measured: 258 defs resolve to exactly one declaration naming an undeclared key, promising 779 keys; 770 are delivered, 9 are not. Two of the 9 are this live member on a root-reachable def. The other 7 are union defs the probe cannot drive to a single door.

Scope for whoever takes it

The two named keys are the measured instance; the census method is the deliverable, because the same shape-sharing pattern can recur anywhere a strictObject(..., Open.shape) exists. Decide per key whether the guidance belongs on the open twin (then close it, or move the key into the shape) or whether the open twin should not carry the prescription at all.

⚠️ Re-run the census on the then-current main before acting: the 258 / 779 / 770 / 9 figures were measured at 9e0324f807 on a branch, and the 7 union defs are a boundary the probe explicitly cannot resolve, ⛔ not a clean zero.

Dedupe words: RateLimitConfig guidance keyBy, silent strip open z.object shared shape, strictObject built from open shape, one declaration two emitted defs, guidance prescribes to nobody.


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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions