Commit 5eebc9e
feat(spec)!: publish the banned-keys rule the tracing filter arm enforces (#19137)
Part of #18670 — item 2, the **fourth** of the ruling's four named arms:
**banned keys**. This body carries no closing keyword for that number on
purpose: measured banned-key sites are still unprojected (§6), and
whether the card closes is the seat's call rather than this PR's.
Clause-②: yes
**Carrier:** the published artefacts
`packages/spec/json-schema/system/TraceSamplingConfig.json` and
`system/TracingConfig.json`. The published JSON Schema **narrows**
toward what the runtime already refuses, and no document the runtime
accepts becomes refused. ⭐ **The `yes` stands on the ruling's own axis**
— a published artefact narrows — and the at-tier review measured that it
stands there **independently of the C5 tell**: `check:api-surface` and
`check:api-surface-declarations` both exit 0 with **no diff at all**,
because `src/shared/refinement-projection.ts` is re-exported by no entry
barrel and is not a `.zod.ts`, so it is not in `files[]`. The C5
widening tell is real — the as-const roster
`PROJECTABLE_REFINEMENT_PATTERNS` gains `banned-keys` and an exported
`bannedKeys()` appears beside it — but that roster is an **internal**
`export const`, not the package's public entry surface. ⛔ The `yes` does
not depend on it either way.
Director ruling batch #154 item 3, letter **C** (maintainer 「同意」,
2026-09-18T04:56Z): 「the projection emits a refinement only where the
rule is a complete, mechanically derivable JSON Schema pattern — banned
keys, required-one-of, non-blank — one ledger row at a time; everything
else stays annotated as `x-dropped-refinements`」.
---
## ⛔ This body was REPLACED WHOLESALE by the seat, and last refreshed at
2026-09-19T00:07Z for head `184615ded9`
The delivering dev writes a PR body once, at creation, and ⛔ does not
patch it; a later correction is named in its report for the seat to
write. That convention met a case it does not cover: **the tree the
first body described no longer exists.** PR #19084 (`ee5812a5e3`)
retired the CEL expression arm at this very slot before this branch
merged `origin/main`, so `condition` is now a plain record and not a
union — and the union framing ran through §0, §1, §3 and §4 alike. A
patch of some sections would have left the artefact self-contradictory
about the only tree it can land on, so the seat replaced it rather than
appending a third correction block.
Five things were stale, and each is now stated for head `384d27ac18`:
| # | was | now |
|:---|:---|:---|
| 1 | the slot framed as a UNION, the ban emitted into `anyOf[0]` | a
RECORD; the ban is conjoined onto it directly (§1, §3) |
| 2 | 「#19005 的普查走到 X 就停了」 — an account of a sibling release being wrong
| **RETRACTED.** The candidate set is TIME-DEPENDENT; #19005 read its
own tree correctly (§0) |
| 3 | the `$`-ban reaches ONE published node | **THREE**, each measured
and named (§6) |
| 4 | `77 derived / 74 exit 0 / 3 exit 3` | **82 derived / 78 run, all
exit 0 / 4 NOT MEASURED** (§7) |
| 5 | a live `Clause-②` disagreement between the claim and the ruling |
settled at **`yes`** on both carriers, and the claim comment carries the
correction |
⛔ Item 4 and item 5 were the **seat's** errors, not the dev's: the dev
copied the claim line verbatim as the dual carrier requires, and only
the seat writes claims and labels. Item 2 was the dev's, and the dev
retracted it itself on measurement. The retracted text is preserved at
the end of this body as HISTORY rather than deleted.
---
## 0. The pre-condition the releasing seat set — and the answer
The release of #19005 set a hard gate on whoever took this card next:
> Whoever takes it next must **re-derive the banned-keys candidate set
FIRST** and, if it is still empty, **return the card rather than
dispatching a dev to find nothing.**
**Re-derived. The set is NOT empty, and its clean member is the card's
own worked instance.**
⭐ **The candidate set is TIME-DEPENDENT, and that is the whole reason
the pre-condition was worth setting.** #19005's census recorded zero
clean candidates, and that was a **correct reading of its own tree** —
the `dialect` predicate at this slot did not exist yet; it arrived with
#18638, hours later. The instruction to re-derive the set FIRST is
exactly what caught a candidate that landed after the last census, and
it is the reason this card had work in it at all. ⛔ No sibling release
was wrong; an earlier draft of this body said one was, and that claim is
withdrawn.
**Instrument:** a TypeScript-AST scan of every `.refine` /
`.superRefine` / `.check` call expression under
`packages/spec/src/**/*.ts` (non-test), dumping each predicate's
argument text — **114 custom-check call sites** across 1008 source files
(`superRefine` 69, `refine` 44, `check` 1; 3 `.overwrite` calls
excluded, they are not custom checks). LIT CONTROL: 6 of those call
sites spell an already-declared arm (`requiredOneOf` ×2,
`NON_BLANK_STRING` ×3, `dependentRequired` ×1), so the scan does see the
population it is supposed to see.
**Radius, by form:** source text of tracked files. **A known target
outside it:** whether a given call site's node is a *ledger row* — the
ledger's sites are computed at run time by the detector against
`packages/spec/json-schema/**`, which is gitignored and returns 0
tracked entries. That is precisely why the earlier shape-only reading on
this card was recorded as "not a reading". So the population question
was answered with the instrument that can see it:
`collectDroppedRefinements` run over the live schemas, plus the
generator's own census.
**Result — 4 of the 114 predicates judge KEYS at all**, and they split
three ways:
| call site | predicate | verdict |
|:---|:---|:---|
| `src/system/tracing.zod.ts` (sampling `condition`) | `!('dialect' in
value)` | ⭐ **clean candidate** — a static, self-contained, finite key
ban. **2 ledger rows.** |
| `src/data/filter.zod.ts:1916` | `!Object.keys(condition).some((key) =>
key.startsWith('$'))` | an **open** key set — not this arm (§6).
Detector verdict `undecidable`, **0 ledger rows**, yet **3 published
nodes**. |
| `src/ui/action.zod.ts:1844` | `Object.keys(hints).every((k) =>
known.has(k))` | allowed keys computed from the sibling `data.params` —
not mechanically derivable; stays dropped and annotated, exactly as the
ruling prescribes. |
| `src/data/driver/common.zod.ts:537` | credential leaks at named paths
| judges **values**, not key names. Not this pattern. |
## 1. The arm
`banned-keys` — "no document may carry any of these keys" — emitted as
`propertyNames` with a `not` over the banned names. Same
closed-vocabulary mechanism the three landed arms use, no second one
introduced: `src/shared/refinement-projection.ts` declares the arm and
builds the predicate from that declaration,
`scripts/lib/refinement-projection.ts` emits it, and both halves still
reach `z.toJSONSchema` through the one shared
`projectPublishedJsonSchema` call.
**The slot is a record, not a union.** #19084 retired the CEL expression
arm of `TraceSamplingConfigSchema.composite[].condition`, so the node is
now a single `z.record(z.string(), z.unknown())` carrying the
retirement's own refusal hook and its `abort: true` message. The
anonymous `.refine((value) => !('dialect' in value))` that guarded it is
replaced by the **declared** `bannedKeys(['dialect'])` — the
retirement's prescription, error hook and message are taken from `main`
whole, and only the predicate is declared. ⛔ The retirement's behaviour
is unchanged by this PR; what changes is that the rule now has a
published form.
**Exact, not approximate.** A JSON object's properties are exactly its
own enumerable string-keyed ones, and `propertyNames` judges exactly
those names — so "none of the banned names is an own property" and "no
property name is one of the banned names" are one sentence read from two
ends. It is presence and never value: a banned key present with a `null`
value is present on both sides.
⛔ **The predicate reads OWN properties and never `key in value`.** `in`
walks the prototype chain, so a ban on a name `Object.prototype` carries
— `toString`, `constructor`, `valueOf` — would refuse `{}` itself while
`propertyNames` accepts it (`'toString' in JSON.parse('{}')` is `true`).
That is a disagreement about a JSON **document**, not an edge outside
the domain, and it is pinned in both directions. The shipped predicate
spells `Object.prototype.hasOwnProperty.call(value, key)` for that
reason.
**The emitted keywords are conjoined, never substituted.** The node is a
record and already states `propertyNames: { type: 'string' }` of its
own; replacing it would trade a key-TYPE rule for a key-NAME rule, which
is a narrowing paid for with a widening. The ban goes under `allOf`, the
same discipline `emitNonBlankString` follows for an existing `pattern`,
and the measured `format-type.ts` hazard is untouched — a top-level
`anyOf` is still never written, and the reference renderer reads neither
`allOf` nor `propertyNames`.
**An empty key list emits nothing**, and for a stronger reason than "it
would ban nothing": `enum` is specified as a non-empty array, so `{ not:
{ enum: [] } }` is an **invalid** schema rather than a vacuous one — ajv
refuses it with "enum must have non-empty array", which would take the
whole published file down instead of leaving a keyword nobody reads. The
declaring signature takes a non-empty tuple, so the guard is
belt-and-braces at a seam two files apart.
## 2. The rows retired, by name
`packages/spec/dropped-refinements.baseline.json`, **202 entries / 553
sites → 200 / 551**:
| row | before | after |
|:---|:---|:---|
| `system/TraceSamplingConfig` | `sites:
["composite.element.condition"]` | **deleted** — drops nothing now |
| `system/TracingConfig` | `sites:
["sampling.composite.element.condition"]` | **deleted** — the same node,
reached through the parent |
1 parent e872ef4 commit 5eebc9e
6 files changed
Lines changed: 323 additions & 17 deletions
File tree
- .changeset
- packages/spec
- scripts
- lib
- src
- system
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
6 | | - | |
7 | | - | |
8 | | - | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| |||
1044 | 1044 | | |
1045 | 1045 | | |
1046 | 1046 | | |
1047 | | - | |
1048 | | - | |
1049 | | - | |
1050 | | - | |
1051 | | - | |
1052 | | - | |
1053 | | - | |
1054 | | - | |
1055 | | - | |
1056 | | - | |
1057 | 1047 | | |
1058 | 1048 | | |
1059 | 1049 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
164 | 164 | | |
165 | 165 | | |
166 | 166 | | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
167 | 197 | | |
168 | 198 | | |
169 | 199 | | |
| |||
176 | 206 | | |
177 | 207 | | |
178 | 208 | | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
179 | 212 | | |
180 | 213 | | |
181 | 214 | | |
| |||
0 commit comments