From d164f4d1508f5709f4fb0253f6564f57c29e0586 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 04:29:22 +0000 Subject: [PATCH 01/15] wip(spec): tombstone connector.connectionTimeoutMs and withdraw the provider-context carry Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx --- .../connector-fetch-policy.test.ts | 16 ++- .../src/integration/connector-fetch-policy.ts | 16 ++- .../src/integration/connector-provider.ts | 41 ++++--- .../spec/src/integration/connector.zod.ts | 109 +++++++++++++++--- 4 files changed, 139 insertions(+), 43 deletions(-) diff --git a/packages/spec/src/integration/connector-fetch-policy.test.ts b/packages/spec/src/integration/connector-fetch-policy.test.ts index 255dec9d215..9b65ce193fe 100644 --- a/packages/spec/src/integration/connector-fetch-policy.test.ts +++ b/packages/spec/src/integration/connector-fetch-policy.test.ts @@ -15,11 +15,17 @@ describe('connectorFetchOptions', () => { }); it('⛔ does NOT map connectionTimeoutMs — one fetch signal cannot bound the connect phase', () => { - // The key is deliberately absent from ConnectorFetchPolicy, so the only - // way it could reach the wrapper is an alias onto `timeoutMs`. This pin - // goes red the moment someone adds one, which is the whole reason it - // exists: two keys silently meaning one thing is the shape this card - // removes, not a shape it may introduce elsewhere. + // The key is absent from ConnectorFetchPolicy, so the only way it could + // reach the wrapper is an alias onto `timeoutMs`. This pin goes red the + // moment someone adds one, which is the whole reason it exists: two keys + // silently meaning one thing is the shape this mapping removes, not a + // shape it may introduce elsewhere. + // + // The key is now RETIRED from `ConnectorSchema` and from + // `ConnectorProviderContext` (ADR-0049) — precisely because it could + // never be mapped here. The pin survives the retirement on purpose: it + // is what makes a re-introduction as a silent alias fail, and a stray + // leftover in a caller-built policy object still has to reach nothing. const opts = connectorFetchOptions( { connectionTimeoutMs: 1500 } as unknown as ConnectorFetchPolicy, ); diff --git a/packages/spec/src/integration/connector-fetch-policy.ts b/packages/spec/src/integration/connector-fetch-policy.ts index fb104458ef5..7b42b4c5c01 100644 --- a/packages/spec/src/integration/connector-fetch-policy.ts +++ b/packages/spec/src/integration/connector-fetch-policy.ts @@ -54,7 +54,7 @@ export type { ResilientFetchOptions } from '../shared/resilient-fetch'; * floors its attempt count at 1, so a connector still makes its one call and * never retries — ONE owner for that floor, not a second one in this mapping. * - * ## ⚠️ `connectionTimeoutMs` is deliberately NOT mapped here + * ## ⚠️ `connectionTimeoutMs` was never mapped here, and is now RETIRED * * Measured at the fetch site rather than assumed: a connector's outbound call is * a WHATWG `fetch`, whose only cancellation surface is one `AbortSignal` @@ -64,11 +64,15 @@ export type { ResilientFetchOptions } from '../shared/resilient-fetch'; * connected upstream that the author meant to allow via a large * `requestTimeoutMs`, i.e. it would break the very promise it claims to keep. * (Node's undici exposes `connectTimeout` through a custom dispatcher, but that - * is Node-only and a new subsystem underneath every connector.) So the key - * stays unenforced and `packages/spec/liveness/connector.json` keeps it `dead` - * with that reason. ⛔ Do not "fix" this by aliasing it onto `timeoutMs`: two - * keys that silently mean one thing is the declared-not-enforced shape this - * mapping exists to remove. + * is Node-only and a new subsystem underneath every connector.) + * + * That measurement is what made this an ADR-0049 removal rather than an + * implementation: the key is gone from `ConnectorSchema` (a `retiredKey()` + * tombstone) and from `ConnectorProviderContext`, so the absent row above is no + * longer a declared-but-unmapped key — there is nothing left to map. ⛔ Do not + * "restore" it by aliasing a connect deadline onto `timeoutMs`: two keys that + * silently mean one thing is the declared-not-enforced shape this mapping + * exists to remove, and it is the shape the retirement just closed. */ /** The slice of a {@link Connector} this mapping reads. */ diff --git a/packages/spec/src/integration/connector-provider.ts b/packages/spec/src/integration/connector-provider.ts index 49b5c950e2b..dd3effa79ac 100644 --- a/packages/spec/src/integration/connector-provider.ts +++ b/packages/spec/src/integration/connector-provider.ts @@ -54,12 +54,15 @@ export interface ConnectorMaterialization { * secrets/env layer, so the factory receives a usable static credential rather * than a raw reference (`undefined` when the entry declares no auth). * - * It also carries the entry's **resilience policy** — `retryConfig`, - * `connectionTimeoutMs`, `requestTimeoutMs` — so a provider that performs its - * own I/O can honour what the author declared. Before that, those keys were - * parsed and then reached nothing: a factory was never handed them and had no - * way to honour them, which is what `packages/spec/liveness/connector.json` - * recorded as `dead`. + * It also carries the entry's **resilience policy** — `retryConfig` and + * `requestTimeoutMs` — so a provider that performs its own I/O can honour what + * the author declared. Before that, those keys were parsed and then reached + * nothing: a factory was never handed them and had no way to honour them, which + * is what `packages/spec/liveness/connector.json` recorded as `dead`. + * + * ⚠️ `connectionTimeoutMs` was a third member and is **removed** with the spec + * key (ADR-0049) — see the comment at its former position below. Being handed a + * value is not the same as honouring it, and nothing ever did. */ export interface ConnectorProviderContext { readonly name: string; @@ -81,19 +84,19 @@ export interface ConnectorProviderContext { * what makes the keys live rather than merely carried. */ readonly retryConfig?: RetryConfigParsed; - /** - * The entry's declared connect deadline (ms), carried verbatim. - * - * ⚠️ **Carried, not enforced by the built-in HTTP path** — a WHATWG `fetch` - * exposes one `AbortSignal` for the whole operation and never the connection - * phase alone, so the platform has nowhere to apply a connect-only bound and - * deliberately does not pretend otherwise (see - * `connector-fetch-policy.ts`). It is handed over because a custom provider - * on a transport that CAN separate the phases (a database client, a pooled - * socket) is able to honour it; `packages/spec/liveness/connector.json` - * records the platform side as `dead` for that reason. - */ - readonly connectionTimeoutMs?: number; + // `connectionTimeoutMs` — REMOVED (ADR-0049 enforce-or-remove). #18975 added + // it here as a pure carry: "handed over so a custom provider on a transport + // that CAN separate the phases could honour it". Measured before removal, no + // provider did — the built-in `rest` and `openapi` factories read + // `ctx.connectionTimeoutMs` only to deposit it back onto the def that + // `GET /connectors` echoes, and `connectorFetchOptions()` was never handed it. + // Carrying an inert number across a published interface is the same + // parsed-unmarked-unenforced state on one more surface, so the carry is + // withdrawn with the key. There is no source for a D2 conversion to rewrite + // here — a factory is code — which is why the withdrawal is declared as the + // D3 semantic entry `connector-provider-context-connection-timeout-ms-retired` + // rather than a conversion. A factory that needs a connect bound reads it from + // its own `providerConfig`, where the provider owns the vocabulary. /** * The entry's declared per-request deadline (ms). The built-in HTTP path * applies it as `resilientFetch`'s per-attempt timeout. diff --git a/packages/spec/src/integration/connector.zod.ts b/packages/spec/src/integration/connector.zod.ts index 44809363687..388c3c55f40 100644 --- a/packages/spec/src/integration/connector.zod.ts +++ b/packages/spec/src/integration/connector.zod.ts @@ -58,17 +58,20 @@ import { retiredKey } from '../shared/retired-key'; * spaces out the calls you already made, it does not cap the rate, so the * sentence above about rate limiting stands unchanged. * - * ⛔ **Two exceptions, both still inert and both still `dead` in - * `packages/spec/liveness/connector.json`.** `health.circuitBreaker`: every - * sub-key is unread and no breaker ever opens — implement circuit breaking in - * the connector provider. `connectionTimeoutMs`: it is carried to a provider - * factory but the platform does not enforce it, because a WHATWG `fetch` + * ⛔ **One exception remains, still inert and still `dead` in + * `packages/spec/liveness/connector.json`:** `health.circuitBreaker` — every + * sub-key is unread and no breaker ever opens; implement circuit breaking in + * the connector provider. + * + * `connectionTimeoutMs` used to be the second exception and is now **removed** + * (ADR-0049, the narrower second decision that surface was owed): it was + * carried to a provider factory but never applied as a deadline anywhere, and + * it is not implementable where it was declared, because a WHATWG `fetch` * exposes one `AbortSignal` over the whole operation and never the connection - * phase alone; `requestTimeoutMs` is the bound the platform can keep, and - * ADR-0049 owes this one key a narrower decision. The full removal reasoning - * for the rate-limit shape is recorded at the removal site: the "REMOVED: - * outbound rate limiting" block in `integration/connector.zod.ts`, and - * `packages/spec/docs/SYNC_ARCHITECTURE.md`. + * phase alone. `requestTimeoutMs` is the bound the platform can keep. The full + * removal reasoning is recorded at each removal site: the "REMOVED: + * `connectionTimeoutMs`" and "REMOVED: outbound rate limiting" blocks in + * `integration/connector.zod.ts`, and `packages/spec/docs/SYNC_ARCHITECTURE.md`. * * **Field mapping does not transform values.** This header used to offer "field * mapping and transformations"; only the first half was ever true. @@ -534,6 +537,80 @@ const ERROR_MAPPING_RETIRED = + "provider's own errors (ADR-0097). " + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; +// ============================================================================ +// REMOVED: `connectionTimeoutMs` — the connect-phase deadline (ADR-0049) +// ============================================================================ +// +// A bounded (`min(1000).max(300000)`), defaulted (`30000`), `.describe()`d key +// on this schema and — through `ConnectorSchema.superRefine` — on +// `DeclarativeConnectorEntrySchema`, so it was authorable from `stack.connectors[]`, +// from `PUT /meta/connector/:name`, and served back by `/meta/connector`. Every +// signal an authoring surface can give said it worked. +// +// ⚠️ It was not merely unimplemented — it is NOT IMPLEMENTABLE AT THE SITE IT +// NAMES, which is why ADR-0049's `实现` arm was unavailable and the second +// decision came out `retire`. A connector's outbound call is a WHATWG `fetch`, +// whose only cancellation surface is ONE `AbortSignal` covering the whole +// operation; nothing in that interface observes the connection phase +// separately. The two honest readings were both losses: bound "time until the +// response arrives" with this key — which kills a slow-but-connected upstream +// the author meant to allow with a large `requestTimeoutMs`, breaking the very +// promise the key makes — or leave it unenforced and say so. (Node's undici +// exposes `connectTimeout` through a custom dispatcher; Node-only, and a new +// subsystem underneath every connector, which the #18975 ruling forbids.) +// +// ⚠️ The carry was real and is what this removal actually withdraws, so do not +// read it as a zero-mention retirement. #18975 put the key on +// `ConnectorProviderContext`, and five sites outside `packages/spec` READ it: +// the materialization fingerprint and the context build in +// `services/service-automation/src/plugin.ts`, `ctx.connectionTimeoutMs` in the +// `rest` and `openapi` provider factories, and the `?? 30000` fallbacks that +// deposited it back onto the reported def. Measured across all five, the +// value's only termini were the def `GET /connectors` echoes and the +// fingerprint that decides whether to re-materialize — never a deadline. +// `connectorFetchOptions()` (`integration/connector-fetch-policy.ts`) is the one +// mapping from authored policy onto the platform's outbound `fetch`, and it was +// handed `{ retryConfig, requestTimeoutMs }` only. Carrying a number is not +// honouring it: ADR-0049 forbids the parsed-unmarked-unenforced state whether +// the inert value travels or sits still. +// +// `requestTimeoutMs` is the replacement and the lit control for every reading +// above — same schema, same census, same files — because it resolves to a real +// read (`opts.timeoutMs`, `resilientFetch`'s per-attempt deadline) since #19388. +// Bound the connect phase at a provider or gateway that can see it. +// +// `ConnectorSchema` is NOT `.strict()`, so a plain delete would be a silent +// strip (ADR-0104); the tombstone below makes the removal audible in the two +// channels an upgrading author actually hits — `tsc` and the parse. Registered +// as `integration/Connector:connectionTimeoutMs` and +// `integration/DeclarativeConnectorEntry:connectionTimeoutMs` in +// `RETIRED_KEYS_BY_MAJOR[18]`; stored rows and authored sources are rewritten by +// the D2 conversion `connector-connection-timeout-ms-removed`, and the withdrawn +// `ConnectorProviderContext` member by the D3 semantic entry +// `connector-provider-context-connection-timeout-ms-retired`. +// +// No orphaned def leaves with it: the key was a bare `z.number()`, not a +// `ConfigSchema` shape, so `RETIRED_DEFS_BY_MAJOR[18]` gains nothing here. + +/** + * The prescription an author meets when they write `connectionTimeoutMs` — in + * `tsc` (the key's input type is `never`) and at parse (this string is the + * issue message). It IS the migration doc for whoever hits it; the closing + * sentence is the house `os migrate meta` form pinned by + * `shared/retired-key-migrate-sentence.test.ts`. + */ +const CONNECTION_TIMEOUT_MS_RETIRED = + '`connector.connectionTimeoutMs` was removed in @objectstack/spec 17 (ADR-0049 ' + + 'enforce-or-remove) — the platform never honoured it and cannot honour it where it was ' + + "declared: a connector's outbound call is a WHATWG `fetch`, whose only cancellation " + + 'surface is one `AbortSignal` over the whole operation, so nothing there observes the ' + + 'connection phase separately, and the value only ever travelled (onto the reported def ' + + 'and the materialization fingerprint) without ever bounding a connect. Delete the key. ' + + 'Use `requestTimeoutMs` for the deadline the platform does keep — it is applied as ' + + "`resilientFetch`'s per-attempt timeout — and bound the connect phase at a connector " + + 'provider or upstream gateway on a transport that can separate the phases. ' + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + // ============================================================================ // Health Check & Circuit Breaker Configuration // ============================================================================ @@ -864,10 +941,16 @@ export const ConnectorSchema = lazySchema(() => z.object({ retryConfig: RetryConfigSchema.optional().describe('Retry configuration'), /** - * Connection timeout in milliseconds + * `connectionTimeoutMs` — RETIRED (ADR-0049 enforce-or-remove). A bounded, + * defaulted, served-back key that no site ever applied as a deadline, and one + * that is not implementable where it was declared: a WHATWG `fetch` exposes a + * single `AbortSignal` over the whole operation and never the connect phase. + * `requestTimeoutMs` below is the bound the platform can keep. The section + * comment above `CONNECTION_TIMEOUT_MS_RETIRED` records the measurement, + * including the five reader sites #18975 created and what this withdraws. */ - connectionTimeoutMs: z.number().min(1000).max(300000).optional().default(30000).describe('Connection timeout in ms'), - + connectionTimeoutMs: retiredKey(CONNECTION_TIMEOUT_MS_RETIRED), + /** * Request timeout in milliseconds */ From 5d1e9e0a167791621bf060b487bed89385438989 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 04:37:09 +0000 Subject: [PATCH 02/15] =?UTF-8?q?feat(spec)!:=20retire=20connector.connect?= =?UTF-8?q?ionTimeoutMs=20=E2=80=94=20D2=20conversion,=20D3=20semantic=20e?= =?UTF-8?q?ntry,=20consumers=20and=20pins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx --- .../connector-mcp/src/mcp-connector.ts | 4 +- .../src/openapi-connector.ts | 13 +- .../connector-openapi/src/openapi-provider.ts | 1 - .../connector-rest/src/rest-connector.ts | 13 +- .../connector-rest/src/rest-provider.test.ts | 10 +- .../connector-rest/src/rest-provider.ts | 1 - .../connector-slack/src/slack-connector.ts | 4 +- .../src/connector-materialization.test.ts | 29 +- .../src/degraded-register-cause.test.ts | 1 - .../services/service-automation/src/plugin.ts | 20 +- packages/spec/docs/SYNC_ARCHITECTURE.md | 32 +- packages/spec/liveness/connector.json | 4 +- packages/spec/src/conversions/registry.ts | 84 ++++ ...ctor-connection-timeout-retirement.test.ts | 407 ++++++++++++++++++ ...gration__Connector__connectionTimeoutMs.ts | 50 +++ ...tiveConnectorEntry__connectionTimeoutMs.ts | 16 + ...r-context-connection-timeout-ms-retired.ts | 46 ++ packages/spec/src/migrations/registry.ts | 30 +- 18 files changed, 715 insertions(+), 50 deletions(-) create mode 100644 packages/spec/src/integration/connector-connection-timeout-retirement.test.ts create mode 100644 packages/spec/src/migrations/entries/retired-keys/18.integration__Connector__connectionTimeoutMs.ts create mode 100644 packages/spec/src/migrations/entries/retired-keys/18.integration__DeclarativeConnectorEntry__connectionTimeoutMs.ts create mode 100644 packages/spec/src/migrations/entries/semantic/18.connector-provider-context-connection-timeout-ms-retired.ts diff --git a/packages/connectors/connector-mcp/src/mcp-connector.ts b/packages/connectors/connector-mcp/src/mcp-connector.ts index e0d508510ee..e0dd0dc5135 100644 --- a/packages/connectors/connector-mcp/src/mcp-connector.ts +++ b/packages/connectors/connector-mcp/src/mcp-connector.ts @@ -244,7 +244,9 @@ export async function createMcpConnector(opts: McpConnectorOptions): Promise ({ key: tool.name, diff --git a/packages/connectors/connector-openapi/src/openapi-connector.ts b/packages/connectors/connector-openapi/src/openapi-connector.ts index a0172843ccb..f221d8d8706 100644 --- a/packages/connectors/connector-openapi/src/openapi-connector.ts +++ b/packages/connectors/connector-openapi/src/openapi-connector.ts @@ -127,12 +127,12 @@ export interface OpenApiConnectorConfig { * (ADR-0049 · #18975). Omitted ⇒ the wrapper's own defaults. */ retryConfig?: RetryConfig; - /** - * Declared connect deadline (ms). Carried onto the def so `GET /connectors` - * reports what the author declared; ⚠️ not enforced — one `fetch` signal - * cannot bound the connection phase alone (`connector-fetch-policy.ts`). - */ - connectionTimeoutMs?: number; + // `connectionTimeoutMs` — REMOVED with the spec key (ADR-0049). It was + // accepted here only to be carried onto the def `GET /connectors` echoes: + // one `fetch` signal cannot bound the connection phase alone, so it never + // reached `connectorFetchOptions` and never bounded a call. Echoing a + // deadline nobody keeps is what the retirement withdraws (mirrors + // connector-rest). /** Per-request deadline (ms) — `resilientFetch`'s per-attempt timeout. */ requestTimeoutMs?: number; /** Injected fetch implementation (defaults to global `fetch`). */ @@ -239,7 +239,6 @@ export function createOpenApiConnector(config: OpenApiConnectorConfig): OpenApiC // the (post-parse) Connector output type (mirrors connector-rest/mcp). status: 'active', enabled: true, - connectionTimeoutMs: config.connectionTimeoutMs ?? 30000, requestTimeoutMs: config.requestTimeoutMs ?? 30000, ...(config.retryConfig === undefined ? {} : { retryConfig: config.retryConfig }), actions, diff --git a/packages/connectors/connector-openapi/src/openapi-provider.ts b/packages/connectors/connector-openapi/src/openapi-provider.ts index 9691be8112c..0abec68b4f1 100644 --- a/packages/connectors/connector-openapi/src/openapi-provider.ts +++ b/packages/connectors/connector-openapi/src/openapi-provider.ts @@ -190,7 +190,6 @@ export function createOpenApiProviderFactory(deps: OpenApiProviderDeps = {}): Co // ADR-0049 · #18975 — the authored resilience policy, already resolved by // the materializer, reaches the transport this bundle closes over. retryConfig: ctx.retryConfig, - connectionTimeoutMs: ctx.connectionTimeoutMs, requestTimeoutMs: ctx.requestTimeoutMs, fetchImpl: deps.fetchImpl, }); diff --git a/packages/connectors/connector-rest/src/rest-connector.ts b/packages/connectors/connector-rest/src/rest-connector.ts index 8d669ff348e..c409d209299 100644 --- a/packages/connectors/connector-rest/src/rest-connector.ts +++ b/packages/connectors/connector-rest/src/rest-connector.ts @@ -39,12 +39,12 @@ export interface RestConnectorOptions { * (ADR-0049 · #18975). Omitted ⇒ the wrapper's own defaults. */ retryConfig?: RetryConfig; - /** - * Declared connect deadline (ms). Carried onto the def so `GET /connectors` - * reports what the author declared; ⚠️ not enforced — one `fetch` signal - * cannot bound the connection phase alone (`connector-fetch-policy.ts`). - */ - connectionTimeoutMs?: number; + // `connectionTimeoutMs` — REMOVED with the spec key (ADR-0049). It was + // accepted here only to be carried onto the def `GET /connectors` echoes: + // one `fetch` signal cannot bound the connection phase alone, so it never + // reached `connectorFetchOptions` and never bounded a call. Echoing a + // deadline nobody keeps is what the retirement withdraws. A connect-only + // bound belongs to a transport that can observe the connect phase. /** Per-request deadline (ms) — `resilientFetch`'s per-attempt timeout. */ requestTimeoutMs?: number; /** Injected for tests; defaults to the global `fetch`. */ @@ -131,7 +131,6 @@ export function createRestConnector(opts: RestConnectorOptions): RestConnectorBu // the (post-parse) Connector output type. status: 'active', enabled: true, - connectionTimeoutMs: opts.connectionTimeoutMs ?? 30000, requestTimeoutMs: opts.requestTimeoutMs ?? 30000, ...(opts.retryConfig === undefined ? {} : { retryConfig: opts.retryConfig }), actions: [ diff --git a/packages/connectors/connector-rest/src/rest-provider.test.ts b/packages/connectors/connector-rest/src/rest-provider.test.ts index 15ee09a0fbd..eb94d218d76 100644 --- a/packages/connectors/connector-rest/src/rest-provider.test.ts +++ b/packages/connectors/connector-rest/src/rest-provider.test.ts @@ -227,18 +227,22 @@ describe('rest provider factory (ADR-0097)', () => { expect(calls).toBe(1); }); - it('carries the declared timeouts onto the def it registers', async () => { + it('carries the declared request timeout onto the def it registers', async () => { const { impl } = stubFetch(); const factory = createRestProviderFactory({ fetchImpl: impl }); const { def } = await factory( ctx({ providerConfig: { baseUrl: 'https://api.example.com' }, - connectionTimeoutMs: 5000, requestTimeoutMs: 7000, }), ); - expect(def.connectionTimeoutMs).toBe(5000); expect(def.requestTimeoutMs).toBe(7000); + // `connectionTimeoutMs` was the second half of this pin and is + // RETIRED (ADR-0049): the factory never applied it, it only echoed + // it back onto the def. The absence pin lives tree-scoped in + // `packages/spec/src/integration/connector.test.ts`; here the point + // is only that the surviving timeout still travels. + expect((def as Record).connectionTimeoutMs).toBeUndefined(); }); }); }); diff --git a/packages/connectors/connector-rest/src/rest-provider.ts b/packages/connectors/connector-rest/src/rest-provider.ts index 51544e13845..56aa358ba7c 100644 --- a/packages/connectors/connector-rest/src/rest-provider.ts +++ b/packages/connectors/connector-rest/src/rest-provider.ts @@ -61,7 +61,6 @@ export function createRestProviderFactory(deps: RestProviderDeps = {}): Connecto // ADR-0049 · #18975 — the authored resilience policy, already resolved by // the materializer, reaches the transport this bundle closes over. retryConfig: ctx.retryConfig, - connectionTimeoutMs: ctx.connectionTimeoutMs, requestTimeoutMs: ctx.requestTimeoutMs, fetchImpl: deps.fetchImpl, }); diff --git a/packages/connectors/connector-slack/src/slack-connector.ts b/packages/connectors/connector-slack/src/slack-connector.ts index a682c7f1c71..aea4fb1c779 100644 --- a/packages/connectors/connector-slack/src/slack-connector.ts +++ b/packages/connectors/connector-slack/src/slack-connector.ts @@ -91,7 +91,9 @@ export function createSlackConnector(opts: SlackConnectorOptions): SlackConnecto // the (post-parse) Connector output type. status: 'active', enabled: true, - connectionTimeoutMs: 30000, + // `connectionTimeoutMs` — REMOVED with the spec key (ADR-0049): it was + // written here only so the literal satisfied the post-parse type, and + // the platform never applied it as a connect deadline. requestTimeoutMs: 30000, actions: [ { diff --git a/packages/services/service-automation/src/connector-materialization.test.ts b/packages/services/service-automation/src/connector-materialization.test.ts index 7bbf70b0cef..26b93d50343 100644 --- a/packages/services/service-automation/src/connector-materialization.test.ts +++ b/packages/services/service-automation/src/connector-materialization.test.ts @@ -842,25 +842,44 @@ describe('ADR-0097 — the entry\'s resilience policy on ConnectorProviderContex await kernel.shutdown(); }); - it('carries both declared timeouts verbatim', async () => { + it('carries the declared request timeout verbatim', async () => { const { factory, calls } = makeFakeProvider(); const kernel = await boot( - [{ ...providerConnector('billing'), connectionTimeoutMs: 5000, requestTimeoutMs: 12000 }], + [{ ...providerConnector('billing'), requestTimeoutMs: 12000 }], { providerFactory: factory }, ); - expect(calls[0]?.connectionTimeoutMs).toBe(5000); expect(calls[0]?.requestTimeoutMs).toBe(12000); await kernel.shutdown(); }); - it('leaves all three undefined when the entry declares none — absence stays absence', async () => { + it('⛔ does NOT carry a stored connectionTimeoutMs onto the context — the carry is retired', async () => { + // ADR-0049: the key was handed to factories as a pure carry and no + // provider ever applied it, so the member left `ConnectorProviderContext` + // with the spec key. A row that still spells it (written before the + // retirement, or by a seam that bypasses the parse) must reach a factory + // with nothing extra — the host does not resurrect the carry. + const { factory, calls } = makeFakeProvider(); + const kernel = await boot( + [{ ...providerConnector('billing'), connectionTimeoutMs: 5000, requestTimeoutMs: 12000 } as Record], + { providerFactory: factory }, + ); + + expect((calls[0] as unknown as Record).connectionTimeoutMs).toBeUndefined(); + // The lit control on the same context object and the same boot: the + // surviving timeout does arrive, so an empty reading above is the + // retirement and not a dead harness. + expect(calls[0]?.requestTimeoutMs).toBe(12000); + + await kernel.shutdown(); + }); + + it('leaves both undefined when the entry declares none — absence stays absence', async () => { const { factory, calls } = makeFakeProvider(); const kernel = await boot([providerConnector('billing')], { providerFactory: factory }); expect(calls[0]?.retryConfig).toBeUndefined(); - expect(calls[0]?.connectionTimeoutMs).toBeUndefined(); expect(calls[0]?.requestTimeoutMs).toBeUndefined(); await kernel.shutdown(); diff --git a/packages/services/service-automation/src/degraded-register-cause.test.ts b/packages/services/service-automation/src/degraded-register-cause.test.ts index 2f248a85f33..a3bc036454e 100644 --- a/packages/services/service-automation/src/degraded-register-cause.test.ts +++ b/packages/services/service-automation/src/degraded-register-cause.test.ts @@ -98,7 +98,6 @@ function huskDef(name: string): Connector { status: 'error', enabled: true, authentication: { type: 'none' }, - connectionTimeoutMs: 30000, requestTimeoutMs: 30000, actions: [], } as Connector; diff --git a/packages/services/service-automation/src/plugin.ts b/packages/services/service-automation/src/plugin.ts index 7a041db8360..695deec8867 100644 --- a/packages/services/service-automation/src/plugin.ts +++ b/packages/services/service-automation/src/plugin.ts @@ -288,7 +288,6 @@ function connectorInstanceSignature(entry: { icon?: unknown; type?: unknown; retryConfig?: unknown; - connectionTimeoutMs?: unknown; requestTimeoutMs?: unknown; }): string { return stableStringify({ @@ -304,7 +303,12 @@ function connectorInstanceSignature(entry: { // edit to it must re-materialize, exactly like a `providerConfig` edit. // Omitting it here would leave the old policy serving until restart. retryConfig: entry.retryConfig ?? null, - connectionTimeoutMs: entry.connectionTimeoutMs ?? null, + // `connectionTimeoutMs` — REMOVED with the spec key (ADR-0049). It was + // in the fingerprint for the reason above, but it was never a + // materialization input: no provider applied it, so an edit to it + // re-materialized a bundle that behaved identically and only changed the + // number the reported def echoed. Dropping it narrows the fingerprint to + // the inputs that actually change the transport. requestTimeoutMs: entry.requestTimeoutMs ?? null, }); } @@ -332,11 +336,15 @@ interface DeclaredConnectorItem { /** * The entry's declared resilience policy, raw as authored — defaults are * NOT applied here (see the note above), so `retryConfig` is parsed on the - * way onto `ConnectorProviderContext` and the two timeouts are carried + * way onto `ConnectorProviderContext` and `requestTimeoutMs` is carried * verbatim, `undefined` standing for "the author stated nothing". + * + * `connectionTimeoutMs` was a third member and is REMOVED with the spec key + * (ADR-0049): the platform never applied it as a connect deadline, and a + * stored row that still carries it is stripped by the D2 conversion + * `connector-connection-timeout-ms-removed` on rehydration. */ retryConfig?: unknown; - connectionTimeoutMs?: number; requestTimeoutMs?: number; } @@ -1586,7 +1594,6 @@ export class AutomationServicePlugin implements Plugin { // `connectorFetchOptions()` → `resilientFetch()`; a custom // provider doing its own I/O reads it here. retryConfig, - connectionTimeoutMs: entry.connectionTimeoutMs, requestTimeoutMs: entry.requestTimeoutMs, // #3016 — lets a factory dereference relative file refs (e.g. // openapi's `providerConfig.spec: './billing-openapi.json'`), @@ -1779,7 +1786,8 @@ export class AutomationServicePlugin implements Plugin { status: 'error', enabled: true, authentication: { type: 'none' }, - connectionTimeoutMs: 30000, + // `connectionTimeoutMs` — REMOVED with the spec key (ADR-0049): it + // was written here only so the literal satisfied the post-parse type. requestTimeoutMs: 30000, actions: [], }; diff --git a/packages/spec/docs/SYNC_ARCHITECTURE.md b/packages/spec/docs/SYNC_ARCHITECTURE.md index 0730c45a472..674da60e3e2 100644 --- a/packages/spec/docs/SYNC_ARCHITECTURE.md +++ b/packages/spec/docs/SYNC_ARCHITECTURE.md @@ -173,14 +173,19 @@ Complete, production-grade integration with external systems. Includes authentic > advice above is unchanged: throttle at the connector provider or upstream > gateway. > -> ⛔ **Two keys on this surface are still inert, and both are still `dead` in -> `packages/spec/liveness/connector.json`.** `health.circuitBreaker`: every -> sub-key is unread and no breaker ever opens — implement circuit breaking in -> the connector provider. `connectionTimeoutMs`: it is carried to a provider -> factory, but the platform does not enforce it, because a WHATWG `fetch` +> ⛔ **One key on this surface is still inert, and still `dead` in +> `packages/spec/liveness/connector.json`:** `health.circuitBreaker` — every +> sub-key is unread and no breaker ever opens; implement circuit breaking in the +> connector provider. +> +> `connectionTimeoutMs` was the second and is **removed** (ADR-0049, the +> narrower second decision it was owed). It was carried to a provider factory +> and echoed back onto the reported def, but never applied as a deadline +> anywhere, and it is not implementable where it was declared: a WHATWG `fetch` > exposes one `AbortSignal` over the whole operation and never the connection -> phase alone — `requestTimeoutMs` is the bound the platform can keep, and -> ADR-0049 owes this one key a narrower decision. +> phase alone. Use `requestTimeoutMs`, the bound the platform can keep, and put +> a connect-only bound in a provider or gateway on a transport that can separate +> the phases. > **Field mapping does not transform values.** The ticked line above used to read > "With transformations and data type conversion". Only the second half was ever @@ -214,7 +219,7 @@ Complete, production-grade integration with external systems. Includes authentic > **The bare `Connector` is the AUTHOR shape.** It is `z.input` of > `ConnectorSchema`, so every key carrying a `.default()` — `enabled`, -> `status`, `connectionTimeoutMs`, `requestTimeoutMs`, all of `syncConfig`'s +> `status`, `requestTimeoutMs`, all of `syncConfig`'s > `strategy` / `direction` / `realtimeSync` / `conflictResolution` / > `batchSize` / `deleteMode`, a mapping's `required` / `syncMode`, a webhook's > `method` / `timeoutMs` / `isActive` / `signatureAlgorithm` — is optional when @@ -338,12 +343,11 @@ const sapConnector: Connector = { }, // `requestTimeoutMs` is each attempt's deadline and is enforced. - // ⛔ `connectionTimeoutMs` is NOT: it is carried to a provider factory, but a - // WHATWG `fetch` exposes one `AbortSignal` over the whole operation and never - // the connection phase alone, so the platform has nowhere to apply it. It - // stays `dead` in `packages/spec/liveness/connector.json` and is owed a - // narrower ADR-0049 decision. - connectionTimeoutMs: 30000, + // ⛔ `connectionTimeoutMs` was here and is REMOVED (ADR-0049): a WHATWG + // `fetch` exposes one `AbortSignal` over the whole operation and never the + // connection phase alone, so the platform had nowhere to apply it and never + // did. Authoring it is now a tsc error and a parse error carrying the + // prescription; bound the connect phase at a provider or gateway. requestTimeoutMs: 60000, status: 'active', enabled: true diff --git a/packages/spec/liveness/connector.json b/packages/spec/liveness/connector.json index fae946abaab..5612884c517 100644 --- a/packages/spec/liveness/connector.json +++ b/packages/spec/liveness/connector.json @@ -300,8 +300,8 @@ }, "connectionTimeoutMs": { "status": "dead", - "verifiedAt": "2026-09-20", - "note": "⚠️ STILL `dead`, and the reason is now MEASURED rather than incidental — read this row before assuming the ADR-0049 ruling on this type covered it. The ruling (实现, maintainer 「同意」 2026-09-18) said the two timeouts land together via `AbortSignal.timeout`; measured at the fetch site, they cannot. A connector's outbound call is a WHATWG `fetch`, whose only cancellation surface is ONE `AbortSignal` covering the whole operation — nothing in that interface observes the connection phase separately. So the honest options were to bound \"time until the response arrives\" with this key, which would kill a slow-but-connected upstream the author meant to allow with a large `requestTimeoutMs` (breaking the very promise the key makes), or to leave it unenforced and say so. It is left unenforced and said so: packages/spec/src/integration/connector-fetch-policy.ts deliberately omits it from `ConnectorFetchPolicy` and carries the measurement, and a pin in packages/spec/src/integration/connector-fetch-policy.test.ts goes red if anyone aliases it onto `timeoutMs`. (Node's undici exposes `connectTimeout` through a custom dispatcher; that is Node-only and a new subsystem underneath every connector, which the same ruling forbids.) ⛔ THE PRIOR NOTE'S SECOND HALF IS NOW FALSE and is corrected here: a provider-bound instance no longer contributes only `name`/`label`/`description`/`icon`/`type`/`providerConfig`/`auth` through `ConnectorProviderContext` — it also carries `retryConfig`, `connectionTimeoutMs` and `requestTimeoutMs`, and this key IS handed over, for a custom provider on a transport that can separate the phases. What keeps it `dead` is that no shipped consumer reads it: the census over packages/ and examples/ finds every occurrence outside packages/spec to be a WRITE (the four connector def literals, the degraded husk in packages/services/service-automation/src/plugin.ts, and the materializer line that carries it onto the context), with `requestTimeoutMs` — same census, same files — as the lit control now that it resolves to a real read. ⇒ what this key is owed is a SECOND ADR-0049 decision, on a narrower question than the one already ruled: retire it, or re-describe it as something the platform can enforce. ⛔ Do not flip this row without a consumer." + "verifiedAt": "2026-09-22", + "note": "RETIRED (ADR-0049 enforce-or-remove; maintainer ruling 2026-09-22, letter A — the narrower SECOND decision this row asked for). Tombstoned with `retiredKey` because `ConnectorSchema` is not `.strict()` and a plain delete would be a silent strip (ADR-0104); the tombstone is inherited by `DeclarativeConnectorEntrySchema`, so `stack.connectors[]` and `/meta/connector` refuse it too. The row stays because `retiredKey` keeps the key in the walked shape (the `rls.priority` precedent). Registered as `integration/Connector:connectionTimeoutMs` and `integration/DeclarativeConnectorEntry:connectionTimeoutMs` in `RETIRED_KEYS_BY_MAJOR[18]`; authored sources and stored rows are rewritten by the D2 conversion `connector-connection-timeout-ms-removed`, and the withdrawn `ConnectorProviderContext.connectionTimeoutMs` — code, with no authored source to rewrite — by the D3 semantic entry `connector-provider-context-connection-timeout-ms-retired`. The tombstone is packages/spec/src/integration/connector.zod.ts#ConnectorSchema. ⛔ THE PRIOR NOTE’S CENSUS CLAIM IS CORRECTED HERE, not carried forward: it said every occurrence outside packages/spec is a WRITE, and that was already false when this retirement was taken. Five sites outside packages/spec READ the key — the materialization fingerprint (packages/services/service-automation/src/plugin.ts#connectorMaterializationFingerprint) and the provider-context build in the same file, `ctx.connectionTimeoutMs` in packages/connectors/connector-rest/src/rest-provider.ts and packages/connectors/connector-openapi/src/openapi-provider.ts, and the `?? 30000` fallbacks in the two `create*Connector` factories. What made the key `dead` was never the absence of readers but the absence of ENFORCEMENT: every one of those five is a pass-through whose only termini are the def `GET /connectors` echoes and the fingerprint that decides whether to re-materialize, and `connectorFetchOptions` — the one mapping onto the platform’s outbound `fetch` — was handed `{ retryConfig, requestTimeoutMs }` only. ⇒ a future census on this type counts READS and asks what each one DOES with the value; a grep count answers neither question. Use `requestTimeoutMs` (live, the row below) for the deadline the platform keeps, and bound the connect phase at a connector provider or upstream gateway on a transport that can separate the phases." }, "requestTimeoutMs": { "status": "live", diff --git a/packages/spec/src/conversions/registry.ts b/packages/spec/src/conversions/registry.ts index 2371b6f4407..e10b5ffe5b8 100644 --- a/packages/spec/src/conversions/registry.ts +++ b/packages/spec/src/conversions/registry.ts @@ -8966,6 +8966,89 @@ const connectorErrorMappingRemoved: MetadataConversion = { }, }; +/** + * `connector.connectionTimeoutMs` removed (protocol 18, ADR-0049 + * enforce-or-remove; maintainer ruling 2026-09-22, letter A). + * + * A bounded (`min(1000).max(300000)`), defaulted (`30000`), `.describe()`d key + * on `ConnectorSchema` — and, because `DeclarativeConnectorEntrySchema` + * `superRefine`s the same shape, on `stack.connectors[]` and the + * `PUT /meta/connector/:name` door — that no site ever applied as a deadline. + * + * ⚠️ NOT a zero-mention retirement, and the distinction is the whole finding: + * five sites outside `packages/spec` READ the key. The materialization + * fingerprint and the provider-context build in + * `services/service-automation/src/plugin.ts`, `ctx.connectionTimeoutMs` in the + * `rest` and `openapi` provider factories, and the `?? 30000` fallbacks that + * deposit it back onto the reported def. Every one of them is a pass-through: + * the value's only termini are the def `GET /connectors` echoes and the + * fingerprint that decides whether to re-materialize. `connectorFetchOptions()` + * — the one mapping from authored policy onto the platform's outbound `fetch` + * (`integration/connector-fetch-policy.ts`) — was handed + * `{ retryConfig, requestTimeoutMs }` only. Carrying a number is not honouring + * it, and ADR-0049 forbids the parsed-unmarked-unenforced state whether the + * inert value travels or sits still. + * + * And it is not implementable where it was declared: a connector's outbound + * call is a WHATWG `fetch`, whose only cancellation surface is one + * `AbortSignal` over the whole operation, so nothing there observes the connect + * phase. `requestTimeoutMs` — live since #19388, the lit control for every + * reading above — is the bound the platform can keep. + * + * A pure lossless delete: the key never had an effect to preserve, so there is + * no value to rewrite into anything. + * + * `retiredFromLoadPath`: `ConnectorSchema` tombstones the key (`retiredKey`, + * tsc `never` + the parse-time prescription — the `errorMapping` posture one + * block over in the same schema), so a live parse refuses loudly rather than + * absorbing a key the author believes bounds a connect. This entry exists + * because a stored connector row CAN carry it: the write door + * `PUT /meta/connector/:name` parses `DeclarativeConnectorEntrySchema` and its + * output retained the authored value, and the rehydration seam + * `applyConversionsToStoredItem('connector', row)` is live for this type — both + * measured before the tombstone landed. So 17.x rows replay clean here, and + * `os migrate meta --from 17` lists the mechanical edits for author sources. + */ +const connectorConnectionTimeoutMsRemoved: MetadataConversion = { + id: 'connector-connection-timeout-ms-removed', + toMajor: 18, + retiredFromLoadPath: true, + surface: 'connector.connectionTimeoutMs', + summary: + "connector key 'connectionTimeoutMs' removed (ADR-0049 — the platform never applied it as a " + + 'deadline and cannot at the site it names: a WHATWG `fetch` exposes one `AbortSignal` over ' + + 'the whole operation and never the connect phase. The value only travelled — onto the ' + + 'reported def and the materialization fingerprint. Use `requestTimeoutMs`, which ' + + "`resilientFetch` applies as each attempt's deadline, and bound the connect phase at a " + + 'provider or gateway that can separate the phases)', + apply(stack, emit) { + return mapCollection(stack, 'connectors', (c, path) => + stripKeys(c, ['connectionTimeoutMs'], emit, path)); + }, + fixture: { + before: { + connectors: [ + // Minimal by the §3 disjointness contract: the retired key and nothing + // else this major's other `connectors[]` entries also walk + // (`errorMapping`, `health.circuitBreaker.monitoringWindow`, + // `triggers[].interval`), so every notice here is attributable to this id. + { name: 'ledger_api', label: 'Ledger API', type: 'api', connectionTimeoutMs: 15000 }, + // A connector that never authored the key keeps its identity — the + // copy-on-write contract `stripKeys` / `mapCollection` are built on. + { name: 'inventory_sync', label: 'Inventory Sync', type: 'saas' }, + ], + }, + after: { + connectors: [ + { name: 'ledger_api', label: 'Ledger API', type: 'api' }, + { name: 'inventory_sync', label: 'Inventory Sync', type: 'saas' }, + ], + }, + // One notice: the one connector carrying the key. + expectedNotices: 1, + }, +}; + /** * `hook.timeout` → `hook.timeoutMs` (protocol 18, #14478; maintainer ruling * 2026-09-02, recorded on the card as "ruled B"). @@ -10048,6 +10131,7 @@ export const CONVERSIONS_BY_MAJOR: Readonly { + it('REJECTS the authored key at path `connectionTimeoutMs`, carrying the prescription', () => { + const result = ConnectorSchema.safeParse({ ...WELL_FORMED, connectionTimeoutMs: AUTHORED_MS }); + expect(result.success).toBe(false); + if (result.success) return; // narrowing; the assertion above already failed + + const issue = result.error.issues.find((i) => i.path[0] === 'connectionTimeoutMs'); + expect(issue, 'the refusal must name `connectionTimeoutMs`').toBeDefined(); + // The machine-readable half of the envelope this surface actually has. + expect(issue!.code).toBe('invalid_type'); + expect(issue!.path).toEqual(['connectionTimeoutMs']); + // …and the human half: the prescription IS the migration doc. + expect(issue!.message).toMatch(PRESCRIPTION); + // It must name the replacement, or an author who hits it learns only that + // something is gone. `requestTimeoutMs` is the bound the platform keeps. + expect(issue!.message).toContain('requestTimeoutMs'); + }); + + it('the inherited carrier refuses it too, so `stack.connectors[]` and the /meta door are covered', () => { + const entry = DeclarativeConnectorEntrySchema.safeParse({ ...WELL_FORMED, connectionTimeoutMs: AUTHORED_MS }); + expect(entry.success).toBe(false); + + // The registry lookup is the real `PUT /meta/connector/:name` entry point: + // a rebinding that pointed `connector` at some third shape would pass the + // pin above and still accept the key in production. + const door = getMetadataTypeSchema('connector'); + expect(door, 'no schema bound for `connector`').toBeDefined(); + expect(door!.safeParse({ ...WELL_FORMED, connectionTimeoutMs: AUTHORED_MS }).success).toBe(false); + + const stack = ObjectStackSchema.safeParse({ + connectors: [{ ...WELL_FORMED, connectionTimeoutMs: AUTHORED_MS }], + }); + expect(stack.success).toBe(false); + if (stack.success) return; + const issue = stack.error.issues.find((i) => i.path.join('.') === 'connectors.0.connectionTimeoutMs'); + expect(issue, 'the stack refusal must locate the key').toBeDefined(); + expect(issue!.path).toEqual(['connectors', 0, 'connectionTimeoutMs']); + + // CONTROL: the same three doors accept the same connector WITHOUT the key, + // so the refusals above are attributable to `connectionTimeoutMs` alone. + expect(DeclarativeConnectorEntrySchema.safeParse(WELL_FORMED).success).toBe(true); + expect(door!.safeParse(WELL_FORMED).success).toBe(true); + expect(ObjectStackSchema.safeParse({ connectors: [WELL_FORMED] }).success).toBe(true); + }); + + it('parses a well-formed connector and grows no `connectionTimeoutMs` property', () => { + const parsed = ConnectorSchema.parse({ ...WELL_FORMED }); + expect(parsed.name).toBe('ledger_api'); + // CONTROL: the live defaults on this schema still apply, so an empty + // reading below is the retirement and not a schema that stopped defaulting. + expect(parsed.enabled).toBe(true); + expect(parsed.requestTimeoutMs).toBe(30000); + // The non-strict strip path: absence must stay absence. If the tombstone + // were ever replaced by a plain deletion, an authored `connectionTimeoutMs` + // would be stripped in silence — this pin plus the refusals above are what + // make that regression loud. + expect(parsed).not.toHaveProperty('connectionTimeoutMs'); + }); + + it('fails tsc at the authoring site: the input type of the key is `never`', () => { + const connector: Connector = { + ...WELL_FORMED, + // @ts-expect-error — `connectionTimeoutMs` is a retiredKey() tombstone: + // its input type is `never`, so a typed literal cannot carry it. + connectionTimeoutMs: AUTHORED_MS, + }; + // The parse channel agrees with the type channel on the same literal. + expect(ConnectorSchema.safeParse(connector).success).toBe(false); + }); +}); + +describe('connector.connectionTimeoutMs retirement — the D2 conversion, and why one is owed', () => { + it('a STORED connector row can carry the key — the measurement that made D2 owed', () => { + // ⭐ This is the ruling's one deliberately-unanswered question ("the dev + // measures"), re-taken here so it cannot rot into an assumption. Two legs: + // + // 1. The write door persists what it parses. `getMetadataTypeSchema + // ('connector')` is what `PUT /api/v1/meta/connector/:name` validates + // against, and before the tombstone its output RETAINED the authored + // value — so the number reached `sys_metadata`. It is refused now, which + // is exactly why rows written before this release still hold it. + // 2. The rehydration seam is live for this type: a stored `connector` row + // replayed through `applyConversionsToStoredItem` reaches the conversion + // chain at all. Measured by the strip below rather than assumed. + // + // Had EITHER leg come back empty — no schema bound for `connector`, or a + // seam that never reaches this type — the answer would have been D3-only, + // which is what the ruling's prescription alone would have produced. + const stored: Record = { + ...WELL_FORMED, + connectionTimeoutMs: AUTHORED_MS, + requestTimeoutMs: 12000, + }; + const notices: { conversionId?: string }[] = []; + const rehydrated = applyConversionsToStoredItem('connector', stored, { + onNotice: (n) => notices.push(n as { conversionId?: string }), + }) as Record; + + expect(notices.map((n) => n.conversionId)).toContain('connector-connection-timeout-ms-removed'); + expect(rehydrated).not.toHaveProperty('connectionTimeoutMs'); + // CONTROL: the seam rewrote the retired key and nothing else — the live + // sibling on the same row survives byte-for-byte. + expect(rehydrated.requestTimeoutMs).toBe(12000); + expect(rehydrated.name).toBe('ledger_api'); + }); + + it('strips the key from `connectors[]` — one attributed notice per connector', () => { + const { stack, notices } = collectConversionNotices( + { + connectors: [ + { ...WELL_FORMED, connectionTimeoutMs: AUTHORED_MS }, + // Never authored the key: rides through untouched. + { name: 'inventory_sync', label: 'Inventory Sync', type: 'saas' }, + ], + }, + { includeRetired: true }, + ); + expect(stack).toEqual({ + connectors: [ + { name: 'ledger_api', label: 'Ledger API', type: 'api' }, + { name: 'inventory_sync', label: 'Inventory Sync', type: 'saas' }, + ], + }); + expect(notices).toHaveLength(1); + expect(notices[0]).toMatchObject({ + conversionId: 'connector-connection-timeout-ms-removed', + toMajor: 18, + path: 'connectors[0].connectionTimeoutMs', + }); + // And the stripped entry parses through the real authoring schema: the + // conversion output is exactly what the tombstone accepts. + const stripped = (stack.connectors as unknown[])[0]; + expect(DeclarativeConnectorEntrySchema.safeParse(stripped).success).toBe(true); + + // Idempotence, measured rather than asserted from `stripKeys`'s shape: a + // second replay over the converted snapshot converts nothing — zero + // notices, and the copy-on-write contract hands the input back by reference. + const replay = collectConversionNotices(stack, { includeRetired: true }); + expect(replay.notices).toHaveLength(0); + expect(replay.stack).toBe(stack); + }); +}); + +describe('connector.connectionTimeoutMs retirement — ADR-0087 registration', () => { + it('declares both carrier keys under major 18, with the D2 conversion in the step-18 chain', () => { + expect(RETIRED_KEYS_BY_MAJOR[18]).toContain('integration/Connector:connectionTimeoutMs'); + expect(RETIRED_KEYS_BY_MAJOR[18]).toContain('integration/DeclarativeConnectorEntry:connectionTimeoutMs'); + + const step = MIGRATIONS_BY_MAJOR[18]; + expect(step, 'the step-18 chain must exist').toBeDefined(); + expect(step!.conversionIds).toContain('connector-connection-timeout-ms-removed'); + }); + + it('declares the withdrawn provider-context member as a D3 semantic entry', () => { + const entry = MIGRATIONS_BY_MAJOR[18]!.semantic + .find((s) => s.id === 'connector-provider-context-connection-timeout-ms-retired'); + expect(entry, 'the withdrawn ConnectorProviderContext member needs its own D3 entry').toBeDefined(); + // Non-empty by contract (`spec-property-retirement` §3), and it must name + // the replacement rather than only the removal. + expect(entry!.reason.length).toBeGreaterThan(0); + expect(entry!.acceptanceCriteria.length).toBeGreaterThan(0); + expect(entry!.replacement).toContain('requestTimeoutMs'); + }); + + it('retires NO def — the key was a bare number, not a config shape', () => { + // Route-dependent reading (`spec-property-retirement` §2): on a whole-def + // removal the four ratchets MUST move, on a key-only tombstone they move by + // exactly the `[RETIRED]` row. Asserting the def table is untouched keeps a + // future reader from judging this retirement against the wrong expectation. + for (const def of RETIRED_DEFS_BY_MAJOR[18] ?? []) { + expect(def, 'no integration def leaves with this key').not.toMatch(/^integration\/Connector(Timeout|Connection)/); + } + }); +}); + +// ─── Tree-scoped absence, with a DECLARED radius ───────────────────────────── +// +// What this leg guarantees. `tsc` is the primary sweeper — `retiredKey()` types +// the key `never`, so every TypeScript authoring site in the monorepo fails to +// compile. The residue is everything `tsc` never compiles: JSON, YAML, MD, MDX +// and untyped `.js` / `.mjs` / `.cjs`. This walk covers that residue across five +// repo roots, each already declared for `@objectstack/spec#test` in +// `scripts/cross-package-test-inputs.mjs` and mirrored in `turbo.json`, so a +// resurrection inside the radius puts this suite into `turbo ls --affected`. +// ⛔ A tree-scoped pin whose radius is undeclared is not a completed retirement +// (`spec-property-retirement` §4) — the declaration is half the pin. +// +// The bound, stated: `docs/**`, `.claude/**`, `.github/**` and the repo-root +// files are outside the walk, as they are for the #15513 pin this copies. +// +// The matcher judges an AUTHORING SHAPE, never a mention: `connectionTimeoutMs` +// in key position, or read off an object. Prose about the retirement spells the +// name inside backticks and is not matched — which is what lets the retirement +// kit itself describe what it removed without reporting itself as an offender. +describe('tree-scoped absence: nothing inside the declared radius still authors the key', () => { + const SPEC_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); + const REPO_ROOT = path.resolve(SPEC_ROOT, '../..'); + const THIS_FILE = path.relative(REPO_ROOT, fileURLToPath(import.meta.url)).split(path.sep).join('/'); + + /** The walked roots — declared in `scripts/cross-package-test-inputs.mjs` under `@objectstack/spec`. */ + const WALK_ROOTS = ['packages', 'examples', 'skills', 'content', 'scripts']; + const SCANNED_EXT = new Set(['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs', '.json', '.md', '.mdx', '.yaml', '.yml']); + /** Under `examples/` only the non-code extensions are scanned AND declared (the #15513 bound). */ + const EXAMPLES_EXT = new Set(['.json', '.md', '.mdx', '.yaml', '.yml']); + const SKIPPED_DIRS = new Set(['node_modules', 'dist', '.git', '.turbo', '.cache', '.objectstack', 'coverage', '.next', '.source']); + + /** + * An AUTHORING of the key, never a prose mention: + * - key position — `connectionTimeoutMs:` in TS/JSON/YAML, not preceded by + * a backtick (prose), a dot (a member read described in prose) or a + * word character (a longer identifier ending in this name); + * - a member read — `.connectionTimeoutMs` followed by a non-word, i.e. a + * consumer pulling the value back off an object. + */ + const AUTHORING = /(^|[^`\w.])connectionTimeoutMs["']?\s*:|\.connectionTimeoutMs\b/m; + + /** + * Structural exclusions — the retirement kit and its projections, each with + * its reason. ⛔ NOT an allowlist file (`spec-property-retirement` §4): every + * entry is a file whose JOB is to spell the retired key. + */ + const EXCLUDED = new Set([ + // The tombstone itself — the key is still a property of the walked shape. + 'packages/spec/src/integration/connector.zod.ts', + // The ledger row, which `retiredKey()` keeps in the walked shape. + 'packages/spec/liveness/connector.json', + // The pin that holds a stray leftover to reaching nothing. + 'packages/spec/src/integration/connector-fetch-policy.test.ts', + // The host-side negative pin: it authors the key to prove the carry is gone. + 'packages/services/service-automation/src/connector-materialization.test.ts', + // This pin names it to assert its absence. + THIS_FILE, + ]); + const EXCLUDED_PREFIXES = [ + // The D2 conversion, its fixture and the strip target. + 'packages/spec/src/conversions/', + // Registers the retirement by key (entries + the generated registry). + 'packages/spec/src/migrations/', + // Generated projections of the registry. + 'packages/spec/spec-changes.json', + // Release-owned prose records the removal; never edited by a code PR. + 'content/docs/releases/', + '.changeset/', + ]; + /** tsup's own bundle of `tsup.config.ts`, written and deleted mid-build (#15513's measured ENOENT). */ + const TSUP_BUNDLED_CONFIG = /\.bundled_[^./]+\.mjs$/; + + const vanished: string[] = []; + /** + * Read a path the walk enumerated, tolerating ONLY its disappearance: a path + * that no longer exists cannot be an authoring that SURVIVES. ⛔ Every other + * read failure is re-raised — a blanket `catch` would turn an unreadable tree + * into a silent green. + */ + const readIfPresent = (full: string, rel: string): string | undefined => { + try { + return fs.readFileSync(full, 'utf-8'); + } catch (err) { + if ((err as NodeJS.ErrnoException)?.code !== 'ENOENT') throw err; + vanished.push(rel); + return undefined; + } + }; + + it('the matcher recognises an authoring and ignores a prose mention (anti-vacuity)', () => { + expect(AUTHORING.test(' connectionTimeoutMs: 30000,')).toBe(true); + expect(AUTHORING.test('connectionTimeoutMs: 5000')).toBe(true); + expect(AUTHORING.test(' "connectionTimeoutMs": 30000,')).toBe(true); + expect(AUTHORING.test('connectionTimeoutMs: 15000 # yaml')).toBe(true); + expect(AUTHORING.test('const ms = ctx.connectionTimeoutMs;')).toBe(true); + expect(AUTHORING.test('entry.connectionTimeoutMs ?? null')).toBe(true); + // Prose: the retirement kit must be able to describe what it removed. + expect(AUTHORING.test('`connectionTimeoutMs` was removed in @objectstack/spec 17')).toBe(false); + expect(AUTHORING.test('the connectionTimeoutMs key is gone')).toBe(false); + expect(AUTHORING.test('"integration/Connector:connectionTimeoutMs",')).toBe(false); + // A longer identifier that merely ends in the name is not this key. + expect(AUTHORING.test(' defaultConnectionTimeoutMs: 30000,')).toBe(false); + }); + + it('a path that VANISHES mid-walk is not a finding, and every other read fault still is', () => { + const before = vanished.length; + const gone = path.join(REPO_ROOT, 'packages/spec/does-not-exist.bundled_probe.mjs'); + expect(fs.existsSync(gone)).toBe(false); + expect(readIfPresent(gone, 'probe/gone')).toBeUndefined(); + expect(vanished.slice(before)).toEqual(['probe/gone']); + // POSITIVE CONTROL: a path that IS there is read, so the guard cannot be + // passing by refusing to read anything. + expect(readIfPresent(fileURLToPath(import.meta.url), THIS_FILE)).toContain('tree-scoped absence'); + expect(vanished.length).toBe(before + 1); + // ⛔ A NON-ENOENT fault is re-raised: reading a DIRECTORY raises EISDIR. + expect(() => readIfPresent(path.join(REPO_ROOT, 'packages/spec'), 'probe/dir')).toThrow(); + expect(vanished.length).toBe(before + 1); + }); + + it('no authoring survives inside the declared radius outside the retirement kit', () => { + const offenders: string[] = []; + let visited = 0; + const walk = (dir: string) => { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + const rel = path.relative(REPO_ROOT, full).split(path.sep).join('/'); + if (entry.isDirectory()) { + if (SKIPPED_DIRS.has(entry.name) || entry.name.startsWith('.')) continue; + walk(full); + continue; + } + if (!entry.isFile()) continue; + const ext = path.extname(entry.name); + if (!(rel.startsWith('examples/') ? EXAMPLES_EXT : SCANNED_EXT).has(ext)) continue; + if (entry.name === 'CHANGELOG.md') continue; // release prose records the removal + if (EXCLUDED.has(rel) || EXCLUDED_PREFIXES.some((p) => rel.startsWith(p))) continue; + if (TSUP_BUNDLED_CONFIG.test(entry.name)) continue; + visited += 1; + const text = readIfPresent(full, rel); + if (text === undefined) continue; + const m = AUTHORING.exec(text); + if (m) offenders.push(`${rel} authors \`${m[0].trim()}\``); + } + }; + for (const root of WALK_ROOTS) walk(path.join(REPO_ROOT, root)); + // Anti-vacuity: the walk really covered the tree. + expect(visited).toBeGreaterThan(1000); + expect(offenders, 'an authoring of the retired key means the retirement is being undone').toEqual([]); + }); +}); diff --git a/packages/spec/src/migrations/entries/retired-keys/18.integration__Connector__connectionTimeoutMs.ts b/packages/spec/src/migrations/entries/retired-keys/18.integration__Connector__connectionTimeoutMs.ts new file mode 100644 index 00000000000..12df87ef523 --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-keys/18.integration__Connector__connectionTimeoutMs.ts @@ -0,0 +1,50 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// ADR-0049 enforce-or-remove on `ConnectorSchema.connectionTimeoutMs` +// (maintainer ruling 2026-09-22, letter A — the narrower SECOND decision this +// key was owed, after the ruling that made its nine ledger siblings live left +// this one dead on a stated reason rather than by oversight). The key was +// bounded (`min(1000).max(300000)`), defaulted (`30000`), `.describe()`d and +// served back by `/meta/connector`: every signal an authoring surface can give +// said it worked. +// +// ⚠️ This is NOT the zero-mention retirement shape, and reading it as one loses +// the finding. FIVE sites outside `packages/spec` READ the key: the +// materialization fingerprint and the provider-context build in +// `services/service-automation/src/plugin.ts`, `ctx.connectionTimeoutMs` in the +// `rest` and `openapi` provider factories, and the `?? 30000` fallbacks that +// deposit it back onto the reported def. Measured across all five, every one is +// a pass-through: the value's only termini are the def `GET /connectors` echoes +// and the fingerprint that decides whether to re-materialize. Never a deadline. +// `connectorFetchOptions()` (`integration/connector-fetch-policy.ts`) is the one +// mapping from authored policy onto the platform's outbound `fetch`, and it was +// handed `{ retryConfig, requestTimeoutMs }` only. Carrying a number is not +// honouring it — ADR-0049 forbids the parsed-unmarked-unenforced state whether +// the inert value travels or sits still. +// +// The `实现` arm was unavailable, which is why the second decision came out +// `retire` rather than `enforce`: a connector's outbound call is a WHATWG +// `fetch`, whose only cancellation surface is ONE `AbortSignal` covering the +// whole operation, so nothing there observes the connect phase. Bounding +// "time until the response arrives" with this key would kill a slow-but- +// connected upstream the author meant to allow with a large `requestTimeoutMs` +// — breaking the very promise the key makes. (undici's `connectTimeout` needs a +// custom dispatcher: Node-only, and a new subsystem underneath every connector.) +// `requestTimeoutMs` — live since PR #19388, and the lit control for every +// census above — is the bound the platform can keep. +// +// Tombstoned with `retiredKey()`: `ConnectorSchema` is a non-strict `z.object`, +// so a bare deletion would be a silent strip (ADR-0104). No def leaves with it — +// the key was a bare `z.number()`, not a `ConfigSchema` shape, so +// `RETIRED_DEFS_BY_MAJOR[18]` gains nothing. Authored sources and stored rows are +// rewritten by the D2 conversion `connector-connection-timeout-ms-removed`; the +// withdrawn `ConnectorProviderContext` member, which is code with no authored +// source, leaves via the D3 semantic entry +// `connector-provider-context-connection-timeout-ms-retired`. +// +// Registered under 18, not 17: v17.0.0 was cut before this landed, so the +// removal ships on the 17.x line (launch-window convention: accept-set +// narrowings ride minor releases) and the prescription lives at the major +// boundary where `migrate meta` users look — the disposition +// `18.integration__Connector__errorMapping.ts` records for the same schema. +export const entry = 'integration/Connector:connectionTimeoutMs'; diff --git a/packages/spec/src/migrations/entries/retired-keys/18.integration__DeclarativeConnectorEntry__connectionTimeoutMs.ts b/packages/spec/src/migrations/entries/retired-keys/18.integration__DeclarativeConnectorEntry__connectionTimeoutMs.ts new file mode 100644 index 00000000000..03166e33526 --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-keys/18.integration__DeclarativeConnectorEntry__connectionTimeoutMs.ts @@ -0,0 +1,16 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// The same tombstone seen through the second carrier. +// `DeclarativeConnectorEntrySchema` is `ConnectorSchema.superRefine(...)`, so the +// `connectionTimeoutMs` tombstone on the base is inherited by the shape that +// `stack.connectors[]` (`stack.zod.ts`) and the `PUT /meta/connector/:name` door +// (`kernel/metadata-type-schemas.ts`) actually parse, and the authorable-surface +// walk publishes the `[RETIRED]` row under this def key as well. One tombstone, +// two registered keys: gate (b) of `scripts/build-schemas.ts` reads EXACT +// `${defKey}:${name}` membership per def, never by radiating from a neighbour. +// +// This carrier is also what made the D2 conversion owed rather than optional: +// the door persists what it parses, so a stored `sys_metadata` connector row can +// carry the key — measured, not assumed, before the tombstone landed. +// See `18.integration__Connector__connectionTimeoutMs.ts` for the retirement record. +export const entry = 'integration/DeclarativeConnectorEntry:connectionTimeoutMs'; diff --git a/packages/spec/src/migrations/entries/semantic/18.connector-provider-context-connection-timeout-ms-retired.ts b/packages/spec/src/migrations/entries/semantic/18.connector-provider-context-connection-timeout-ms-retired.ts new file mode 100644 index 00000000000..31671d940ce --- /dev/null +++ b/packages/spec/src/migrations/entries/semantic/18.connector-provider-context-connection-timeout-ms-retired.ts @@ -0,0 +1,46 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import type { SemanticMigration } from '../../types.js'; + +export const entry: SemanticMigration = { + id: 'connector-provider-context-connection-timeout-ms-retired', + surface: 'ConnectorProviderContext.connectionTimeoutMs, the declared connect deadline handed ' + + 'to every ConnectorProviderFactory (integration/connector-provider.ts)', + replacement: 'requestTimeoutMs for the deadline the platform keeps; for a connect-only bound, ' + + "the provider's own providerConfig, where the provider owns the vocabulary", + reason: + 'ADR-0049 enforce-or-remove, maintainer ruling 2026-09-22 letter A: retire ' + + 'connector.connectionTimeoutMs. The spec key is tombstoned and its authored sources are ' + + 'rewritten by the D2 conversion connector-connection-timeout-ms-removed; this entry ' + + 'carries the half a conversion cannot reach. The key was placed on this context by the ' + + 'round that made the connector resilience policy live, explicitly as a CARRY — handed ' + + 'over so that a custom provider on a transport able to separate the phases could honour ' + + 'it. Measured before removal, none did, and the carry itself was the last thing keeping ' + + 'the key alive in argument: the built-in rest and openapi factories read ' + + 'ctx.connectionTimeoutMs only to deposit it back onto the def that GET /connectors ' + + 'echoes, and connectorFetchOptions — the one mapping from authored policy onto the ' + + "platform's outbound fetch — was never handed it. Being handed a value is not honouring " + + 'it, so the carry is the same parsed-unmarked-unenforced state on one more surface, and ' + + 'it leaves with the key rather than outliving it as an orphan a factory could still ' + + 'read. Why a semantic entry and not a D2 conversion: a provider factory is CODE. There is ' + + 'no authored source and no sys_metadata row holding a read of ctx.connectionTimeoutMs, so ' + + 'the chain has no seam to rewrite — the removal reaches a factory author as a tsc error ' + + 'and as this entry, never as a mechanical edit. The declaration cannot be made honest by ' + + 'implementing it either: a WHATWG fetch exposes one AbortSignal over the whole operation ' + + 'and never the connect phase, so bounding time-to-response with this key would kill a ' + + 'slow-but-connected upstream the author meant to allow with a large requestTimeoutMs. ' + + 'ADR-0087, ADR-0097.', + acceptanceCriteria: + 'No ConnectorProviderFactory reads ctx.connectionTimeoutMs; the member does not exist on ' + + 'ConnectorProviderContext and reading it fails to compile. A factory that genuinely needs ' + + 'a connect-phase bound declares it in its own providerConfig and applies it itself, on a ' + + 'transport that can observe the connect phase — it does not receive one from the host. ' + + 'Behaviour is unchanged for every shipped provider, because none applied the value: a ' + + 'connector that authored connectionTimeoutMs made exactly the same calls with exactly ' + + 'the same deadlines before and after. What does change is observable and intended: the ' + + 'def served by GET /connectors no longer echoes a connect deadline nobody keeps, and ' + + 'requestTimeoutMs — which resilientFetch applies as each attempt deadline — is the only ' + + 'timeout on the surface. The sibling members retryConfig and requestTimeoutMs ' + + 'deliberately do NOT move, and a sweep that removed either has over-applied this entry: ' + + 'both resolve to real reads at the fetch site.', +}; diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index 9d067af88f3..685caa81fac 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -5211,7 +5211,34 @@ const step18: MigrationStep = { + 'dashboard widgets only — `ReportChartSchema` and the inline-data react `` ' + 'tier keep their own axes — and the paired semantic entry carries what the stripped ' + 'keys were saying, because an authored axis field may name a column the widget never ' - + 'selected and no walker can move that intent into the dataset.', + + 'selected and no walker can move that intent into the dataset. ' + + 'It also retires `connector.connectionTimeoutMs` (ADR-0049 enforce-or-remove; ' + + 'maintainer ruling 2026-09-22, letter A — the narrower SECOND decision the key was ' + + 'owed after the ruling that made its nine ledger siblings live deliberately left this ' + + 'one dead). Bounded, defaulted, `.describe()`d and served back by `/meta/connector`, ' + + 'so an author had every signal it worked — and no site ever applied it as a deadline. ' + + 'This retirement is NOT the zero-mention shape: five sites outside `packages/spec` ' + + 'read the key (the materialization fingerprint and the provider-context build in the ' + + 'automation service, `ctx.connectionTimeoutMs` in the `rest` and `openapi` provider ' + + 'factories, and the `?? 30000` fallbacks that put it back on the reported def), but ' + + 'every one is a pass-through whose only termini are the def `GET /connectors` echoes ' + + 'and the fingerprint that decides whether to re-materialize. The one mapping from ' + + 'authored policy onto the platform\'s outbound `fetch` was handed `retryConfig` and ' + + '`requestTimeoutMs` only, so the key was carried and never honoured — the same ' + + 'parsed-unmarked-unenforced state ADR-0049 forbids, wearing a longer route. Nor was ' + + 'the `实现` arm available: a WHATWG `fetch` exposes one `AbortSignal` over the whole ' + + 'operation and never the connect phase, so bounding time-to-response with it would ' + + 'kill a slow-but-connected upstream the author meant to allow with a large ' + + '`requestTimeoutMs`. `requestTimeoutMs` is the replacement and the bound the platform ' + + 'can keep. The carrier key is a retiredKey tombstone on the non-strict ' + + '`ConnectorSchema` (a bare deletion would be a silent strip), registered under both ' + + 'def keys because `DeclarativeConnectorEntrySchema` inherits it; the D2 conversion ' + + 'strips it from `connectors[]` as a pure lossless delete — it never had an effect to ' + + 'lose — because a stored connector row CAN carry it (the `PUT /meta/connector/:name` ' + + 'door persists the authored value and the stored-row rehydration seam is live for ' + + 'this type, both measured); and the withdrawn `ConnectorProviderContext` member, ' + + 'which is code and has no authored source to rewrite, leaves via the paired semantic ' + + 'entry instead.', conversionIds: [ 'field-malformed-scale-precision-removed', 'record-chatter-position-vocabulary', @@ -5232,6 +5259,7 @@ const step18: MigrationStep = { 'form-view-option-default-removed', 'field-reference-to-alias', 'connector-error-mapping-removed', + 'connector-connection-timeout-ms-removed', 'hook-timeout-to-timeout-ms', 'job-timeout-to-timeout-ms', 'api-endpoint-cache-ttl-to-cache-ttl-seconds', From d7917c16c758d30149ac2c9c915b43dae1d7791b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 04:41:26 +0000 Subject: [PATCH 03/15] chore(spec): regenerate migration registry, authorable baselines and reference docs Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx --- .../docs/references/integration/connector.mdx | 27 +++-- .../spec/authorable-defaults/integration.json | 2 - .../spec/authorable-surface/integration.json | 4 +- packages/spec/src/migrations/registry.ts | 104 ++++++++++++++++++ 4 files changed, 121 insertions(+), 16 deletions(-) diff --git a/content/docs/references/integration/connector.mdx b/content/docs/references/integration/connector.mdx index 32d98968500..f8dccb6e1fa 100644 --- a/content/docs/references/integration/connector.mdx +++ b/content/docs/references/integration/connector.mdx @@ -55,17 +55,20 @@ construction. ⚠️ Retrying a `429` is not throttling it: a retry policy spaces out the calls you already made, it does not cap the rate, so the sentence above about rate limiting stands unchanged. -⛔ **Two exceptions, both still inert and both still `dead` in -`packages/spec/liveness/connector.json`.** `health.circuitBreaker`: every -sub-key is unread and no breaker ever opens — implement circuit breaking in -the connector provider. `connectionTimeoutMs`: it is carried to a provider -factory but the platform does not enforce it, because a WHATWG `fetch` +⛔ **One exception remains, still inert and still `dead` in +`packages/spec/liveness/connector.json`:** `health.circuitBreaker` — every +sub-key is unread and no breaker ever opens; implement circuit breaking in +the connector provider. + +`connectionTimeoutMs` used to be the second exception and is now **removed** +(ADR-0049, the narrower second decision that surface was owed): it was +carried to a provider factory but never applied as a deadline anywhere, and +it is not implementable where it was declared, because a WHATWG `fetch` exposes one `AbortSignal` over the whole operation and never the connection -phase alone; `requestTimeoutMs` is the bound the platform can keep, and -ADR-0049 owes this one key a narrower decision. The full removal reasoning -for the rate-limit shape is recorded at the removal site: the "REMOVED: -outbound rate limiting" block in `integration/connector.zod.ts`, and -`packages/spec/docs/SYNC_ARCHITECTURE.md`. +phase alone. `requestTimeoutMs` is the bound the platform can keep. The full +removal reasoning is recorded at each removal site: the "REMOVED: +`connectionTimeoutMs`" and "REMOVED: outbound rate limiting" blocks in +`integration/connector.zod.ts`, and `packages/spec/docs/SYNC_ARCHITECTURE.md`. **Field mapping does not transform values.** This header used to offer "field mapping and transformations"; only the first half was ever true. @@ -200,7 +203,7 @@ Circuit breaker configuration | **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete' \| 'bulk_update' \| 'bulk_delete'>[]; … }[]` | optional | Webhook configurations | | **rateLimitConfig** | `never` | optional | [REMOVED] `connector.rateLimitConfig` was removed in @objectstack/spec 17.0.0 (ADR-0049 D2) — the entire shape is gone, not just this key: `ConnectorRateLimitConfig` and its `RateLimitStrategy` enum were removed with it, because no outbound rate-limiting engine ever existed. The platform's only token bucket (runtime `security/rate-limit.ts`) throttles INBOUND requests to us; nothing throttled the calls a connector makes out, so every knob here was inert while reading like a configured cap. Delete the key. Do NOT substitute `shared` `RateLimitConfig` — that is the inbound limiter and would cap the wrong direction; until an outbound throttle exists, rate-limit at the connector provider or upstream gateway. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | | **retryConfig** | `{ strategy: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts: number; initialDelayMs: number; maxDelayMs: number; … }` | optional | Retry configuration | -| **connectionTimeoutMs** | `number` | optional (default: `30000`) | Connection timeout in ms | +| **connectionTimeoutMs** | `never` | optional | [REMOVED] `connector.connectionTimeoutMs` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — the platform never honoured it and cannot honour it where it was declared: a connector's outbound call is a WHATWG `fetch`, whose only cancellation surface is one `AbortSignal` over the whole operation, so nothing there observes the connection phase separately, and the value only ever travelled (onto the reported def and the materialization fingerprint) without ever bounding a connect. Delete the key. Use `requestTimeoutMs` for the deadline the platform does keep — it is applied as `resilientFetch`'s per-attempt timeout — and bound the connect phase at a connector provider or upstream gateway on a transport that can separate the phases. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | | **requestTimeoutMs** | `number` | optional (default: `30000`) | Request timeout in ms | | **status** | `Enum<'active' \| 'inactive' \| 'error' \| 'configuring'>` | optional (default: `"inactive"`) | Connector status | | **enabled** | `boolean` | optional (default: `true`) | Enable connector. On declarative stack entries, false marks a deliberate catalog-only descriptor. | @@ -684,7 +687,7 @@ Connector type | **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete' \| 'bulk_update' \| 'bulk_delete'>[]; … }[]` | optional | Webhook configurations | | **rateLimitConfig** | `never` | optional | [REMOVED] `connector.rateLimitConfig` was removed in @objectstack/spec 17.0.0 (ADR-0049 D2) — the entire shape is gone, not just this key: `ConnectorRateLimitConfig` and its `RateLimitStrategy` enum were removed with it, because no outbound rate-limiting engine ever existed. The platform's only token bucket (runtime `security/rate-limit.ts`) throttles INBOUND requests to us; nothing throttled the calls a connector makes out, so every knob here was inert while reading like a configured cap. Delete the key. Do NOT substitute `shared` `RateLimitConfig` — that is the inbound limiter and would cap the wrong direction; until an outbound throttle exists, rate-limit at the connector provider or upstream gateway. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | | **retryConfig** | `{ strategy: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts: number; initialDelayMs: number; maxDelayMs: number; … }` | optional | Retry configuration | -| **connectionTimeoutMs** | `number` | optional (default: `30000`) | Connection timeout in ms | +| **connectionTimeoutMs** | `never` | optional | [REMOVED] `connector.connectionTimeoutMs` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — the platform never honoured it and cannot honour it where it was declared: a connector's outbound call is a WHATWG `fetch`, whose only cancellation surface is one `AbortSignal` over the whole operation, so nothing there observes the connection phase separately, and the value only ever travelled (onto the reported def and the materialization fingerprint) without ever bounding a connect. Delete the key. Use `requestTimeoutMs` for the deadline the platform does keep — it is applied as `resilientFetch`'s per-attempt timeout — and bound the connect phase at a connector provider or upstream gateway on a transport that can separate the phases. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | | **requestTimeoutMs** | `number` | optional (default: `30000`) | Request timeout in ms | | **status** | `Enum<'active' \| 'inactive' \| 'error' \| 'configuring'>` | optional (default: `"inactive"`) | Connector status | | **enabled** | `boolean` | optional (default: `true`) | Enable connector. On declarative stack entries, false marks a deliberate catalog-only descriptor. | diff --git a/packages/spec/authorable-defaults/integration.json b/packages/spec/authorable-defaults/integration.json index 2c74177ff77..762e255eca5 100644 --- a/packages/spec/authorable-defaults/integration.json +++ b/packages/spec/authorable-defaults/integration.json @@ -7,7 +7,6 @@ "integration/CircuitBreakerConfig:monitoringWindowMs = 60000", "integration/CircuitBreakerConfig:resetTimeoutMs = 30000", "integration/Connector:authentication = {\"type\":\"none\"}", - "integration/Connector:connectionTimeoutMs = 30000", "integration/Connector:enabled = true", "integration/Connector:requestTimeoutMs = 30000", "integration/Connector:status = \"inactive\"", @@ -20,7 +19,6 @@ "integration/DataSyncConfig:realtimeSync = false", "integration/DataSyncConfig:strategy = \"incremental\"", "integration/DeclarativeConnectorEntry:authentication = {\"type\":\"none\"}", - "integration/DeclarativeConnectorEntry:connectionTimeoutMs = 30000", "integration/DeclarativeConnectorEntry:enabled = true", "integration/DeclarativeConnectorEntry:requestTimeoutMs = 30000", "integration/DeclarativeConnectorEntry:status = \"inactive\"", diff --git a/packages/spec/authorable-surface/integration.json b/packages/spec/authorable-surface/integration.json index abb46657470..d2f63a82b2c 100644 --- a/packages/spec/authorable-surface/integration.json +++ b/packages/spec/authorable-surface/integration.json @@ -19,7 +19,7 @@ "integration/Connector:actions", "integration/Connector:auth", "integration/Connector:authentication", - "integration/Connector:connectionTimeoutMs", + "integration/Connector:connectionTimeoutMs [RETIRED]", "integration/Connector:description", "integration/Connector:enabled", "integration/Connector:errorMapping [RETIRED]", @@ -88,7 +88,7 @@ "integration/DeclarativeConnectorEntry:actions", "integration/DeclarativeConnectorEntry:auth", "integration/DeclarativeConnectorEntry:authentication", - "integration/DeclarativeConnectorEntry:connectionTimeoutMs", + "integration/DeclarativeConnectorEntry:connectionTimeoutMs [RETIRED]", "integration/DeclarativeConnectorEntry:description", "integration/DeclarativeConnectorEntry:enabled", "integration/DeclarativeConnectorEntry:errorMapping [RETIRED]", diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index 685caa81fac..be4ab59c95b 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -6559,6 +6559,48 @@ const step18: MigrationStep = { + 'fail tsc on upgrade; the fix is choosing a shipped driver, never ' + 'widening a local mirror of the enum.', }, + { + id: 'connector-provider-context-connection-timeout-ms-retired', + surface: 'ConnectorProviderContext.connectionTimeoutMs, the declared connect deadline handed ' + + 'to every ConnectorProviderFactory (integration/connector-provider.ts)', + replacement: 'requestTimeoutMs for the deadline the platform keeps; for a connect-only bound, ' + + "the provider's own providerConfig, where the provider owns the vocabulary", + reason: + 'ADR-0049 enforce-or-remove, maintainer ruling 2026-09-22 letter A: retire ' + + 'connector.connectionTimeoutMs. The spec key is tombstoned and its authored sources are ' + + 'rewritten by the D2 conversion connector-connection-timeout-ms-removed; this entry ' + + 'carries the half a conversion cannot reach. The key was placed on this context by the ' + + 'round that made the connector resilience policy live, explicitly as a CARRY — handed ' + + 'over so that a custom provider on a transport able to separate the phases could honour ' + + 'it. Measured before removal, none did, and the carry itself was the last thing keeping ' + + 'the key alive in argument: the built-in rest and openapi factories read ' + + 'ctx.connectionTimeoutMs only to deposit it back onto the def that GET /connectors ' + + 'echoes, and connectorFetchOptions — the one mapping from authored policy onto the ' + + "platform's outbound fetch — was never handed it. Being handed a value is not honouring " + + 'it, so the carry is the same parsed-unmarked-unenforced state on one more surface, and ' + + 'it leaves with the key rather than outliving it as an orphan a factory could still ' + + 'read. Why a semantic entry and not a D2 conversion: a provider factory is CODE. There is ' + + 'no authored source and no sys_metadata row holding a read of ctx.connectionTimeoutMs, so ' + + 'the chain has no seam to rewrite — the removal reaches a factory author as a tsc error ' + + 'and as this entry, never as a mechanical edit. The declaration cannot be made honest by ' + + 'implementing it either: a WHATWG fetch exposes one AbortSignal over the whole operation ' + + 'and never the connect phase, so bounding time-to-response with this key would kill a ' + + 'slow-but-connected upstream the author meant to allow with a large requestTimeoutMs. ' + + 'ADR-0087, ADR-0097.', + acceptanceCriteria: + 'No ConnectorProviderFactory reads ctx.connectionTimeoutMs; the member does not exist on ' + + 'ConnectorProviderContext and reading it fails to compile. A factory that genuinely needs ' + + 'a connect-phase bound declares it in its own providerConfig and applies it itself, on a ' + + 'transport that can observe the connect phase — it does not receive one from the host. ' + + 'Behaviour is unchanged for every shipped provider, because none applied the value: a ' + + 'connector that authored connectionTimeoutMs made exactly the same calls with exactly ' + + 'the same deadlines before and after. What does change is observable and intended: the ' + + 'def served by GET /connectors no longer echoes a connect deadline nobody keeps, and ' + + 'requestTimeoutMs — which resilientFetch applies as each attempt deadline — is the only ' + + 'timeout on the surface. The sibling members retryConfig and requestTimeoutMs ' + + 'deliberately do NOT move, and a sweep that removed either has over-applied this entry: ' + + 'both resolve to real reads at the fetch site.', + }, { id: 'cube-join-sql-and-relationship-retired', // No backticks in `surface` — build-upgrade-guide.ts renders it inside a code @@ -14163,6 +14205,54 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly> // `connectors:` is a stack collection and a published connector row lands whole // in `sys_metadata`, so the chain has a seam that sees it. 'integration/CircuitBreakerConfig:monitoringWindow', + // ADR-0049 enforce-or-remove on `ConnectorSchema.connectionTimeoutMs` + // (maintainer ruling 2026-09-22, letter A — the narrower SECOND decision this + // key was owed, after the ruling that made its nine ledger siblings live left + // this one dead on a stated reason rather than by oversight). The key was + // bounded (`min(1000).max(300000)`), defaulted (`30000`), `.describe()`d and + // served back by `/meta/connector`: every signal an authoring surface can give + // said it worked. + // + // ⚠️ This is NOT the zero-mention retirement shape, and reading it as one loses + // the finding. FIVE sites outside `packages/spec` READ the key: the + // materialization fingerprint and the provider-context build in + // `services/service-automation/src/plugin.ts`, `ctx.connectionTimeoutMs` in the + // `rest` and `openapi` provider factories, and the `?? 30000` fallbacks that + // deposit it back onto the reported def. Measured across all five, every one is + // a pass-through: the value's only termini are the def `GET /connectors` echoes + // and the fingerprint that decides whether to re-materialize. Never a deadline. + // `connectorFetchOptions()` (`integration/connector-fetch-policy.ts`) is the one + // mapping from authored policy onto the platform's outbound `fetch`, and it was + // handed `{ retryConfig, requestTimeoutMs }` only. Carrying a number is not + // honouring it — ADR-0049 forbids the parsed-unmarked-unenforced state whether + // the inert value travels or sits still. + // + // The `实现` arm was unavailable, which is why the second decision came out + // `retire` rather than `enforce`: a connector's outbound call is a WHATWG + // `fetch`, whose only cancellation surface is ONE `AbortSignal` covering the + // whole operation, so nothing there observes the connect phase. Bounding + // "time until the response arrives" with this key would kill a slow-but- + // connected upstream the author meant to allow with a large `requestTimeoutMs` + // — breaking the very promise the key makes. (undici's `connectTimeout` needs a + // custom dispatcher: Node-only, and a new subsystem underneath every connector.) + // `requestTimeoutMs` — live since PR #19388, and the lit control for every + // census above — is the bound the platform can keep. + // + // Tombstoned with `retiredKey()`: `ConnectorSchema` is a non-strict `z.object`, + // so a bare deletion would be a silent strip (ADR-0104). No def leaves with it — + // the key was a bare `z.number()`, not a `ConfigSchema` shape, so + // `RETIRED_DEFS_BY_MAJOR[18]` gains nothing. Authored sources and stored rows are + // rewritten by the D2 conversion `connector-connection-timeout-ms-removed`; the + // withdrawn `ConnectorProviderContext` member, which is code with no authored + // source, leaves via the D3 semantic entry + // `connector-provider-context-connection-timeout-ms-retired`. + // + // Registered under 18, not 17: v17.0.0 was cut before this landed, so the + // removal ships on the 17.x line (launch-window convention: accept-set + // narrowings ride minor releases) and the prescription lives at the major + // boundary where `migrate meta` users look — the disposition + // `18.integration__Connector__errorMapping.ts` records for the same schema. + 'integration/Connector:connectionTimeoutMs', // #14676 — ADR-0049 enforce-or-remove on `ConnectorSchema.errorMapping` (triage // ruling 2026-09-02: removal via the `spec-property-retirement` playbook; the // split condition — a downstream consumer in objectui or a customer stack — @@ -14203,6 +14293,20 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly> // by it). The rename does not change that; it makes the declaration honest // about its unit for whoever implements the loop. 'integration/ConnectorTrigger:interval', + // The same tombstone seen through the second carrier. + // `DeclarativeConnectorEntrySchema` is `ConnectorSchema.superRefine(...)`, so the + // `connectionTimeoutMs` tombstone on the base is inherited by the shape that + // `stack.connectors[]` (`stack.zod.ts`) and the `PUT /meta/connector/:name` door + // (`kernel/metadata-type-schemas.ts`) actually parse, and the authorable-surface + // walk publishes the `[RETIRED]` row under this def key as well. One tombstone, + // two registered keys: gate (b) of `scripts/build-schemas.ts` reads EXACT + // `${defKey}:${name}` membership per def, never by radiating from a neighbour. + // + // This carrier is also what made the D2 conversion owed rather than optional: + // the door persists what it parses, so a stored `sys_metadata` connector row can + // carry the key — measured, not assumed, before the tombstone landed. + // See `18.integration__Connector__connectionTimeoutMs.ts` for the retirement record. + 'integration/DeclarativeConnectorEntry:connectionTimeoutMs', // #14676 — the same tombstone seen through the second carrier. // `DeclarativeConnectorEntrySchema` is `ConnectorSchema.superRefine(...)`, so the // `errorMapping` tombstone on the base is inherited by the shape that From a0eef99dfc49e1cbe211fa43909972b5f2773d6d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 04:43:18 +0000 Subject: [PATCH 04/15] test(spec): narrow the tree-scoped absence matcher to authoring shapes Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx --- .../connector-rest/src/rest-provider.test.ts | 2 +- ...ctor-connection-timeout-retirement.test.ts | 55 +++++++++++++------ 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/packages/connectors/connector-rest/src/rest-provider.test.ts b/packages/connectors/connector-rest/src/rest-provider.test.ts index eb94d218d76..ae1605f73f3 100644 --- a/packages/connectors/connector-rest/src/rest-provider.test.ts +++ b/packages/connectors/connector-rest/src/rest-provider.test.ts @@ -242,7 +242,7 @@ describe('rest provider factory (ADR-0097)', () => { // it back onto the def. The absence pin lives tree-scoped in // `packages/spec/src/integration/connector.test.ts`; here the point // is only that the surviving timeout still travels. - expect((def as Record).connectionTimeoutMs).toBeUndefined(); + expect(Object.keys(def)).not.toContain('connectionTimeoutMs'); }); }); }); diff --git a/packages/spec/src/integration/connector-connection-timeout-retirement.test.ts b/packages/spec/src/integration/connector-connection-timeout-retirement.test.ts index 733bab37831..b1b5b877c68 100644 --- a/packages/spec/src/integration/connector-connection-timeout-retirement.test.ts +++ b/packages/spec/src/integration/connector-connection-timeout-retirement.test.ts @@ -289,12 +289,20 @@ describe('tree-scoped absence: nothing inside the declared radius still authors /** * An AUTHORING of the key, never a prose mention: * - key position — `connectionTimeoutMs:` in TS/JSON/YAML, not preceded by - * a backtick (prose), a dot (a member read described in prose) or a - * word character (a longer identifier ending in this name); - * - a member read — `.connectionTimeoutMs` followed by a non-word, i.e. a - * consumer pulling the value back off an object. + * a word character (a longer identifier ending in this name); + * - a member read — `.connectionTimeoutMs`, i.e. a consumer pulling the + * value back off an object. */ - const AUTHORING = /(^|[^`\w.])connectionTimeoutMs["']?\s*:|\.connectionTimeoutMs\b/m; + const AUTHORING = /(^|[^\w.])connectionTimeoutMs["']?\s*:|\.connectionTimeoutMs\b/m; + + /** + * Prose mentions are spelled in INLINE CODE throughout this repo — the house + * style `check:doc-authoring` enforces — so stripping single-backtick spans + * separates "the retirement kit describing what it removed" from "a source + * still writing it". Deliberately newline-bounded: a fenced block's content + * is NOT stripped, so an authoring inside a fenced example is still caught. + */ + const stripInlineCode = (text: string): string => text.replace(/`[^`\n]*`/g, ''); /** * Structural exclusions — the retirement kit and its projections, each with @@ -323,6 +331,14 @@ describe('tree-scoped absence: nothing inside the declared radius still authors // Release-owned prose records the removal; never edited by a code PR. 'content/docs/releases/', '.changeset/', + // GITIGNORED build output (`.gitignore` line for `packages/spec/json-schema/`), + // reached only because this is a FILESYSTEM walk rather than a git walk. + // `retiredKey()` emits the tombstoned property into the generated JSON + // Schema, exactly as it already does for the two retired siblings on this + // same schema — measured: `Connector.json` carries `rateLimitConfig` and + // `errorMapping` the same way. Excluding it costs no coverage: the source + // it is generated from is `connector.zod.ts`, which this walk reads. + 'packages/spec/json-schema/', ]; /** tsup's own bundle of `tsup.config.ts`, written and deleted mid-build (#15513's measured ENOENT). */ const TSUP_BUNDLED_CONFIG = /\.bundled_[^./]+\.mjs$/; @@ -345,18 +361,25 @@ describe('tree-scoped absence: nothing inside the declared radius still authors }; it('the matcher recognises an authoring and ignores a prose mention (anti-vacuity)', () => { - expect(AUTHORING.test(' connectionTimeoutMs: 30000,')).toBe(true); - expect(AUTHORING.test('connectionTimeoutMs: 5000')).toBe(true); - expect(AUTHORING.test(' "connectionTimeoutMs": 30000,')).toBe(true); - expect(AUTHORING.test('connectionTimeoutMs: 15000 # yaml')).toBe(true); - expect(AUTHORING.test('const ms = ctx.connectionTimeoutMs;')).toBe(true); - expect(AUTHORING.test('entry.connectionTimeoutMs ?? null')).toBe(true); + const judge = (line: string): boolean => AUTHORING.test(stripInlineCode(line)); + + expect(judge(' connectionTimeoutMs: 30000,')).toBe(true); + expect(judge('connectionTimeoutMs: 5000')).toBe(true); + expect(judge(' "connectionTimeoutMs": 30000,')).toBe(true); + expect(judge('connectionTimeoutMs: 15000 # yaml')).toBe(true); + expect(judge('const ms = ctx.connectionTimeoutMs;')).toBe(true); + expect(judge('entry.connectionTimeoutMs ?? null')).toBe(true); + // ⛔ NARROWNESS of the strip: it must not swallow a real authoring that + // merely shares a line with inline code, or the pin goes quiet. + expect(judge('// see `requestTimeoutMs` — connectionTimeoutMs: 30000,')).toBe(true); // Prose: the retirement kit must be able to describe what it removed. - expect(AUTHORING.test('`connectionTimeoutMs` was removed in @objectstack/spec 17')).toBe(false); - expect(AUTHORING.test('the connectionTimeoutMs key is gone')).toBe(false); - expect(AUTHORING.test('"integration/Connector:connectionTimeoutMs",')).toBe(false); + expect(judge('`connectionTimeoutMs` was removed in @objectstack/spec 17')).toBe(false); + expect(judge('the factories read `ctx.connectionTimeoutMs` only to echo it')).toBe(false); + expect(judge('| **connectionTimeoutMs** | `never` | optional | [REMOVED] `connector.connectionTimeoutMs` was removed |')).toBe(false); + expect(judge('the connectionTimeoutMs key is gone')).toBe(false); + expect(judge('"integration/Connector:connectionTimeoutMs",')).toBe(false); // A longer identifier that merely ends in the name is not this key. - expect(AUTHORING.test(' defaultConnectionTimeoutMs: 30000,')).toBe(false); + expect(judge(' defaultConnectionTimeoutMs: 30000,')).toBe(false); }); it('a path that VANISHES mid-walk is not a finding, and every other read fault still is', () => { @@ -395,7 +418,7 @@ describe('tree-scoped absence: nothing inside the declared radius still authors visited += 1; const text = readIfPresent(full, rel); if (text === undefined) continue; - const m = AUTHORING.exec(text); + const m = AUTHORING.exec(stripInlineCode(text)); if (m) offenders.push(`${rel} authors \`${m[0].trim()}\``); } }; From dbcc8906735b5ad87ec53c33af18f53cfd6d666a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 04:53:30 +0000 Subject: [PATCH 05/15] docs(changeset): declare the connector.connectionTimeoutMs retirement Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx --- ...-retire-connector-connection-timeout-ms.md | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 .changeset/19580-retire-connector-connection-timeout-ms.md diff --git a/.changeset/19580-retire-connector-connection-timeout-ms.md b/.changeset/19580-retire-connector-connection-timeout-ms.md new file mode 100644 index 00000000000..35ad06cf28b --- /dev/null +++ b/.changeset/19580-retire-connector-connection-timeout-ms.md @@ -0,0 +1,112 @@ +--- +'@objectstack/spec': minor +'@objectstack/connector-rest': patch +'@objectstack/connector-openapi': patch +'@objectstack/connector-mcp': patch +'@objectstack/connector-slack': patch +'@objectstack/service-automation': patch +--- + +feat(spec)!: retire `connector.connectionTimeoutMs` — declared, bounded, defaulted, served back, and never applied as a deadline + +**BREAKING** — `connector.connectionTimeoutMs` is removed. ADR-0049 +enforce-or-remove; maintainer ruling 2026-09-22, letter A. It is the narrower +**second** decision this key was owed: the earlier ruling that made its nine +liveness siblings live (`retryConfig.*`, `requestTimeoutMs`) left this one dead +on a stated reason rather than by oversight, and `packages/spec/liveness/connector.json` +has been asking for this decision since. + +The key was bounded (`min(1000).max(300000)`), defaulted (`30000`), +`.describe()`d, authorable on both carriers and served back by +`/meta/connector`. Every signal an authoring surface can give said it worked. + +### FROM → TO + +| removed | what to write instead | +| --- | --- | +| `connector.connectionTimeoutMs` (on `Connector` and on `DeclarativeConnectorEntry`, so `stack.connectors[]` and `PUT /meta/connector/:name`) | `requestTimeoutMs` — the deadline the platform keeps, applied as `resilientFetch`'s per-attempt timeout. For a connect-only bound, configure it at a connector provider or upstream gateway on a transport that can separate the phases. | +| `ConnectorProviderContext.connectionTimeoutMs` (handed to every `ConnectorProviderFactory`) | `ctx.requestTimeoutMs`, or the factory's own `providerConfig` where the provider owns the vocabulary. | + +**The one-line fix: delete the key** — and, for a custom provider factory, stop +reading `ctx.connectionTimeoutMs`. `os migrate meta --from 17` lists the +mechanical edits for existing sources; apply them by hand. + +⚠️ Runtime behaviour is **unchanged for every shipped provider**, because none +ever applied the value: a connector that authored `connectionTimeoutMs: 1000` +made exactly the same calls, with exactly the same deadlines, as one that did +not. What does change is observable and intended: the def served by +`GET /connectors` no longer echoes a connect deadline nobody keeps. + +### ⭐ This is NOT the zero-mention retirement shape + +Five sites outside `packages/spec` **read** the key before this landed, and +reading the retirement as "nothing referenced it" loses the finding. The +materialization fingerprint and the provider-context build in +`service-automation`, `ctx.connectionTimeoutMs` in the `rest` and `openapi` +provider factories, and the `?? 30000` fallbacks that deposited it back onto the +reported def. Measured across all five, every one is a **pass-through**: the +value's only termini were the def `GET /connectors` echoes and the fingerprint +that decides whether to re-materialize. `connectorFetchOptions()` — the one +mapping from authored policy onto the platform's outbound `fetch` — was handed +`{ retryConfig, requestTimeoutMs }` only. Carrying a number is not honouring it, +and ADR-0049 forbids the parsed-unmarked-unenforced state whether the inert +value travels or sits still. + +Nor was the `实现` arm available. A connector's outbound call is a WHATWG +`fetch`, whose only cancellation surface is ONE `AbortSignal` covering the whole +operation; nothing in that interface observes the connection phase. Bounding +"time until the response arrives" with this key would kill a slow-but-connected +upstream the author meant to allow with a large `requestTimeoutMs` — breaking +the very promise the key makes. (undici's `connectTimeout` needs a custom +dispatcher: Node-only, and a new subsystem underneath every connector, which the +ruling that made the siblings live forbids.) + +### The retirement kit + +- The **authorable key** is a `retiredKey()` tombstone on `ConnectorSchema`, + registered as `integration/Connector:connectionTimeoutMs` and + `integration/DeclarativeConnectorEntry:connectionTimeoutMs` in + `RETIRED_KEYS_BY_MAJOR[18]`. The schema is not `.strict()`, so a bare deletion + would strip an authored key in silence (ADR-0104): the tombstone is audible in + both channels — `tsc` (input type `never`) and the parse, which raises the + prescription itself. `DeclarativeConnectorEntrySchema` inherits it, so + `stack.connectors[]` and the `/meta/connector` door refuse it too. +- **A D2 conversion, `connector-connection-timeout-ms-removed`** — one strip per + `connectors[]` entry, a pure lossless delete. ⭐ The ruling left whether one was + owed to be **measured** ("a D2 conversion only if a stored connector row can + carry the key"). It can, and both legs were measured before the tombstone + landed: `getMetadataTypeSchema('connector')` — what `PUT /meta/connector/:name` + validates against — parsed a body carrying the key and its output **retained** + the authored value, so the number reached `sys_metadata`; and + `applyConversionsToStoredItem('connector', …)` is live for this type. Rows + written on 17.x therefore replay clean. +- **A D3 semantic entry, + `connector-provider-context-connection-timeout-ms-retired`**, for the withdrawn + `ConnectorProviderContext` member. A provider factory is code: there is no + authored source and no `sys_metadata` row for a conversion to rewrite, so the + removal reaches a factory author as a `tsc` error and as that entry. +- **No def leaves.** The key was a bare `z.number()`, never a `ConfigSchema` + shape, so `RETIRED_DEFS_BY_MAJOR[18]` gains nothing — and `api-surface/` and + `json-schema.manifest/` are byte-identical, which is the correct reading for a + key-only tombstone rather than a missed regeneration. +- `authorable-surface/integration.json` gains two `[RETIRED]` rows; + `authorable-defaults/integration.json` loses the two `= 30000` rows. +- The liveness row **stays** `dead` with a `REMOVED` note, because `retiredKey()` + keeps the key in the walked shape. Its previous note claimed "every occurrence + outside `packages/spec` is a WRITE"; that was already false and is corrected + there rather than carried forward. +- **No deprecation window** (maintainer 2026-08-27: 「项目在创业阶段,用户也很少,短期不考虑渐进」), + and no staged retirement. + +⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` +is published, so this is breaking for consumers no download, dependent or source +telemetry was consulted for. The pinned sibling checkout **was** measured: zero +occurrences of the name at objectui `87af769e`, against a lit control on the same +command and scope, so no sibling fix or pin bump rides with this. + +`Clause-②: yes (narrowing)` — a published authorable key is removed on two +carriers and a published interface member leaves `ConnectorProviderContext`, so +the accept set a consumer writes against narrows. Nothing is widened and nothing +is renamed. Contract-review tier. + + From d0db2dd8b871eb5ac774bf8a05b39f5f594e4936 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 04:57:43 +0000 Subject: [PATCH 06/15] test(spec): declare the retirement pin's walk radius on the repo project Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx --- packages/spec/vitest.repo-tests.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/spec/vitest.repo-tests.json b/packages/spec/vitest.repo-tests.json index 98e1689f9e6..59cf0def517 100644 --- a/packages/spec/vitest.repo-tests.json +++ b/packages/spec/vitest.repo-tests.json @@ -28,6 +28,7 @@ "src/api/error-catalog-docs.test.ts", "src/data/api-methods-batch-conformance.test.ts", "src/identity/position-delegatable-enforcer.pin.test.ts", + "src/integration/connector-connection-timeout-retirement.test.ts", "src/shared/retired-key-migrate-sentence.test.ts", "src/system/compliance-families-retirement.test.ts", "src/system/constants/platform-object-names.test.ts", From f8c23b8425f92194add22b8d05fb163c569b04d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 05:39:06 +0000 Subject: [PATCH 07/15] fix(spec): cite the live record for requestTimeoutMs instead of a deleted card number MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four #19388 citations this change added do not resolve: probed [deleted] — minted, absent from the board, and the web endpoint 404s. The claim they attributed is unchanged and independently checkable in the tree, so each site now names connector-fetch-policy.ts, where connectorFetchOptions() maps requestTimeoutMs onto resilientFetch's per-attempt timeoutMs, pinned by connector-fetch-policy.test.ts. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx --- packages/spec/src/conversions/registry.ts | 5 +++-- packages/spec/src/integration/connector.zod.ts | 7 +++++-- .../18.integration__Connector__connectionTimeoutMs.ts | 9 +++++++-- packages/spec/src/migrations/registry.ts | 9 +++++++-- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/spec/src/conversions/registry.ts b/packages/spec/src/conversions/registry.ts index e10b5ffe5b8..a5ff22e78de 100644 --- a/packages/spec/src/conversions/registry.ts +++ b/packages/spec/src/conversions/registry.ts @@ -8992,8 +8992,9 @@ const connectorErrorMappingRemoved: MetadataConversion = { * And it is not implementable where it was declared: a connector's outbound * call is a WHATWG `fetch`, whose only cancellation surface is one * `AbortSignal` over the whole operation, so nothing there observes the connect - * phase. `requestTimeoutMs` — live since #19388, the lit control for every - * reading above — is the bound the platform can keep. + * phase. `requestTimeoutMs` — the lit control for every reading above, live at + * `integration/connector-fetch-policy.ts` where it becomes `resilientFetch`'s + * per-attempt `timeoutMs` — is the bound the platform can keep. * * A pure lossless delete: the key never had an effect to preserve, so there is * no value to rewrite into anything. diff --git a/packages/spec/src/integration/connector.zod.ts b/packages/spec/src/integration/connector.zod.ts index 388c3c55f40..74c79bc9dc7 100644 --- a/packages/spec/src/integration/connector.zod.ts +++ b/packages/spec/src/integration/connector.zod.ts @@ -576,8 +576,11 @@ const ERROR_MAPPING_RETIRED = // // `requestTimeoutMs` is the replacement and the lit control for every reading // above — same schema, same census, same files — because it resolves to a real -// read (`opts.timeoutMs`, `resilientFetch`'s per-attempt deadline) since #19388. -// Bound the connect phase at a provider or gateway that can see it. +// read. The live record is in the tree, not in a card number: +// `integration/connector-fetch-policy.ts` maps it onto `opts.timeoutMs`, +// `resilientFetch`'s per-attempt deadline, and `connector-fetch-policy.test.ts` +// pins that mapping. Bound the connect phase at a provider or gateway that can +// see it. // // `ConnectorSchema` is NOT `.strict()`, so a plain delete would be a silent // strip (ADR-0104); the tombstone below makes the removal audible in the two diff --git a/packages/spec/src/migrations/entries/retired-keys/18.integration__Connector__connectionTimeoutMs.ts b/packages/spec/src/migrations/entries/retired-keys/18.integration__Connector__connectionTimeoutMs.ts index 12df87ef523..aa97a561fa7 100644 --- a/packages/spec/src/migrations/entries/retired-keys/18.integration__Connector__connectionTimeoutMs.ts +++ b/packages/spec/src/migrations/entries/retired-keys/18.integration__Connector__connectionTimeoutMs.ts @@ -30,8 +30,13 @@ // connected upstream the author meant to allow with a large `requestTimeoutMs` // — breaking the very promise the key makes. (undici's `connectTimeout` needs a // custom dispatcher: Node-only, and a new subsystem underneath every connector.) -// `requestTimeoutMs` — live since PR #19388, and the lit control for every -// census above — is the bound the platform can keep. +// `requestTimeoutMs` — the lit control for every census above — is the bound +// the platform can keep. Its live record is cited as the code rather than as a +// card number on purpose: the PR that made it live has been DELETED from the +// board (probed: minted, absent, and the web endpoint 404s), so the only +// durable anchor is `integration/connector-fetch-policy.ts`, where +// `connectorFetchOptions()` maps the key onto `resilientFetch`'s per-attempt +// `timeoutMs`, pinned by `connector-fetch-policy.test.ts`. // // Tombstoned with `retiredKey()`: `ConnectorSchema` is a non-strict `z.object`, // so a bare deletion would be a silent strip (ADR-0104). No def leaves with it — diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index be4ab59c95b..8303daeaec1 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -14235,8 +14235,13 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly> // connected upstream the author meant to allow with a large `requestTimeoutMs` // — breaking the very promise the key makes. (undici's `connectTimeout` needs a // custom dispatcher: Node-only, and a new subsystem underneath every connector.) - // `requestTimeoutMs` — live since PR #19388, and the lit control for every - // census above — is the bound the platform can keep. + // `requestTimeoutMs` — the lit control for every census above — is the bound + // the platform can keep. Its live record is cited as the code rather than as a + // card number on purpose: the PR that made it live has been DELETED from the + // board (probed: minted, absent, and the web endpoint 404s), so the only + // durable anchor is `integration/connector-fetch-policy.ts`, where + // `connectorFetchOptions()` maps the key onto `resilientFetch`'s per-attempt + // `timeoutMs`, pinned by `connector-fetch-policy.test.ts`. // // Tombstoned with `retiredKey()`: `ConnectorSchema` is a non-strict `z.object`, // so a bare deletion would be a silent strip (ADR-0104). No def leaves with it — From 6f14491ac021c8d68ca8f4c64328f4be594d77d2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 07:29:08 +0000 Subject: [PATCH 08/15] fix(spec): accept the retired connectionTimeoutMs default as residue; correct the census note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F1 — the key was `.optional().default(30000)`, so a 17.x parse materialized it into every connector. Measured across two builds: the base build emits it for an entry that authored only name/label/type, and the tombstoned build refused that exact object at connectors.0.connectionTimeoutMs. Adopts the ruled acceptRetiredDefaultResidue stage on both carriers. A D2 does not discharge this obligation — the ObjectPermission precedent carries both — because AutomationEngine.registerConnector parses ConnectorSchema for a def a plugin builds in code, where no conversion runs. Nothing is un-retired: z.input stays never, the [RETIRED] row stays, and any other value keeps the refusal. The residue wrapper is a preprocess pipe, so the ADR-0097 refinements move onto its OUT side; dropped-refinements.baseline.json moves the five site paths with them, as the gate required in the same change. F2 — the card's five-writes table was CORRECT at the SHA it cited (0870fb5418) and was superseded by b929e0a662. It is stale, not false, and the ledger note, the entry and the changeset now state both readings with their trees: thirteen non-test source occurrences over seven files in five packages at origin/main — six reads, four type declarations, three surviving hardcoded writes. N2 — the absence-pin pointer names the file the pin actually lives in. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx --- ...-retire-connector-connection-timeout-ms.md | 30 ++++++--- .../connector-rest/src/rest-provider.test.ts | 4 +- .../spec/dropped-refinements.baseline.json | 10 +-- packages/spec/liveness/connector.json | 2 +- ...ctor-connection-timeout-retirement.test.ts | 55 +++++++++++++++ .../spec/src/integration/connector.zod.ts | 67 +++++++++++++++++-- ...gration__Connector__connectionTimeoutMs.ts | 57 ++++++++++++++-- packages/spec/src/migrations/registry.ts | 57 ++++++++++++++-- 8 files changed, 248 insertions(+), 34 deletions(-) diff --git a/.changeset/19580-retire-connector-connection-timeout-ms.md b/.changeset/19580-retire-connector-connection-timeout-ms.md index 35ad06cf28b..40f0a2033cd 100644 --- a/.changeset/19580-retire-connector-connection-timeout-ms.md +++ b/.changeset/19580-retire-connector-connection-timeout-ms.md @@ -39,12 +39,13 @@ not. What does change is observable and intended: the def served by ### ⭐ This is NOT the zero-mention retirement shape -Five sites outside `packages/spec` **read** the key before this landed, and -reading the retirement as "nothing referenced it" loses the finding. The -materialization fingerprint and the provider-context build in -`service-automation`, `ctx.connectionTimeoutMs` in the `rest` and `openapi` -provider factories, and the `?? 30000` fallbacks that deposited it back onto the -reported def. Measured across all five, every one is a **pass-through**: the +Measured with `git grep -n connectionTimeoutMs SHA -- . ':!packages/spec'` at +`origin/main`: **thirteen** non-test source occurrences over seven files in five +packages — **six reads** (`openapi-connector.ts:242`, `openapi-provider.ts:193`, +`rest-connector.ts:134`, `rest-provider.ts:64`, `plugin.ts:307`, +`plugin.ts:1589`), **four type declarations**, and **three** surviving hardcoded +`30000` writes. Reading the retirement as "nothing referenced it" loses the +finding. Measured across all six reads, every one is a **pass-through**: the value's only termini were the def `GET /connectors` echoes and the fingerprint that decides whether to re-materialize. `connectorFetchOptions()` — the one mapping from authored policy onto the platform's outbound `fetch` — was handed @@ -93,8 +94,21 @@ ruling that made the siblings live forbids.) `authorable-defaults/integration.json` loses the two `= 30000` rows. - The liveness row **stays** `dead` with a `REMOVED` note, because `retiredKey()` keeps the key in the walked shape. Its previous note claimed "every occurrence - outside `packages/spec` is a WRITE"; that was already false and is corrected - there rather than carried forward. + outside `packages/spec` is a WRITE". That reading was **correct at the SHA the + card cited and dated** (`0870fb5418` — exactly five non-spec source hits, all + five `connectionTimeoutMs: 30000,`) and was superseded by `b929e0a662`, the PR + the card itself flagged as pending. It is **stale, not false**, and the row now + carries both readings with their trees rather than one undated claim. +- **An `acceptRetiredDefaultResidue` stage** (#12840), `{ connectionTimeoutMs: 30000 }` + on both carriers. The key was `.optional().default(30000)`, so a 17.x parse + materialized it into **every** connector — measured across two builds: the base + build emits it for an entry that authored only `name`/`label`/`type`, and the + tombstoned build refuses that exact object at `connectors.0.connectionTimeoutMs`. + The D2 does **not** discharge this: `ObjectPermission:allowPurge` carries both, + because `AutomationEngine.registerConnector` parses `ConnectorSchema` for a def + a plugin builds **in code**, where no conversion runs. So the emitted `30000` + is accepted-and-stripped while `15000` keeps the tombstone's refusal, and + nothing is un-retired: `z.input` stays `never` and the `[RETIRED]` row stays. - **No deprecation window** (maintainer 2026-08-27: 「项目在创业阶段,用户也很少,短期不考虑渐进」), and no staged retirement. diff --git a/packages/connectors/connector-rest/src/rest-provider.test.ts b/packages/connectors/connector-rest/src/rest-provider.test.ts index ae1605f73f3..296109f7201 100644 --- a/packages/connectors/connector-rest/src/rest-provider.test.ts +++ b/packages/connectors/connector-rest/src/rest-provider.test.ts @@ -240,8 +240,8 @@ describe('rest provider factory (ADR-0097)', () => { // `connectionTimeoutMs` was the second half of this pin and is // RETIRED (ADR-0049): the factory never applied it, it only echoed // it back onto the def. The absence pin lives tree-scoped in - // `packages/spec/src/integration/connector.test.ts`; here the point - // is only that the surviving timeout still travels. + // `packages/spec/src/integration/connector-connection-timeout-retirement.test.ts`; + // here the point is only that the surviving timeout still travels. expect(Object.keys(def)).not.toContain('connectionTimeoutMs'); }); }); diff --git a/packages/spec/dropped-refinements.baseline.json b/packages/spec/dropped-refinements.baseline.json index 38c06eb037d..a8bedaedd3d 100644 --- a/packages/spec/dropped-refinements.baseline.json +++ b/packages/spec/dropped-refinements.baseline.json @@ -52,7 +52,7 @@ "sites": [ "manifest.actions.element.in", "manifest.actions.element.in.params.element.in", - "manifest.connectors.element", + "manifest.connectors.element.out", "manifest.dashboards.element.globalFilters.element", "manifest.dashboards.element.widgets.element", "manifest.datasets.element", @@ -149,7 +149,7 @@ "data.options[0].manifest.navigationContributions.element.items.element.lazy.options[0]", "data.options[1].manifest.actions.element.in", "data.options[1].manifest.actions.element.in.params.element.in", - "data.options[1].manifest.connectors.element", + "data.options[1].manifest.connectors.element.out", "data.options[1].manifest.dashboards.element.globalFilters.element", "data.options[1].manifest.dashboards.element.widgets.element", "data.options[1].manifest.datasets.element", @@ -230,7 +230,7 @@ "options[0].manifest.navigationContributions.element.items.element.lazy.options[0]", "options[1].manifest.actions.element.in", "options[1].manifest.actions.element.in.params.element.in", - "options[1].manifest.connectors.element", + "options[1].manifest.connectors.element.out", "options[1].manifest.dashboards.element.globalFilters.element", "options[1].manifest.dashboards.element.widgets.element", "options[1].manifest.datasets.element", @@ -272,7 +272,7 @@ "data.packages.element.options[0].manifest.navigationContributions.element.items.element.lazy.options[0]", "data.packages.element.options[1].manifest.actions.element.in", "data.packages.element.options[1].manifest.actions.element.in.params.element.in", - "data.packages.element.options[1].manifest.connectors.element", + "data.packages.element.options[1].manifest.connectors.element.out", "data.packages.element.options[1].manifest.dashboards.element.globalFilters.element", "data.packages.element.options[1].manifest.dashboards.element.widgets.element", "data.packages.element.options[1].manifest.datasets.element", @@ -806,7 +806,7 @@ }, "integration/DeclarativeConnectorEntry": { "sites": [ - "" + "out" ] }, "kernel/DisablePackageResponse": { diff --git a/packages/spec/liveness/connector.json b/packages/spec/liveness/connector.json index 5612884c517..dd5261e0c48 100644 --- a/packages/spec/liveness/connector.json +++ b/packages/spec/liveness/connector.json @@ -301,7 +301,7 @@ "connectionTimeoutMs": { "status": "dead", "verifiedAt": "2026-09-22", - "note": "RETIRED (ADR-0049 enforce-or-remove; maintainer ruling 2026-09-22, letter A — the narrower SECOND decision this row asked for). Tombstoned with `retiredKey` because `ConnectorSchema` is not `.strict()` and a plain delete would be a silent strip (ADR-0104); the tombstone is inherited by `DeclarativeConnectorEntrySchema`, so `stack.connectors[]` and `/meta/connector` refuse it too. The row stays because `retiredKey` keeps the key in the walked shape (the `rls.priority` precedent). Registered as `integration/Connector:connectionTimeoutMs` and `integration/DeclarativeConnectorEntry:connectionTimeoutMs` in `RETIRED_KEYS_BY_MAJOR[18]`; authored sources and stored rows are rewritten by the D2 conversion `connector-connection-timeout-ms-removed`, and the withdrawn `ConnectorProviderContext.connectionTimeoutMs` — code, with no authored source to rewrite — by the D3 semantic entry `connector-provider-context-connection-timeout-ms-retired`. The tombstone is packages/spec/src/integration/connector.zod.ts#ConnectorSchema. ⛔ THE PRIOR NOTE’S CENSUS CLAIM IS CORRECTED HERE, not carried forward: it said every occurrence outside packages/spec is a WRITE, and that was already false when this retirement was taken. Five sites outside packages/spec READ the key — the materialization fingerprint (packages/services/service-automation/src/plugin.ts#connectorMaterializationFingerprint) and the provider-context build in the same file, `ctx.connectionTimeoutMs` in packages/connectors/connector-rest/src/rest-provider.ts and packages/connectors/connector-openapi/src/openapi-provider.ts, and the `?? 30000` fallbacks in the two `create*Connector` factories. What made the key `dead` was never the absence of readers but the absence of ENFORCEMENT: every one of those five is a pass-through whose only termini are the def `GET /connectors` echoes and the fingerprint that decides whether to re-materialize, and `connectorFetchOptions` — the one mapping onto the platform’s outbound `fetch` — was handed `{ retryConfig, requestTimeoutMs }` only. ⇒ a future census on this type counts READS and asks what each one DOES with the value; a grep count answers neither question. Use `requestTimeoutMs` (live, the row below) for the deadline the platform keeps, and bound the connect phase at a connector provider or upstream gateway on a transport that can separate the phases." + "note": "RETIRED (ADR-0049 enforce-or-remove; maintainer ruling 2026-09-22, letter A — the narrower SECOND decision this row asked for). Tombstoned with `retiredKey` because `ConnectorSchema` is not `.strict()` and a plain delete would be a silent strip (ADR-0104); the tombstone is inherited by `DeclarativeConnectorEntrySchema`, so `stack.connectors[]` and `/meta/connector` refuse it too. The row stays because `retiredKey` keeps the key in the walked shape (the `rls.priority` precedent). Registered as `integration/Connector:connectionTimeoutMs` and `integration/DeclarativeConnectorEntry:connectionTimeoutMs` in `RETIRED_KEYS_BY_MAJOR[18]`; authored sources and stored rows are rewritten by the D2 conversion `connector-connection-timeout-ms-removed`, and the withdrawn `ConnectorProviderContext.connectionTimeoutMs` — code, with no authored source to rewrite — by the D3 semantic entry `connector-provider-context-connection-timeout-ms-retired`. The tombstone is packages/spec/src/integration/connector.zod.ts#ConnectorSchema. ⭐ THE CARD’S CENSUS IS STALE, NOT FALSE — and the earlier draft of this note got that wrong, so it is restated as measured. The card’s Leg 2 said every occurrence outside packages/spec is a WRITE of a hardcoded 30000. Re-measured with `git grep -n connectionTimeoutMs SHA -- . ':!packages/spec'` at three trees: at the card’s OWN cited SHA 0870fb5418 that is EXACTLY RIGHT — five non-spec source hits, all five `connectionTimeoutMs: 30000,` (connector-mcp mcp-connector.ts:247, connector-openapi openapi-connector.ts:220, connector-rest rest-connector.ts:113, connector-slack slack-connector.ts:94, service-automation plugin.ts:1734), the card’s table line for line. What superseded it is b929e0a662, the very PR the card flagged as pending. At `origin/main` the same instrument returns THIRTEEN non-test source occurrences over SEVEN files in FIVE packages: six READS (openapi-connector.ts:242, openapi-provider.ts:193, rest-connector.ts:134, rest-provider.ts:64, plugin.ts:307, plugin.ts:1589), four TYPE DECLARATIONS (openapi-connector.ts:135, rest-connector.ts:47, plugin.ts:291, plugin.ts:339), and THREE surviving hardcoded 30000 writes (mcp-connector.ts:247, slack-connector.ts:94, plugin.ts:1782). ⇒ read a census with the tree it was taken against, or it is not a census. What made the key `dead` was never the absence of readers but the absence of ENFORCEMENT: every one of those six reads is a pass-through whose only termini are the def `GET /connectors` echoes and the fingerprint that decides whether to re-materialize, and `connectorFetchOptions` — the one mapping onto the platform’s outbound `fetch` — was handed `{ retryConfig, requestTimeoutMs }` only. ⇒ a future census on this type counts READS and asks what each one DOES with the value; a grep count answers neither question. Use `requestTimeoutMs` (live, the row below) for the deadline the platform keeps, and bound the connect phase at a connector provider or upstream gateway on a transport that can separate the phases." }, "requestTimeoutMs": { "status": "live", diff --git a/packages/spec/src/integration/connector-connection-timeout-retirement.test.ts b/packages/spec/src/integration/connector-connection-timeout-retirement.test.ts index b1b5b877c68..287e3debb31 100644 --- a/packages/spec/src/integration/connector-connection-timeout-retirement.test.ts +++ b/packages/spec/src/integration/connector-connection-timeout-retirement.test.ts @@ -139,6 +139,61 @@ describe('connector.connectionTimeoutMs retirement — the tombstone', () => { expect(parsed).not.toHaveProperty('connectionTimeoutMs'); }); + it('accepts and STRIPS the retired default as inert residue, on both carriers', () => { + // #12840, maintainer ruling 2026-08-28. The key was + // `.optional().default(30000)`, so a released 17.x toolchain materialized + // `30000` into EVERY connector — authored or not — and the second door + // (`AutomationEngine.registerConnector`, which parses `ConnectorSchema` for + // a def a plugin builds IN CODE) runs no conversion that could strip it. + // So the emitted default parses as inert residue and is stripped; the + // normalized output does not carry the key, so a parse then serialize + // round-trip converges on the clean shape. + for (const [label, schema] of [ + ['base', ConnectorSchema], + ['the /meta + stack.connectors carrier', DeclarativeConnectorEntrySchema], + ] as const) { + const r = schema.safeParse({ ...WELL_FORMED, connectionTimeoutMs: 30000 }); + expect(r.success, `${label} must accept the retired default as residue`).toBe(true); + if (!r.success) continue; + expect(r.data, `${label} must STRIP it, not carry it`).not.toHaveProperty('connectionTimeoutMs'); + // CONTROL: the live sibling on the same shape is untouched by the stage. + expect(r.data.requestTimeoutMs, `${label} keeps the live sibling`).toBe(30000); + } + }); + + it('⛔ keeps the tombstone refusal for every value that is NOT the retired default', () => { + // The whole point of discriminating by value: nothing is un-retired. A + // number an author actually chose still meets the prescription. + for (const value of [15000, 1000, 300000, 29999]) { + const r = ConnectorSchema.safeParse({ ...WELL_FORMED, connectionTimeoutMs: value }); + expect(r.success, `${value} must still be refused`).toBe(false); + if (r.success) continue; + const issue = r.error.issues.find((i) => i.path[0] === 'connectionTimeoutMs'); + expect(issue, `${value} must be refused AT the key`).toBeDefined(); + expect(issue!.message, `${value} must carry the prescription`).toMatch(PRESCRIPTION); + } + // And the residue tolerance is value-identity, not type-loose: the string + // "30000" is not the emitted default. + expect(ConnectorSchema.safeParse({ ...WELL_FORMED, connectionTimeoutMs: '30000' }).success).toBe(false); + }); + + it('the residue stage leaves the walked shape intact — the pipe reads through to the base', () => { + // `acceptRetiredDefaultResidue` returns a preprocess PIPE, not a ZodObject. + // The authorable-surface and liveness walkers duck-test `.shape`, so a + // wrapper that lost it would silently drop this whole def from both + // ratchets while every parse pin above stayed green. + for (const [label, schema] of [ + ['base', ConnectorSchema], + ['entry', DeclarativeConnectorEntrySchema], + ] as const) { + const shape = (schema as unknown as { shape?: Record }).shape; + expect(shape, `${label} must expose a read-through shape`).toBeDefined(); + expect(Object.keys(shape!), `${label} keeps the retired key in the walked shape`) + .toContain('connectionTimeoutMs'); + expect(Object.keys(shape!), `${label} keeps its live neighbours`).toContain('requestTimeoutMs'); + } + }); + it('fails tsc at the authoring site: the input type of the key is `never`', () => { const connector: Connector = { ...WELL_FORMED, diff --git a/packages/spec/src/integration/connector.zod.ts b/packages/spec/src/integration/connector.zod.ts index 74c79bc9dc7..8b79e69db47 100644 --- a/packages/spec/src/integration/connector.zod.ts +++ b/packages/spec/src/integration/connector.zod.ts @@ -5,7 +5,7 @@ import { WebhookSchema } from '../automation/webhook.zod'; import { ConnectorAuthConfigSchema, ConnectorInstanceAuthSchema } from '../shared/connector-auth.zod'; import { FieldMappingSchema as BaseFieldMappingSchema } from '../shared/mapping.zod'; import { MetadataProtectionFields } from '../kernel/metadata-protection.zod'; -import { retiredKey } from '../shared/retired-key'; +import { acceptRetiredDefaultResidue, retiredKey } from '../shared/retired-key'; /** * Connector Protocol - LEVEL 3: Enterprise Connector @@ -614,6 +614,42 @@ const CONNECTION_TIMEOUT_MS_RETIRED = + 'provider or upstream gateway on a transport that can separate the phases. ' + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; +/** + * The retired default the published 17.x toolchain MATERIALIZED, captured as a + * literal because nothing else records it once the declaration is gone + * (#12840, maintainer ruling 2026-08-28; the class rule is + * `shared/retired-key.ts` — "the next retirement of a defaulted key reuses this + * helper with its own captured literal instead of re-inventing the judgement"). + * + * ⭐ WHY THIS RETIREMENT IS OWED THE STAGE, measured rather than argued by + * analogy. `connectionTimeoutMs` was declared `.optional().default(30000)`, so a + * 17.x parse emitted it into EVERY connector — authored or not. Measured across + * two builds: on the base build `ObjectStackSchema.parse({ connectors: [{ name, + * label, type }] })` returns an entry whose keys are `authentication`, + * `connectionTimeoutMs`, `enabled`, `label`, `name`, `requestTimeoutMs`, + * `status`, `type` — the author typed three of those. Feed that exact emitted + * object back to the tombstoned build and it is refused at + * `connectors.0.connectionTimeoutMs`. + * + * That residue is not hypothetical, because this schema has TWO DOORS (the fact + * `liveness/connector.json` records at the top of its `_note`): besides the + * authoring doors, `AutomationEngine.registerConnector` parses `ConnectorSchema` + * for a def a PLUGIN or an ADR-0097 provider factory builds IN CODE. A connector + * package still compiled against 17.x carries the materialized `30000` in that + * def literal — every one of the four shipped connectors did, which is what the + * card counted as its five hardcoded writes — so without this stage a 17.x + * plugin fails registration on a value its author never typed. + * + * ⛔ Nothing is un-retired: `z.input` stays `never` (authoring it is still a tsc + * error), the `[RETIRED]` authorable-surface row stays, and any OTHER value — + * `15000`, `1000` — keeps the tombstone's refusal with the prescription + * byte-for-byte. Only the emitted default is accepted, and it is STRIPPED, so a + * parse → serialize round-trip converges on the clean shape. + */ +const CONNECTOR_RETIRED_KEY_RESIDUE = { + connectionTimeoutMs: 30000, +} as const; + // ============================================================================ // Health Check & Circuit Breaker Configuration // ============================================================================ @@ -815,7 +851,7 @@ export type ConnectorTrigger = z.input; * Base Connector Schema * Core connector configuration shared across all connector types */ -export const ConnectorSchema = lazySchema(() => z.object({ +const ConnectorBaseSchema = lazySchema(() => z.object({ /** * Machine name (snake_case) */ @@ -1032,6 +1068,21 @@ export const ConnectorSchema = lazySchema(() => z.object({ ...MetadataProtectionFields, })); +/** + * Core connector configuration — the authorable shape behind the ruled + * retired-default residue stage (see {@link CONNECTOR_RETIRED_KEY_RESIDUE}). + * + * The wrapper is a `z.preprocess` PIPE, not a `ZodObject`: it keeps a + * read-through `shape` so the schema walkers and shape-reading consumers see + * the inner authorable truth, but ⛔ it cannot be `.extend()`ed or + * `.superRefine()`d directly — do that on `ConnectorBaseSchema` and re-wrap, + * the way `DeclarativeConnectorEntrySchema` below does (the + * `EffectiveObjectPermissionSchema` precedent). + */ +export const ConnectorSchema = lazySchema(() => + acceptRetiredDefaultResidue(ConnectorBaseSchema, CONNECTOR_RETIRED_KEY_RESIDUE), +); + export type Connector = z.input; /** Post-parse shape of {@link Connector} — defaults applied, transforms run (ADR-0122). */ export type ConnectorParsed = z.infer; @@ -1069,7 +1120,15 @@ export function defineConnector(config: z.input): Connec * authoring both the instance and its actions reintroduces drift (§5 non-goals). */ export const DeclarativeConnectorEntrySchema = lazySchema(() => - ConnectorSchema.superRefine((entry, ctx) => { + // [#12840 precedent] The ADR-0097 refusals ride on the BASE, INSIDE the + // residue stage, for the reason `ObjectPermissionSchema` records: in zod 4 + // `.superRefine()` on a `ZodObject` returns a `ZodObject` that keeps + // `.shape`, while the same call on the residue PIPE returns a schema with no + // `.shape` — and the pipe's read-through `shape` is exactly what the + // authorable-surface / liveness walkers duck-test. Wrapping second also keeps + // this door's residue tolerance identical to the base's rather than a second + // dialect. + acceptRetiredDefaultResidue(ConnectorBaseSchema.superRefine((entry, ctx) => { const isInstance = typeof entry.provider === 'string' && entry.provider.length > 0; // #7990 — the one rule that binds EVERY authored entry, descriptor and // instance alike: `authentication` is the RUNTIME shape (its secret fields @@ -1116,7 +1175,7 @@ export const DeclarativeConnectorEntrySchema = lazySchema(() => message: `Provider-bound connector instance '${entry.name}' must not author \`triggers\` — the '${entry.provider}' provider derives them from the upstream at boot (ADR-0097 §5).`, }); } - }), + }), CONNECTOR_RETIRED_KEY_RESIDUE), ); export type DeclarativeConnectorEntry = z.input; diff --git a/packages/spec/src/migrations/entries/retired-keys/18.integration__Connector__connectionTimeoutMs.ts b/packages/spec/src/migrations/entries/retired-keys/18.integration__Connector__connectionTimeoutMs.ts index aa97a561fa7..40db7dc3b80 100644 --- a/packages/spec/src/migrations/entries/retired-keys/18.integration__Connector__connectionTimeoutMs.ts +++ b/packages/spec/src/migrations/entries/retired-keys/18.integration__Connector__connectionTimeoutMs.ts @@ -9,13 +9,25 @@ // said it worked. // // ⚠️ This is NOT the zero-mention retirement shape, and reading it as one loses -// the finding. FIVE sites outside `packages/spec` READ the key: the -// materialization fingerprint and the provider-context build in -// `services/service-automation/src/plugin.ts`, `ctx.connectionTimeoutMs` in the -// `rest` and `openapi` provider factories, and the `?? 30000` fallbacks that -// deposit it back onto the reported def. Measured across all five, every one is -// a pass-through: the value's only termini are the def `GET /connectors` echoes -// and the fingerprint that decides whether to re-materialize. Never a deadline. +// the finding. Measured with `git grep -n connectionTimeoutMs SHA -- . +// ':!packages/spec'` at `origin/main`: THIRTEEN non-test source occurrences over +// seven files in five packages — SIX READS (openapi-connector.ts:242, +// openapi-provider.ts:193, rest-connector.ts:134, rest-provider.ts:64, +// plugin.ts:307, plugin.ts:1589), FOUR TYPE DECLARATIONS +// (openapi-connector.ts:135, rest-connector.ts:47, plugin.ts:291, +// plugin.ts:339), and THREE surviving hardcoded `30000` writes +// (mcp-connector.ts:247, slack-connector.ts:94, plugin.ts:1782). +// +// ⭐ The card's own Leg-2 table — "five non-spec mentions, all writes of a +// hardcoded 30000" — was CORRECT at the SHA it cited and dated (0870fb5418: +// exactly those five, line for line). It is STALE, not false; b929e0a662, the +// PR the card itself flagged as pending, is what moved it. ⛔ Do not re-cite +// either reading without its tree: a census is a count plus the commit it was +// taken against. +// +// Measured across all six reads, every one is a pass-through: the value's only +// termini are the def `GET /connectors` echoes and the fingerprint that decides +// whether to re-materialize. Never a deadline. // `connectorFetchOptions()` (`integration/connector-fetch-policy.ts`) is the one // mapping from authored policy onto the platform's outbound `fetch`, and it was // handed `{ retryConfig, requestTimeoutMs }` only. Carrying a number is not @@ -47,6 +59,37 @@ // source, leaves via the D3 semantic entry // `connector-provider-context-connection-timeout-ms-retired`. // +// ⭐ RETIRED-DEFAULT RESIDUE: the `acceptRetiredDefaultResidue` stage IS owed +// here, and is adopted — `{ connectionTimeoutMs: 30000 }` on both carriers +// (#12840, maintainer ruling 2026-08-28; the class rule lives in +// `shared/retired-key.ts`). The discriminator across the three siblings is +// whether a released toolchain MATERIALIZED the default into something that is +// later re-parsed — `security/ObjectPermission:allowPurge` adopted the stage +// for exactly that reason, `kernel/PluginQualityMetrics:securityScan` did not +// because the key carried no default of its own, and +// `api/ListInstalledPackagesRequest:limit` did not because nothing ever parses +// through that schema so its `.default(50)` was never materialized anywhere. +// +// This key is in the FIRST bucket, measured across two builds rather than +// argued: on the base build `ObjectStackSchema.parse({ connectors: [{ name, +// label, type }] })` returns an entry carrying `connectionTimeoutMs: 30000` +// (emitted keys: authentication, connectionTimeoutMs, enabled, label, name, +// requestTimeoutMs, status, type — the author typed three), and feeding that +// exact object back to the tombstoned build is refused at +// `connectors.0.connectionTimeoutMs`. +// +// ⛔ The presence of a D2 conversion does NOT discharge this obligation, and +// reading it that way is the trap: `security/ObjectPermission:allowPurge` has +// BOTH a D2 (`permission-allow-restore-purge-removed`, in the same step-18 +// chain) AND the residue stage. The reason is the SECOND door — the fact +// `liveness/connector.json` opens its `_note` with: besides the authoring +// doors, `AutomationEngine.registerConnector` parses `ConnectorSchema` for a def +// a PLUGIN or an ADR-0097 provider factory builds IN CODE. No conversion runs +// there. A connector package still compiled against 17.x carries the +// materialized `30000` in that def literal — all four shipped connectors did, +// which is what the card counted as its hardcoded writes — so without this +// stage a 17.x plugin fails registration on a value its author never typed. +// // Registered under 18, not 17: v17.0.0 was cut before this landed, so the // removal ships on the 17.x line (launch-window convention: accept-set // narrowings ride minor releases) and the prescription lives at the major diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index 8303daeaec1..e412468918a 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -14214,13 +14214,25 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly> // said it worked. // // ⚠️ This is NOT the zero-mention retirement shape, and reading it as one loses - // the finding. FIVE sites outside `packages/spec` READ the key: the - // materialization fingerprint and the provider-context build in - // `services/service-automation/src/plugin.ts`, `ctx.connectionTimeoutMs` in the - // `rest` and `openapi` provider factories, and the `?? 30000` fallbacks that - // deposit it back onto the reported def. Measured across all five, every one is - // a pass-through: the value's only termini are the def `GET /connectors` echoes - // and the fingerprint that decides whether to re-materialize. Never a deadline. + // the finding. Measured with `git grep -n connectionTimeoutMs SHA -- . + // ':!packages/spec'` at `origin/main`: THIRTEEN non-test source occurrences over + // seven files in five packages — SIX READS (openapi-connector.ts:242, + // openapi-provider.ts:193, rest-connector.ts:134, rest-provider.ts:64, + // plugin.ts:307, plugin.ts:1589), FOUR TYPE DECLARATIONS + // (openapi-connector.ts:135, rest-connector.ts:47, plugin.ts:291, + // plugin.ts:339), and THREE surviving hardcoded `30000` writes + // (mcp-connector.ts:247, slack-connector.ts:94, plugin.ts:1782). + // + // ⭐ The card's own Leg-2 table — "five non-spec mentions, all writes of a + // hardcoded 30000" — was CORRECT at the SHA it cited and dated (0870fb5418: + // exactly those five, line for line). It is STALE, not false; b929e0a662, the + // PR the card itself flagged as pending, is what moved it. ⛔ Do not re-cite + // either reading without its tree: a census is a count plus the commit it was + // taken against. + // + // Measured across all six reads, every one is a pass-through: the value's only + // termini are the def `GET /connectors` echoes and the fingerprint that decides + // whether to re-materialize. Never a deadline. // `connectorFetchOptions()` (`integration/connector-fetch-policy.ts`) is the one // mapping from authored policy onto the platform's outbound `fetch`, and it was // handed `{ retryConfig, requestTimeoutMs }` only. Carrying a number is not @@ -14252,6 +14264,37 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly> // source, leaves via the D3 semantic entry // `connector-provider-context-connection-timeout-ms-retired`. // + // ⭐ RETIRED-DEFAULT RESIDUE: the `acceptRetiredDefaultResidue` stage IS owed + // here, and is adopted — `{ connectionTimeoutMs: 30000 }` on both carriers + // (#12840, maintainer ruling 2026-08-28; the class rule lives in + // `shared/retired-key.ts`). The discriminator across the three siblings is + // whether a released toolchain MATERIALIZED the default into something that is + // later re-parsed — `security/ObjectPermission:allowPurge` adopted the stage + // for exactly that reason, `kernel/PluginQualityMetrics:securityScan` did not + // because the key carried no default of its own, and + // `api/ListInstalledPackagesRequest:limit` did not because nothing ever parses + // through that schema so its `.default(50)` was never materialized anywhere. + // + // This key is in the FIRST bucket, measured across two builds rather than + // argued: on the base build `ObjectStackSchema.parse({ connectors: [{ name, + // label, type }] })` returns an entry carrying `connectionTimeoutMs: 30000` + // (emitted keys: authentication, connectionTimeoutMs, enabled, label, name, + // requestTimeoutMs, status, type — the author typed three), and feeding that + // exact object back to the tombstoned build is refused at + // `connectors.0.connectionTimeoutMs`. + // + // ⛔ The presence of a D2 conversion does NOT discharge this obligation, and + // reading it that way is the trap: `security/ObjectPermission:allowPurge` has + // BOTH a D2 (`permission-allow-restore-purge-removed`, in the same step-18 + // chain) AND the residue stage. The reason is the SECOND door — the fact + // `liveness/connector.json` opens its `_note` with: besides the authoring + // doors, `AutomationEngine.registerConnector` parses `ConnectorSchema` for a def + // a PLUGIN or an ADR-0097 provider factory builds IN CODE. No conversion runs + // there. A connector package still compiled against 17.x carries the + // materialized `30000` in that def literal — all four shipped connectors did, + // which is what the card counted as its hardcoded writes — so without this + // stage a 17.x plugin fails registration on a value its author never typed. + // // Registered under 18, not 17: v17.0.0 was cut before this landed, so the // removal ships on the 17.x line (launch-window convention: accept-set // narrowings ride minor releases) and the prescription lives at the major From 4ca39575ed234beddf5261eb5aa9a3d9909e30a2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 08:40:28 +0000 Subject: [PATCH 09/15] docs(spec): retire two false claims the residue pipe left behind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FB1 — connector.zod.ts still promised that the base export 'stays a plain object so connector subtypes can still .extend() it'. Both published carriers are z.preprocess pipes since the residue stage, so that is false, and the sibling repo quotes the sentence verbatim in its own code. Measured on the built entry against a plain-object control (WebhookConfigSchema, which keeps all nine): .extend/.omit/.pick/.partial/.merge/.strict/.keyof/.safeExtend are gone from both. .superRefine SURVIVES — it lives on zod's base type — but returns a schema with no read-through shape, so my own new docblock overstated it and is corrected too. The affordance withdrawal is now a FROM to TO row in the changeset with the extend-the-base-and-re-wrap remedy. FB2 — five records asserted a composition this change abolished: the two carriers no longer derive from one another, they are siblings wrapping one private ConnectorBaseSchema. Three of them I authored in the round that fixed the same defect class. Corrected in the two retired-key entries, the two conversion docblocks, the schema docblock, the reachability comment in connector.test.ts and the liveness _note, whose walk mechanism is restated and whose conclusion is re-measured: 30 keys on each carrier, byte-identical key sets, no entry-only and no base-only key. NB5 — the changeset said allowPurge carries both 'because' registerConnector, compressing two different reasons into one. Separated. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx --- ...-retire-connector-connection-timeout-ms.md | 12 +++-- packages/spec/liveness/connector.json | 2 +- packages/spec/src/conversions/registry.ts | 6 +-- .../spec/src/integration/connector.test.ts | 11 +++-- .../spec/src/integration/connector.zod.ts | 44 ++++++++++++++++--- ...tiveConnectorEntry__connectionTimeoutMs.ts | 9 +++- ...DeclarativeConnectorEntry__errorMapping.ts | 9 +++- packages/spec/src/migrations/registry.ts | 18 ++++++-- 8 files changed, 86 insertions(+), 25 deletions(-) diff --git a/.changeset/19580-retire-connector-connection-timeout-ms.md b/.changeset/19580-retire-connector-connection-timeout-ms.md index 40f0a2033cd..7a7508de185 100644 --- a/.changeset/19580-retire-connector-connection-timeout-ms.md +++ b/.changeset/19580-retire-connector-connection-timeout-ms.md @@ -26,6 +26,7 @@ The key was bounded (`min(1000).max(300000)`), defaulted (`30000`), | --- | --- | | `connector.connectionTimeoutMs` (on `Connector` and on `DeclarativeConnectorEntry`, so `stack.connectors[]` and `PUT /meta/connector/:name`) | `requestTimeoutMs` — the deadline the platform keeps, applied as `resilientFetch`'s per-attempt timeout. For a connect-only bound, configure it at a connector provider or upstream gateway on a transport that can separate the phases. | | `ConnectorProviderContext.connectionTimeoutMs` (handed to every `ConnectorProviderFactory`) | `ctx.requestTimeoutMs`, or the factory's own `providerConfig` where the provider owns the vocabulary. | +| The `ZodObject` combinators on `ConnectorSchema` and `DeclarativeConnectorEntrySchema` — `.extend()`, `.omit()`, `.pick()`, `.partial()`, `.merge()`, `.strict()`, `.keyof()`, `.safeExtend()` | Both exports are now `z.preprocess` **pipes** (the residue stage below), so those methods no longer exist on them. **Build on the object and re-wrap:** `acceptRetiredDefaultResidue(, { connectionTimeoutMs: 30000 })`, the `EffectiveObjectPermissionSchema` route. ⚠️ `.superRefine()` still *exists* on a pipe but returns a schema with no read-through `shape`, so refine before wrapping, not after. Parsing, `z.input` / `z.infer`, and the read-through `.shape` are unchanged. | **The one-line fix: delete the key** — and, for a custom provider factory, stop reading `ctx.connectionTimeoutMs`. `os migrate meta --from 17` lists the @@ -104,9 +105,14 @@ ruling that made the siblings live forbids.) materialized it into **every** connector — measured across two builds: the base build emits it for an entry that authored only `name`/`label`/`type`, and the tombstoned build refuses that exact object at `connectors.0.connectionTimeoutMs`. - The D2 does **not** discharge this: `ObjectPermission:allowPurge` carries both, - because `AutomationEngine.registerConnector` parses `ConnectorSchema` for a def - a plugin builds **in code**, where no conversion runs. So the emitted `30000` + The D2 does **not** discharge the obligation, and the precedent shows it: + `ObjectPermission:allowPurge` carries a D2 **and** the residue stage, for its + own reason (a released toolchain materialized its default into every built + artifact's entries). The reason *here* is a different one — this schema has a + second door: `AutomationEngine.registerConnector` parses `ConnectorSchema` for + a def a plugin or provider factory builds **in code**, where no conversion + ever runs, and all four shipped connector packages put the materialized value + straight into that def literal. So the emitted `30000` is accepted-and-stripped while `15000` keeps the tombstone's refusal, and nothing is un-retired: `z.input` stays `never` and the `[RETIRED]` row stays. - **No deprecation window** (maintainer 2026-08-27: 「项目在创业阶段,用户也很少,短期不考虑渐进」), diff --git a/packages/spec/liveness/connector.json b/packages/spec/liveness/connector.json index dd5261e0c48..1d6c856b9e9 100644 --- a/packages/spec/liveness/connector.json +++ b/packages/spec/liveness/connector.json @@ -1,6 +1,6 @@ { "type": "connector", - "_note": "DeclarativeConnectorEntrySchema (packages/spec/src/integration/connector.zod.ts). Seeded 2026-09-17 (#18582) together with `analytics_cube`: the last two of the three PENDING_GOVERNANCE debts #18133 declared when PR #18581 widened the governance denominator to `authorableTypes()` (`sharing_rule` was paid first, PR #18587). Their landing empties that map. NOT a registered metadata KIND — bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reached through `getMetadataTypeSchema`'s unregistered-kind fallback. WHAT THE WALKER ACTUALLY RESOLVES, measured rather than assumed: the binding names `DeclarativeConnectorEntrySchema`, and that schema is `ConnectorSchema.superRefine(...)`. In Zod 4 a `superRefine` attaches a CHECK to the same object def rather than wrapping it, so `shapeOf()` returns `ConnectorSchema`'s shape unchanged and the key set walked here is byte-identical to the base's — the ADR-0097 cross-field rules add no key and remove none. The gate therefore cannot tell the two schemas apart; what the binding buys is REFUSALS, which are invisible to the walk and visible only in the `authentication` / `actions` / `triggers` rows below, where they are the whole verdict. THE SHAPE FACT THAT DECIDES EVERY ROW: one schema, TWO doors. This ledger's denominator entry exists because of the AUTHORING doors (`defineStack({ connectors })` and `PUT /api/v1/meta/connector/:name`); the same `ConnectorSchema` is ALSO what `AutomationEngine.registerConnector` parses for a def a PLUGIN or an ADR-0097 provider factory builds in code. So a key can have a real consumer and still do nothing when a metadata author writes it, and every row below says WHICH door its consumer is fed from. The keys an authored entry can reach are exactly the `ConnectorProviderContext` fields plus `name` and `enabled`; everything else in an authored entry is stored, served back by `/meta/connector`, and read by no runtime. That asymmetry is the trap this type carries, and it is recorded per key rather than asserted once. PRIOR MEASUREMENTS RE-VERIFIED, not inherited: the ADR-0087 conversion registry's `connector-field-mapping-transform-removed` entry recorded 'Execution: none — `fieldMappings` is spelled only inside packages/spec' (2026-08-06), the `syncConfig.schedule` retirement recorded '`syncConfig` has no reader outside `packages/spec`' (#16320, 2026-09-10), and `ConnectorTriggerSchema`'s own docblock says 'NOT YET ENFORCED — declared but never read by the runtime (#3197)'. All three were re-run on this checkout and all three still hold; the counts are in the rows. One prior claim FAILED re-verification and is corrected here: a comment in packages/spec/src/conversions/registry.ts asserts that `retryConfig` 'and the timeouts beside it are untouched — they are live'. They are not read anywhere; see those three rows. PREVIEW READ POINTS ENUMERATED (the #7131 mechanical rule, objectui @dda8f3815): `registerBuiltinPreviews()` registers nineteen types and `connector` is NOT one of them — this type has no registered metadata-admin preview. What objectui DOES consume is (a) the whole SHAPE, via `clientValidation.ts`, which maps `connector` to `DeclarativeConnectorEntrySchema` on BOTH the create and the edit door (it is not strict, so it may judge a stored body), and (b) the RUNTIME registry projection `GET /api/v1/automation/connectors`, from which `connectorsToOptions` reads `name`/`label`/`origin`, `connectorActionsToOptions` reads `actions[].key`/`.label`, and `connectorActionInputSchema` reads `actions[].inputSchema`. Those three are the cross-repo citations below. ADR-0054: no row carries a `proof` and none is owed — no high-risk class binds a `connector/*` path.", + "_note": "DeclarativeConnectorEntrySchema (packages/spec/src/integration/connector.zod.ts). Seeded 2026-09-17 (#18582) together with `analytics_cube`: the last two of the three PENDING_GOVERNANCE debts #18133 declared when PR #18581 widened the governance denominator to `authorableTypes()` (`sharing_rule` was paid first, PR #18587). Their landing empties that map. NOT a registered metadata KIND — bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reached through `getMetadataTypeSchema`'s unregistered-kind fallback. WHAT THE WALKER ACTUALLY RESOLVES, measured rather than assumed: the binding names `DeclarativeConnectorEntrySchema`. ⚠️ THE MECHANISM CHANGED WITH THE `connectionTimeoutMs` RETIREMENT and the prior sentence here is corrected rather than carried: that schema USED TO BE `ConnectorSchema.superRefine(...)`, a Zod 4 check attached to the same object def. It is now a `z.preprocess` PIPE — both published carriers wrap one shared private `ConnectorBaseSchema` in the ADR-0049 retired-default residue stage, the entry schema adding the ADR-0097 cross-field rules on the base before wrapping, so the two are SIBLINGS rather than parent and child. The CONCLUSION is unchanged and re-measured on the built entry: the pipe keeps a read-through `shape`, both carriers expose 30 keys, and the key sets are byte-identical with no entry-only and no base-only key — the ADR-0097 cross-field rules add no key and remove none. The gate therefore cannot tell the two schemas apart; what the binding buys is REFUSALS, which are invisible to the walk and visible only in the `authentication` / `actions` / `triggers` rows below, where they are the whole verdict. THE SHAPE FACT THAT DECIDES EVERY ROW: one schema, TWO doors. This ledger's denominator entry exists because of the AUTHORING doors (`defineStack({ connectors })` and `PUT /api/v1/meta/connector/:name`); the same `ConnectorSchema` is ALSO what `AutomationEngine.registerConnector` parses for a def a PLUGIN or an ADR-0097 provider factory builds in code. So a key can have a real consumer and still do nothing when a metadata author writes it, and every row below says WHICH door its consumer is fed from. The keys an authored entry can reach are exactly the `ConnectorProviderContext` fields plus `name` and `enabled`; everything else in an authored entry is stored, served back by `/meta/connector`, and read by no runtime. That asymmetry is the trap this type carries, and it is recorded per key rather than asserted once. PRIOR MEASUREMENTS RE-VERIFIED, not inherited: the ADR-0087 conversion registry's `connector-field-mapping-transform-removed` entry recorded 'Execution: none — `fieldMappings` is spelled only inside packages/spec' (2026-08-06), the `syncConfig.schedule` retirement recorded '`syncConfig` has no reader outside `packages/spec`' (#16320, 2026-09-10), and `ConnectorTriggerSchema`'s own docblock says 'NOT YET ENFORCED — declared but never read by the runtime (#3197)'. All three were re-run on this checkout and all three still hold; the counts are in the rows. One prior claim FAILED re-verification and is corrected here: a comment in packages/spec/src/conversions/registry.ts asserts that `retryConfig` 'and the timeouts beside it are untouched — they are live'. They are not read anywhere; see those three rows. PREVIEW READ POINTS ENUMERATED (the #7131 mechanical rule, objectui @dda8f3815): `registerBuiltinPreviews()` registers nineteen types and `connector` is NOT one of them — this type has no registered metadata-admin preview. What objectui DOES consume is (a) the whole SHAPE, via `clientValidation.ts`, which maps `connector` to `DeclarativeConnectorEntrySchema` on BOTH the create and the edit door (it is not strict, so it may judge a stored body), and (b) the RUNTIME registry projection `GET /api/v1/automation/connectors`, from which `connectorsToOptions` reads `name`/`label`/`origin`, `connectorActionsToOptions` reads `actions[].key`/`.label`, and `connectorActionInputSchema` reads `actions[].inputSchema`. Those three are the cross-repo citations below. ADR-0054: no row carries a `proof` and none is owed — no high-risk class binds a `connector/*` path.", "props": { "name": { "status": "live", diff --git a/packages/spec/src/conversions/registry.ts b/packages/spec/src/conversions/registry.ts index a5ff22e78de..6ab9683fb53 100644 --- a/packages/spec/src/conversions/registry.ts +++ b/packages/spec/src/conversions/registry.ts @@ -8880,7 +8880,7 @@ const fieldReferenceToAlias: MetadataConversion = { * `logUnmapped`) and `ErrorMappingRuleSchema` (`sourceCode`, `sourceMessage`, * `targetCode`, `targetCategory`, `severity`, `retryable`, `userMessage`) were * authorable through `ConnectorSchema.errorMapping` — and, because - * `DeclarativeConnectorEntrySchema` `superRefine`s the same shape, through + * `DeclarativeConnectorEntrySchema` carries the same shape, through * `stack.connectors[]` and the `PUT /meta/connector/:name` door — and NOTHING * read them: measured on `origin/main`, the only reference outside the * declaring file and its unit test was a type-identity pin. No provider, @@ -8971,8 +8971,8 @@ const connectorErrorMappingRemoved: MetadataConversion = { * enforce-or-remove; maintainer ruling 2026-09-22, letter A). * * A bounded (`min(1000).max(300000)`), defaulted (`30000`), `.describe()`d key - * on `ConnectorSchema` — and, because `DeclarativeConnectorEntrySchema` - * `superRefine`s the same shape, on `stack.connectors[]` and the + * on `ConnectorSchema` — and, because `DeclarativeConnectorEntrySchema` wraps + * the same private base object, on `stack.connectors[]` and the * `PUT /meta/connector/:name` door — that no site ever applied as a deadline. * * ⚠️ NOT a zero-mention retirement, and the distinction is the whole finding: diff --git a/packages/spec/src/integration/connector.test.ts b/packages/spec/src/integration/connector.test.ts index bf9d44f754a..4ac2948ca4d 100644 --- a/packages/spec/src/integration/connector.test.ts +++ b/packages/spec/src/integration/connector.test.ts @@ -718,7 +718,8 @@ describe('[#4911] `./integration` no longer publishes an outbound rate-limit sha // Reachability, demonstrated rather than asserted from prose: the tombstone // is only worth anything if it fires through `stack.connectors[]`, which is // the surface an author actually writes (`DeclarativeConnectorEntrySchema` - // is `ConnectorSchema.superRefine(…)`, so it inherits the tombstone). + // wraps the same private `ConnectorBaseSchema` the base export does, so it + // carries the tombstone). // No top-level `name` here: it was never a declared stack key — the strip- // mode schema used to swallow it, and the #8687 strict close refuses it, // which would have made the positive control below fail for the wrong @@ -1196,8 +1197,12 @@ describe('ADR-0010 protection envelope (#6362)', () => { }); it('declaring the envelope did not open the schema to arbitrary `_` keys', () => { - // `ConnectorSchema` is deliberately non-strict (subtypes `.extend()` it), - // so an unknown key is stripped rather than refused. The point of this pin + // `ConnectorSchema` is deliberately non-strict, so an unknown key is + // stripped rather than refused. (This used to say "subtypes `.extend()` + // it" — the export is a residue-stage PIPE since the ADR-0049 + // `connectionTimeoutMs` retirement and `.extend()` no longer exists on it; + // a subtype extends `ConnectorBaseSchema` and re-wraps. The non-strictness + // this pin is about is the INNER object's and is unchanged.) The point // is the converse of the ones above: the spread adds SEVEN named keys, not // a passthrough — an underscore key nobody declared still does not survive. const parsed = ConnectorSchema.parse({ diff --git a/packages/spec/src/integration/connector.zod.ts b/packages/spec/src/integration/connector.zod.ts index 8b79e69db47..4433a2c6695 100644 --- a/packages/spec/src/integration/connector.zod.ts +++ b/packages/spec/src/integration/connector.zod.ts @@ -542,7 +542,8 @@ const ERROR_MAPPING_RETIRED = // ============================================================================ // // A bounded (`min(1000).max(300000)`), defaulted (`30000`), `.describe()`d key -// on this schema and — through `ConnectorSchema.superRefine` — on +// on this schema and — because both published carriers wrap the same private +// `ConnectorBaseSchema` — on // `DeclarativeConnectorEntrySchema`, so it was authorable from `stack.connectors[]`, // from `PUT /meta/connector/:name`, and served back by `/meta/connector`. Every // signal an authoring surface can give said it worked. @@ -1017,8 +1018,11 @@ const ConnectorBaseSchema = lazySchema(() => z.object({ * nobody anything. `ConnectorSchema` is NOT `.strict()`, so a plain delete * would be a silent strip (ADR-0104); the tombstone makes the removal audible * in the two channels an upgrading author actually hits — `tsc` and the - * parse — and `DeclarativeConnectorEntrySchema` (`ConnectorSchema.superRefine`) - * inherits it, so `stack.connectors[]` and the `/meta/connector` door refuse + * parse — and `DeclarativeConnectorEntrySchema` carries it too (both + * published carriers wrap the same private `ConnectorBaseSchema`; until the + * `connectionTimeoutMs` retirement the entry schema was literally + * `ConnectorSchema.superRefine(...)`), so `stack.connectors[]` and the + * `/meta/connector` door refuse * it too. Registered as `integration/Connector:errorMapping` and * `integration/DeclarativeConnectorEntry:errorMapping` in * `RETIRED_KEYS_BY_MAJOR[18]`; sources are rewritten by the D2 conversion @@ -1074,8 +1078,16 @@ const ConnectorBaseSchema = lazySchema(() => z.object({ * * The wrapper is a `z.preprocess` PIPE, not a `ZodObject`: it keeps a * read-through `shape` so the schema walkers and shape-reading consumers see - * the inner authorable truth, but ⛔ it cannot be `.extend()`ed or - * `.superRefine()`d directly — do that on `ConnectorBaseSchema` and re-wrap, + * the inner authorable truth, but the `ZodObject` combinators do NOT survive + * it. Measured on the built entry against a plain-object control + * (`WebhookConfigSchema`, which keeps all nine): `.extend()`, `.omit()`, + * `.pick()`, `.partial()`, `.merge()`, `.strict()`, `.keyof()` and + * `.safeExtend()` are gone. + * + * ⚠️ `.superRefine()` is the exception and the trap — it lives on zod's base + * type, so it is still CALLABLE here and silently returns a schema with no + * read-through `shape`, which is precisely what the authorable-surface and + * liveness walkers duck-test. So: build on `ConnectorBaseSchema` and re-wrap, * the way `DeclarativeConnectorEntrySchema` below does (the * `EffectiveObjectPermissionSchema` precedent). */ @@ -1101,8 +1113,26 @@ export function defineConnector(config: z.input): Connec * plus the cross-field rules that apply only when a connector is *authored inside * a stack*, as opposed to a def a plugin builds at runtime and hands to * `registerConnector`. `stack.zod.ts` validates the `connectors:` array against - * this; the base {@link ConnectorSchema} stays a plain object so connector - * *subtypes* (github / database / …) can still `.extend()` it. + * this. + * + * ⚠️ This used to end "the base {@link ConnectorSchema} stays a plain object so + * connector *subtypes* (github / database / …) can still `.extend()` it". That + * is NO LONGER TRUE and the sentence is corrected rather than deleted, because + * it was quoted verbatim downstream: both published exports are now + * `z.preprocess` PIPES (the ADR-0049 retired-default residue stage), and a pipe + * is not a `ZodObject`. Measured on the built entry, against a plain-object + * control (`WebhookConfigSchema`) that keeps all nine: `.extend()`, `.omit()`, + * `.pick()`, `.partial()`, `.merge()`, `.strict()`, `.keyof()` and + * `.safeExtend()` are all gone from `ConnectorSchema` and from this schema. + * `.superRefine()` is the one that survives — it lives on zod's base type — but + * ⛔ calling it on a pipe returns a schema with NO read-through `shape`, which + * is what the authorable-surface and liveness walkers duck-test, so a + * refinement still belongs on the inner object. + * + * **Subtype route:** extend `ConnectorBaseSchema` and re-wrap the result with + * `acceptRetiredDefaultResidue(…, CONNECTOR_RETIRED_KEY_RESIDUE)` — exactly + * what this schema does below, and the `EffectiveObjectPermissionSchema` + * precedent. * * One rule applies to EVERY authored entry (#7990, maintainer-ruled 2026-08-12): * - NO entry may inline secrets via `authentication`. A published connector diff --git a/packages/spec/src/migrations/entries/retired-keys/18.integration__DeclarativeConnectorEntry__connectionTimeoutMs.ts b/packages/spec/src/migrations/entries/retired-keys/18.integration__DeclarativeConnectorEntry__connectionTimeoutMs.ts index 03166e33526..b6e92aeefb4 100644 --- a/packages/spec/src/migrations/entries/retired-keys/18.integration__DeclarativeConnectorEntry__connectionTimeoutMs.ts +++ b/packages/spec/src/migrations/entries/retired-keys/18.integration__DeclarativeConnectorEntry__connectionTimeoutMs.ts @@ -1,8 +1,13 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. // The same tombstone seen through the second carrier. -// `DeclarativeConnectorEntrySchema` is `ConnectorSchema.superRefine(...)`, so the -// `connectionTimeoutMs` tombstone on the base is inherited by the shape that +// `DeclarativeConnectorEntrySchema` and `ConnectorSchema` are now SIBLINGS, not +// parent and child: each wraps the shared private `ConnectorBaseSchema` in the +// retired-default residue stage, the entry schema adding the ADR-0097 +// cross-field rules on the base before wrapping. (Until this retirement the +// entry schema was literally `ConnectorSchema.superRefine(...)`; that spelling +// is gone with the pipe.) So the `connectionTimeoutMs` tombstone is carried by +// the shape that // `stack.connectors[]` (`stack.zod.ts`) and the `PUT /meta/connector/:name` door // (`kernel/metadata-type-schemas.ts`) actually parse, and the authorable-surface // walk publishes the `[RETIRED]` row under this def key as well. One tombstone, diff --git a/packages/spec/src/migrations/entries/retired-keys/18.integration__DeclarativeConnectorEntry__errorMapping.ts b/packages/spec/src/migrations/entries/retired-keys/18.integration__DeclarativeConnectorEntry__errorMapping.ts index 9343431e214..cf7fd18b419 100644 --- a/packages/spec/src/migrations/entries/retired-keys/18.integration__DeclarativeConnectorEntry__errorMapping.ts +++ b/packages/spec/src/migrations/entries/retired-keys/18.integration__DeclarativeConnectorEntry__errorMapping.ts @@ -1,8 +1,13 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. // #14676 — the same tombstone seen through the second carrier. -// `DeclarativeConnectorEntrySchema` is `ConnectorSchema.superRefine(...)`, so the -// `errorMapping` tombstone on the base is inherited by the shape that +// `DeclarativeConnectorEntrySchema` and `ConnectorSchema` both wrap the shared +// private `ConnectorBaseSchema` in the retired-default residue stage, the entry +// schema adding the ADR-0097 cross-field rules on the base before wrapping. +// (When this entry was written the two were parent and child — +// `ConnectorSchema.superRefine(...)` — and the `connectionTimeoutMs` retirement +// replaced that with the sibling shape.) Either way the `errorMapping` +// tombstone on the base is carried by the shape that // `stack.connectors[]` (`stack.zod.ts`) and the `PUT /meta/connector/:name` door // (`kernel/metadata-type-schemas.ts`) actually parse, and the authorable-surface // walk publishes the `[RETIRED]` row under this def key as well. One tombstone, diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index e412468918a..9b8ba346431 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -14342,8 +14342,13 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly> // about its unit for whoever implements the loop. 'integration/ConnectorTrigger:interval', // The same tombstone seen through the second carrier. - // `DeclarativeConnectorEntrySchema` is `ConnectorSchema.superRefine(...)`, so the - // `connectionTimeoutMs` tombstone on the base is inherited by the shape that + // `DeclarativeConnectorEntrySchema` and `ConnectorSchema` are now SIBLINGS, not + // parent and child: each wraps the shared private `ConnectorBaseSchema` in the + // retired-default residue stage, the entry schema adding the ADR-0097 + // cross-field rules on the base before wrapping. (Until this retirement the + // entry schema was literally `ConnectorSchema.superRefine(...)`; that spelling + // is gone with the pipe.) So the `connectionTimeoutMs` tombstone is carried by + // the shape that // `stack.connectors[]` (`stack.zod.ts`) and the `PUT /meta/connector/:name` door // (`kernel/metadata-type-schemas.ts`) actually parse, and the authorable-surface // walk publishes the `[RETIRED]` row under this def key as well. One tombstone, @@ -14356,8 +14361,13 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly> // See `18.integration__Connector__connectionTimeoutMs.ts` for the retirement record. 'integration/DeclarativeConnectorEntry:connectionTimeoutMs', // #14676 — the same tombstone seen through the second carrier. - // `DeclarativeConnectorEntrySchema` is `ConnectorSchema.superRefine(...)`, so the - // `errorMapping` tombstone on the base is inherited by the shape that + // `DeclarativeConnectorEntrySchema` and `ConnectorSchema` both wrap the shared + // private `ConnectorBaseSchema` in the retired-default residue stage, the entry + // schema adding the ADR-0097 cross-field rules on the base before wrapping. + // (When this entry was written the two were parent and child — + // `ConnectorSchema.superRefine(...)` — and the `connectionTimeoutMs` retirement + // replaced that with the sibling shape.) Either way the `errorMapping` + // tombstone on the base is carried by the shape that // `stack.connectors[]` (`stack.zod.ts`) and the `PUT /meta/connector/:name` door // (`kernel/metadata-type-schemas.ts`) actually parse, and the authorable-surface // walk publishes the `[RETIRED]` row under this def key as well. One tombstone, From 8cdcafc3bd37affb5931e13ada7dc1381a2cc70d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 09:08:29 +0000 Subject: [PATCH 10/15] docs(spec): correct the connector row's walk mechanism in the liveness README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Declared widening, one table row. The row asserted in the present tense that DeclarativeConnectorEntrySchema IS ConnectorSchema.superRefine(...) and rested its byte-identical-key-set conclusion on that Zod 4 attachment. This PR falsifies both halves: the two carriers are now siblings wrapping one private ConnectorBaseSchema in the residue stage, and what preserves the walked shape is the pipe's read-through shape, not a superRefine attachment. Mechanism corrected, conclusion kept and re-measured on the built entry (30 keys each, byte-identical, zero entry-only, zero base-only), and the row says which spelling moved and when — the form used on the six sibling sites. This file is hand-written Notes prose by .gitattributes' own split, not a driver- managed artifact, so a hand correction is the right act; regenerating a Note would fabricate a verdict. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx --- packages/spec/liveness/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index c7736aeff19..c107a575f82 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -939,7 +939,7 @@ marker where the Notes cell goes, never a guess at what belongs there. | rest_api | seeded 2026-09-21 (#14640) — the FIFTH `RestServerConfig` sub-object, enrolled a round after the four above and deliberately so. #14369 left `RestApiConfigSchema` out because the `api` block's consumption seam was then still VALIDATE-ONLY (#11637 ran the declared contract and discarded its output), so a census would have recorded a half that was about to move; the gate source and four rows of this table said as much. That fence was re-tested before a line of this ledger was written and it has EXPIRED: `RestServer.normalizeConfig` now BUILDS the `api` block from `parseDeclaredApiConfig`'s output — “the asymmetry is gone and all five now build from their parsed output” — and the change is RELEASED, not in flight, with `packages/rest/CHANGELOG.md` re-stating the same zero this file records. ⛔ **The ledger is `rest_api.json`, NOT `api.json`**: that name was already taken by `ApiEndpointSchema`, the registered `api` metadata type with real consumers in the matcher, executor, policy chain and mapping layer — one spelling, two unrelated meanings inside `packages/spec`, and filing here would have published one file's measurement under the other's name. Live 12 = `version` / `basePath` / `apiPath`, which `getApiBasePath` splices into the prefix of EVERY mounted route (read through a whole-block destructure, which is why the dead-key census below had to sweep destructuring shapes and not a property-access pattern alone), the eight `enable*` switches, each gating a mount and most of them also the discovery document's capability block, and `projectResolution`. Dead 14 = the `requireAuth` tombstone (#3963, still `.omit()`ed by this seam because #3963 chose warn-and-ignore and converting that to a boot failure is that decision's to make), plus the two declared containers `documentation` (drilled to ten, including its nested `contact` / `license`) and `responseFormat` (three) — normalized into `this.config.api` and read back by nothing, so `responseFormat.envelope: false` unwraps no response and `documentation.title` retitles no served document. Every zero carries a lit control on the same instrument (twelve sibling keys on the same block return 1-2 reads), each of the three shapes a spelling sweep is blind to was swept with its own control, and the backstop is structural rather than textual: `NormalizedRestServerConfig` is module-local with no `export` and `RestServer.config` is `private`, so the normalized block cannot be reached from outside that one class. ⛔ **The two dead containers do NOT share one verdict**: `documentation`'s members are OpenAPI `info` fields whose enforce route collides with a recorded ownership decision (`info` is written by `build-openapi.ts` and passed through untouched by #11646), while `responseFormat`'s enforce route means making the response envelope configurable — a larger claim. This file records status; the enforce-or-remove call per key is a follow-up on the human floor. `evidenceScope` stays `in-repo`: objectui was measured clean at the pinned sha and at head against a lit control, but the closed cloud runtime was not reachable from the measuring container, so #14796's structural reading is cited as a standing reading rather than re-claimed as a sweep | | realtime_subscription | seeded 2026-09-04 (#14446) — a TRANSPORT-PROTOCOL surface, the fifth category the `SPEC_ONLY_SCHEMAS` override has had to reach. `SubscriptionSchema` (`packages/spec/src/api/realtime.zod.ts`) is what a client declares to open a realtime subscription: the item type of `RealtimeConfigSchema.subscriptions` and the `Subscription` the generated API reference publishes. Like `query` it is a request surface rather than stored metadata, and like `query` that is exactly why it went unasked — no registry holds it, `RealtimeConfigSchema` is `.passthrough()` so nothing downstream even refuses an unknown key, and the whole vocabulary sat outside the denominator while the reference kept publishing it. Rooted on `SubscriptionSchema` rather than on `RealtimeConfigSchema` for the reason the four `RestServerConfig` sub-objects document one row up: the walk drilled exactly ONE level when this was rooted (it recurses as of #17424; the rooting stands), so with the config as the root `events[].type` and `events[].filters` would inherit a container verdict instead of carrying rows of their own — #4956's shape. **Dead 6 = every key it has, and the CONTAINER is the finding**: nothing outside `packages/spec` imports `SubscriptionSchema`, `SubscriptionEventSchema` or `RealtimeConfigSchema` at all, so no key beneath them can be read (the `manifest.contributes` reasoning). The two keys the card measured are the sharp ones. `events[].type` accepts `RealtimeEventType`, whose four members (`record.created` / `record.updated` / `record.deleted` / `field.changed`) are DISJOINT from what the engine publishes (`DataEventType`'s `data.record.*`, live emitter in `service-knowledge`), so an author who writes the enum's own `record.created` gets a subscription that silently never fires — and the enum is what the API reference shows them. Its direction is settled by the 2026-09-02 triage and quoted verbatim in the row: enforce means REPOINTING THE ENUM, never changing what the runtime publishes. `field.changed` is the same spelling the sibling `DataEventType` REMOVED in 17.0.0 (#4673, PR #4685) for having no producer; it survives here only because this enum was never in a ratchet's denominator. `events[].filters` is `z.unknown().optional()` — the textbook ADR-0049 fourth state, no shape and no reader, failing in the permissive direction (a subscriber who filters receives every event). ⚠️ Three spellings of a realtime subscription exist and only the third is executed: this one, `websocket.zod.ts#EventSubscriptionSchema`, and the plain interface `contracts/realtime-service.ts#RealtimeSubscriptionOptions` that `in-memory-realtime-adapter.ts#matchesSubscription` actually reads. The file note names the same-name-different-shape traps so the next census does not mistake one for a consumer. Zero live | | sharing_rule | seeded 2026-09-17 (#18582) — the second of the three `PENDING_GOVERNANCE` debts #18133 declared, and the first one PAID (`connector` and `analytics_cube` are still owed on that card). Not a registered kind: it is bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reaches the walk through `getMetadataTypeSchema`'s unregistered-kind fallback, so this ledger governs a type `listMetadataTypeSchemaTypes()` still does not enumerate. One shape fact decides every row: the AUTHORING shape is not the ENFORCED shape. ADR-0057 D6 makes the `sys_sharing_rule` row canonical (`object_name` + `criteria_json` + `recipient_type`/`recipient_id` + `access_level`) and `bootstrapDeclaredSharingRules` translates each authored key into it at boot — nothing re-parses `SharingRuleSchema` at enforcement time — so every consumer cited reads a COLUMN and every row carries the `producer` (#4837) that populates it, which is the `seed.env` lesson applied to a whole type rather than to one key. Preview read points ENUMERATED per the #7131 rule and the answer recorded rather than skipped: `registerBuiltinPreviews()` (objectui @dda8f381) registers twenty types and `sharing_rule` is not one of them; what objectui does consume is the whole shape, on the CREATE door only (`AUTHOR_SHAPE_ONLY_TYPES` — the EDIT door is deliberately ungated because a served body carries the `_diagnostics` decoration this `.strict()` schema rejects). The single non-`live` row is `type`, the `SharingRuleType` discriminator: one member, `criteria`, whose only reader is a defensive `=== 'owner'` comparison that is unreachable for every value the schema admits. `planned` on the `action.operation` precedent (a one-member discriminator held `planned` until a runtime half dispatched on it, #15080), and deliberately NOT an enforce-or-remove candidate: the key is required, so removing it would break every authored rule to delete nothing. | -| connector | seeded 2026-09-17 (#18582) — the second of the three `PENDING_GOVERNANCE` debts #18133 declared, paid in the same diff as `analytics_cube`, which empties that map. Not a registered kind: bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reached through `getMetadataTypeSchema`'s unregistered-kind fallback. **What the walk actually resolves, measured:** the binding names `DeclarativeConnectorEntrySchema`, and that schema is `ConnectorSchema.superRefine(...)` — in Zod 4 a `superRefine` attaches a check to the same object def rather than wrapping it, so the walked key set is byte-identical to the base's. The gate cannot tell the two schemas apart; what the entry schema buys is REFUSALS, invisible to the walk and visible only in the three rows where they are the whole verdict. **ONE SCHEMA, TWO DOORS** is the shape fact behind the 20/1/53 split: the ledger's denominator entry exists for the AUTHORING doors (`defineStack({ connectors })`, `PUT /meta/connector/:name`), while the same `ConnectorSchema` is what `AutomationEngine.registerConnector` parses for a def a PLUGIN or an ADR-0097 provider factory builds in code — so a key can have a real consumer and still do nothing when a metadata author writes it. The keys an authored entry can reach are exactly the `ConnectorProviderContext` fields plus `name` and `enabled`; `type` and `icon` reach that context and are dropped by all three shipped factories, and each says so on its own row. `authentication` is the ledger's `planned`: refused outright by ADR-0097 §3 (#7990) rather than ignored. The 53 `dead` are four declared subsystems with no engine — `syncConfig` (7), `fieldMappings` (7), `retryConfig` (8), `health` (14, both sub-blocks) — plus `triggers` (6, and the schema's own docblock says so: #3197), the connector's nested `webhooks` (one blanket verdict, recorded in the undrilled baseline), `status`, `metadata`, the two timeouts, `actions.description`/`.outputSchema`, and four `retiredKey` tombstones that keep their rows because the key stays in the walked shape (the `rls.priority` precedent). **A prior in-repo claim was falsified here**: the conversion registry's note that `retryConfig` "and the timeouts beside it are untouched — they are live" is corrected on those rows — the word does not occur outside `packages/spec` at all | +| connector | seeded 2026-09-17 (#18582) — the second of the three `PENDING_GOVERNANCE` debts #18133 declared, paid in the same diff as `analytics_cube`, which empties that map. Not a registered kind: bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reached through `getMetadataTypeSchema`'s unregistered-kind fallback. **What the walk actually resolves, measured:** the binding names `DeclarativeConnectorEntrySchema`. ⚠️ The MECHANISM changed with the `connectionTimeoutMs` retirement and the prior sentence here is corrected rather than carried: that schema USED TO BE `ConnectorSchema.superRefine(...)`, a Zod 4 check attached to the same object def, and the key-set conclusion used to rest on that attachment. It is now a `z.preprocess` PIPE — both published carriers wrap one shared private `ConnectorBaseSchema` in the ADR-0049 retired-default residue stage, the entry schema adding the ADR-0097 cross-field rules on the base before wrapping, so the two are SIBLINGS rather than parent and child, and what preserves the walked shape is the pipe's read-through `shape`, NOT a `superRefine` attachment. The CONCLUSION is unchanged and re-measured on the built entry rather than inherited: both carriers expose 30 keys and the key sets are byte-identical, with no entry-only and no base-only key. The gate cannot tell the two schemas apart; what the entry schema buys is REFUSALS, invisible to the walk and visible only in the three rows where they are the whole verdict. **ONE SCHEMA, TWO DOORS** is the shape fact behind the 20/1/53 split: the ledger's denominator entry exists for the AUTHORING doors (`defineStack({ connectors })`, `PUT /meta/connector/:name`), while the same `ConnectorSchema` is what `AutomationEngine.registerConnector` parses for a def a PLUGIN or an ADR-0097 provider factory builds in code — so a key can have a real consumer and still do nothing when a metadata author writes it. The keys an authored entry can reach are exactly the `ConnectorProviderContext` fields plus `name` and `enabled`; `type` and `icon` reach that context and are dropped by all three shipped factories, and each says so on its own row. `authentication` is the ledger's `planned`: refused outright by ADR-0097 §3 (#7990) rather than ignored. The 53 `dead` are four declared subsystems with no engine — `syncConfig` (7), `fieldMappings` (7), `retryConfig` (8), `health` (14, both sub-blocks) — plus `triggers` (6, and the schema's own docblock says so: #3197), the connector's nested `webhooks` (one blanket verdict, recorded in the undrilled baseline), `status`, `metadata`, the two timeouts, `actions.description`/`.outputSchema`, and four `retiredKey` tombstones that keep their rows because the key stays in the walked shape (the `rls.priority` precedent). **A prior in-repo claim was falsified here**: the conversion registry's note that `retryConfig` "and the timeouts beside it are untouched — they are live" is corrected on those rows — the word does not occur outside `packages/spec` at all | | analytics_cube | seeded 2026-09-17 (#18582) — the third debt, paid in the same diff as `connector`. Not a registered kind either: bound in `UNREGISTERED_KIND_SCHEMAS` by #10194 and reached through the same unregistered-kind fallback. **ONE Cube shape, THREE producers, one registry** is what decides every row: `cube-registry.ts` names them itself — authored cubes (`analyticsCubes[]` / `defineCube()`, threaded by the CLI into `AnalyticsServiceConfig.cubes`), COMPILED DATASETS (ADR-0021, where `dataset-compiler` mints a Cube), and ad-hoc query inference. Only the first is the authoring door governed here, so a key whose only reader sits on the compiled-dataset path is not live for an authored cube however busy that reader is — the #4837 producer rule on a shape with three producers. That is `dimensions.granularities` (read by `dataset-executor#granularityOf`, whose argument is a `CompiledDataset` an authored cube never becomes) and `measures.format` (written by the compiler, threaded to the wire from the DATASET measure instead). The query path is genuinely live: `sql` is the FROM table AND the object whose RLS read scope is injected, `measures.type` picks the aggregate, `measures.sql`/`dimensions.sql` the column, `joins[].name` the joined table. The 10 `dead` are the caching block (`refreshKey.every`/`.sql` — no refresh scheduler exists anywhere), the access-control flag (`public` — three sites write `false`, nothing reads it: a knob that was never wired, not a hole that was opened), the three `description`s, and the inner `name` on each of `measures`/`dimensions`, where the record KEY is the identity. It was 12 until #18612 RETIRED `joins[].relationship` and the REQUIRED `joins[].sql` (ADR-0049 enforce-or-remove, maintainer-ruled batch #154): the ON clause is SYNTHESISED as an FK equality and the authored one was never consulted, so a declared join condition came back REPLACED under a 200. `CubeJoinSchema` is a `strictObject`, so the route was strict deletion plus a `guidance` prescription and the two rows left this ledger with the keys — not the `retiredKey()` route, which keeps the row. **#10238 is not prejudged**: whether cube authoring is live end to end is still its own measurement — this ledger answers the per-key question only | The `dead` set across types is the enforce-or-remove worklist (ADR-0049); every From c33220bc3d8d0c42279f38d9d187bbd40d60581c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 10:08:46 +0000 Subject: [PATCH 11/15] docs(spec): re-measure the whole connector liveness row against this head MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FB-A — forty words after the sentence round 4 corrected, the row still said 'four retiredKey tombstones'. Two instruments disagreed on the absolute number and agreed on the delta, so I established the scope the sentence means before counting: it enumerates the contents of THIS ledger's dead set, so the population is this file's dead rows that are retiredKey tombstones kept because the key stays in the walked shape. One instrument over both refs reads 5 on origin/main and 6 at head. The six are now named individually rather than totalled, with the double-count that made the old tail drift called out: three of them already sit inside the fieldMappings, triggers and health counts. NB-1 — the rest of the row was stale too (not introduced here; byte-identical on origin/main). The 20/1/53 split and the 53 dead become 29/1/44 and 44, cited to the generated state-counts row. retryConfig (8) leaves the dead list entirely: all eight sub-keys are live since #18975, which is the same measurement this row's own falsification note records. 'The two timeouts' is corrected: requestTimeoutMs is live, connectionTimeoutMs is the tombstone. The decomposition is partitioned so every dead row is counted once and sums to 44. NB-2 — the four PR-authored 'inherits' spellings contradicted this PR's own 'siblings, not parent and child'. Respelled the way connector.zod.ts already does. The pre-existing ones are left for their own round. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx --- .changeset/19580-retire-connector-connection-timeout-ms.md | 3 ++- packages/spec/liveness/README.md | 2 +- packages/spec/liveness/connector.json | 2 +- .../connector-connection-timeout-retirement.test.ts | 6 ++++-- packages/spec/src/migrations/registry.ts | 3 ++- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.changeset/19580-retire-connector-connection-timeout-ms.md b/.changeset/19580-retire-connector-connection-timeout-ms.md index 7a7508de185..8252183817a 100644 --- a/.changeset/19580-retire-connector-connection-timeout-ms.md +++ b/.changeset/19580-retire-connector-connection-timeout-ms.md @@ -71,7 +71,8 @@ ruling that made the siblings live forbids.) `RETIRED_KEYS_BY_MAJOR[18]`. The schema is not `.strict()`, so a bare deletion would strip an authored key in silence (ADR-0104): the tombstone is audible in both channels — `tsc` (input type `never`) and the parse, which raises the - prescription itself. `DeclarativeConnectorEntrySchema` inherits it, so + prescription itself. `DeclarativeConnectorEntrySchema` carries it too — both + published carriers wrap the same private `ConnectorBaseSchema` — so `stack.connectors[]` and the `/meta/connector` door refuse it too. - **A D2 conversion, `connector-connection-timeout-ms-removed`** — one strip per `connectors[]` entry, a pure lossless delete. ⭐ The ruling left whether one was diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index c107a575f82..03bc10c92b4 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -939,7 +939,7 @@ marker where the Notes cell goes, never a guess at what belongs there. | rest_api | seeded 2026-09-21 (#14640) — the FIFTH `RestServerConfig` sub-object, enrolled a round after the four above and deliberately so. #14369 left `RestApiConfigSchema` out because the `api` block's consumption seam was then still VALIDATE-ONLY (#11637 ran the declared contract and discarded its output), so a census would have recorded a half that was about to move; the gate source and four rows of this table said as much. That fence was re-tested before a line of this ledger was written and it has EXPIRED: `RestServer.normalizeConfig` now BUILDS the `api` block from `parseDeclaredApiConfig`'s output — “the asymmetry is gone and all five now build from their parsed output” — and the change is RELEASED, not in flight, with `packages/rest/CHANGELOG.md` re-stating the same zero this file records. ⛔ **The ledger is `rest_api.json`, NOT `api.json`**: that name was already taken by `ApiEndpointSchema`, the registered `api` metadata type with real consumers in the matcher, executor, policy chain and mapping layer — one spelling, two unrelated meanings inside `packages/spec`, and filing here would have published one file's measurement under the other's name. Live 12 = `version` / `basePath` / `apiPath`, which `getApiBasePath` splices into the prefix of EVERY mounted route (read through a whole-block destructure, which is why the dead-key census below had to sweep destructuring shapes and not a property-access pattern alone), the eight `enable*` switches, each gating a mount and most of them also the discovery document's capability block, and `projectResolution`. Dead 14 = the `requireAuth` tombstone (#3963, still `.omit()`ed by this seam because #3963 chose warn-and-ignore and converting that to a boot failure is that decision's to make), plus the two declared containers `documentation` (drilled to ten, including its nested `contact` / `license`) and `responseFormat` (three) — normalized into `this.config.api` and read back by nothing, so `responseFormat.envelope: false` unwraps no response and `documentation.title` retitles no served document. Every zero carries a lit control on the same instrument (twelve sibling keys on the same block return 1-2 reads), each of the three shapes a spelling sweep is blind to was swept with its own control, and the backstop is structural rather than textual: `NormalizedRestServerConfig` is module-local with no `export` and `RestServer.config` is `private`, so the normalized block cannot be reached from outside that one class. ⛔ **The two dead containers do NOT share one verdict**: `documentation`'s members are OpenAPI `info` fields whose enforce route collides with a recorded ownership decision (`info` is written by `build-openapi.ts` and passed through untouched by #11646), while `responseFormat`'s enforce route means making the response envelope configurable — a larger claim. This file records status; the enforce-or-remove call per key is a follow-up on the human floor. `evidenceScope` stays `in-repo`: objectui was measured clean at the pinned sha and at head against a lit control, but the closed cloud runtime was not reachable from the measuring container, so #14796's structural reading is cited as a standing reading rather than re-claimed as a sweep | | realtime_subscription | seeded 2026-09-04 (#14446) — a TRANSPORT-PROTOCOL surface, the fifth category the `SPEC_ONLY_SCHEMAS` override has had to reach. `SubscriptionSchema` (`packages/spec/src/api/realtime.zod.ts`) is what a client declares to open a realtime subscription: the item type of `RealtimeConfigSchema.subscriptions` and the `Subscription` the generated API reference publishes. Like `query` it is a request surface rather than stored metadata, and like `query` that is exactly why it went unasked — no registry holds it, `RealtimeConfigSchema` is `.passthrough()` so nothing downstream even refuses an unknown key, and the whole vocabulary sat outside the denominator while the reference kept publishing it. Rooted on `SubscriptionSchema` rather than on `RealtimeConfigSchema` for the reason the four `RestServerConfig` sub-objects document one row up: the walk drilled exactly ONE level when this was rooted (it recurses as of #17424; the rooting stands), so with the config as the root `events[].type` and `events[].filters` would inherit a container verdict instead of carrying rows of their own — #4956's shape. **Dead 6 = every key it has, and the CONTAINER is the finding**: nothing outside `packages/spec` imports `SubscriptionSchema`, `SubscriptionEventSchema` or `RealtimeConfigSchema` at all, so no key beneath them can be read (the `manifest.contributes` reasoning). The two keys the card measured are the sharp ones. `events[].type` accepts `RealtimeEventType`, whose four members (`record.created` / `record.updated` / `record.deleted` / `field.changed`) are DISJOINT from what the engine publishes (`DataEventType`'s `data.record.*`, live emitter in `service-knowledge`), so an author who writes the enum's own `record.created` gets a subscription that silently never fires — and the enum is what the API reference shows them. Its direction is settled by the 2026-09-02 triage and quoted verbatim in the row: enforce means REPOINTING THE ENUM, never changing what the runtime publishes. `field.changed` is the same spelling the sibling `DataEventType` REMOVED in 17.0.0 (#4673, PR #4685) for having no producer; it survives here only because this enum was never in a ratchet's denominator. `events[].filters` is `z.unknown().optional()` — the textbook ADR-0049 fourth state, no shape and no reader, failing in the permissive direction (a subscriber who filters receives every event). ⚠️ Three spellings of a realtime subscription exist and only the third is executed: this one, `websocket.zod.ts#EventSubscriptionSchema`, and the plain interface `contracts/realtime-service.ts#RealtimeSubscriptionOptions` that `in-memory-realtime-adapter.ts#matchesSubscription` actually reads. The file note names the same-name-different-shape traps so the next census does not mistake one for a consumer. Zero live | | sharing_rule | seeded 2026-09-17 (#18582) — the second of the three `PENDING_GOVERNANCE` debts #18133 declared, and the first one PAID (`connector` and `analytics_cube` are still owed on that card). Not a registered kind: it is bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reaches the walk through `getMetadataTypeSchema`'s unregistered-kind fallback, so this ledger governs a type `listMetadataTypeSchemaTypes()` still does not enumerate. One shape fact decides every row: the AUTHORING shape is not the ENFORCED shape. ADR-0057 D6 makes the `sys_sharing_rule` row canonical (`object_name` + `criteria_json` + `recipient_type`/`recipient_id` + `access_level`) and `bootstrapDeclaredSharingRules` translates each authored key into it at boot — nothing re-parses `SharingRuleSchema` at enforcement time — so every consumer cited reads a COLUMN and every row carries the `producer` (#4837) that populates it, which is the `seed.env` lesson applied to a whole type rather than to one key. Preview read points ENUMERATED per the #7131 rule and the answer recorded rather than skipped: `registerBuiltinPreviews()` (objectui @dda8f381) registers twenty types and `sharing_rule` is not one of them; what objectui does consume is the whole shape, on the CREATE door only (`AUTHOR_SHAPE_ONLY_TYPES` — the EDIT door is deliberately ungated because a served body carries the `_diagnostics` decoration this `.strict()` schema rejects). The single non-`live` row is `type`, the `SharingRuleType` discriminator: one member, `criteria`, whose only reader is a defensive `=== 'owner'` comparison that is unreachable for every value the schema admits. `planned` on the `action.operation` precedent (a one-member discriminator held `planned` until a runtime half dispatched on it, #15080), and deliberately NOT an enforce-or-remove candidate: the key is required, so removing it would break every authored rule to delete nothing. | -| connector | seeded 2026-09-17 (#18582) — the second of the three `PENDING_GOVERNANCE` debts #18133 declared, paid in the same diff as `analytics_cube`, which empties that map. Not a registered kind: bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reached through `getMetadataTypeSchema`'s unregistered-kind fallback. **What the walk actually resolves, measured:** the binding names `DeclarativeConnectorEntrySchema`. ⚠️ The MECHANISM changed with the `connectionTimeoutMs` retirement and the prior sentence here is corrected rather than carried: that schema USED TO BE `ConnectorSchema.superRefine(...)`, a Zod 4 check attached to the same object def, and the key-set conclusion used to rest on that attachment. It is now a `z.preprocess` PIPE — both published carriers wrap one shared private `ConnectorBaseSchema` in the ADR-0049 retired-default residue stage, the entry schema adding the ADR-0097 cross-field rules on the base before wrapping, so the two are SIBLINGS rather than parent and child, and what preserves the walked shape is the pipe's read-through `shape`, NOT a `superRefine` attachment. The CONCLUSION is unchanged and re-measured on the built entry rather than inherited: both carriers expose 30 keys and the key sets are byte-identical, with no entry-only and no base-only key. The gate cannot tell the two schemas apart; what the entry schema buys is REFUSALS, invisible to the walk and visible only in the three rows where they are the whole verdict. **ONE SCHEMA, TWO DOORS** is the shape fact behind the 20/1/53 split: the ledger's denominator entry exists for the AUTHORING doors (`defineStack({ connectors })`, `PUT /meta/connector/:name`), while the same `ConnectorSchema` is what `AutomationEngine.registerConnector` parses for a def a PLUGIN or an ADR-0097 provider factory builds in code — so a key can have a real consumer and still do nothing when a metadata author writes it. The keys an authored entry can reach are exactly the `ConnectorProviderContext` fields plus `name` and `enabled`; `type` and `icon` reach that context and are dropped by all three shipped factories, and each says so on its own row. `authentication` is the ledger's `planned`: refused outright by ADR-0097 §3 (#7990) rather than ignored. The 53 `dead` are four declared subsystems with no engine — `syncConfig` (7), `fieldMappings` (7), `retryConfig` (8), `health` (14, both sub-blocks) — plus `triggers` (6, and the schema's own docblock says so: #3197), the connector's nested `webhooks` (one blanket verdict, recorded in the undrilled baseline), `status`, `metadata`, the two timeouts, `actions.description`/`.outputSchema`, and four `retiredKey` tombstones that keep their rows because the key stays in the walked shape (the `rls.priority` precedent). **A prior in-repo claim was falsified here**: the conversion registry's note that `retryConfig` "and the timeouts beside it are untouched — they are live" is corrected on those rows — the word does not occur outside `packages/spec` at all | +| connector | seeded 2026-09-17 (#18582) — the second of the three `PENDING_GOVERNANCE` debts #18133 declared, paid in the same diff as `analytics_cube`, which empties that map. Not a registered kind: bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reached through `getMetadataTypeSchema`'s unregistered-kind fallback. **What the walk actually resolves, measured:** the binding names `DeclarativeConnectorEntrySchema`. ⚠️ The MECHANISM changed with the `connectionTimeoutMs` retirement and the prior sentence here is corrected rather than carried: that schema USED TO BE `ConnectorSchema.superRefine(...)`, a Zod 4 check attached to the same object def, and the key-set conclusion used to rest on that attachment. It is now a `z.preprocess` PIPE — both published carriers wrap one shared private `ConnectorBaseSchema` in the ADR-0049 retired-default residue stage, the entry schema adding the ADR-0097 cross-field rules on the base before wrapping, so the two are SIBLINGS rather than parent and child, and what preserves the walked shape is the pipe's read-through `shape`, NOT a `superRefine` attachment. The CONCLUSION is unchanged and re-measured on the built entry rather than inherited: both carriers expose 30 keys and the key sets are byte-identical, with no entry-only and no base-only key. The gate cannot tell the two schemas apart; what the entry schema buys is REFUSALS, invisible to the walk and visible only in the three rows where they are the whole verdict. **ONE SCHEMA, TWO DOORS** is the shape fact behind the 29/1/44 split (live/planned/dead; counts read from the generated `state-counts.md` row, never hand-kept here): the ledger's denominator entry exists for the AUTHORING doors (`defineStack({ connectors })`, `PUT /meta/connector/:name`), while the same `ConnectorSchema` is what `AutomationEngine.registerConnector` parses for a def a PLUGIN or an ADR-0097 provider factory builds in code — so a key can have a real consumer and still do nothing when a metadata author writes it. The keys an authored entry can reach are exactly the `ConnectorProviderContext` fields plus `name` and `enabled`; `type` and `icon` reach that context and are dropped by all three shipped factories, and each says so on its own row. `authentication` is the ledger's `planned`: refused outright by ADR-0097 §3 (#7990) rather than ignored. The 44 `dead`, re-measured at this head and partitioned so every row is counted exactly once: three declared subsystems with no engine — `syncConfig` (8), `fieldMappings` (7), `health` (15, both sub-blocks) — plus `triggers` (6, and the schema's own docblock says so: #3197), the connector's nested `webhooks` (one blanket verdict, recorded in the undrilled baseline), `status`, `metadata`, `actions.description`/`.outputSchema`, and the three top-level `retiredKey` tombstones `rateLimitConfig`, `errorMapping` and `connectionTimeoutMs`. That sums to 44, the dead count the generated `state-counts.md` row carries. ⚠️ `retryConfig` IS NO LONGER IN THIS LIST: all eight of its sub-keys went `live` when #18975 made the declared policy execute at the one platform fetch site, which is the same measurement the falsification note at the end of this row records — so a reader who still finds "`retryConfig` (8)" among the dead is reading a stale copy. ⚠️ Nor is it "the two timeouts" any more: `requestTimeoutMs` is `live` (it becomes `resilientFetch`'s per-attempt deadline) and `connectionTimeoutMs` is the retired tombstone named above. ⭐ SIX rows in this ledger are `retiredKey` tombstones that keep their rows because the key stays in the walked shape (the `rls.priority` precedent) — `rateLimitConfig`, `errorMapping`, `connectionTimeoutMs`, `fieldMappings.transform`, `triggers.interval` and `health.circuitBreaker.monitoringWindow` — but ⛔ that six is NOT a separate addend: the last three are already inside the `fieldMappings`, `triggers` and `health` counts above, which is exactly the double-count that made the previous "and four `retiredKey` tombstones" tail drift. Count them by name, never by adding the tail. **A prior in-repo claim was falsified here**: the conversion registry's note that `retryConfig` "and the timeouts beside it are untouched — they are live" is corrected on those rows — the word does not occur outside `packages/spec` at all | | analytics_cube | seeded 2026-09-17 (#18582) — the third debt, paid in the same diff as `connector`. Not a registered kind either: bound in `UNREGISTERED_KIND_SCHEMAS` by #10194 and reached through the same unregistered-kind fallback. **ONE Cube shape, THREE producers, one registry** is what decides every row: `cube-registry.ts` names them itself — authored cubes (`analyticsCubes[]` / `defineCube()`, threaded by the CLI into `AnalyticsServiceConfig.cubes`), COMPILED DATASETS (ADR-0021, where `dataset-compiler` mints a Cube), and ad-hoc query inference. Only the first is the authoring door governed here, so a key whose only reader sits on the compiled-dataset path is not live for an authored cube however busy that reader is — the #4837 producer rule on a shape with three producers. That is `dimensions.granularities` (read by `dataset-executor#granularityOf`, whose argument is a `CompiledDataset` an authored cube never becomes) and `measures.format` (written by the compiler, threaded to the wire from the DATASET measure instead). The query path is genuinely live: `sql` is the FROM table AND the object whose RLS read scope is injected, `measures.type` picks the aggregate, `measures.sql`/`dimensions.sql` the column, `joins[].name` the joined table. The 10 `dead` are the caching block (`refreshKey.every`/`.sql` — no refresh scheduler exists anywhere), the access-control flag (`public` — three sites write `false`, nothing reads it: a knob that was never wired, not a hole that was opened), the three `description`s, and the inner `name` on each of `measures`/`dimensions`, where the record KEY is the identity. It was 12 until #18612 RETIRED `joins[].relationship` and the REQUIRED `joins[].sql` (ADR-0049 enforce-or-remove, maintainer-ruled batch #154): the ON clause is SYNTHESISED as an FK equality and the authored one was never consulted, so a declared join condition came back REPLACED under a 200. `CubeJoinSchema` is a `strictObject`, so the route was strict deletion plus a `guidance` prescription and the two rows left this ledger with the keys — not the `retiredKey()` route, which keeps the row. **#10238 is not prejudged**: whether cube authoring is live end to end is still its own measurement — this ledger answers the per-key question only | The `dead` set across types is the enforce-or-remove worklist (ADR-0049); every diff --git a/packages/spec/liveness/connector.json b/packages/spec/liveness/connector.json index 1d6c856b9e9..3fa4811139a 100644 --- a/packages/spec/liveness/connector.json +++ b/packages/spec/liveness/connector.json @@ -301,7 +301,7 @@ "connectionTimeoutMs": { "status": "dead", "verifiedAt": "2026-09-22", - "note": "RETIRED (ADR-0049 enforce-or-remove; maintainer ruling 2026-09-22, letter A — the narrower SECOND decision this row asked for). Tombstoned with `retiredKey` because `ConnectorSchema` is not `.strict()` and a plain delete would be a silent strip (ADR-0104); the tombstone is inherited by `DeclarativeConnectorEntrySchema`, so `stack.connectors[]` and `/meta/connector` refuse it too. The row stays because `retiredKey` keeps the key in the walked shape (the `rls.priority` precedent). Registered as `integration/Connector:connectionTimeoutMs` and `integration/DeclarativeConnectorEntry:connectionTimeoutMs` in `RETIRED_KEYS_BY_MAJOR[18]`; authored sources and stored rows are rewritten by the D2 conversion `connector-connection-timeout-ms-removed`, and the withdrawn `ConnectorProviderContext.connectionTimeoutMs` — code, with no authored source to rewrite — by the D3 semantic entry `connector-provider-context-connection-timeout-ms-retired`. The tombstone is packages/spec/src/integration/connector.zod.ts#ConnectorSchema. ⭐ THE CARD’S CENSUS IS STALE, NOT FALSE — and the earlier draft of this note got that wrong, so it is restated as measured. The card’s Leg 2 said every occurrence outside packages/spec is a WRITE of a hardcoded 30000. Re-measured with `git grep -n connectionTimeoutMs SHA -- . ':!packages/spec'` at three trees: at the card’s OWN cited SHA 0870fb5418 that is EXACTLY RIGHT — five non-spec source hits, all five `connectionTimeoutMs: 30000,` (connector-mcp mcp-connector.ts:247, connector-openapi openapi-connector.ts:220, connector-rest rest-connector.ts:113, connector-slack slack-connector.ts:94, service-automation plugin.ts:1734), the card’s table line for line. What superseded it is b929e0a662, the very PR the card flagged as pending. At `origin/main` the same instrument returns THIRTEEN non-test source occurrences over SEVEN files in FIVE packages: six READS (openapi-connector.ts:242, openapi-provider.ts:193, rest-connector.ts:134, rest-provider.ts:64, plugin.ts:307, plugin.ts:1589), four TYPE DECLARATIONS (openapi-connector.ts:135, rest-connector.ts:47, plugin.ts:291, plugin.ts:339), and THREE surviving hardcoded 30000 writes (mcp-connector.ts:247, slack-connector.ts:94, plugin.ts:1782). ⇒ read a census with the tree it was taken against, or it is not a census. What made the key `dead` was never the absence of readers but the absence of ENFORCEMENT: every one of those six reads is a pass-through whose only termini are the def `GET /connectors` echoes and the fingerprint that decides whether to re-materialize, and `connectorFetchOptions` — the one mapping onto the platform’s outbound `fetch` — was handed `{ retryConfig, requestTimeoutMs }` only. ⇒ a future census on this type counts READS and asks what each one DOES with the value; a grep count answers neither question. Use `requestTimeoutMs` (live, the row below) for the deadline the platform keeps, and bound the connect phase at a connector provider or upstream gateway on a transport that can separate the phases." + "note": "RETIRED (ADR-0049 enforce-or-remove; maintainer ruling 2026-09-22, letter A — the narrower SECOND decision this row asked for). Tombstoned with `retiredKey` because `ConnectorSchema` is not `.strict()` and a plain delete would be a silent strip (ADR-0104); the tombstone is carried by `DeclarativeConnectorEntrySchema` too — both published carriers wrap the same private `ConnectorBaseSchema`, so they are siblings rather than parent and child — so `stack.connectors[]` and `/meta/connector` refuse it too. The row stays because `retiredKey` keeps the key in the walked shape (the `rls.priority` precedent). Registered as `integration/Connector:connectionTimeoutMs` and `integration/DeclarativeConnectorEntry:connectionTimeoutMs` in `RETIRED_KEYS_BY_MAJOR[18]`; authored sources and stored rows are rewritten by the D2 conversion `connector-connection-timeout-ms-removed`, and the withdrawn `ConnectorProviderContext.connectionTimeoutMs` — code, with no authored source to rewrite — by the D3 semantic entry `connector-provider-context-connection-timeout-ms-retired`. The tombstone is packages/spec/src/integration/connector.zod.ts#ConnectorSchema. ⭐ THE CARD’S CENSUS IS STALE, NOT FALSE — and the earlier draft of this note got that wrong, so it is restated as measured. The card’s Leg 2 said every occurrence outside packages/spec is a WRITE of a hardcoded 30000. Re-measured with `git grep -n connectionTimeoutMs SHA -- . ':!packages/spec'` at three trees: at the card’s OWN cited SHA 0870fb5418 that is EXACTLY RIGHT — five non-spec source hits, all five `connectionTimeoutMs: 30000,` (connector-mcp mcp-connector.ts:247, connector-openapi openapi-connector.ts:220, connector-rest rest-connector.ts:113, connector-slack slack-connector.ts:94, service-automation plugin.ts:1734), the card’s table line for line. What superseded it is b929e0a662, the very PR the card flagged as pending. At `origin/main` the same instrument returns THIRTEEN non-test source occurrences over SEVEN files in FIVE packages: six READS (openapi-connector.ts:242, openapi-provider.ts:193, rest-connector.ts:134, rest-provider.ts:64, plugin.ts:307, plugin.ts:1589), four TYPE DECLARATIONS (openapi-connector.ts:135, rest-connector.ts:47, plugin.ts:291, plugin.ts:339), and THREE surviving hardcoded 30000 writes (mcp-connector.ts:247, slack-connector.ts:94, plugin.ts:1782). ⇒ read a census with the tree it was taken against, or it is not a census. What made the key `dead` was never the absence of readers but the absence of ENFORCEMENT: every one of those six reads is a pass-through whose only termini are the def `GET /connectors` echoes and the fingerprint that decides whether to re-materialize, and `connectorFetchOptions` — the one mapping onto the platform’s outbound `fetch` — was handed `{ retryConfig, requestTimeoutMs }` only. ⇒ a future census on this type counts READS and asks what each one DOES with the value; a grep count answers neither question. Use `requestTimeoutMs` (live, the row below) for the deadline the platform keeps, and bound the connect phase at a connector provider or upstream gateway on a transport that can separate the phases." }, "requestTimeoutMs": { "status": "live", diff --git a/packages/spec/src/integration/connector-connection-timeout-retirement.test.ts b/packages/spec/src/integration/connector-connection-timeout-retirement.test.ts index 287e3debb31..7a020825be6 100644 --- a/packages/spec/src/integration/connector-connection-timeout-retirement.test.ts +++ b/packages/spec/src/integration/connector-connection-timeout-retirement.test.ts @@ -26,8 +26,10 @@ * Bookkeeping shapes, pinned below: * 1. `connectionTimeoutMs:` — `retiredKey()` tombstone on the non-strict * `ConnectorSchema` (a bare deletion would be a SILENT STRIP, ADR-0104), - * inherited by `DeclarativeConnectorEntrySchema` (`superRefine`), so the - * refusal reaches `stack.connectors[]` and the `/meta/connector` door; + * carried by `DeclarativeConnectorEntrySchema` too — both published + * carriers wrap the same private `ConnectorBaseSchema`, so they are + * siblings, not parent and child — so the refusal reaches + * `stack.connectors[]` and the `/meta/connector` door; * `integration/Connector:connectionTimeoutMs` and * `integration/DeclarativeConnectorEntry:connectionTimeoutMs` in * `RETIRED_KEYS_BY_MAJOR[18]`. diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index 9b8ba346431..acb46b71592 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -5232,7 +5232,8 @@ const step18: MigrationStep = { + '`requestTimeoutMs`. `requestTimeoutMs` is the replacement and the bound the platform ' + 'can keep. The carrier key is a retiredKey tombstone on the non-strict ' + '`ConnectorSchema` (a bare deletion would be a silent strip), registered under both ' - + 'def keys because `DeclarativeConnectorEntrySchema` inherits it; the D2 conversion ' + + 'def keys because `DeclarativeConnectorEntrySchema` carries it too, both carriers ' + + 'wrapping the same private `ConnectorBaseSchema`; the D2 conversion ' + 'strips it from `connectors[]` as a pure lossless delete — it never had an effect to ' + 'lose — because a stored connector row CAN carry it (the `PUT /meta/connector/:name` ' + 'door persists the authored value and the stored-row rehydration seam is live for ' From 14fdebd766df5a3a2a81eb69ab6c087fc206e46c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 10:53:22 +0000 Subject: [PATCH 12/15] docs(spec): the connector row's falsification note was overtaken and its census stale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found by doing what the round asked — re-reading the WHOLE row against head rather than the named sentences. The closing note asserted two things that are no longer true, and one of them contradicted the correction this same round made forty words earlier: - it recorded retryConfig's 'they are live' claim as falsified, but #18975 made the declared policy execute at the one platform fetch site, so those eight sub-keys are live now and the claim came true after the fact; - its supporting census, 'the word does not occur outside packages/spec at all', is false at this head: git grep over the tree minus packages/spec returns 54 hits over 10 files. Both halves are recorded rather than overwritten — the history of how the type got here is what this row is for — and the census is restated with the command and the tree behind it. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx --- packages/spec/liveness/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index 03bc10c92b4..b9410b5d0f9 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -939,7 +939,7 @@ marker where the Notes cell goes, never a guess at what belongs there. | rest_api | seeded 2026-09-21 (#14640) — the FIFTH `RestServerConfig` sub-object, enrolled a round after the four above and deliberately so. #14369 left `RestApiConfigSchema` out because the `api` block's consumption seam was then still VALIDATE-ONLY (#11637 ran the declared contract and discarded its output), so a census would have recorded a half that was about to move; the gate source and four rows of this table said as much. That fence was re-tested before a line of this ledger was written and it has EXPIRED: `RestServer.normalizeConfig` now BUILDS the `api` block from `parseDeclaredApiConfig`'s output — “the asymmetry is gone and all five now build from their parsed output” — and the change is RELEASED, not in flight, with `packages/rest/CHANGELOG.md` re-stating the same zero this file records. ⛔ **The ledger is `rest_api.json`, NOT `api.json`**: that name was already taken by `ApiEndpointSchema`, the registered `api` metadata type with real consumers in the matcher, executor, policy chain and mapping layer — one spelling, two unrelated meanings inside `packages/spec`, and filing here would have published one file's measurement under the other's name. Live 12 = `version` / `basePath` / `apiPath`, which `getApiBasePath` splices into the prefix of EVERY mounted route (read through a whole-block destructure, which is why the dead-key census below had to sweep destructuring shapes and not a property-access pattern alone), the eight `enable*` switches, each gating a mount and most of them also the discovery document's capability block, and `projectResolution`. Dead 14 = the `requireAuth` tombstone (#3963, still `.omit()`ed by this seam because #3963 chose warn-and-ignore and converting that to a boot failure is that decision's to make), plus the two declared containers `documentation` (drilled to ten, including its nested `contact` / `license`) and `responseFormat` (three) — normalized into `this.config.api` and read back by nothing, so `responseFormat.envelope: false` unwraps no response and `documentation.title` retitles no served document. Every zero carries a lit control on the same instrument (twelve sibling keys on the same block return 1-2 reads), each of the three shapes a spelling sweep is blind to was swept with its own control, and the backstop is structural rather than textual: `NormalizedRestServerConfig` is module-local with no `export` and `RestServer.config` is `private`, so the normalized block cannot be reached from outside that one class. ⛔ **The two dead containers do NOT share one verdict**: `documentation`'s members are OpenAPI `info` fields whose enforce route collides with a recorded ownership decision (`info` is written by `build-openapi.ts` and passed through untouched by #11646), while `responseFormat`'s enforce route means making the response envelope configurable — a larger claim. This file records status; the enforce-or-remove call per key is a follow-up on the human floor. `evidenceScope` stays `in-repo`: objectui was measured clean at the pinned sha and at head against a lit control, but the closed cloud runtime was not reachable from the measuring container, so #14796's structural reading is cited as a standing reading rather than re-claimed as a sweep | | realtime_subscription | seeded 2026-09-04 (#14446) — a TRANSPORT-PROTOCOL surface, the fifth category the `SPEC_ONLY_SCHEMAS` override has had to reach. `SubscriptionSchema` (`packages/spec/src/api/realtime.zod.ts`) is what a client declares to open a realtime subscription: the item type of `RealtimeConfigSchema.subscriptions` and the `Subscription` the generated API reference publishes. Like `query` it is a request surface rather than stored metadata, and like `query` that is exactly why it went unasked — no registry holds it, `RealtimeConfigSchema` is `.passthrough()` so nothing downstream even refuses an unknown key, and the whole vocabulary sat outside the denominator while the reference kept publishing it. Rooted on `SubscriptionSchema` rather than on `RealtimeConfigSchema` for the reason the four `RestServerConfig` sub-objects document one row up: the walk drilled exactly ONE level when this was rooted (it recurses as of #17424; the rooting stands), so with the config as the root `events[].type` and `events[].filters` would inherit a container verdict instead of carrying rows of their own — #4956's shape. **Dead 6 = every key it has, and the CONTAINER is the finding**: nothing outside `packages/spec` imports `SubscriptionSchema`, `SubscriptionEventSchema` or `RealtimeConfigSchema` at all, so no key beneath them can be read (the `manifest.contributes` reasoning). The two keys the card measured are the sharp ones. `events[].type` accepts `RealtimeEventType`, whose four members (`record.created` / `record.updated` / `record.deleted` / `field.changed`) are DISJOINT from what the engine publishes (`DataEventType`'s `data.record.*`, live emitter in `service-knowledge`), so an author who writes the enum's own `record.created` gets a subscription that silently never fires — and the enum is what the API reference shows them. Its direction is settled by the 2026-09-02 triage and quoted verbatim in the row: enforce means REPOINTING THE ENUM, never changing what the runtime publishes. `field.changed` is the same spelling the sibling `DataEventType` REMOVED in 17.0.0 (#4673, PR #4685) for having no producer; it survives here only because this enum was never in a ratchet's denominator. `events[].filters` is `z.unknown().optional()` — the textbook ADR-0049 fourth state, no shape and no reader, failing in the permissive direction (a subscriber who filters receives every event). ⚠️ Three spellings of a realtime subscription exist and only the third is executed: this one, `websocket.zod.ts#EventSubscriptionSchema`, and the plain interface `contracts/realtime-service.ts#RealtimeSubscriptionOptions` that `in-memory-realtime-adapter.ts#matchesSubscription` actually reads. The file note names the same-name-different-shape traps so the next census does not mistake one for a consumer. Zero live | | sharing_rule | seeded 2026-09-17 (#18582) — the second of the three `PENDING_GOVERNANCE` debts #18133 declared, and the first one PAID (`connector` and `analytics_cube` are still owed on that card). Not a registered kind: it is bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reaches the walk through `getMetadataTypeSchema`'s unregistered-kind fallback, so this ledger governs a type `listMetadataTypeSchemaTypes()` still does not enumerate. One shape fact decides every row: the AUTHORING shape is not the ENFORCED shape. ADR-0057 D6 makes the `sys_sharing_rule` row canonical (`object_name` + `criteria_json` + `recipient_type`/`recipient_id` + `access_level`) and `bootstrapDeclaredSharingRules` translates each authored key into it at boot — nothing re-parses `SharingRuleSchema` at enforcement time — so every consumer cited reads a COLUMN and every row carries the `producer` (#4837) that populates it, which is the `seed.env` lesson applied to a whole type rather than to one key. Preview read points ENUMERATED per the #7131 rule and the answer recorded rather than skipped: `registerBuiltinPreviews()` (objectui @dda8f381) registers twenty types and `sharing_rule` is not one of them; what objectui does consume is the whole shape, on the CREATE door only (`AUTHOR_SHAPE_ONLY_TYPES` — the EDIT door is deliberately ungated because a served body carries the `_diagnostics` decoration this `.strict()` schema rejects). The single non-`live` row is `type`, the `SharingRuleType` discriminator: one member, `criteria`, whose only reader is a defensive `=== 'owner'` comparison that is unreachable for every value the schema admits. `planned` on the `action.operation` precedent (a one-member discriminator held `planned` until a runtime half dispatched on it, #15080), and deliberately NOT an enforce-or-remove candidate: the key is required, so removing it would break every authored rule to delete nothing. | -| connector | seeded 2026-09-17 (#18582) — the second of the three `PENDING_GOVERNANCE` debts #18133 declared, paid in the same diff as `analytics_cube`, which empties that map. Not a registered kind: bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reached through `getMetadataTypeSchema`'s unregistered-kind fallback. **What the walk actually resolves, measured:** the binding names `DeclarativeConnectorEntrySchema`. ⚠️ The MECHANISM changed with the `connectionTimeoutMs` retirement and the prior sentence here is corrected rather than carried: that schema USED TO BE `ConnectorSchema.superRefine(...)`, a Zod 4 check attached to the same object def, and the key-set conclusion used to rest on that attachment. It is now a `z.preprocess` PIPE — both published carriers wrap one shared private `ConnectorBaseSchema` in the ADR-0049 retired-default residue stage, the entry schema adding the ADR-0097 cross-field rules on the base before wrapping, so the two are SIBLINGS rather than parent and child, and what preserves the walked shape is the pipe's read-through `shape`, NOT a `superRefine` attachment. The CONCLUSION is unchanged and re-measured on the built entry rather than inherited: both carriers expose 30 keys and the key sets are byte-identical, with no entry-only and no base-only key. The gate cannot tell the two schemas apart; what the entry schema buys is REFUSALS, invisible to the walk and visible only in the three rows where they are the whole verdict. **ONE SCHEMA, TWO DOORS** is the shape fact behind the 29/1/44 split (live/planned/dead; counts read from the generated `state-counts.md` row, never hand-kept here): the ledger's denominator entry exists for the AUTHORING doors (`defineStack({ connectors })`, `PUT /meta/connector/:name`), while the same `ConnectorSchema` is what `AutomationEngine.registerConnector` parses for a def a PLUGIN or an ADR-0097 provider factory builds in code — so a key can have a real consumer and still do nothing when a metadata author writes it. The keys an authored entry can reach are exactly the `ConnectorProviderContext` fields plus `name` and `enabled`; `type` and `icon` reach that context and are dropped by all three shipped factories, and each says so on its own row. `authentication` is the ledger's `planned`: refused outright by ADR-0097 §3 (#7990) rather than ignored. The 44 `dead`, re-measured at this head and partitioned so every row is counted exactly once: three declared subsystems with no engine — `syncConfig` (8), `fieldMappings` (7), `health` (15, both sub-blocks) — plus `triggers` (6, and the schema's own docblock says so: #3197), the connector's nested `webhooks` (one blanket verdict, recorded in the undrilled baseline), `status`, `metadata`, `actions.description`/`.outputSchema`, and the three top-level `retiredKey` tombstones `rateLimitConfig`, `errorMapping` and `connectionTimeoutMs`. That sums to 44, the dead count the generated `state-counts.md` row carries. ⚠️ `retryConfig` IS NO LONGER IN THIS LIST: all eight of its sub-keys went `live` when #18975 made the declared policy execute at the one platform fetch site, which is the same measurement the falsification note at the end of this row records — so a reader who still finds "`retryConfig` (8)" among the dead is reading a stale copy. ⚠️ Nor is it "the two timeouts" any more: `requestTimeoutMs` is `live` (it becomes `resilientFetch`'s per-attempt deadline) and `connectionTimeoutMs` is the retired tombstone named above. ⭐ SIX rows in this ledger are `retiredKey` tombstones that keep their rows because the key stays in the walked shape (the `rls.priority` precedent) — `rateLimitConfig`, `errorMapping`, `connectionTimeoutMs`, `fieldMappings.transform`, `triggers.interval` and `health.circuitBreaker.monitoringWindow` — but ⛔ that six is NOT a separate addend: the last three are already inside the `fieldMappings`, `triggers` and `health` counts above, which is exactly the double-count that made the previous "and four `retiredKey` tombstones" tail drift. Count them by name, never by adding the tail. **A prior in-repo claim was falsified here**: the conversion registry's note that `retryConfig` "and the timeouts beside it are untouched — they are live" is corrected on those rows — the word does not occur outside `packages/spec` at all | +| connector | seeded 2026-09-17 (#18582) — the second of the three `PENDING_GOVERNANCE` debts #18133 declared, paid in the same diff as `analytics_cube`, which empties that map. Not a registered kind: bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reached through `getMetadataTypeSchema`'s unregistered-kind fallback. **What the walk actually resolves, measured:** the binding names `DeclarativeConnectorEntrySchema`. ⚠️ The MECHANISM changed with the `connectionTimeoutMs` retirement and the prior sentence here is corrected rather than carried: that schema USED TO BE `ConnectorSchema.superRefine(...)`, a Zod 4 check attached to the same object def, and the key-set conclusion used to rest on that attachment. It is now a `z.preprocess` PIPE — both published carriers wrap one shared private `ConnectorBaseSchema` in the ADR-0049 retired-default residue stage, the entry schema adding the ADR-0097 cross-field rules on the base before wrapping, so the two are SIBLINGS rather than parent and child, and what preserves the walked shape is the pipe's read-through `shape`, NOT a `superRefine` attachment. The CONCLUSION is unchanged and re-measured on the built entry rather than inherited: both carriers expose 30 keys and the key sets are byte-identical, with no entry-only and no base-only key. The gate cannot tell the two schemas apart; what the entry schema buys is REFUSALS, invisible to the walk and visible only in the three rows where they are the whole verdict. **ONE SCHEMA, TWO DOORS** is the shape fact behind the 29/1/44 split (live/planned/dead; counts read from the generated `state-counts.md` row, never hand-kept here): the ledger's denominator entry exists for the AUTHORING doors (`defineStack({ connectors })`, `PUT /meta/connector/:name`), while the same `ConnectorSchema` is what `AutomationEngine.registerConnector` parses for a def a PLUGIN or an ADR-0097 provider factory builds in code — so a key can have a real consumer and still do nothing when a metadata author writes it. The keys an authored entry can reach are exactly the `ConnectorProviderContext` fields plus `name` and `enabled`; `type` and `icon` reach that context and are dropped by all three shipped factories, and each says so on its own row. `authentication` is the ledger's `planned`: refused outright by ADR-0097 §3 (#7990) rather than ignored. The 44 `dead`, re-measured at this head and partitioned so every row is counted exactly once: three declared subsystems with no engine — `syncConfig` (8), `fieldMappings` (7), `health` (15, both sub-blocks) — plus `triggers` (6, and the schema's own docblock says so: #3197), the connector's nested `webhooks` (one blanket verdict, recorded in the undrilled baseline), `status`, `metadata`, `actions.description`/`.outputSchema`, and the three top-level `retiredKey` tombstones `rateLimitConfig`, `errorMapping` and `connectionTimeoutMs`. That sums to 44, the dead count the generated `state-counts.md` row carries. ⚠️ `retryConfig` IS NO LONGER IN THIS LIST: all eight of its sub-keys went `live` when #18975 made the declared policy execute at the one platform fetch site, which is the same measurement the falsification note at the end of this row records — so a reader who still finds "`retryConfig` (8)" among the dead is reading a stale copy. ⚠️ Nor is it "the two timeouts" any more: `requestTimeoutMs` is `live` (it becomes `resilientFetch`'s per-attempt deadline) and `connectionTimeoutMs` is the retired tombstone named above. ⭐ SIX rows in this ledger are `retiredKey` tombstones that keep their rows because the key stays in the walked shape (the `rls.priority` precedent) — `rateLimitConfig`, `errorMapping`, `connectionTimeoutMs`, `fieldMappings.transform`, `triggers.interval` and `health.circuitBreaker.monitoringWindow` — but ⛔ that six is NOT a separate addend: the last three are already inside the `fieldMappings`, `triggers` and `health` counts above, which is exactly the double-count that made the previous "and four `retiredKey` tombstones" tail drift. Count them by name, never by adding the tail. **A prior in-repo claim was falsified here, and has since been OVERTAKEN — both halves recorded, because this row's job is the history of how the type got here**: the conversion registry's note that `retryConfig` "and the timeouts beside it are untouched — they are live" was false WHEN SEEDED (2026-09-17), and the ledger corrected it on those rows. It is no longer false of `retryConfig`: #18975 made the declared policy execute at the one platform fetch site (`connectorFetchOptions` → `resilientFetch`), so those eight sub-keys are `live` on their own rows now and the note's claim about them came true after the fact rather than at the time. ⚠️ The seeding note's supporting census — "the word does not occur outside `packages/spec` at all" — is FALSE at this head and is corrected rather than carried: `git grep -n retryConfig -- . ':!packages/spec'` returns 54 hits over 10 files (the materializer and the rest/openapi providers and connectors, plus their tests). ⛔ Re-read that as the standing lesson of this row: a census is a count plus the tree it was taken against, and a bare "does not occur" with no commit behind it is the shape that rots first. The timeouts half is settled on its own rows: `requestTimeoutMs` is `live`, `connectionTimeoutMs` is retired | | analytics_cube | seeded 2026-09-17 (#18582) — the third debt, paid in the same diff as `connector`. Not a registered kind either: bound in `UNREGISTERED_KIND_SCHEMAS` by #10194 and reached through the same unregistered-kind fallback. **ONE Cube shape, THREE producers, one registry** is what decides every row: `cube-registry.ts` names them itself — authored cubes (`analyticsCubes[]` / `defineCube()`, threaded by the CLI into `AnalyticsServiceConfig.cubes`), COMPILED DATASETS (ADR-0021, where `dataset-compiler` mints a Cube), and ad-hoc query inference. Only the first is the authoring door governed here, so a key whose only reader sits on the compiled-dataset path is not live for an authored cube however busy that reader is — the #4837 producer rule on a shape with three producers. That is `dimensions.granularities` (read by `dataset-executor#granularityOf`, whose argument is a `CompiledDataset` an authored cube never becomes) and `measures.format` (written by the compiler, threaded to the wire from the DATASET measure instead). The query path is genuinely live: `sql` is the FROM table AND the object whose RLS read scope is injected, `measures.type` picks the aggregate, `measures.sql`/`dimensions.sql` the column, `joins[].name` the joined table. The 10 `dead` are the caching block (`refreshKey.every`/`.sql` — no refresh scheduler exists anywhere), the access-control flag (`public` — three sites write `false`, nothing reads it: a knob that was never wired, not a hole that was opened), the three `description`s, and the inner `name` on each of `measures`/`dimensions`, where the record KEY is the identity. It was 12 until #18612 RETIRED `joins[].relationship` and the REQUIRED `joins[].sql` (ADR-0049 enforce-or-remove, maintainer-ruled batch #154): the ON clause is SYNTHESISED as an FK equality and the authored one was never consulted, so a declared join condition came back REPLACED under a 200. `CubeJoinSchema` is a `strictObject`, so the route was strict deletion plus a `guidance` prescription and the two rows left this ledger with the keys — not the `retiredKey()` route, which keeps the row. **#10238 is not prejudged**: whether cube authoring is live end to end is still its own measurement — this ledger answers the per-key question only | The `dead` set across types is the enforce-or-remove worklist (ADR-0049); every From 0d119d5ed32443fc4040d85b61d30d77aea72f9f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 11:32:36 +0000 Subject: [PATCH 13/15] docs(spec): pin the connector row's retryConfig census to its tree and command The census clause printed 54 hits over 10 files while printing a command that returns 67 over 15: the reading was taken with `.changeset` excluded and the exclusion was never written down, and the parenthetical covered neither of the two `content/docs` pages it returns. Print the command that produces the number, pinned to the tree it was taken against, and make the parenthetical account for all fifteen files. Same row: `name` is itself a `ConnectorProviderContext` field, so the "plus `name`" tail double-counted it, while `provider` -- which selects the factory and never reaches the context -- sat outside the "exactly". `loadPackageFile` is host-injected rather than authored. Correct the set. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude --- packages/spec/liveness/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index b9410b5d0f9..a5056de49a2 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -939,7 +939,7 @@ marker where the Notes cell goes, never a guess at what belongs there. | rest_api | seeded 2026-09-21 (#14640) — the FIFTH `RestServerConfig` sub-object, enrolled a round after the four above and deliberately so. #14369 left `RestApiConfigSchema` out because the `api` block's consumption seam was then still VALIDATE-ONLY (#11637 ran the declared contract and discarded its output), so a census would have recorded a half that was about to move; the gate source and four rows of this table said as much. That fence was re-tested before a line of this ledger was written and it has EXPIRED: `RestServer.normalizeConfig` now BUILDS the `api` block from `parseDeclaredApiConfig`'s output — “the asymmetry is gone and all five now build from their parsed output” — and the change is RELEASED, not in flight, with `packages/rest/CHANGELOG.md` re-stating the same zero this file records. ⛔ **The ledger is `rest_api.json`, NOT `api.json`**: that name was already taken by `ApiEndpointSchema`, the registered `api` metadata type with real consumers in the matcher, executor, policy chain and mapping layer — one spelling, two unrelated meanings inside `packages/spec`, and filing here would have published one file's measurement under the other's name. Live 12 = `version` / `basePath` / `apiPath`, which `getApiBasePath` splices into the prefix of EVERY mounted route (read through a whole-block destructure, which is why the dead-key census below had to sweep destructuring shapes and not a property-access pattern alone), the eight `enable*` switches, each gating a mount and most of them also the discovery document's capability block, and `projectResolution`. Dead 14 = the `requireAuth` tombstone (#3963, still `.omit()`ed by this seam because #3963 chose warn-and-ignore and converting that to a boot failure is that decision's to make), plus the two declared containers `documentation` (drilled to ten, including its nested `contact` / `license`) and `responseFormat` (three) — normalized into `this.config.api` and read back by nothing, so `responseFormat.envelope: false` unwraps no response and `documentation.title` retitles no served document. Every zero carries a lit control on the same instrument (twelve sibling keys on the same block return 1-2 reads), each of the three shapes a spelling sweep is blind to was swept with its own control, and the backstop is structural rather than textual: `NormalizedRestServerConfig` is module-local with no `export` and `RestServer.config` is `private`, so the normalized block cannot be reached from outside that one class. ⛔ **The two dead containers do NOT share one verdict**: `documentation`'s members are OpenAPI `info` fields whose enforce route collides with a recorded ownership decision (`info` is written by `build-openapi.ts` and passed through untouched by #11646), while `responseFormat`'s enforce route means making the response envelope configurable — a larger claim. This file records status; the enforce-or-remove call per key is a follow-up on the human floor. `evidenceScope` stays `in-repo`: objectui was measured clean at the pinned sha and at head against a lit control, but the closed cloud runtime was not reachable from the measuring container, so #14796's structural reading is cited as a standing reading rather than re-claimed as a sweep | | realtime_subscription | seeded 2026-09-04 (#14446) — a TRANSPORT-PROTOCOL surface, the fifth category the `SPEC_ONLY_SCHEMAS` override has had to reach. `SubscriptionSchema` (`packages/spec/src/api/realtime.zod.ts`) is what a client declares to open a realtime subscription: the item type of `RealtimeConfigSchema.subscriptions` and the `Subscription` the generated API reference publishes. Like `query` it is a request surface rather than stored metadata, and like `query` that is exactly why it went unasked — no registry holds it, `RealtimeConfigSchema` is `.passthrough()` so nothing downstream even refuses an unknown key, and the whole vocabulary sat outside the denominator while the reference kept publishing it. Rooted on `SubscriptionSchema` rather than on `RealtimeConfigSchema` for the reason the four `RestServerConfig` sub-objects document one row up: the walk drilled exactly ONE level when this was rooted (it recurses as of #17424; the rooting stands), so with the config as the root `events[].type` and `events[].filters` would inherit a container verdict instead of carrying rows of their own — #4956's shape. **Dead 6 = every key it has, and the CONTAINER is the finding**: nothing outside `packages/spec` imports `SubscriptionSchema`, `SubscriptionEventSchema` or `RealtimeConfigSchema` at all, so no key beneath them can be read (the `manifest.contributes` reasoning). The two keys the card measured are the sharp ones. `events[].type` accepts `RealtimeEventType`, whose four members (`record.created` / `record.updated` / `record.deleted` / `field.changed`) are DISJOINT from what the engine publishes (`DataEventType`'s `data.record.*`, live emitter in `service-knowledge`), so an author who writes the enum's own `record.created` gets a subscription that silently never fires — and the enum is what the API reference shows them. Its direction is settled by the 2026-09-02 triage and quoted verbatim in the row: enforce means REPOINTING THE ENUM, never changing what the runtime publishes. `field.changed` is the same spelling the sibling `DataEventType` REMOVED in 17.0.0 (#4673, PR #4685) for having no producer; it survives here only because this enum was never in a ratchet's denominator. `events[].filters` is `z.unknown().optional()` — the textbook ADR-0049 fourth state, no shape and no reader, failing in the permissive direction (a subscriber who filters receives every event). ⚠️ Three spellings of a realtime subscription exist and only the third is executed: this one, `websocket.zod.ts#EventSubscriptionSchema`, and the plain interface `contracts/realtime-service.ts#RealtimeSubscriptionOptions` that `in-memory-realtime-adapter.ts#matchesSubscription` actually reads. The file note names the same-name-different-shape traps so the next census does not mistake one for a consumer. Zero live | | sharing_rule | seeded 2026-09-17 (#18582) — the second of the three `PENDING_GOVERNANCE` debts #18133 declared, and the first one PAID (`connector` and `analytics_cube` are still owed on that card). Not a registered kind: it is bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reaches the walk through `getMetadataTypeSchema`'s unregistered-kind fallback, so this ledger governs a type `listMetadataTypeSchemaTypes()` still does not enumerate. One shape fact decides every row: the AUTHORING shape is not the ENFORCED shape. ADR-0057 D6 makes the `sys_sharing_rule` row canonical (`object_name` + `criteria_json` + `recipient_type`/`recipient_id` + `access_level`) and `bootstrapDeclaredSharingRules` translates each authored key into it at boot — nothing re-parses `SharingRuleSchema` at enforcement time — so every consumer cited reads a COLUMN and every row carries the `producer` (#4837) that populates it, which is the `seed.env` lesson applied to a whole type rather than to one key. Preview read points ENUMERATED per the #7131 rule and the answer recorded rather than skipped: `registerBuiltinPreviews()` (objectui @dda8f381) registers twenty types and `sharing_rule` is not one of them; what objectui does consume is the whole shape, on the CREATE door only (`AUTHOR_SHAPE_ONLY_TYPES` — the EDIT door is deliberately ungated because a served body carries the `_diagnostics` decoration this `.strict()` schema rejects). The single non-`live` row is `type`, the `SharingRuleType` discriminator: one member, `criteria`, whose only reader is a defensive `=== 'owner'` comparison that is unreachable for every value the schema admits. `planned` on the `action.operation` precedent (a one-member discriminator held `planned` until a runtime half dispatched on it, #15080), and deliberately NOT an enforce-or-remove candidate: the key is required, so removing it would break every authored rule to delete nothing. | -| connector | seeded 2026-09-17 (#18582) — the second of the three `PENDING_GOVERNANCE` debts #18133 declared, paid in the same diff as `analytics_cube`, which empties that map. Not a registered kind: bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reached through `getMetadataTypeSchema`'s unregistered-kind fallback. **What the walk actually resolves, measured:** the binding names `DeclarativeConnectorEntrySchema`. ⚠️ The MECHANISM changed with the `connectionTimeoutMs` retirement and the prior sentence here is corrected rather than carried: that schema USED TO BE `ConnectorSchema.superRefine(...)`, a Zod 4 check attached to the same object def, and the key-set conclusion used to rest on that attachment. It is now a `z.preprocess` PIPE — both published carriers wrap one shared private `ConnectorBaseSchema` in the ADR-0049 retired-default residue stage, the entry schema adding the ADR-0097 cross-field rules on the base before wrapping, so the two are SIBLINGS rather than parent and child, and what preserves the walked shape is the pipe's read-through `shape`, NOT a `superRefine` attachment. The CONCLUSION is unchanged and re-measured on the built entry rather than inherited: both carriers expose 30 keys and the key sets are byte-identical, with no entry-only and no base-only key. The gate cannot tell the two schemas apart; what the entry schema buys is REFUSALS, invisible to the walk and visible only in the three rows where they are the whole verdict. **ONE SCHEMA, TWO DOORS** is the shape fact behind the 29/1/44 split (live/planned/dead; counts read from the generated `state-counts.md` row, never hand-kept here): the ledger's denominator entry exists for the AUTHORING doors (`defineStack({ connectors })`, `PUT /meta/connector/:name`), while the same `ConnectorSchema` is what `AutomationEngine.registerConnector` parses for a def a PLUGIN or an ADR-0097 provider factory builds in code — so a key can have a real consumer and still do nothing when a metadata author writes it. The keys an authored entry can reach are exactly the `ConnectorProviderContext` fields plus `name` and `enabled`; `type` and `icon` reach that context and are dropped by all three shipped factories, and each says so on its own row. `authentication` is the ledger's `planned`: refused outright by ADR-0097 §3 (#7990) rather than ignored. The 44 `dead`, re-measured at this head and partitioned so every row is counted exactly once: three declared subsystems with no engine — `syncConfig` (8), `fieldMappings` (7), `health` (15, both sub-blocks) — plus `triggers` (6, and the schema's own docblock says so: #3197), the connector's nested `webhooks` (one blanket verdict, recorded in the undrilled baseline), `status`, `metadata`, `actions.description`/`.outputSchema`, and the three top-level `retiredKey` tombstones `rateLimitConfig`, `errorMapping` and `connectionTimeoutMs`. That sums to 44, the dead count the generated `state-counts.md` row carries. ⚠️ `retryConfig` IS NO LONGER IN THIS LIST: all eight of its sub-keys went `live` when #18975 made the declared policy execute at the one platform fetch site, which is the same measurement the falsification note at the end of this row records — so a reader who still finds "`retryConfig` (8)" among the dead is reading a stale copy. ⚠️ Nor is it "the two timeouts" any more: `requestTimeoutMs` is `live` (it becomes `resilientFetch`'s per-attempt deadline) and `connectionTimeoutMs` is the retired tombstone named above. ⭐ SIX rows in this ledger are `retiredKey` tombstones that keep their rows because the key stays in the walked shape (the `rls.priority` precedent) — `rateLimitConfig`, `errorMapping`, `connectionTimeoutMs`, `fieldMappings.transform`, `triggers.interval` and `health.circuitBreaker.monitoringWindow` — but ⛔ that six is NOT a separate addend: the last three are already inside the `fieldMappings`, `triggers` and `health` counts above, which is exactly the double-count that made the previous "and four `retiredKey` tombstones" tail drift. Count them by name, never by adding the tail. **A prior in-repo claim was falsified here, and has since been OVERTAKEN — both halves recorded, because this row's job is the history of how the type got here**: the conversion registry's note that `retryConfig` "and the timeouts beside it are untouched — they are live" was false WHEN SEEDED (2026-09-17), and the ledger corrected it on those rows. It is no longer false of `retryConfig`: #18975 made the declared policy execute at the one platform fetch site (`connectorFetchOptions` → `resilientFetch`), so those eight sub-keys are `live` on their own rows now and the note's claim about them came true after the fact rather than at the time. ⚠️ The seeding note's supporting census — "the word does not occur outside `packages/spec` at all" — is FALSE at this head and is corrected rather than carried: `git grep -n retryConfig -- . ':!packages/spec'` returns 54 hits over 10 files (the materializer and the rest/openapi providers and connectors, plus their tests). ⛔ Re-read that as the standing lesson of this row: a census is a count plus the tree it was taken against, and a bare "does not occur" with no commit behind it is the shape that rots first. The timeouts half is settled on its own rows: `requestTimeoutMs` is `live`, `connectionTimeoutMs` is retired | +| connector | seeded 2026-09-17 (#18582) — the second of the three `PENDING_GOVERNANCE` debts #18133 declared, paid in the same diff as `analytics_cube`, which empties that map. Not a registered kind: bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reached through `getMetadataTypeSchema`'s unregistered-kind fallback. **What the walk actually resolves, measured:** the binding names `DeclarativeConnectorEntrySchema`. ⚠️ The MECHANISM changed with the `connectionTimeoutMs` retirement and the prior sentence here is corrected rather than carried: that schema USED TO BE `ConnectorSchema.superRefine(...)`, a Zod 4 check attached to the same object def, and the key-set conclusion used to rest on that attachment. It is now a `z.preprocess` PIPE — both published carriers wrap one shared private `ConnectorBaseSchema` in the ADR-0049 retired-default residue stage, the entry schema adding the ADR-0097 cross-field rules on the base before wrapping, so the two are SIBLINGS rather than parent and child, and what preserves the walked shape is the pipe's read-through `shape`, NOT a `superRefine` attachment. The CONCLUSION is unchanged and re-measured on the built entry rather than inherited: both carriers expose 30 keys and the key sets are byte-identical, with no entry-only and no base-only key. The gate cannot tell the two schemas apart; what the entry schema buys is REFUSALS, invisible to the walk and visible only in the three rows where they are the whole verdict. **ONE SCHEMA, TWO DOORS** is the shape fact behind the 29/1/44 split (live/planned/dead; counts read from the generated `state-counts.md` row, never hand-kept here): the ledger's denominator entry exists for the AUTHORING doors (`defineStack({ connectors })`, `PUT /meta/connector/:name`), while the same `ConnectorSchema` is what `AutomationEngine.registerConnector` parses for a def a PLUGIN or an ADR-0097 provider factory builds in code — so a key can have a real consumer and still do nothing when a metadata author writes it. The keys an authored entry can reach are exactly the author-supplied `ConnectorProviderContext` fields plus `provider` and `enabled` — `name` is itself one of those fields (the former "plus `name`" tail double-counted it), `loadPackageFile` is host-injected rather than authored, and `provider` selects the factory without ever reaching the context; `type` and `icon` reach that context and are dropped by all three shipped factories, and each says so on its own row. `authentication` is the ledger's `planned`: refused outright by ADR-0097 §3 (#7990) rather than ignored. The 44 `dead`, re-measured at this head and partitioned so every row is counted exactly once: three declared subsystems with no engine — `syncConfig` (8), `fieldMappings` (7), `health` (15, both sub-blocks) — plus `triggers` (6, and the schema's own docblock says so: #3197), the connector's nested `webhooks` (one blanket verdict, recorded in the undrilled baseline), `status`, `metadata`, `actions.description`/`.outputSchema`, and the three top-level `retiredKey` tombstones `rateLimitConfig`, `errorMapping` and `connectionTimeoutMs`. That sums to 44, the dead count the generated `state-counts.md` row carries. ⚠️ `retryConfig` IS NO LONGER IN THIS LIST: all eight of its sub-keys went `live` when #18975 made the declared policy execute at the one platform fetch site, which is the same measurement the falsification note at the end of this row records — so a reader who still finds "`retryConfig` (8)" among the dead is reading a stale copy. ⚠️ Nor is it "the two timeouts" any more: `requestTimeoutMs` is `live` (it becomes `resilientFetch`'s per-attempt deadline) and `connectionTimeoutMs` is the retired tombstone named above. ⭐ SIX rows in this ledger are `retiredKey` tombstones that keep their rows because the key stays in the walked shape (the `rls.priority` precedent) — `rateLimitConfig`, `errorMapping`, `connectionTimeoutMs`, `fieldMappings.transform`, `triggers.interval` and `health.circuitBreaker.monitoringWindow` — but ⛔ that six is NOT a separate addend: the last three are already inside the `fieldMappings`, `triggers` and `health` counts above, which is exactly the double-count that made the previous "and four `retiredKey` tombstones" tail drift. Count them by name, never by adding the tail. **A prior in-repo claim was falsified here, and has since been OVERTAKEN — both halves recorded, because this row's job is the history of how the type got here**: the conversion registry's note that `retryConfig` "and the timeouts beside it are untouched — they are live" was false WHEN SEEDED (2026-09-17), and the ledger corrected it on those rows. It is no longer false of `retryConfig`: #18975 made the declared policy execute at the one platform fetch site (`connectorFetchOptions` → `resilientFetch`), so those eight sub-keys are `live` on their own rows now and the note's claim about them came true after the fact rather than at the time. ⚠️ The seeding note's supporting census — "the word does not occur outside `packages/spec` at all" — is FALSE at this head and is corrected rather than carried: `git grep -n retryConfig 14fdebd766 -- . ':!packages/spec'` returns 67 hits over 15 files (26 in the materializer `packages/services/service-automation/src/plugin.ts` and its materialization test, 22 across `connector-rest` and `connector-openapi` — providers, connectors and their tests — 13 in five `.changeset` fragments, and 6 on two `content/docs` pages). ⛔ Re-read that as the standing lesson of this row: a census is a count plus the tree it was taken against, and a bare "does not occur" with no commit behind it is the shape that rots first. The timeouts half is settled on its own rows: `requestTimeoutMs` is `live`, `connectionTimeoutMs` is retired | | analytics_cube | seeded 2026-09-17 (#18582) — the third debt, paid in the same diff as `connector`. Not a registered kind either: bound in `UNREGISTERED_KIND_SCHEMAS` by #10194 and reached through the same unregistered-kind fallback. **ONE Cube shape, THREE producers, one registry** is what decides every row: `cube-registry.ts` names them itself — authored cubes (`analyticsCubes[]` / `defineCube()`, threaded by the CLI into `AnalyticsServiceConfig.cubes`), COMPILED DATASETS (ADR-0021, where `dataset-compiler` mints a Cube), and ad-hoc query inference. Only the first is the authoring door governed here, so a key whose only reader sits on the compiled-dataset path is not live for an authored cube however busy that reader is — the #4837 producer rule on a shape with three producers. That is `dimensions.granularities` (read by `dataset-executor#granularityOf`, whose argument is a `CompiledDataset` an authored cube never becomes) and `measures.format` (written by the compiler, threaded to the wire from the DATASET measure instead). The query path is genuinely live: `sql` is the FROM table AND the object whose RLS read scope is injected, `measures.type` picks the aggregate, `measures.sql`/`dimensions.sql` the column, `joins[].name` the joined table. The 10 `dead` are the caching block (`refreshKey.every`/`.sql` — no refresh scheduler exists anywhere), the access-control flag (`public` — three sites write `false`, nothing reads it: a knob that was never wired, not a hole that was opened), the three `description`s, and the inner `name` on each of `measures`/`dimensions`, where the record KEY is the identity. It was 12 until #18612 RETIRED `joins[].relationship` and the REQUIRED `joins[].sql` (ADR-0049 enforce-or-remove, maintainer-ruled batch #154): the ON clause is SYNTHESISED as an FK equality and the authored one was never consulted, so a declared join condition came back REPLACED under a 200. `CubeJoinSchema` is a `strictObject`, so the route was strict deletion plus a `guidance` prescription and the two rows left this ledger with the keys — not the `retiredKey()` route, which keeps the row. **#10238 is not prejudged**: whether cube authoring is live end to end is still its own measurement — this ledger answers the per-key question only | The `dead` set across types is the enforce-or-remove worklist (ADR-0049); every From 657788103c9bd6cfca04ad4aaae85b6920476514 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 13:00:50 +0000 Subject: [PATCH 14/15] docs(spec): correct the connector ledger's key-reach and authentication claims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `_note` in `packages/spec/liveness/connector.json` still carried the uncorrected form of the key-reach claim after the README row was fixed, and its tail asserted something the same file's own `actions.key` row already contradicted. Four measured corrections, all in one sentence: 1. `name` IS a `ConnectorProviderContext` field (`connector-provider.ts:68`), so "plus `name`" double-counted it; 2. `provider` is read on the AUTHORING door and is not on the interface — `plugin.ts:1478` gates the desired set on it and `:1533` selects the factory via `engine.getConnectorProvider(provider)` — so it was left out of the "exactly"; 3. `loadPackageFile` IS a context field (`:117`) that no authored key reaches — `plugin.ts:1601` injects `createPackageFileLoader(...)` — so it was over-included; 4. "read by no runtime" is FALSE: `plugin.ts:433` `findInertDeclaredConnectors` reads `(c.actions?.length ?? 0) > 0` on every descriptor at boot, which the `actions.key` row already records as the #2612 inert-descriptor warning. Reaching no provider factory and being read by nothing are two different claims; only the first holds of the remainder. The whole entry-read census is now stated: the materializer reads exactly `name`, `provider`, `enabled`, `label`, `description`, `icon`, `type`, `providerConfig`, `auth`, `retryConfig`, `requestTimeoutMs` plus that one `actions` read, the last nine also being `connectorInstanceSignature`. README row 942, `authentication` clause: "refused outright by ADR-0097 §3" is contradicted by all three instruments including the one it cites. The key is accepted (`connector.zod.ts:893` `.optional().default({ type: 'none' })`); `:1168` refuses a non-`none` VALUE and `:1174`'s message prescribes "drop `authentication` (or set `{ type: 'none' }`)"; ADR-0097 §3 "Credentials are references" rejects INLINE SECRETS, not the key. Accepted-and-ignored plus a loud refusal of every other value is the basis of the `planned` verdict the row already stated. Same file, `auth` row: "whose other half (`authentication`) is refused" compressed to the same wrong claim; scoped to "any value but `{ type: 'none' }`". README row 942, census sentence: "67 hits" -> "67 matching lines", with the `git grep -o` reading (77 occurrences) beside it — re-measured at 14fdebd766 on this checkout, 67 lines / 15 files / 77 occurrences. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude --- packages/spec/liveness/README.md | 2 +- packages/spec/liveness/connector.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index a5056de49a2..51d53b366ae 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -939,7 +939,7 @@ marker where the Notes cell goes, never a guess at what belongs there. | rest_api | seeded 2026-09-21 (#14640) — the FIFTH `RestServerConfig` sub-object, enrolled a round after the four above and deliberately so. #14369 left `RestApiConfigSchema` out because the `api` block's consumption seam was then still VALIDATE-ONLY (#11637 ran the declared contract and discarded its output), so a census would have recorded a half that was about to move; the gate source and four rows of this table said as much. That fence was re-tested before a line of this ledger was written and it has EXPIRED: `RestServer.normalizeConfig` now BUILDS the `api` block from `parseDeclaredApiConfig`'s output — “the asymmetry is gone and all five now build from their parsed output” — and the change is RELEASED, not in flight, with `packages/rest/CHANGELOG.md` re-stating the same zero this file records. ⛔ **The ledger is `rest_api.json`, NOT `api.json`**: that name was already taken by `ApiEndpointSchema`, the registered `api` metadata type with real consumers in the matcher, executor, policy chain and mapping layer — one spelling, two unrelated meanings inside `packages/spec`, and filing here would have published one file's measurement under the other's name. Live 12 = `version` / `basePath` / `apiPath`, which `getApiBasePath` splices into the prefix of EVERY mounted route (read through a whole-block destructure, which is why the dead-key census below had to sweep destructuring shapes and not a property-access pattern alone), the eight `enable*` switches, each gating a mount and most of them also the discovery document's capability block, and `projectResolution`. Dead 14 = the `requireAuth` tombstone (#3963, still `.omit()`ed by this seam because #3963 chose warn-and-ignore and converting that to a boot failure is that decision's to make), plus the two declared containers `documentation` (drilled to ten, including its nested `contact` / `license`) and `responseFormat` (three) — normalized into `this.config.api` and read back by nothing, so `responseFormat.envelope: false` unwraps no response and `documentation.title` retitles no served document. Every zero carries a lit control on the same instrument (twelve sibling keys on the same block return 1-2 reads), each of the three shapes a spelling sweep is blind to was swept with its own control, and the backstop is structural rather than textual: `NormalizedRestServerConfig` is module-local with no `export` and `RestServer.config` is `private`, so the normalized block cannot be reached from outside that one class. ⛔ **The two dead containers do NOT share one verdict**: `documentation`'s members are OpenAPI `info` fields whose enforce route collides with a recorded ownership decision (`info` is written by `build-openapi.ts` and passed through untouched by #11646), while `responseFormat`'s enforce route means making the response envelope configurable — a larger claim. This file records status; the enforce-or-remove call per key is a follow-up on the human floor. `evidenceScope` stays `in-repo`: objectui was measured clean at the pinned sha and at head against a lit control, but the closed cloud runtime was not reachable from the measuring container, so #14796's structural reading is cited as a standing reading rather than re-claimed as a sweep | | realtime_subscription | seeded 2026-09-04 (#14446) — a TRANSPORT-PROTOCOL surface, the fifth category the `SPEC_ONLY_SCHEMAS` override has had to reach. `SubscriptionSchema` (`packages/spec/src/api/realtime.zod.ts`) is what a client declares to open a realtime subscription: the item type of `RealtimeConfigSchema.subscriptions` and the `Subscription` the generated API reference publishes. Like `query` it is a request surface rather than stored metadata, and like `query` that is exactly why it went unasked — no registry holds it, `RealtimeConfigSchema` is `.passthrough()` so nothing downstream even refuses an unknown key, and the whole vocabulary sat outside the denominator while the reference kept publishing it. Rooted on `SubscriptionSchema` rather than on `RealtimeConfigSchema` for the reason the four `RestServerConfig` sub-objects document one row up: the walk drilled exactly ONE level when this was rooted (it recurses as of #17424; the rooting stands), so with the config as the root `events[].type` and `events[].filters` would inherit a container verdict instead of carrying rows of their own — #4956's shape. **Dead 6 = every key it has, and the CONTAINER is the finding**: nothing outside `packages/spec` imports `SubscriptionSchema`, `SubscriptionEventSchema` or `RealtimeConfigSchema` at all, so no key beneath them can be read (the `manifest.contributes` reasoning). The two keys the card measured are the sharp ones. `events[].type` accepts `RealtimeEventType`, whose four members (`record.created` / `record.updated` / `record.deleted` / `field.changed`) are DISJOINT from what the engine publishes (`DataEventType`'s `data.record.*`, live emitter in `service-knowledge`), so an author who writes the enum's own `record.created` gets a subscription that silently never fires — and the enum is what the API reference shows them. Its direction is settled by the 2026-09-02 triage and quoted verbatim in the row: enforce means REPOINTING THE ENUM, never changing what the runtime publishes. `field.changed` is the same spelling the sibling `DataEventType` REMOVED in 17.0.0 (#4673, PR #4685) for having no producer; it survives here only because this enum was never in a ratchet's denominator. `events[].filters` is `z.unknown().optional()` — the textbook ADR-0049 fourth state, no shape and no reader, failing in the permissive direction (a subscriber who filters receives every event). ⚠️ Three spellings of a realtime subscription exist and only the third is executed: this one, `websocket.zod.ts#EventSubscriptionSchema`, and the plain interface `contracts/realtime-service.ts#RealtimeSubscriptionOptions` that `in-memory-realtime-adapter.ts#matchesSubscription` actually reads. The file note names the same-name-different-shape traps so the next census does not mistake one for a consumer. Zero live | | sharing_rule | seeded 2026-09-17 (#18582) — the second of the three `PENDING_GOVERNANCE` debts #18133 declared, and the first one PAID (`connector` and `analytics_cube` are still owed on that card). Not a registered kind: it is bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reaches the walk through `getMetadataTypeSchema`'s unregistered-kind fallback, so this ledger governs a type `listMetadataTypeSchemaTypes()` still does not enumerate. One shape fact decides every row: the AUTHORING shape is not the ENFORCED shape. ADR-0057 D6 makes the `sys_sharing_rule` row canonical (`object_name` + `criteria_json` + `recipient_type`/`recipient_id` + `access_level`) and `bootstrapDeclaredSharingRules` translates each authored key into it at boot — nothing re-parses `SharingRuleSchema` at enforcement time — so every consumer cited reads a COLUMN and every row carries the `producer` (#4837) that populates it, which is the `seed.env` lesson applied to a whole type rather than to one key. Preview read points ENUMERATED per the #7131 rule and the answer recorded rather than skipped: `registerBuiltinPreviews()` (objectui @dda8f381) registers twenty types and `sharing_rule` is not one of them; what objectui does consume is the whole shape, on the CREATE door only (`AUTHOR_SHAPE_ONLY_TYPES` — the EDIT door is deliberately ungated because a served body carries the `_diagnostics` decoration this `.strict()` schema rejects). The single non-`live` row is `type`, the `SharingRuleType` discriminator: one member, `criteria`, whose only reader is a defensive `=== 'owner'` comparison that is unreachable for every value the schema admits. `planned` on the `action.operation` precedent (a one-member discriminator held `planned` until a runtime half dispatched on it, #15080), and deliberately NOT an enforce-or-remove candidate: the key is required, so removing it would break every authored rule to delete nothing. | -| connector | seeded 2026-09-17 (#18582) — the second of the three `PENDING_GOVERNANCE` debts #18133 declared, paid in the same diff as `analytics_cube`, which empties that map. Not a registered kind: bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reached through `getMetadataTypeSchema`'s unregistered-kind fallback. **What the walk actually resolves, measured:** the binding names `DeclarativeConnectorEntrySchema`. ⚠️ The MECHANISM changed with the `connectionTimeoutMs` retirement and the prior sentence here is corrected rather than carried: that schema USED TO BE `ConnectorSchema.superRefine(...)`, a Zod 4 check attached to the same object def, and the key-set conclusion used to rest on that attachment. It is now a `z.preprocess` PIPE — both published carriers wrap one shared private `ConnectorBaseSchema` in the ADR-0049 retired-default residue stage, the entry schema adding the ADR-0097 cross-field rules on the base before wrapping, so the two are SIBLINGS rather than parent and child, and what preserves the walked shape is the pipe's read-through `shape`, NOT a `superRefine` attachment. The CONCLUSION is unchanged and re-measured on the built entry rather than inherited: both carriers expose 30 keys and the key sets are byte-identical, with no entry-only and no base-only key. The gate cannot tell the two schemas apart; what the entry schema buys is REFUSALS, invisible to the walk and visible only in the three rows where they are the whole verdict. **ONE SCHEMA, TWO DOORS** is the shape fact behind the 29/1/44 split (live/planned/dead; counts read from the generated `state-counts.md` row, never hand-kept here): the ledger's denominator entry exists for the AUTHORING doors (`defineStack({ connectors })`, `PUT /meta/connector/:name`), while the same `ConnectorSchema` is what `AutomationEngine.registerConnector` parses for a def a PLUGIN or an ADR-0097 provider factory builds in code — so a key can have a real consumer and still do nothing when a metadata author writes it. The keys an authored entry can reach are exactly the author-supplied `ConnectorProviderContext` fields plus `provider` and `enabled` — `name` is itself one of those fields (the former "plus `name`" tail double-counted it), `loadPackageFile` is host-injected rather than authored, and `provider` selects the factory without ever reaching the context; `type` and `icon` reach that context and are dropped by all three shipped factories, and each says so on its own row. `authentication` is the ledger's `planned`: refused outright by ADR-0097 §3 (#7990) rather than ignored. The 44 `dead`, re-measured at this head and partitioned so every row is counted exactly once: three declared subsystems with no engine — `syncConfig` (8), `fieldMappings` (7), `health` (15, both sub-blocks) — plus `triggers` (6, and the schema's own docblock says so: #3197), the connector's nested `webhooks` (one blanket verdict, recorded in the undrilled baseline), `status`, `metadata`, `actions.description`/`.outputSchema`, and the three top-level `retiredKey` tombstones `rateLimitConfig`, `errorMapping` and `connectionTimeoutMs`. That sums to 44, the dead count the generated `state-counts.md` row carries. ⚠️ `retryConfig` IS NO LONGER IN THIS LIST: all eight of its sub-keys went `live` when #18975 made the declared policy execute at the one platform fetch site, which is the same measurement the falsification note at the end of this row records — so a reader who still finds "`retryConfig` (8)" among the dead is reading a stale copy. ⚠️ Nor is it "the two timeouts" any more: `requestTimeoutMs` is `live` (it becomes `resilientFetch`'s per-attempt deadline) and `connectionTimeoutMs` is the retired tombstone named above. ⭐ SIX rows in this ledger are `retiredKey` tombstones that keep their rows because the key stays in the walked shape (the `rls.priority` precedent) — `rateLimitConfig`, `errorMapping`, `connectionTimeoutMs`, `fieldMappings.transform`, `triggers.interval` and `health.circuitBreaker.monitoringWindow` — but ⛔ that six is NOT a separate addend: the last three are already inside the `fieldMappings`, `triggers` and `health` counts above, which is exactly the double-count that made the previous "and four `retiredKey` tombstones" tail drift. Count them by name, never by adding the tail. **A prior in-repo claim was falsified here, and has since been OVERTAKEN — both halves recorded, because this row's job is the history of how the type got here**: the conversion registry's note that `retryConfig` "and the timeouts beside it are untouched — they are live" was false WHEN SEEDED (2026-09-17), and the ledger corrected it on those rows. It is no longer false of `retryConfig`: #18975 made the declared policy execute at the one platform fetch site (`connectorFetchOptions` → `resilientFetch`), so those eight sub-keys are `live` on their own rows now and the note's claim about them came true after the fact rather than at the time. ⚠️ The seeding note's supporting census — "the word does not occur outside `packages/spec` at all" — is FALSE at this head and is corrected rather than carried: `git grep -n retryConfig 14fdebd766 -- . ':!packages/spec'` returns 67 hits over 15 files (26 in the materializer `packages/services/service-automation/src/plugin.ts` and its materialization test, 22 across `connector-rest` and `connector-openapi` — providers, connectors and their tests — 13 in five `.changeset` fragments, and 6 on two `content/docs` pages). ⛔ Re-read that as the standing lesson of this row: a census is a count plus the tree it was taken against, and a bare "does not occur" with no commit behind it is the shape that rots first. The timeouts half is settled on its own rows: `requestTimeoutMs` is `live`, `connectionTimeoutMs` is retired | +| connector | seeded 2026-09-17 (#18582) — the second of the three `PENDING_GOVERNANCE` debts #18133 declared, paid in the same diff as `analytics_cube`, which empties that map. Not a registered kind: bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reached through `getMetadataTypeSchema`'s unregistered-kind fallback. **What the walk actually resolves, measured:** the binding names `DeclarativeConnectorEntrySchema`. ⚠️ The MECHANISM changed with the `connectionTimeoutMs` retirement and the prior sentence here is corrected rather than carried: that schema USED TO BE `ConnectorSchema.superRefine(...)`, a Zod 4 check attached to the same object def, and the key-set conclusion used to rest on that attachment. It is now a `z.preprocess` PIPE — both published carriers wrap one shared private `ConnectorBaseSchema` in the ADR-0049 retired-default residue stage, the entry schema adding the ADR-0097 cross-field rules on the base before wrapping, so the two are SIBLINGS rather than parent and child, and what preserves the walked shape is the pipe's read-through `shape`, NOT a `superRefine` attachment. The CONCLUSION is unchanged and re-measured on the built entry rather than inherited: both carriers expose 30 keys and the key sets are byte-identical, with no entry-only and no base-only key. The gate cannot tell the two schemas apart; what the entry schema buys is REFUSALS, invisible to the walk and visible only in the three rows where they are the whole verdict. **ONE SCHEMA, TWO DOORS** is the shape fact behind the 29/1/44 split (live/planned/dead; counts read from the generated `state-counts.md` row, never hand-kept here): the ledger's denominator entry exists for the AUTHORING doors (`defineStack({ connectors })`, `PUT /meta/connector/:name`), while the same `ConnectorSchema` is what `AutomationEngine.registerConnector` parses for a def a PLUGIN or an ADR-0097 provider factory builds in code — so a key can have a real consumer and still do nothing when a metadata author writes it. The keys an authored entry can reach are exactly the author-supplied `ConnectorProviderContext` fields plus `provider` and `enabled` — `name` is itself one of those fields (the former "plus `name`" tail double-counted it), `loadPackageFile` is host-injected rather than authored, and `provider` selects the factory without ever reaching the context; `type` and `icon` reach that context and are dropped by all three shipped factories, and each says so on its own row. `authentication` is the ledger's `planned`, and ⛔ NOT "refused outright" — the former tail here said exactly that and all three instruments contradict it, including the one it cites: the KEY is ACCEPTED (`connector.zod.ts` declares `authentication: ConnectorAuthConfigSchema.optional().default({ type: 'none' })`, and the accepted value does nothing); what #7990 refuses is a non-`none` VALUE (`if (entry.authentication && entry.authentication.type !== 'none')`, whose own message prescribes "drop `authentication` (or set `{ type: 'none' }`)"); and ADR-0097 §3, titled "Credentials are references", rejects **inline secrets** in stack metadata, not the key. Accepted-and-ignored, plus a loud refusal of every value but `{ type: 'none' }`, is exactly the basis of the `planned` verdict — which the row itself already stated ("the accepted value does nothing"), so the summary, not the row, was the wrong half. The 44 `dead`, re-measured at this head and partitioned so every row is counted exactly once: three declared subsystems with no engine — `syncConfig` (8), `fieldMappings` (7), `health` (15, both sub-blocks) — plus `triggers` (6, and the schema's own docblock says so: #3197), the connector's nested `webhooks` (one blanket verdict, recorded in the undrilled baseline), `status`, `metadata`, `actions.description`/`.outputSchema`, and the three top-level `retiredKey` tombstones `rateLimitConfig`, `errorMapping` and `connectionTimeoutMs`. That sums to 44, the dead count the generated `state-counts.md` row carries. ⚠️ `retryConfig` IS NO LONGER IN THIS LIST: all eight of its sub-keys went `live` when #18975 made the declared policy execute at the one platform fetch site, which is the same measurement the falsification note at the end of this row records — so a reader who still finds "`retryConfig` (8)" among the dead is reading a stale copy. ⚠️ Nor is it "the two timeouts" any more: `requestTimeoutMs` is `live` (it becomes `resilientFetch`'s per-attempt deadline) and `connectionTimeoutMs` is the retired tombstone named above. ⭐ SIX rows in this ledger are `retiredKey` tombstones that keep their rows because the key stays in the walked shape (the `rls.priority` precedent) — `rateLimitConfig`, `errorMapping`, `connectionTimeoutMs`, `fieldMappings.transform`, `triggers.interval` and `health.circuitBreaker.monitoringWindow` — but ⛔ that six is NOT a separate addend: the last three are already inside the `fieldMappings`, `triggers` and `health` counts above, which is exactly the double-count that made the previous "and four `retiredKey` tombstones" tail drift. Count them by name, never by adding the tail. **A prior in-repo claim was falsified here, and has since been OVERTAKEN — both halves recorded, because this row's job is the history of how the type got here**: the conversion registry's note that `retryConfig` "and the timeouts beside it are untouched — they are live" was false WHEN SEEDED (2026-09-17), and the ledger corrected it on those rows. It is no longer false of `retryConfig`: #18975 made the declared policy execute at the one platform fetch site (`connectorFetchOptions` → `resilientFetch`), so those eight sub-keys are `live` on their own rows now and the note's claim about them came true after the fact rather than at the time. ⚠️ The seeding note's supporting census — "the word does not occur outside `packages/spec` at all" — is FALSE at this head and is corrected rather than carried: `git grep -n retryConfig 14fdebd766 -- . ':!packages/spec'` returns 67 **matching lines** over 15 files — `git grep -o` on the same tree and pathspec returns 77 **occurrences**, and a line is not an occurrence, which is the trap a re-measurer falls into next (26 matching lines in the materializer `packages/services/service-automation/src/plugin.ts` and its materialization test, 22 across `connector-rest` and `connector-openapi` — providers, connectors and their tests — 13 in five `.changeset` fragments, and 6 on two `content/docs` pages). ⛔ Re-read that as the standing lesson of this row: a census is a count plus the tree it was taken against, and a bare "does not occur" with no commit behind it is the shape that rots first. The timeouts half is settled on its own rows: `requestTimeoutMs` is `live`, `connectionTimeoutMs` is retired | | analytics_cube | seeded 2026-09-17 (#18582) — the third debt, paid in the same diff as `connector`. Not a registered kind either: bound in `UNREGISTERED_KIND_SCHEMAS` by #10194 and reached through the same unregistered-kind fallback. **ONE Cube shape, THREE producers, one registry** is what decides every row: `cube-registry.ts` names them itself — authored cubes (`analyticsCubes[]` / `defineCube()`, threaded by the CLI into `AnalyticsServiceConfig.cubes`), COMPILED DATASETS (ADR-0021, where `dataset-compiler` mints a Cube), and ad-hoc query inference. Only the first is the authoring door governed here, so a key whose only reader sits on the compiled-dataset path is not live for an authored cube however busy that reader is — the #4837 producer rule on a shape with three producers. That is `dimensions.granularities` (read by `dataset-executor#granularityOf`, whose argument is a `CompiledDataset` an authored cube never becomes) and `measures.format` (written by the compiler, threaded to the wire from the DATASET measure instead). The query path is genuinely live: `sql` is the FROM table AND the object whose RLS read scope is injected, `measures.type` picks the aggregate, `measures.sql`/`dimensions.sql` the column, `joins[].name` the joined table. The 10 `dead` are the caching block (`refreshKey.every`/`.sql` — no refresh scheduler exists anywhere), the access-control flag (`public` — three sites write `false`, nothing reads it: a knob that was never wired, not a hole that was opened), the three `description`s, and the inner `name` on each of `measures`/`dimensions`, where the record KEY is the identity. It was 12 until #18612 RETIRED `joins[].relationship` and the REQUIRED `joins[].sql` (ADR-0049 enforce-or-remove, maintainer-ruled batch #154): the ON clause is SYNTHESISED as an FK equality and the authored one was never consulted, so a declared join condition came back REPLACED under a 200. `CubeJoinSchema` is a `strictObject`, so the route was strict deletion plus a `guidance` prescription and the two rows left this ledger with the keys — not the `retiredKey()` route, which keeps the row. **#10238 is not prejudged**: whether cube authoring is live end to end is still its own measurement — this ledger answers the per-key question only | The `dead` set across types is the enforce-or-remove worklist (ADR-0049); every diff --git a/packages/spec/liveness/connector.json b/packages/spec/liveness/connector.json index 3fa4811139a..c2b991b2067 100644 --- a/packages/spec/liveness/connector.json +++ b/packages/spec/liveness/connector.json @@ -1,6 +1,6 @@ { "type": "connector", - "_note": "DeclarativeConnectorEntrySchema (packages/spec/src/integration/connector.zod.ts). Seeded 2026-09-17 (#18582) together with `analytics_cube`: the last two of the three PENDING_GOVERNANCE debts #18133 declared when PR #18581 widened the governance denominator to `authorableTypes()` (`sharing_rule` was paid first, PR #18587). Their landing empties that map. NOT a registered metadata KIND — bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reached through `getMetadataTypeSchema`'s unregistered-kind fallback. WHAT THE WALKER ACTUALLY RESOLVES, measured rather than assumed: the binding names `DeclarativeConnectorEntrySchema`. ⚠️ THE MECHANISM CHANGED WITH THE `connectionTimeoutMs` RETIREMENT and the prior sentence here is corrected rather than carried: that schema USED TO BE `ConnectorSchema.superRefine(...)`, a Zod 4 check attached to the same object def. It is now a `z.preprocess` PIPE — both published carriers wrap one shared private `ConnectorBaseSchema` in the ADR-0049 retired-default residue stage, the entry schema adding the ADR-0097 cross-field rules on the base before wrapping, so the two are SIBLINGS rather than parent and child. The CONCLUSION is unchanged and re-measured on the built entry: the pipe keeps a read-through `shape`, both carriers expose 30 keys, and the key sets are byte-identical with no entry-only and no base-only key — the ADR-0097 cross-field rules add no key and remove none. The gate therefore cannot tell the two schemas apart; what the binding buys is REFUSALS, which are invisible to the walk and visible only in the `authentication` / `actions` / `triggers` rows below, where they are the whole verdict. THE SHAPE FACT THAT DECIDES EVERY ROW: one schema, TWO doors. This ledger's denominator entry exists because of the AUTHORING doors (`defineStack({ connectors })` and `PUT /api/v1/meta/connector/:name`); the same `ConnectorSchema` is ALSO what `AutomationEngine.registerConnector` parses for a def a PLUGIN or an ADR-0097 provider factory builds in code. So a key can have a real consumer and still do nothing when a metadata author writes it, and every row below says WHICH door its consumer is fed from. The keys an authored entry can reach are exactly the `ConnectorProviderContext` fields plus `name` and `enabled`; everything else in an authored entry is stored, served back by `/meta/connector`, and read by no runtime. That asymmetry is the trap this type carries, and it is recorded per key rather than asserted once. PRIOR MEASUREMENTS RE-VERIFIED, not inherited: the ADR-0087 conversion registry's `connector-field-mapping-transform-removed` entry recorded 'Execution: none — `fieldMappings` is spelled only inside packages/spec' (2026-08-06), the `syncConfig.schedule` retirement recorded '`syncConfig` has no reader outside `packages/spec`' (#16320, 2026-09-10), and `ConnectorTriggerSchema`'s own docblock says 'NOT YET ENFORCED — declared but never read by the runtime (#3197)'. All three were re-run on this checkout and all three still hold; the counts are in the rows. One prior claim FAILED re-verification and is corrected here: a comment in packages/spec/src/conversions/registry.ts asserts that `retryConfig` 'and the timeouts beside it are untouched — they are live'. They are not read anywhere; see those three rows. PREVIEW READ POINTS ENUMERATED (the #7131 mechanical rule, objectui @dda8f3815): `registerBuiltinPreviews()` registers nineteen types and `connector` is NOT one of them — this type has no registered metadata-admin preview. What objectui DOES consume is (a) the whole SHAPE, via `clientValidation.ts`, which maps `connector` to `DeclarativeConnectorEntrySchema` on BOTH the create and the edit door (it is not strict, so it may judge a stored body), and (b) the RUNTIME registry projection `GET /api/v1/automation/connectors`, from which `connectorsToOptions` reads `name`/`label`/`origin`, `connectorActionsToOptions` reads `actions[].key`/`.label`, and `connectorActionInputSchema` reads `actions[].inputSchema`. Those three are the cross-repo citations below. ADR-0054: no row carries a `proof` and none is owed — no high-risk class binds a `connector/*` path.", + "_note": "DeclarativeConnectorEntrySchema (packages/spec/src/integration/connector.zod.ts). Seeded 2026-09-17 (#18582) together with `analytics_cube`: the last two of the three PENDING_GOVERNANCE debts #18133 declared when PR #18581 widened the governance denominator to `authorableTypes()` (`sharing_rule` was paid first, PR #18587). Their landing empties that map. NOT a registered metadata KIND — bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reached through `getMetadataTypeSchema`'s unregistered-kind fallback. WHAT THE WALKER ACTUALLY RESOLVES, measured rather than assumed: the binding names `DeclarativeConnectorEntrySchema`. ⚠️ THE MECHANISM CHANGED WITH THE `connectionTimeoutMs` RETIREMENT and the prior sentence here is corrected rather than carried: that schema USED TO BE `ConnectorSchema.superRefine(...)`, a Zod 4 check attached to the same object def. It is now a `z.preprocess` PIPE — both published carriers wrap one shared private `ConnectorBaseSchema` in the ADR-0049 retired-default residue stage, the entry schema adding the ADR-0097 cross-field rules on the base before wrapping, so the two are SIBLINGS rather than parent and child. The CONCLUSION is unchanged and re-measured on the built entry: the pipe keeps a read-through `shape`, both carriers expose 30 keys, and the key sets are byte-identical with no entry-only and no base-only key — the ADR-0097 cross-field rules add no key and remove none. The gate therefore cannot tell the two schemas apart; what the binding buys is REFUSALS, which are invisible to the walk and visible only in the `authentication` / `actions` / `triggers` rows below, where they are the whole verdict. THE SHAPE FACT THAT DECIDES EVERY ROW: one schema, TWO doors. This ledger's denominator entry exists because of the AUTHORING doors (`defineStack({ connectors })` and `PUT /api/v1/meta/connector/:name`); the same `ConnectorSchema` is ALSO what `AutomationEngine.registerConnector` parses for a def a PLUGIN or an ADR-0097 provider factory builds in code. So a key can have a real consumer and still do nothing when a metadata author writes it, and every row below says WHICH door its consumer is fed from. The keys an authored entry can reach are exactly the AUTHOR-SUPPLIED `ConnectorProviderContext` fields plus `provider` and `enabled`, and the three corrections in that sentence are each measured: `name` is itself one of those fields (`connector-provider.ts#ConnectorProviderContext` declares it), so the former 'plus `name`' tail DOUBLE-COUNTED it; `loadPackageFile` is HOST-INJECTED rather than authored (the materializer passes `createPackageFileLoader(this.options.packageRoot)`, and no authored key reaches it), so the bare 'the `ConnectorProviderContext` fields' OVER-INCLUDED it; and `provider` is read on the AUTHORING door itself — it gates the desired set (`typeof entry.provider !== 'string' … continue`) and selects the factory (`engine.getConnectorProvider(provider)`) — without ever reaching the context, so it was LEFT OUT of the 'exactly'. Everything else in an authored entry is stored and served back by `/meta/connector` WITHOUT REACHING A PROVIDER FACTORY. ⛔ That is NOT the same claim as 'read by no runtime', which the former tail here asserted and which this file's own `actions.key` row already contradicted: `packages/services/service-automation/src/plugin.ts#findInertDeclaredConnectors` reads `(c.actions?.length ?? 0) > 0` on EVERY descriptor at boot and `auditDeclaredConnectors` turns it into the #2612 inert-descriptor warning, so `actions` is a real runtime read of a key no factory is handed. Measured rather than asserted, and this is the whole census: the only reads of a declared entry anywhere in the materializer are `name`, `provider`, `enabled`, `label`, `description`, `icon`, `type`, `providerConfig`, `auth`, `retryConfig` and `requestTimeoutMs` (the last nine are also exactly `connectorInstanceSignature`'s fingerprint, which is why `metadata`, `status`, `syncConfig`, `health`, `triggers` and `webhooks` keep their `read by nothing` rows) plus that one `actions` read. Reaching no factory and being read by nothing are two different claims; only the first holds of the whole remainder. That asymmetry is the trap this type carries, and it is recorded per key rather than asserted once. PRIOR MEASUREMENTS RE-VERIFIED, not inherited: the ADR-0087 conversion registry's `connector-field-mapping-transform-removed` entry recorded 'Execution: none — `fieldMappings` is spelled only inside packages/spec' (2026-08-06), the `syncConfig.schedule` retirement recorded '`syncConfig` has no reader outside `packages/spec`' (#16320, 2026-09-10), and `ConnectorTriggerSchema`'s own docblock says 'NOT YET ENFORCED — declared but never read by the runtime (#3197)'. All three were re-run on this checkout and all three still hold; the counts are in the rows. One prior claim FAILED re-verification and is corrected here: a comment in packages/spec/src/conversions/registry.ts asserts that `retryConfig` 'and the timeouts beside it are untouched — they are live'. They are not read anywhere; see those three rows. PREVIEW READ POINTS ENUMERATED (the #7131 mechanical rule, objectui @dda8f3815): `registerBuiltinPreviews()` registers nineteen types and `connector` is NOT one of them — this type has no registered metadata-admin preview. What objectui DOES consume is (a) the whole SHAPE, via `clientValidation.ts`, which maps `connector` to `DeclarativeConnectorEntrySchema` on BOTH the create and the edit door (it is not strict, so it may judge a stored body), and (b) the RUNTIME registry projection `GET /api/v1/automation/connectors`, from which `connectorsToOptions` reads `name`/`label`/`origin`, `connectorActionsToOptions` reads `actions[].key`/`.label`, and `connectorActionInputSchema` reads `actions[].inputSchema`. Those three are the cross-repo citations below. ADR-0054: no row carries a `proof` and none is owed — no high-risk class binds a `connector/*` path.", "props": { "name": { "status": "live", @@ -64,7 +64,7 @@ "verifiedAt": "2026-09-17", "evidence": "packages/services/service-automation/src/plugin.ts#materializeDeclaredConnectors — `auth = await this.resolveInstanceAuth(entry.auth, resolver, name, provider)` resolves the declared `credentialRef` through the secrets/env layer and puts the RESOLVED credential on the provider context; a failure there degrades or fails the instance by name (ADR-0097 §3). packages/connectors/connector-rest/src/rest-provider.ts, packages/connectors/connector-openapi/src/openapi-provider.ts and packages/connectors/connector-mcp/src/mcp-provider.ts each read `ctx.auth` and use it to sign the upstream call. packages/spec/src/integration/connector.zod.ts#DeclarativeConnectorEntrySchema refuses it on an entry with no `provider`.", "producer": "packages/services/service-automation/src/plugin.ts#materializeDeclaredConnectors — the ADR-0097 reconcile: it builds the desired set from the declared `connectors:` items, resolves the provider factory and calls `engine.registerConnector(def, handlers, 'declarative')`. This is the ONLY seam by which an authored entry reaches the connector registry, so it is the producer every `live` row below depends on (#4837): without it a declared connector is a stored document and nothing else.", - "note": "The live half of the pair whose other half (`authentication`) is refused: this key is how an authored connector carries a credential at all, and it carries a REFERENCE, never the secret." + "note": "The live half of the pair whose other half (`authentication`) is refused ANY VALUE BUT `{ type: 'none' }` — the key itself is accepted and inert, so ⛔ do not compress this to '`authentication` is refused' (see that row): this key is how an authored connector carries a credential at all, and it carries a REFERENCE, never the secret." }, "actions": { "children": { From 9e2843a4d31da6cb185c5c33631691650373add4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 14:06:45 +0000 Subject: [PATCH 15/15] docs(spec): re-derive the connector ledger's claims against their instruments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rounds 3-6 each fixed the sentence they were handed and left the identical claim standing one sentence, or one row, over. This round decomposed both lines into their individual claims and re-derived each against the instrument it names. Five corrections, four of them measured here: 1. `connectorInstanceSignature` (`plugin.ts:282-314`) hashes NINE keys — `provider`, `providerConfig`, `auth`, `label`, `description`, `icon`, `type`, `retryConfig`, `requestTimeoutMs` — identical in its parameter type and its hashed object. So "the last nine" of the eleven-key census was wrong in BOTH directions: it swept in `enabled`, which is never hashed, and dropped `provider`, which is. The spelling that holds is "all but `name` and `enabled`". 2. The conversion-registry comment is quoted as it reads TODAY. Inside `connector-rate-limit-config-removed`'s fixture (`registry.ts:4503`) it says the timeouts are "untouched by THIS conversion - a statement about its scope, not a liveness verdict. They are not live". It asserts they are NOT live; both lines quoted it as asserting the opposite. Measured direction: TRUE when this ledger was seeded, STALE now (#18975). The stale comment is #19729's and is not rewritten here. The tail's "They are not read anywhere" is false besides - `plugin.ts:1596-1597` hands both to `ConnectorProviderContext`, and the same line's own census lists both among the eleven. 3. The ADR-0087 entry is `field-mapping-transform-removed`; there is no `connector-` prefix on it (`registry.ts:4594`). The `connector-` prefixed neighbour is `connector-rate-limit-config-removed`. Both the `_note` and the `fieldMappings.transform` row carried the fused name; the second was found by sweeping the file rather than by being handed the line. 4. That entry's "Execution: none" quote was truncated before its own scoping gloss, which is what made it falsifiable: `fieldMappings` occurs on 14 lines over 6 files outside `packages/spec`, all prose. The gloss - no read in the connector packages, the engine, REST or objectui - holds, and the split is now recorded rather than smoothed. The `fieldMappings.transform` row's own census, scoped to `packages/` and `examples/`, was re-derived and HOLDS with two positive controls, so only its id was touched. 5. `registerBuiltinPreviews()` at objectui dda8f3815 makes 22 `registerMetadataPreview(` calls naming 22 distinct types over 20 distinct components. "nineteen" matched no reading; the reading is now stated. The load-bearing clause - `connector` is not registered - is unchanged and still true. Re-derived and unchanged: the 30-key byte-identical carrier pair (measured on the built entry, with controls), the 29/1/44 split against `state-counts.md`, the 44-row dead partition, the six `retiredKey` tombstones and their containment, `retryConfig`'s eight `live` sub-rows, `requestTimeoutMs` live / `connectionTimeoutMs` retired, the eleven-key materializer census plus the one `actions` read, `type`/`icon` dropped by all three shipped factories, and every objectui consumption citation. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx --- packages/spec/liveness/README.md | 2 +- packages/spec/liveness/connector.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index 51d53b366ae..203b1d25702 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -939,7 +939,7 @@ marker where the Notes cell goes, never a guess at what belongs there. | rest_api | seeded 2026-09-21 (#14640) — the FIFTH `RestServerConfig` sub-object, enrolled a round after the four above and deliberately so. #14369 left `RestApiConfigSchema` out because the `api` block's consumption seam was then still VALIDATE-ONLY (#11637 ran the declared contract and discarded its output), so a census would have recorded a half that was about to move; the gate source and four rows of this table said as much. That fence was re-tested before a line of this ledger was written and it has EXPIRED: `RestServer.normalizeConfig` now BUILDS the `api` block from `parseDeclaredApiConfig`'s output — “the asymmetry is gone and all five now build from their parsed output” — and the change is RELEASED, not in flight, with `packages/rest/CHANGELOG.md` re-stating the same zero this file records. ⛔ **The ledger is `rest_api.json`, NOT `api.json`**: that name was already taken by `ApiEndpointSchema`, the registered `api` metadata type with real consumers in the matcher, executor, policy chain and mapping layer — one spelling, two unrelated meanings inside `packages/spec`, and filing here would have published one file's measurement under the other's name. Live 12 = `version` / `basePath` / `apiPath`, which `getApiBasePath` splices into the prefix of EVERY mounted route (read through a whole-block destructure, which is why the dead-key census below had to sweep destructuring shapes and not a property-access pattern alone), the eight `enable*` switches, each gating a mount and most of them also the discovery document's capability block, and `projectResolution`. Dead 14 = the `requireAuth` tombstone (#3963, still `.omit()`ed by this seam because #3963 chose warn-and-ignore and converting that to a boot failure is that decision's to make), plus the two declared containers `documentation` (drilled to ten, including its nested `contact` / `license`) and `responseFormat` (three) — normalized into `this.config.api` and read back by nothing, so `responseFormat.envelope: false` unwraps no response and `documentation.title` retitles no served document. Every zero carries a lit control on the same instrument (twelve sibling keys on the same block return 1-2 reads), each of the three shapes a spelling sweep is blind to was swept with its own control, and the backstop is structural rather than textual: `NormalizedRestServerConfig` is module-local with no `export` and `RestServer.config` is `private`, so the normalized block cannot be reached from outside that one class. ⛔ **The two dead containers do NOT share one verdict**: `documentation`'s members are OpenAPI `info` fields whose enforce route collides with a recorded ownership decision (`info` is written by `build-openapi.ts` and passed through untouched by #11646), while `responseFormat`'s enforce route means making the response envelope configurable — a larger claim. This file records status; the enforce-or-remove call per key is a follow-up on the human floor. `evidenceScope` stays `in-repo`: objectui was measured clean at the pinned sha and at head against a lit control, but the closed cloud runtime was not reachable from the measuring container, so #14796's structural reading is cited as a standing reading rather than re-claimed as a sweep | | realtime_subscription | seeded 2026-09-04 (#14446) — a TRANSPORT-PROTOCOL surface, the fifth category the `SPEC_ONLY_SCHEMAS` override has had to reach. `SubscriptionSchema` (`packages/spec/src/api/realtime.zod.ts`) is what a client declares to open a realtime subscription: the item type of `RealtimeConfigSchema.subscriptions` and the `Subscription` the generated API reference publishes. Like `query` it is a request surface rather than stored metadata, and like `query` that is exactly why it went unasked — no registry holds it, `RealtimeConfigSchema` is `.passthrough()` so nothing downstream even refuses an unknown key, and the whole vocabulary sat outside the denominator while the reference kept publishing it. Rooted on `SubscriptionSchema` rather than on `RealtimeConfigSchema` for the reason the four `RestServerConfig` sub-objects document one row up: the walk drilled exactly ONE level when this was rooted (it recurses as of #17424; the rooting stands), so with the config as the root `events[].type` and `events[].filters` would inherit a container verdict instead of carrying rows of their own — #4956's shape. **Dead 6 = every key it has, and the CONTAINER is the finding**: nothing outside `packages/spec` imports `SubscriptionSchema`, `SubscriptionEventSchema` or `RealtimeConfigSchema` at all, so no key beneath them can be read (the `manifest.contributes` reasoning). The two keys the card measured are the sharp ones. `events[].type` accepts `RealtimeEventType`, whose four members (`record.created` / `record.updated` / `record.deleted` / `field.changed`) are DISJOINT from what the engine publishes (`DataEventType`'s `data.record.*`, live emitter in `service-knowledge`), so an author who writes the enum's own `record.created` gets a subscription that silently never fires — and the enum is what the API reference shows them. Its direction is settled by the 2026-09-02 triage and quoted verbatim in the row: enforce means REPOINTING THE ENUM, never changing what the runtime publishes. `field.changed` is the same spelling the sibling `DataEventType` REMOVED in 17.0.0 (#4673, PR #4685) for having no producer; it survives here only because this enum was never in a ratchet's denominator. `events[].filters` is `z.unknown().optional()` — the textbook ADR-0049 fourth state, no shape and no reader, failing in the permissive direction (a subscriber who filters receives every event). ⚠️ Three spellings of a realtime subscription exist and only the third is executed: this one, `websocket.zod.ts#EventSubscriptionSchema`, and the plain interface `contracts/realtime-service.ts#RealtimeSubscriptionOptions` that `in-memory-realtime-adapter.ts#matchesSubscription` actually reads. The file note names the same-name-different-shape traps so the next census does not mistake one for a consumer. Zero live | | sharing_rule | seeded 2026-09-17 (#18582) — the second of the three `PENDING_GOVERNANCE` debts #18133 declared, and the first one PAID (`connector` and `analytics_cube` are still owed on that card). Not a registered kind: it is bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reaches the walk through `getMetadataTypeSchema`'s unregistered-kind fallback, so this ledger governs a type `listMetadataTypeSchemaTypes()` still does not enumerate. One shape fact decides every row: the AUTHORING shape is not the ENFORCED shape. ADR-0057 D6 makes the `sys_sharing_rule` row canonical (`object_name` + `criteria_json` + `recipient_type`/`recipient_id` + `access_level`) and `bootstrapDeclaredSharingRules` translates each authored key into it at boot — nothing re-parses `SharingRuleSchema` at enforcement time — so every consumer cited reads a COLUMN and every row carries the `producer` (#4837) that populates it, which is the `seed.env` lesson applied to a whole type rather than to one key. Preview read points ENUMERATED per the #7131 rule and the answer recorded rather than skipped: `registerBuiltinPreviews()` (objectui @dda8f381) registers twenty types and `sharing_rule` is not one of them; what objectui does consume is the whole shape, on the CREATE door only (`AUTHOR_SHAPE_ONLY_TYPES` — the EDIT door is deliberately ungated because a served body carries the `_diagnostics` decoration this `.strict()` schema rejects). The single non-`live` row is `type`, the `SharingRuleType` discriminator: one member, `criteria`, whose only reader is a defensive `=== 'owner'` comparison that is unreachable for every value the schema admits. `planned` on the `action.operation` precedent (a one-member discriminator held `planned` until a runtime half dispatched on it, #15080), and deliberately NOT an enforce-or-remove candidate: the key is required, so removing it would break every authored rule to delete nothing. | -| connector | seeded 2026-09-17 (#18582) — the second of the three `PENDING_GOVERNANCE` debts #18133 declared, paid in the same diff as `analytics_cube`, which empties that map. Not a registered kind: bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reached through `getMetadataTypeSchema`'s unregistered-kind fallback. **What the walk actually resolves, measured:** the binding names `DeclarativeConnectorEntrySchema`. ⚠️ The MECHANISM changed with the `connectionTimeoutMs` retirement and the prior sentence here is corrected rather than carried: that schema USED TO BE `ConnectorSchema.superRefine(...)`, a Zod 4 check attached to the same object def, and the key-set conclusion used to rest on that attachment. It is now a `z.preprocess` PIPE — both published carriers wrap one shared private `ConnectorBaseSchema` in the ADR-0049 retired-default residue stage, the entry schema adding the ADR-0097 cross-field rules on the base before wrapping, so the two are SIBLINGS rather than parent and child, and what preserves the walked shape is the pipe's read-through `shape`, NOT a `superRefine` attachment. The CONCLUSION is unchanged and re-measured on the built entry rather than inherited: both carriers expose 30 keys and the key sets are byte-identical, with no entry-only and no base-only key. The gate cannot tell the two schemas apart; what the entry schema buys is REFUSALS, invisible to the walk and visible only in the three rows where they are the whole verdict. **ONE SCHEMA, TWO DOORS** is the shape fact behind the 29/1/44 split (live/planned/dead; counts read from the generated `state-counts.md` row, never hand-kept here): the ledger's denominator entry exists for the AUTHORING doors (`defineStack({ connectors })`, `PUT /meta/connector/:name`), while the same `ConnectorSchema` is what `AutomationEngine.registerConnector` parses for a def a PLUGIN or an ADR-0097 provider factory builds in code — so a key can have a real consumer and still do nothing when a metadata author writes it. The keys an authored entry can reach are exactly the author-supplied `ConnectorProviderContext` fields plus `provider` and `enabled` — `name` is itself one of those fields (the former "plus `name`" tail double-counted it), `loadPackageFile` is host-injected rather than authored, and `provider` selects the factory without ever reaching the context; `type` and `icon` reach that context and are dropped by all three shipped factories, and each says so on its own row. `authentication` is the ledger's `planned`, and ⛔ NOT "refused outright" — the former tail here said exactly that and all three instruments contradict it, including the one it cites: the KEY is ACCEPTED (`connector.zod.ts` declares `authentication: ConnectorAuthConfigSchema.optional().default({ type: 'none' })`, and the accepted value does nothing); what #7990 refuses is a non-`none` VALUE (`if (entry.authentication && entry.authentication.type !== 'none')`, whose own message prescribes "drop `authentication` (or set `{ type: 'none' }`)"); and ADR-0097 §3, titled "Credentials are references", rejects **inline secrets** in stack metadata, not the key. Accepted-and-ignored, plus a loud refusal of every value but `{ type: 'none' }`, is exactly the basis of the `planned` verdict — which the row itself already stated ("the accepted value does nothing"), so the summary, not the row, was the wrong half. The 44 `dead`, re-measured at this head and partitioned so every row is counted exactly once: three declared subsystems with no engine — `syncConfig` (8), `fieldMappings` (7), `health` (15, both sub-blocks) — plus `triggers` (6, and the schema's own docblock says so: #3197), the connector's nested `webhooks` (one blanket verdict, recorded in the undrilled baseline), `status`, `metadata`, `actions.description`/`.outputSchema`, and the three top-level `retiredKey` tombstones `rateLimitConfig`, `errorMapping` and `connectionTimeoutMs`. That sums to 44, the dead count the generated `state-counts.md` row carries. ⚠️ `retryConfig` IS NO LONGER IN THIS LIST: all eight of its sub-keys went `live` when #18975 made the declared policy execute at the one platform fetch site, which is the same measurement the falsification note at the end of this row records — so a reader who still finds "`retryConfig` (8)" among the dead is reading a stale copy. ⚠️ Nor is it "the two timeouts" any more: `requestTimeoutMs` is `live` (it becomes `resilientFetch`'s per-attempt deadline) and `connectionTimeoutMs` is the retired tombstone named above. ⭐ SIX rows in this ledger are `retiredKey` tombstones that keep their rows because the key stays in the walked shape (the `rls.priority` precedent) — `rateLimitConfig`, `errorMapping`, `connectionTimeoutMs`, `fieldMappings.transform`, `triggers.interval` and `health.circuitBreaker.monitoringWindow` — but ⛔ that six is NOT a separate addend: the last three are already inside the `fieldMappings`, `triggers` and `health` counts above, which is exactly the double-count that made the previous "and four `retiredKey` tombstones" tail drift. Count them by name, never by adding the tail. **A prior in-repo claim was falsified here, and has since been OVERTAKEN — both halves recorded, because this row's job is the history of how the type got here**: the conversion registry's note that `retryConfig` "and the timeouts beside it are untouched — they are live" was false WHEN SEEDED (2026-09-17), and the ledger corrected it on those rows. It is no longer false of `retryConfig`: #18975 made the declared policy execute at the one platform fetch site (`connectorFetchOptions` → `resilientFetch`), so those eight sub-keys are `live` on their own rows now and the note's claim about them came true after the fact rather than at the time. ⚠️ The seeding note's supporting census — "the word does not occur outside `packages/spec` at all" — is FALSE at this head and is corrected rather than carried: `git grep -n retryConfig 14fdebd766 -- . ':!packages/spec'` returns 67 **matching lines** over 15 files — `git grep -o` on the same tree and pathspec returns 77 **occurrences**, and a line is not an occurrence, which is the trap a re-measurer falls into next (26 matching lines in the materializer `packages/services/service-automation/src/plugin.ts` and its materialization test, 22 across `connector-rest` and `connector-openapi` — providers, connectors and their tests — 13 in five `.changeset` fragments, and 6 on two `content/docs` pages). ⛔ Re-read that as the standing lesson of this row: a census is a count plus the tree it was taken against, and a bare "does not occur" with no commit behind it is the shape that rots first. The timeouts half is settled on its own rows: `requestTimeoutMs` is `live`, `connectionTimeoutMs` is retired | +| connector | seeded 2026-09-17 (#18582) — the second of the three `PENDING_GOVERNANCE` debts #18133 declared, paid in the same diff as `analytics_cube`, which empties that map. Not a registered kind: bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reached through `getMetadataTypeSchema`'s unregistered-kind fallback. **What the walk actually resolves, measured:** the binding names `DeclarativeConnectorEntrySchema`. ⚠️ The MECHANISM changed with the `connectionTimeoutMs` retirement and the prior sentence here is corrected rather than carried: that schema USED TO BE `ConnectorSchema.superRefine(...)`, a Zod 4 check attached to the same object def, and the key-set conclusion used to rest on that attachment. It is now a `z.preprocess` PIPE — both published carriers wrap one shared private `ConnectorBaseSchema` in the ADR-0049 retired-default residue stage, the entry schema adding the ADR-0097 cross-field rules on the base before wrapping, so the two are SIBLINGS rather than parent and child, and what preserves the walked shape is the pipe's read-through `shape`, NOT a `superRefine` attachment. The CONCLUSION is unchanged and re-measured on the built entry rather than inherited: both carriers expose 30 keys and the key sets are byte-identical, with no entry-only and no base-only key. The gate cannot tell the two schemas apart; what the entry schema buys is REFUSALS, invisible to the walk and visible only in the three rows where they are the whole verdict. **ONE SCHEMA, TWO DOORS** is the shape fact behind the 29/1/44 split (live/planned/dead; counts read from the generated `state-counts.md` row, never hand-kept here): the ledger's denominator entry exists for the AUTHORING doors (`defineStack({ connectors })`, `PUT /meta/connector/:name`), while the same `ConnectorSchema` is what `AutomationEngine.registerConnector` parses for a def a PLUGIN or an ADR-0097 provider factory builds in code — so a key can have a real consumer and still do nothing when a metadata author writes it. The keys an authored entry can reach are exactly the author-supplied `ConnectorProviderContext` fields plus `provider` and `enabled` — `name` is itself one of those fields (the former "plus `name`" tail double-counted it), `loadPackageFile` is host-injected rather than authored, and `provider` selects the factory without ever reaching the context; `type` and `icon` reach that context and are dropped by all three shipped factories, and each says so on its own row. `authentication` is the ledger's `planned`, and ⛔ NOT "refused outright" — the former tail here said exactly that and all three instruments contradict it, including the one it cites: the KEY is ACCEPTED (`connector.zod.ts` declares `authentication: ConnectorAuthConfigSchema.optional().default({ type: 'none' })`, and the accepted value does nothing); what #7990 refuses is a non-`none` VALUE (`if (entry.authentication && entry.authentication.type !== 'none')`, whose own message prescribes "drop `authentication` (or set `{ type: 'none' }`)"); and ADR-0097 §3, titled "Credentials are references", rejects **inline secrets** in stack metadata, not the key. Accepted-and-ignored, plus a loud refusal of every value but `{ type: 'none' }`, is exactly the basis of the `planned` verdict — which the row itself already stated ("the accepted value does nothing"), so the summary, not the row, was the wrong half. The 44 `dead`, re-measured at this head and partitioned so every row is counted exactly once: three declared subsystems with no engine — `syncConfig` (8), `fieldMappings` (7), `health` (15, both sub-blocks) — plus `triggers` (6, and the schema's own docblock says so: #3197), the connector's nested `webhooks` (one blanket verdict, recorded in the undrilled baseline), `status`, `metadata`, `actions.description`/`.outputSchema`, and the three top-level `retiredKey` tombstones `rateLimitConfig`, `errorMapping` and `connectionTimeoutMs`. That sums to 44, the dead count the generated `state-counts.md` row carries. ⚠️ `retryConfig` IS NO LONGER IN THIS LIST: all eight of its sub-keys went `live` when #18975 made the declared policy execute at the one platform fetch site, which is the same measurement the falsification note at the end of this row records — so a reader who still finds "`retryConfig` (8)" among the dead is reading a stale copy. ⚠️ Nor is it "the two timeouts" any more: `requestTimeoutMs` is `live` (it becomes `resilientFetch`'s per-attempt deadline) and `connectionTimeoutMs` is the retired tombstone named above. ⭐ SIX rows in this ledger are `retiredKey` tombstones that keep their rows because the key stays in the walked shape (the `rls.priority` precedent) — `rateLimitConfig`, `errorMapping`, `connectionTimeoutMs`, `fieldMappings.transform`, `triggers.interval` and `health.circuitBreaker.monitoringWindow` — but ⛔ that six is NOT a separate addend: the last three are already inside the `fieldMappings`, `triggers` and `health` counts above, which is exactly the double-count that made the previous "and four `retiredKey` tombstones" tail drift. Count them by name, never by adding the tail. **A prior in-repo claim is recorded here with its DIRECTION measured rather than remembered, because this row's job is the history of how the type got here**: the conversion registry's note inside `connector-rate-limit-config-removed`'s fixture reads "`retryConfig` and the timeouts beside it are untouched by THIS conversion — a statement about its scope, not a liveness verdict. They are not live: declared, defaulted and documented, and read by nothing." ⚠️ It asserts they are NOT live, and it scopes "untouched" to that one conversion. The former tail here quoted it as asserting the OPPOSITE ("they are live") and called it false when seeded — an inversion that turned this whole passage upside down, and it is corrected rather than carried. Measured direction: the note was TRUE when this ledger was seeded (2026-09-17) and is STALE now, #18975 having made the declared policy execute at the one platform fetch site (`connectorFetchOptions` → `resilientFetch`), so `retryConfig`'s eight sub-keys are `live` on their own rows and `requestTimeoutMs` is `live` beside them; only `connectionTimeoutMs` still answers to it, as the retired tombstone. ⛔ The stale comment is not rewritten from here — it is #19729's, as a dated note beside it — and it is not a line this PR's diff touches. ⚠️ The seeding note's supporting census — "the word does not occur outside `packages/spec` at all" — is FALSE at this head and is corrected rather than carried: `git grep -n retryConfig 14fdebd766 -- . ':!packages/spec'` returns 67 **matching lines** over 15 files — `git grep -o` on the same tree and pathspec returns 77 **occurrences**, and a line is not an occurrence, which is the trap a re-measurer falls into next (26 matching lines in the materializer `packages/services/service-automation/src/plugin.ts` and its materialization test, 22 across `connector-rest` and `connector-openapi` — providers, connectors and their tests — 13 in five `.changeset` fragments, and 6 on two `content/docs` pages). ⛔ Re-read that as the standing lesson of this row: a census is a count plus the tree it was taken against, and a bare "does not occur" with no commit behind it is the shape that rots first. The timeouts half is settled on its own rows: `requestTimeoutMs` is `live`, `connectionTimeoutMs` is retired | | analytics_cube | seeded 2026-09-17 (#18582) — the third debt, paid in the same diff as `connector`. Not a registered kind either: bound in `UNREGISTERED_KIND_SCHEMAS` by #10194 and reached through the same unregistered-kind fallback. **ONE Cube shape, THREE producers, one registry** is what decides every row: `cube-registry.ts` names them itself — authored cubes (`analyticsCubes[]` / `defineCube()`, threaded by the CLI into `AnalyticsServiceConfig.cubes`), COMPILED DATASETS (ADR-0021, where `dataset-compiler` mints a Cube), and ad-hoc query inference. Only the first is the authoring door governed here, so a key whose only reader sits on the compiled-dataset path is not live for an authored cube however busy that reader is — the #4837 producer rule on a shape with three producers. That is `dimensions.granularities` (read by `dataset-executor#granularityOf`, whose argument is a `CompiledDataset` an authored cube never becomes) and `measures.format` (written by the compiler, threaded to the wire from the DATASET measure instead). The query path is genuinely live: `sql` is the FROM table AND the object whose RLS read scope is injected, `measures.type` picks the aggregate, `measures.sql`/`dimensions.sql` the column, `joins[].name` the joined table. The 10 `dead` are the caching block (`refreshKey.every`/`.sql` — no refresh scheduler exists anywhere), the access-control flag (`public` — three sites write `false`, nothing reads it: a knob that was never wired, not a hole that was opened), the three `description`s, and the inner `name` on each of `measures`/`dimensions`, where the record KEY is the identity. It was 12 until #18612 RETIRED `joins[].relationship` and the REQUIRED `joins[].sql` (ADR-0049 enforce-or-remove, maintainer-ruled batch #154): the ON clause is SYNTHESISED as an FK equality and the authored one was never consulted, so a declared join condition came back REPLACED under a 200. `CubeJoinSchema` is a `strictObject`, so the route was strict deletion plus a `guidance` prescription and the two rows left this ledger with the keys — not the `retiredKey()` route, which keeps the row. **#10238 is not prejudged**: whether cube authoring is live end to end is still its own measurement — this ledger answers the per-key question only | The `dead` set across types is the enforce-or-remove worklist (ADR-0049); every diff --git a/packages/spec/liveness/connector.json b/packages/spec/liveness/connector.json index c2b991b2067..193ad584b19 100644 --- a/packages/spec/liveness/connector.json +++ b/packages/spec/liveness/connector.json @@ -1,6 +1,6 @@ { "type": "connector", - "_note": "DeclarativeConnectorEntrySchema (packages/spec/src/integration/connector.zod.ts). Seeded 2026-09-17 (#18582) together with `analytics_cube`: the last two of the three PENDING_GOVERNANCE debts #18133 declared when PR #18581 widened the governance denominator to `authorableTypes()` (`sharing_rule` was paid first, PR #18587). Their landing empties that map. NOT a registered metadata KIND — bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reached through `getMetadataTypeSchema`'s unregistered-kind fallback. WHAT THE WALKER ACTUALLY RESOLVES, measured rather than assumed: the binding names `DeclarativeConnectorEntrySchema`. ⚠️ THE MECHANISM CHANGED WITH THE `connectionTimeoutMs` RETIREMENT and the prior sentence here is corrected rather than carried: that schema USED TO BE `ConnectorSchema.superRefine(...)`, a Zod 4 check attached to the same object def. It is now a `z.preprocess` PIPE — both published carriers wrap one shared private `ConnectorBaseSchema` in the ADR-0049 retired-default residue stage, the entry schema adding the ADR-0097 cross-field rules on the base before wrapping, so the two are SIBLINGS rather than parent and child. The CONCLUSION is unchanged and re-measured on the built entry: the pipe keeps a read-through `shape`, both carriers expose 30 keys, and the key sets are byte-identical with no entry-only and no base-only key — the ADR-0097 cross-field rules add no key and remove none. The gate therefore cannot tell the two schemas apart; what the binding buys is REFUSALS, which are invisible to the walk and visible only in the `authentication` / `actions` / `triggers` rows below, where they are the whole verdict. THE SHAPE FACT THAT DECIDES EVERY ROW: one schema, TWO doors. This ledger's denominator entry exists because of the AUTHORING doors (`defineStack({ connectors })` and `PUT /api/v1/meta/connector/:name`); the same `ConnectorSchema` is ALSO what `AutomationEngine.registerConnector` parses for a def a PLUGIN or an ADR-0097 provider factory builds in code. So a key can have a real consumer and still do nothing when a metadata author writes it, and every row below says WHICH door its consumer is fed from. The keys an authored entry can reach are exactly the AUTHOR-SUPPLIED `ConnectorProviderContext` fields plus `provider` and `enabled`, and the three corrections in that sentence are each measured: `name` is itself one of those fields (`connector-provider.ts#ConnectorProviderContext` declares it), so the former 'plus `name`' tail DOUBLE-COUNTED it; `loadPackageFile` is HOST-INJECTED rather than authored (the materializer passes `createPackageFileLoader(this.options.packageRoot)`, and no authored key reaches it), so the bare 'the `ConnectorProviderContext` fields' OVER-INCLUDED it; and `provider` is read on the AUTHORING door itself — it gates the desired set (`typeof entry.provider !== 'string' … continue`) and selects the factory (`engine.getConnectorProvider(provider)`) — without ever reaching the context, so it was LEFT OUT of the 'exactly'. Everything else in an authored entry is stored and served back by `/meta/connector` WITHOUT REACHING A PROVIDER FACTORY. ⛔ That is NOT the same claim as 'read by no runtime', which the former tail here asserted and which this file's own `actions.key` row already contradicted: `packages/services/service-automation/src/plugin.ts#findInertDeclaredConnectors` reads `(c.actions?.length ?? 0) > 0` on EVERY descriptor at boot and `auditDeclaredConnectors` turns it into the #2612 inert-descriptor warning, so `actions` is a real runtime read of a key no factory is handed. Measured rather than asserted, and this is the whole census: the only reads of a declared entry anywhere in the materializer are `name`, `provider`, `enabled`, `label`, `description`, `icon`, `type`, `providerConfig`, `auth`, `retryConfig` and `requestTimeoutMs` (the last nine are also exactly `connectorInstanceSignature`'s fingerprint, which is why `metadata`, `status`, `syncConfig`, `health`, `triggers` and `webhooks` keep their `read by nothing` rows) plus that one `actions` read. Reaching no factory and being read by nothing are two different claims; only the first holds of the whole remainder. That asymmetry is the trap this type carries, and it is recorded per key rather than asserted once. PRIOR MEASUREMENTS RE-VERIFIED, not inherited: the ADR-0087 conversion registry's `connector-field-mapping-transform-removed` entry recorded 'Execution: none — `fieldMappings` is spelled only inside packages/spec' (2026-08-06), the `syncConfig.schedule` retirement recorded '`syncConfig` has no reader outside `packages/spec`' (#16320, 2026-09-10), and `ConnectorTriggerSchema`'s own docblock says 'NOT YET ENFORCED — declared but never read by the runtime (#3197)'. All three were re-run on this checkout and all three still hold; the counts are in the rows. One prior claim FAILED re-verification and is corrected here: a comment in packages/spec/src/conversions/registry.ts asserts that `retryConfig` 'and the timeouts beside it are untouched — they are live'. They are not read anywhere; see those three rows. PREVIEW READ POINTS ENUMERATED (the #7131 mechanical rule, objectui @dda8f3815): `registerBuiltinPreviews()` registers nineteen types and `connector` is NOT one of them — this type has no registered metadata-admin preview. What objectui DOES consume is (a) the whole SHAPE, via `clientValidation.ts`, which maps `connector` to `DeclarativeConnectorEntrySchema` on BOTH the create and the edit door (it is not strict, so it may judge a stored body), and (b) the RUNTIME registry projection `GET /api/v1/automation/connectors`, from which `connectorsToOptions` reads `name`/`label`/`origin`, `connectorActionsToOptions` reads `actions[].key`/`.label`, and `connectorActionInputSchema` reads `actions[].inputSchema`. Those three are the cross-repo citations below. ADR-0054: no row carries a `proof` and none is owed — no high-risk class binds a `connector/*` path.", + "_note": "DeclarativeConnectorEntrySchema (packages/spec/src/integration/connector.zod.ts). Seeded 2026-09-17 (#18582) together with `analytics_cube`: the last two of the three PENDING_GOVERNANCE debts #18133 declared when PR #18581 widened the governance denominator to `authorableTypes()` (`sharing_rule` was paid first, PR #18587). Their landing empties that map. NOT a registered metadata KIND — bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reached through `getMetadataTypeSchema`'s unregistered-kind fallback. WHAT THE WALKER ACTUALLY RESOLVES, measured rather than assumed: the binding names `DeclarativeConnectorEntrySchema`. ⚠️ THE MECHANISM CHANGED WITH THE `connectionTimeoutMs` RETIREMENT and the prior sentence here is corrected rather than carried: that schema USED TO BE `ConnectorSchema.superRefine(...)`, a Zod 4 check attached to the same object def. It is now a `z.preprocess` PIPE — both published carriers wrap one shared private `ConnectorBaseSchema` in the ADR-0049 retired-default residue stage, the entry schema adding the ADR-0097 cross-field rules on the base before wrapping, so the two are SIBLINGS rather than parent and child. The CONCLUSION is unchanged and re-measured on the built entry: the pipe keeps a read-through `shape`, both carriers expose 30 keys, and the key sets are byte-identical with no entry-only and no base-only key — the ADR-0097 cross-field rules add no key and remove none. The gate therefore cannot tell the two schemas apart; what the binding buys is REFUSALS, which are invisible to the walk and visible only in the `authentication` / `actions` / `triggers` rows below, where they are the whole verdict. THE SHAPE FACT THAT DECIDES EVERY ROW: one schema, TWO doors. This ledger's denominator entry exists because of the AUTHORING doors (`defineStack({ connectors })` and `PUT /api/v1/meta/connector/:name`); the same `ConnectorSchema` is ALSO what `AutomationEngine.registerConnector` parses for a def a PLUGIN or an ADR-0097 provider factory builds in code. So a key can have a real consumer and still do nothing when a metadata author writes it, and every row below says WHICH door its consumer is fed from. The keys an authored entry can reach are exactly the AUTHOR-SUPPLIED `ConnectorProviderContext` fields plus `provider` and `enabled`, and the three corrections in that sentence are each measured: `name` is itself one of those fields (`connector-provider.ts#ConnectorProviderContext` declares it), so the former 'plus `name`' tail DOUBLE-COUNTED it; `loadPackageFile` is HOST-INJECTED rather than authored (the materializer passes `createPackageFileLoader(this.options.packageRoot)`, and no authored key reaches it), so the bare 'the `ConnectorProviderContext` fields' OVER-INCLUDED it; and `provider` is read on the AUTHORING door itself — it gates the desired set (`typeof entry.provider !== 'string' … continue`) and selects the factory (`engine.getConnectorProvider(provider)`) — without ever reaching the context, so it was LEFT OUT of the 'exactly'. Everything else in an authored entry is stored and served back by `/meta/connector` WITHOUT REACHING A PROVIDER FACTORY. ⛔ That is NOT the same claim as 'read by no runtime', which the former tail here asserted and which this file's own `actions.key` row already contradicted: `packages/services/service-automation/src/plugin.ts#findInertDeclaredConnectors` reads `(c.actions?.length ?? 0) > 0` on EVERY descriptor at boot and `auditDeclaredConnectors` turns it into the #2612 inert-descriptor warning, so `actions` is a real runtime read of a key no factory is handed. Measured rather than asserted, and this is the whole census: the only reads of a declared entry anywhere in the materializer are `name`, `provider`, `enabled`, `label`, `description`, `icon`, `type`, `providerConfig`, `auth`, `retryConfig` and `requestTimeoutMs` (ALL BUT `name` AND `enabled` are also exactly `connectorInstanceSignature`'s fingerprint, and the former 'the last nine' tail was wrong in BOTH directions: `plugin.ts#connectorInstanceSignature` declares NINE parameter keys and hashes the same nine — `provider`, `providerConfig`, `auth`, `label`, `description`, `icon`, `type`, `retryConfig`, `requestTimeoutMs` — so the old tail swept in `enabled`, which is never hashed, and dropped `provider`, which is. Measured on both halves of that function, its parameter type and its hashed object, which agree key for key. The fingerprint is why `metadata`, `status`, `syncConfig`, `health`, `triggers` and `webhooks` keep their `read by nothing` rows) plus that one `actions` read. Reaching no factory and being read by nothing are two different claims; only the first holds of the whole remainder. That asymmetry is the trap this type carries, and it is recorded per key rather than asserted once. PRIOR MEASUREMENTS RE-VERIFIED, not inherited, and each named by its REAL id: the ADR-0087 conversion registry entry is `field-mapping-transform-removed`, with NO `connector-` prefix — the `connector-`-prefixed neighbour is `connector-rate-limit-config-removed`, a different retirement, and the former tail here fused the two names. That entry records 'Execution: none. `fieldMappings` is spelled only inside `packages/spec` — the four connector packages, the automation engine, REST and objectui never read it, and nothing anywhere switches on `transform.type`' (2026-08-06). Re-run at this head it SPLITS, and the split is recorded rather than smoothed: the gloss holds — no read in the connector packages, the engine, REST or objectui — but the literal 'spelled only inside `packages/spec`' is FALSE, `fieldMappings` occurring on 14 lines over 6 files outside `packages/spec`, every one of them prose (one changeset, two `content/docs` pages, ADR-0097, the protocol upgrade guide, one upgrade skill). Quoting that clause WITHOUT its gloss is what makes it falsifiable, which is the same trap the `retryConfig` census sentence in this type's README row records. The other two still hold outright: the `syncConfig.schedule` retirement recorded '`syncConfig` has no reader outside `packages/spec`' (#16320, 2026-09-10), and at this head the only non-`packages/spec` hit in code is a pair of COMMENT lines in `packages/qa/dogfood/test/expression-conformance.ledger.ts`, not a read; and `ConnectorTriggerSchema`'s own docblock still says 'NOT YET ENFORCED — declared but never read by the runtime (#3197)' verbatim. WHAT THE CONVERSION REGISTRY ACTUALLY SAYS TODAY, quoted rather than paraphrased, because the former tail here INVERTED it: the comment inside `connector-rate-limit-config-removed`'s fixture reads '`retryConfig` and the timeouts beside it are untouched by THIS conversion — a statement about its scope, not a liveness verdict. They are not live: declared, defaulted and documented, and read by nothing.' It asserts they are NOT live and it scopes 'untouched' to its own conversion; the former tail quoted it as asserting 'they are live' and then answered 'They are not read anywhere', which was wrong twice — it inverted the source, and 'not read anywhere' is contradicted both by the materializer handing BOTH `retryConfig` and `requestTimeoutMs` to `ConnectorProviderContext` and by this very line's census, which lists both among the eleven. Measured direction: the comment was TRUE when this ledger was seeded and is STALE now — those three rows read `live` on all eight `retryConfig` sub-keys, `live` on `requestTimeoutMs`, and RETIRED on `connectionTimeoutMs`. ⛔ The stale comment is NOT rewritten from here: it is #19729's, as a dated note beside it, and it is not a line this PR's diff touches. PREVIEW READ POINTS ENUMERATED (the #7131 mechanical rule, objectui @dda8f3815): `registerBuiltinPreviews()` (packages/app-shell/src/views/metadata-admin/previews/index.ts) makes twenty-two unconditional `registerMetadataPreview(` calls naming twenty-two DISTINCT metadata types, which share twenty distinct preview components (`PermissionPreview` serves `permission` and `profile`, `PositionPreview` serves `position` and `role`) — the READING is stated because those two numbers differ and a bare count is unfalsifiable; the former 'nineteen' here matched neither. `connector` is NOT among those twenty-two type names — this type has no registered metadata-admin preview. What objectui DOES consume is (a) the whole SHAPE, via `clientValidation.ts`, which maps `connector` to `DeclarativeConnectorEntrySchema` on BOTH the create and the edit door (it is not strict, so it may judge a stored body), and (b) the RUNTIME registry projection `GET /api/v1/automation/connectors`, from which `connectorsToOptions` reads `name`/`label`/`origin`, `connectorActionsToOptions` reads `actions[].key`/`.label`, and `connectorActionInputSchema` reads `actions[].inputSchema`. Those three are the cross-repo citations below. ADR-0054: no row carries a `proof` and none is owed — no high-risk class binds a `connector/*` path.", "props": { "name": { "status": "live", @@ -194,7 +194,7 @@ "source": { "status": "dead", "verifiedAt": "2026-09-17", - "note": "Re-verified, not inherited: the ADR-0087 conversion registry's `connector-field-mapping-transform-removed` entry measured (2026-08-06) that '`fieldMappings` is spelled only inside packages/spec — the four connector packages, the automation engine, REST and objectui never read it'. That still holds on this checkout: zero occurrences of `fieldMappings` anywhere in packages/ or examples/ outside packages/spec. A connector field mapping is parsed, stored, and moves no value. ⛔ Same disposition as `syncConfig`: a decision, not a sweep — the whole L3 sync layer it belongs to is declared and unbuilt. Sub-keys below are dead for this one reason." + "note": "Re-verified, not inherited: the ADR-0087 conversion registry's `field-mapping-transform-removed` entry (that is the id in full — no `connector-` prefix; the `connector-`-prefixed neighbour is `connector-rate-limit-config-removed`, a different retirement) measured (2026-08-06) that '`fieldMappings` is spelled only inside packages/spec — the four connector packages, the automation engine, REST and objectui never read it'. That still holds on this checkout: zero occurrences of `fieldMappings` anywhere in packages/ or examples/ outside packages/spec. A connector field mapping is parsed, stored, and moves no value. ⛔ Same disposition as `syncConfig`: a decision, not a sweep — the whole L3 sync layer it belongs to is declared and unbuilt. Sub-keys below are dead for this one reason." }, "target": { "status": "dead",