Skip to content

Commit 50df124

Browse files
fix(spec)!: defineConnector() refuses a top-level key the connector does not declare (#28815)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9RavNfuxRRXtHCcMEJuJFjH)_ Fixes #28738 Clause-②: no (narrowing) Metadata-protocol: narrowed Skills-docs: none — the published skills' connector recipes (`skills/objectstack-automation/references/integration-recipes.md`) author declared keys only and say nothing about the helper's key posture, so no skill text turns false. ## 维护者速读 - 改了什么:`defineConnector()` 改为解析闭合的连接器形状;写了未声明的顶层键会当场报错并点名,附处方(凭据类键指向 `auth.credentialRef`,提供方设置指向 `providerConfig`,`timeout` 指向 `requestTimeoutMs`)。 - 为什么改:它原来解析开放的 `ConnectorSchema`,先把未声明的键悄悄丢掉,`defineStack` 等闭合门再也看不到,作者放错位置的设置(比如把 `timeout` 写在根上)无声丢失。现在它和其它所有连接器入口一样响亮拒收。 - 风险与回滚:属于收窄(破坏性)。仓内 7 处调用和文档示例都只写已声明键,不受影响;第三方若在 `defineConnector` 里写了未声明键,加载时会报错。插件给 `registerConnector` 的定义不变。回滚只需把 `defineConnector` 函数体改回 `ConnectorSchema.parse`。 - 先看:`packages/spec/src/integration/connector.zod.ts` 的 `DefineConnectorSchema` 与 `defineConnector` 函数体;`connector-entry-undeclared-keys.test.ts` 新增的一组测试;`.changeset/harden-connector-undeclared-keys.md` 改动的那一句。 - 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run src/integration/connector-entry-undeclared-keys.test.ts`,应看到 24 条全过。 - 需要你做的:在本 PR 上书面确认对 #28688 待发 changeset 那一句的更正(Check Changeset 因此保持红,它不是必需检查,不挡合并)。 ## What changes - `defineConnector()` (`@objectstack/spec`, `@objectstack/spec/integration`) parses a module-private `DefineConnectorSchema`: the closed carrier `DeclarativeConnectorEntryBaseSchema` behind the same retired-default residue stage (`CONNECTOR_RETIRED_KEY_RESIDUE`) that `ConnectorSchema` and `DeclarativeConnectorEntrySchema` use. An undeclared top-level key now throws a `ZodError` with one `unrecognized_keys` issue at the root, carrying the entry's own message, aliases and guidance. The signature is unchanged: input `z.input` of `ConnectorSchema`, return `ConnectorParsed`; `check:api-surface` and the rest of `check:generated` needed no regeneration. - `ConnectorSchema` (the def a plugin hands to `registerConnector`) keeps its open posture; option C of the card is not taken. - The helper does not apply the stack-only ADR-0097 / ADR-0143 refinements of `DeclarativeConnectorEntrySchema` (no inline `authentication`, `providerConfig` / `auth` need a `provider`, no `actions` on a provider-bound instance, the oauth2 / signed instance rules). Every stack door still applies them to the value the helper returns. See the Acceptance notes for why, and the measured fact that the two choices agree on every in-repo caller. - The ADR-0087 entry `connector-entry-undeclared-key-refused` (landed with #28688, not yet released) now names `defineConnector()` in its surface and reason. This PR's changeset disposes as `not-required (already-registered connector-entry-undeclared-key-refused)`. - Docblocks in `connector.zod.ts` and the `integration/` row of `docs/audits/2026-07-unknown-key-strictness-ledger.md` stop saying the helper parses the open shape. ### A pending release note this PR corrects `.changeset/harden-connector-undeclared-keys.md` (from #28688, still pending) said: "`ConnectorSchema` — the def a plugin hands to `registerConnector`, and the shape `defineConnector()` parses — keeps its posture." This PR makes the `defineConnector()` half false, so the clause is removed and the sentence now reads "`ConnectorSchema` — the def a plugin hands to `registerConnector` — keeps its posture." `check-empty-changeset` refuses this edit by design (the DELIBERATE CORRECTION class), so **Check Changeset stays red** until a maintainer confirms the correction here in writing. It is not a required context. ## Tests New `describe` block in `packages/spec/src/integration/connector-entry-undeclared-keys.test.ts` (12 cases; the file had 12): for an instance and a descriptor, each refusal pinned by `code`, `path` and `keys` on the thrown `ZodError`, the value never echoed; `timeout` → `requestTimeoutMs` with the declared spelling as control; the credentialRef and `providerConfig` guidance; the helper's issue equal to the stack door's issue on the same entry; residue (`status: 'inactive'`) and the ADR-0010 envelope still accepted; and a pin that the helper applies no stack-only refinement. Forced test edits: none; no existing test changed. Ablation at 4185e6a (`node scripts/ablation-replace.mjs`, body put back to `ConnectorSchema.parse(config)`, anchor 1 → 0, restored blob equal to HEAD): **7 failed | 17 passed (24)**. The 7 are exactly the refusal cases; controls, residue, envelope and the no-refinement pin pass on both sides, as they should. <details><summary>Readings (packages/spec and the consumers that import defineConnector)</summary> - `@objectstack/spec` test at f668513: Test Files 788 passed (788), Tests 23279 passed, 1 todo. Typecheck (incl. `check:test-typecheck`): exit 0. - `@objectstack/spec` `src/integration/` at bb3b9bc: Test Files 14 passed (14), Tests 264 passed. `check:generated` at bb3b9bc: all 14 artifacts up to date. - Consumers, found by `git grep "defineConnector("`, closure built first (`turbo run build`, 65 tasks), at f668513: - `@objectstack/example-showcase` (4 calls): typecheck exit 0; Test Files 42 passed (42), Tests 511 passed. - `@objectstack/connector-app-github` (1 call): Test Files 2 passed (2), Tests 13 passed; typecheck exit 0. - `@objectstack/connector-app-stripe` (1 call): Test Files 2 passed (2), Tests 19 passed; typecheck exit 0. - `@objectstack/downstream-contract` (source-resolution identity pin on both import paths): Test Files 3 passed (3), Tests 31 passed; typecheck exit 0. - Full dogfood (`pnpm --filter @objectstack/dogfood test`, no shard, at f668513): VERDICT command-exit 0; Test Files 377 passed, 3 skipped (380); Tests 3101 passed, 28 skipped. One named skip is `print-service-render` (no built Console on this host). - The final commit bb3b9bc edits one docblock line. The rebuilt `packages/spec/dist` at bb3b9bc equals the dist the dogfood run read (f668513): 360 of 360 non-map files byte-identical once esbuild's chunk-hash names are normalized. So the dogfood reading carries to the final head. </details> ## Acceptance notes - **Why the closed base, not the full entry schema.** The card's option B says the helper "parses the closed twin". The full `DeclarativeConnectorEntrySchema` would also refuse an inline non-`none` `authentication` and the other stack-only rules at the helper. That would break any plugin code that builds a `registerConnector` def through `defineConnector`, which is the code-to-code break option C was kept out of this card for. Measured: all 7 in-repo callers and all 5 `os:check` blocks in `content/docs/automation/connectors.mdx` give the same verdict under either choice, so the choice changes nothing in this repo. It is listed as an open question for the seat. - The helper's ZodError message is the strict entry's message, because the parse runs through the same `strictObject` carrier. It says "this connector entry", which fits the documented use. - `content/docs/releases/v17/17-6.mdx` names `defineConnector` among the doors that refuse retired keys. That is release-owned and still true; not touched. ## Verification Gate list derived by `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (84 commands) and run at the final head bb3b9bc; reconciled with `--ran` (exit codes recorded): 84 derived, 84 run, 0 NOT-MEASURED, 0 UNRUN. 83 exit 0. One is red by design: `node scripts/check-empty-changeset.mjs --base origin/main` exit 1, the DELIBERATE CORRECTION class above. <details><summary>Gate notes</summary> - An earlier sweep at f668513 had two exit-3 prerequisites, both met before the final sweep: `check-plugin-teardown-shape.mjs --self-test` needed its pinned fixture commit in the shallow clone (fetched; exit 0, 48 cases), and `pnpm check:dual-build-cjs-loads` needed 7 packages built (built; exit 0, 115 require entry points across 70 packages load). - `check-adr-0087-registration.mjs --base origin/main`: exit 0. The new changeset reads `[BREAKING+bang+clause-②-narrowing+metadata-protocol-narrowed]`, disposition `not-required (already-registered)`. - Local-only `pnpm check:dev-prereqs`: exit 1. It reads this host's build state (35 `ui/` packages with no dist), not this diff. Reported, not acted on. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes: eslint on the changed files exits 0 locally; the repo-wide run is CI's. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed (none were stale). - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. --- _Generated by [Claude Code](https://claude.ai/code/session_01ADHkmT9tAeotFczB7jYquW)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3e05ed1 commit 50df124

6 files changed

Lines changed: 180 additions & 13 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
fix(spec)!: `defineConnector()` refuses a top-level key the connector does not declare, as every other connector door does
6+
7+
Clause-②: no (narrowing)
8+
Metadata-protocol: narrowed
9+
10+
<!-- adr-0087: not-required (already-registered connector-entry-undeclared-key-refused) the same narrowing as that entry, reached through one more door; its surface now names defineConnector() -->
11+
12+
**BREAKING** accept-set narrowing on `defineConnector()` (`@objectstack/spec` and `@objectstack/spec/integration`).
13+
14+
- **Before.** The helper parsed the open `ConnectorSchema`, so a top-level key the connector does not declare was dropped without a word. Because the helper parses first, the closed authored entry behind `defineStack({ connectors })`, `os validate` / `os compile`, the artifact load and the metadata save door never saw the key either: the one helper every documented `connectors:` entry is wrapped in was the one door that stayed silent. TypeScript's excess-property check catches the key only in an object literal, so the gap was reached through a spread, a variable or a cast.
15+
- **Now.** The helper parses the closed connector carrier, behind the same retired-default residue stage as `ConnectorSchema`. Such a key throws a `ZodError` naming it (`unrecognized_keys` at the root), with the message the authored entry gives: a credential-shaped key (`apiKey`, `token`, `secret`, `password`, …) meets the `auth.credentialRef` prescription, a provider setting written at the top level (`baseUrl`, `spec`, `transport`, `headers`, …) is pointed at `providerConfig`, and `timeout` / `timeoutMs` are pointed at `requestTimeoutMs`. The ADR-0010 protection envelope stays declared and a retired key's emitted default (`status: 'inactive'`) stays accepted as residue.
16+
- **Unchanged.** The return type (`ConnectorParsed`). `ConnectorSchema`, the def a plugin hands to `registerConnector`, keeps its open posture. The stack-only rules of an authored entry (no inline `authentication`, `providerConfig` / `auth` need a `provider`, no `actions` on a provider-bound instance) stay at the stack doors, which still apply them to the value the helper returns.
17+
18+
## FROM → TO
19+
20+
```ts
21+
// before: returned without `timeout`; the request timeout stayed the 30 s default
22+
defineConnector({ ...billingApi, timeout: 5000 });
23+
24+
// after: `timeout` is refused by name and pointed at the declared key
25+
defineConnector({ ...billingApi, requestTimeoutMs: 5000 });
26+
```
27+
28+
Move a provider setting under `providerConfig`, put a secret into the secrets layer under the name `auth.credentialRef` gives, and delete any other undeclared key. The upgrade guide entry is `connector-entry-undeclared-key-refused`.

‎.changeset/harden-connector-undeclared-keys.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Metadata-protocol: narrowed
1313

1414
- **Before.** The entry was a plain object: a top-level key it does not declare parsed green and meant nothing to the runtime.
1515
- **Now.** The entry is closed. Such a key is refused by name with a located error (`unrecognized_keys` at the entry; at the save door `422 INVALID_METADATA`). A credential-shaped key (`apiKey`, `token`, `secret`, `password`, …) meets the `auth.credentialRef` prescription, and a provider setting written at the top level (`baseUrl`, `spec`, `transport`, `headers`, …) is pointed at `providerConfig`. The ADR-0010 protection envelope stays declared, so a package-stamped entry still parses. An artifact `os build` wrote carries no undeclared key (it serializes the parsed stack), so only a hand-assembled artifact can meet the refusal at boot.
16-
- **Unchanged.** `ConnectorSchema` — the def a plugin hands to `registerConnector`, and the shape `defineConnector()` parses — keeps its posture. Nothing in the runtime ever read an undeclared entry key, so no connector's behaviour moves.
16+
- **Unchanged.** `ConnectorSchema` — the def a plugin hands to `registerConnector` — keeps its posture. Nothing in the runtime ever read an undeclared entry key, so no connector's behaviour moves.
1717
- **Read half.** The built-in `connector` redactor also withholds, on every read exit that serves a stored body through the metadata-type redactor registry: the userinfo and the credential query parameters of every URL under `providerConfig` (`baseUrl`, `transport.url`, a URL `spec`, and any other URL a provider keeps at object depth), the rest of each URL served byte for byte; and an `auth` or `authentication` value that is not an object, whole. A save of the served form keeps the stored value (the metadata protocol's carry-forward), and the runtime keeps reading the unredacted execution view.
1818
- `redactUrlCredentialQueryParams` (`@objectstack/spec/data`) takes an optional parameter-name list; its default, and every existing caller, is unchanged.
1919

‎docs/audits/2026-07-unknown-key-strictness-ledger.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1376,7 +1376,7 @@ rest at #5107.
13761376
| `kernel/` | **mixed · `manifest.zod.ts` authorable, the rest wire** | ⚠️ **Split at #14192 — the flat `wire` verdict this row carried was right for the directory and wrong for one file in it, the `api/` row's finding one directory over.** `kernel/manifest.zod.ts` is `manifest:` — the hand-authored identity block of every `objectstack.config.ts`, every `os init` template, every example app, the Studio package form and the `objectstack.plugin.json` that `os plugin build` reads — and it was a plain open `z.object` under a cell that read "code-to-code". Measured first (2026-09-02 at `bd4aa4e49`, from `src` and through the built `dist`, identical readings): an unknown key parsed green and was DROPPED at all seven doors that reach `ManifestSchema` — the stack `manifest:` member (`defineStack` / `os validate` / `os compile`), `devPlugins[]`, the artifact `packages[]` entry in both its authoring and assembled forms, `os plugin build`, the `InstalledPackage` row schema, and the six package request/snapshot wire shapes that embed it. A transposed `namesapce` left `manifest.namespace` undefined at every one of them with exit 0 — and that key decides table names, REST paths and the install-time namespace gate (ADR-0129 D1–D2, ADR-0048 §3.2, ADR-0130 D1/D3). The same silent drop held one level down for `contributes` (`kind:` for `kinds:`), `contributes.kinds[]` and `engines` (`protocl:` for `protocol:`, which switches the load-time protocol handshake off). **The order the `api/` row records was kept: measurement, then the close.** No stored-row caller feeds this schema — `registry.validate('package' \| 'plugin')` has no live caller (`installPackage` stores the caller's object verbatim, `registerPlugin` is uncalled), the durable `sys_packages` row is re-registered through `registerApp` without a parse, and no bookkeeping key (`packageId` / `state` / `published*` / `package`) reaches any door — so the `peelStoredEnvelope` debt the `api/` row had to pay first does not exist here, and no bookkeeping key was taught to the authoring schema. Exactly ONE undeclared key was found at a real door, and it was DECLARED rather than curated: `main`, which `os plugin build` reads off the source `objectstack.plugin.json` to pick its entry and writes back into the compiled manifest as `dist/index.mjs`; three CLI fixtures author it, and a naked `strictObject` would have refused every plugin that names its entry — the shape this row would otherwise have recorded as refused. `ManifestSchema`, `contributes`, `contributes.kinds[]`, `engine` and `PluginEnginesSchema` are `strictObject` now: an undeclared key is a named rejection carrying the surface, the key and a rename (`namesapce` → `namespace`, `protocl` → `protocol`); the retired keys keep their `retiredKey()` tombstones (a tombstone types the key `never` and carries the upgrade text, which a bare refusal would not, and `acceptsNothing` keeps them out of the rename candidates); one curated wrong-layer pointer ships, `specVersion` (the retired CLI advisory axis — the live one is `engines.protocol`). `AssembledPackageBodySchema` (`stack.zod.ts`) inherits the closed posture through `.extend()`, so the load gate refuses an undeclared key on an assembled body too; that file's docblock sentence "NOT `strictObject`: `ManifestSchema` is an open object" is stale as of this change and is reported for its owner rather than edited here. Blast radius, measured with a strict preview before the close: 0 undeclared keys across the four example apps, the three `os init` templates, the `create-objectstack` blank template, the downstream-contract stack, the empty-kernel host and the i18n service manifest; the only shipped-block failure — `packages/cli/src/commands/create.ts`'s template manifest — fails the OPEN schema too (it lacks `id` and `type`) and is reported separately. Cloud is NOT MEASURED for this file (no checkout in the container, code search refused); if cloud parses a published `.osplugin` manifest through this schema, the compiled manifest it receives carries only declared keys (`main` and `integrity` included) by construction of `os plugin build`. The rest of `kernel/` is unchanged: plugin/kernel contracts, code-to-code |
13771377
| `cloud/` | wire | multi-tenant runtime |
13781378
| `ai/` | mixed | agent/tool/skill definitions authored (partially strict already); model/provider payloads wire |
1379-
| `integration/` | **mixed · `connector.zod.ts`'s declarative entry authorable, the rest wire** | ⚠️ **Split at the connector-entry close — the flat `wire` verdict this row carried was right for the payloads and wrong for one shape, the `api/` and `kernel/` rows' finding again.** `DeclarativeConnectorEntrySchema` is the `connectors:` stack collection and the `connector` row the metadata save door keeps (`PUT /api/v1/meta/connector/:name`, bound in `UNREGISTERED_KIND_SCHEMAS`, which stores the request body for every type but `view` — ADR-0005 appendix (c), ruling B): an AUTHORED, STORED, SERVED type, not an upstream payload, yet it was a plain `z.object`, so an undeclared top-level key parsed green at every door that reads it. It is closed through a dedicated `strictObject` carrier (`DeclarativeConnectorEntryBaseSchema`) built from the same shape factory as the open one, each with its own field instances so `computeGuidanceRoutes()` never pairs the open `Connector` def with the closed declaration's `guidance`. Every door that parses the entry — `defineStack({ connectors })`, `os validate` / `os compile`, the artifact load at boot and reload, and the save door (`422 INVALID_METADATA`) — refuses the key by name, located at it; two curated families ride the refusal: credential-shaped keys → `auth.credentialRef`, provider settings → `providerConfig`. The ADR-0010 envelope was already declared on the shape (commit b5404f496), so no stamped entry is refused. Governing texts read before the close: ADR-0097, ADR-0143, ADR-0104 and the type-schema suite's #2657 fence — none keeps the authored entry open; the `ConnectorSchema is NOT .strict()` sentences across `connector.zod.ts` state the fact that made each retirement a `retiredKey()` tombstone, not a decision to stay open. ⛔ Deliberately NOT closed: `ConnectorSchema`, the def a plugin hands to `registerConnector` (code-to-code — the wire half of this row) and the shape `defineConnector()` parses, so `defineConnector({ … })` still drops an undeclared key before the entry sees it; closing that carrier is its own decision. The rest of `integration/` is unchanged: connector payloads, upstream adds fields freely |
1379+
| `integration/` | **mixed · `connector.zod.ts`'s declarative entry authorable, the rest wire** | ⚠️ **Split at the connector-entry close — the flat `wire` verdict this row carried was right for the payloads and wrong for one shape, the `api/` and `kernel/` rows' finding again.** `DeclarativeConnectorEntrySchema` is the `connectors:` stack collection and the `connector` row the metadata save door keeps (`PUT /api/v1/meta/connector/:name`, bound in `UNREGISTERED_KIND_SCHEMAS`, which stores the request body for every type but `view` — ADR-0005 appendix (c), ruling B): an AUTHORED, STORED, SERVED type, not an upstream payload, yet it was a plain `z.object`, so an undeclared top-level key parsed green at every door that reads it. It is closed through a dedicated `strictObject` carrier (`DeclarativeConnectorEntryBaseSchema`) built from the same shape factory as the open one, each with its own field instances so `computeGuidanceRoutes()` never pairs the open `Connector` def with the closed declaration's `guidance`. Every door that parses the entry — `defineStack({ connectors })`, `os validate` / `os compile`, the artifact load at boot and reload, and the save door (`422 INVALID_METADATA`) — refuses the key by name, located at it; two curated families ride the refusal: credential-shaped keys → `auth.credentialRef`, provider settings → `providerConfig`. The ADR-0010 envelope was already declared on the shape (commit b5404f496), so no stamped entry is refused. Governing texts read before the close: ADR-0097, ADR-0143, ADR-0104 and the type-schema suite's #2657 fence — none keeps the authored entry open; the `ConnectorSchema is NOT .strict()` sentences across `connector.zod.ts` state the fact that made each retirement a `retiredKey()` tombstone, not a decision to stay open. ⛔ Deliberately NOT closed: `ConnectorSchema`, the def a plugin hands to `registerConnector` (code-to-code — the wire half of this row); closing that carrier is its own decision. `defineConnector()`, the helper an authored entry is wrapped in, used to parse it too, so the helper dropped an undeclared key before the entry saw it; it now parses the closed carrier behind the same retired-default residue stage and refuses the key with the entry's own message. The rest of `integration/` is unchanged: connector payloads, upstream adds fields freely |
13801380
| `identity/` | mixed | position/user shapes authored (`PositionSchema` **strict as of #4001 step 2**, with the ADR-0010 envelope declared); auth payloads wire. **34 → 33 in #4641**: `identity.zod.ts` lost its `SessionSchema` site — a second, importerless declaration of a name `api/auth.zod.ts` already owned (the #4411 dual-source trap), deleted rather than reclassified |
13811381
| `shared/` | **mixed · `protection.zod.ts` and `http.zod.ts`'s rate-limit budget authorable, the rest as written** | ⚠️ **Annotated again at #18578 — a FOURTH instance of the same shape, and this one was invisible to a sweep that went looking for it.** `shared/http.zod.ts`'s `RateLimitConfigSchema` is a building block with two mounts, and this row's rationale — *strictness decided at the consuming schema* — held for exactly one of them: `system/stack-server.zod.ts` re-postured it closed, and it did so by building `strictObject(…, RateLimitConfigSchema.shape)` **from this shape object**, so ONE declaration answered for TWO emitted defs with OPPOSITE doors. `api/endpoint.zod.ts` mounts the same schema BARE on `apis[].rateLimit` — a registered metadata type since #5312, authored through `defineStack({ apis })`, the Studio form and `PUT /meta/api/:name` — where nothing re-postures it, so the two `guidance` entries (`keyBy`, `store`) prescribed to nobody and a misspelled budget was worse: `windowSeconds: 60` parsed green and metered the 60000 ms default, a thousandfold miss reported as success. ⚠️ **The sweep that should have caught it could not.** A `CONTRACT_REVIEW_TIER` review swept this exact class with live controls (5 `.strip()` sites, 0 `z.object(XSchema.shape…)`, 0 `z.object(bareIdentifier)`) and found nothing, because the sharing runs the other way round here: it hunted an OPEN clone built from a STRICT schema's shape, and this is the STRICT one built from the OPEN one's shape — every grep shape in that sweep is blind to that direction by construction. It was found instead by driving the gate's own instrument (`computeGuidanceRoutes()` in `scripts/build-schemas.ts`) over every emitted def: match each def to its declaration, then ask the def what it actually does with the key. Census on `42f8df1723`: 1527 emitted defs, 258 resolving to exactly one declaration, 779 keys promised, **770 delivered, 9 not** — 2 of them this live silent strip, the other 7 union defs whose discriminator the probe's one-key document cannot supply (they DO deliver to an author who writes a whole document). The close moves the strictness and both tables onto the shared schema, leaving ONE declaration and ONE door for both defs: 772 delivered, 7 not. ⚠️ The residual 7 are a PROBE boundary, ⛔ not a clean zero and ⛔ not a finding. ⚠️ **Annotated at #16845 — the flat `n/a` verdict this row carried was right for the directory and wrong for one file in it, the `api/` and `kernel/` rows' finding a third time.** — the flat `n/a` verdict this row carried was right for the directory and wrong for one file in it, the `api/` and `kernel/` rows' finding a third time.** ⛔ **This directory is still UNTRIAGED and this is not a per-file row** — `shared/` has never had per-file rows, so `shared/protection.zod.ts` was **never enumerated** by this campaign; it was not deliberately accepted, because no verdict was ever taken on it. Two measurements say why the silence carried no information. **①** This row's own rationale — *strictness decided at the consuming schema* — is false for this file: all **16** `protection: ProtectionSchema` mounts across **14** files are the bare `ProtectionSchema.optional()`, not one `.extend()` / `.merge()` / `.omit()` / re-posture among them, so both the strictness and the MESSAGE are decided here in `shared/` and inherited verbatim by objects, views, dashboards, datasets, reports, apps, flows, webhooks, permissions, positions, email templates, agents, tools and skills — very nearly every authorable metadata type in the platform. A row is read as licence, which is what this ledger's own gate exists to prevent. **②** Even a per-file row would not have surfaced the defect, and this is the part worth keeping: `ProtectionSchema` has been `.strict()` since it was introduced, so it counts in the `strict` column and could never carry a remaining-strip row. **This ratchet's axis is CLOSURE; the defect was MESSAGE QUALITY** — a closed shape with no error map, refusing `lockk` with zod's bare `Unrecognized key: "lockk"`: no surface, no declared-key list, no rename, on every one of those mounts. `strictObject` at #16845 (the `PluginPermissionsSchema` conversion at #16328 one directory over is the precedent). ⛔ The population of *closed shapes still carrying zod's bare message* is **NOT MEASURED** here and is not this row's claim — #14722's rule stands that a sweep needs its own card with its own measured count. The rest of `shared/` is unchanged: utilities and building blocks whose strictness really is decided at the consuming schema |
13821382
| `qa/` | n/a | test fixtures |

0 commit comments

Comments
 (0)