Skip to content

feat(spec)!: retire connector.connectionTimeoutMs — carried everywhere, applied nowhere - #19657

Merged
os-justin merged 18 commits into
mainfrom
claude/issue-19580-retire-connector-connection-timeout
Sep 23, 2026
Merged

os-justin merged 18 commits into
mainfrom
claude/issue-19580-retire-connector-connection-timeout

Conversation

@os-warren

@os-warren os-warren commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #19580

Clause-②: yes (narrowing)

Ruled: comment 5770606746, batch #211 item 1, letter A — retire connector.connectionTimeoutMs (ADR-0049 enforce-or-remove; the standing 2026-09-10 「以协议为准」 ruling; the 2026-08-27 「no staged retirement」). Removal route: the spec-property-retirement playbook.

What this removes

A fully authorable key — bounded (min(1000).max(300000)), defaulted (30000), .describe()d, writable on ConnectorSchema and, through DeclarativeConnectorEntrySchema, on stack.connectors[] and PUT /meta/connector/:name, and served back by /meta/connector. Every signal an authoring surface can give said it worked. Plus the ConnectorProviderContext.connectionTimeoutMs member handed to every provider factory.

requestTimeoutMs is the replacement: the deadline the platform actually keeps, applied as resilientFetch's per-attempt timeout.

⭐ The measurement the ruling left to the dev — D2 or D3

The ruling prescribed a D3 semantic entry and said a D2 conversion is owed only if a stored connector row can carry the key — 「the dev measures」. It can, so both ship.

Measured first-hand on this branch, before the tombstone landed, against the built packages/spec/dist:

leg reading
getMetadataTypeSchema('connector') bound? true — it resolves DeclarativeConnectorEntrySchema, the shape PUT /api/v1/meta/connector/:name validates against
door parses a body carrying the key? true
does its output retain the authored value? 4321 — so the number reaches sys_metadata
control on the same instrument the already-retired sibling errorMapping on the same schema is refused — the door discriminates rather than accepting everything
applyConversionsToStoredItem('connector', row) live for this type? yes — it fired connector-error-mapping-removed and stripped that key from a stored row
control on the same seam the same row's requestTimeoutMs survived untouched, so the strip is attributable

What would have made it the other answer: getMetadataTypeSchema('connector') returning undefined (no write door ⇒ no stored row), or the door's output dropping the key, or the rehydration seam never reaching connector rows. All three fired the other way, so a D2 conversion is owed and a D3-only kit would have left 17.x rows carrying a key the schema now refuses.

Both dispositions are re-measured by pins in packages/spec/src/integration/connector-connection-timeout-retirement.test.ts, so the answer cannot rot into an assumption.

⚠️ The card is STALE, not wrong — and this section previously said otherwise

⛔ This section's earlier premise is known false and is replaced rather than patched. It claimed the card's Leg-2 table 「was already false when this retirement was taken」. It was not.

At the SHA the card cites and dates — 0870fb5418 — git grep -n connectionTimeoutMs SHA -- . ':!packages/spec' returns exactly five non-spec source hits, and all five are connectionTimeoutMs: 30000,: the card's table, line for line. ⇒ the card was correct when measured. What moved it is b929e0a662 (#19388) — the very PR the card itself flagged as pending.

At origin/main the same instrument returns thirteen non-test source occurrences over seven files in five packages: six reads, four type declarations, and three surviving pure hardcoded 30000 writes (connector-mcp/src/mcp-connector.ts, connector-slack/src/slack-connector.ts, service-automation/src/plugin.ts). ⚠️ Seven files, not five — five is the count of packages, and conflating the two is how the earlier number was reached.

site what it is
services/service-automation/src/plugin.ts:307 reads entry.connectionTimeoutMs into the materialization fingerprint
services/service-automation/src/plugin.ts:1589 reads it onto ConnectorProviderContext
connectors/connector-rest/src/rest-provider.ts:64 reads ctx.connectionTimeoutMs
connectors/connector-openapi/src/openapi-provider.ts:193 reads ctx.connectionTimeoutMs
connector-rest/src/rest-connector.ts:134, connector-openapi/src/openapi-connector.ts:242 ?? 30000 — read the opts and deposit the value on the reported def

The ruling's premise survives, and the mechanism is unchanged. Every read is a pass-through. The value's only termini are (a) the def GET /connectors echoes and (b) the fingerprint that decides whether to re-materialize. connectorFetchOptions() (integration/connector-fetch-policy.ts) is handed { retryConfig, requestTimeoutMs } only, and a pin has asserted since #18975 that nothing aliases this key onto timeoutMs. Carrying a number is not honouring it — the 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 this key would kill a slow-but-connected upstream the author meant to allow with a large requestTimeoutMs.

Zero-enforcement verification, with its control

  • Claim: nothing applies the value as a deadline. Instrument: git grep -n connectionTimeoutMs over the whole worktree (45 hits, hand-read, not counted) plus the source of connectorFetchOptions(). Radius: the monorepo. Control: requestTimeoutMs — same schema, same census, same files — resolves to a real read (opts.timeoutMs = policy.requestTimeoutMs), so the instrument is not dead.
  • Pinned sibling checkout: git grep connectionTimeoutMs at objectui 87af769e9a3ee28ace099fdd653d3ebd79fe82e2 (the .objectui-sha pin) → exit 1, zero hits; control connector on the same command and scope returns 458 lines across 66 files; and requestTimeoutMs is exit 1 / 0 lines there, so it is not a usable control in that repo (it is in objectstack). ⇒ the Console Pin Gate needs no sibling fix and no pin bump with this removal.
    ⚠️ That clearance is about the sibling BUILD, and it is not the whole picture. The residue stage makes both carriers z.preprocess pipes, and objectui's packages/app-shell/src/views/metadata-admin/clientValidation.optOuts.test.ts:468 asserts checks(DeclarativeConnectorEntrySchema) > 0 — 1 against main, 0 here, because a pipe def has no checks array. The SPA still builds and Console Pin Gate never runs that suite, so no gate in either repo sees it. objectui resolves @objectstack/spec from the registry at ^17.0.0, so ⛔ main does not go red on merge — the break lands at objectui's next spec bump. Tracked at objectui#10211; the gate-reach gap at objectstack#19692. ⛔ A .objectui-sha bump is never a rider on another PR, so neither rides here.
  • tsc is the real sweeper. retiredKey() types the key never, so every authoring site in the monorepo fails to compile. All six affected packages typecheck green after the cleanup, which is what says the census is complete rather than the grep.

The retirement kit

  • retiredKey() tombstone on the non-strict ConnectorSchema (a bare delete would be a silent strip, ADR-0104), inherited by DeclarativeConnectorEntrySchema.
  • RETIRED_KEYS_BY_MAJOR[18] × 2 — integration/Connector:connectionTimeoutMs and integration/DeclarativeConnectorEntry:connectionTimeoutMs — as one-file-per-entry under migrations/entries/retired-keys/.
  • D2 connector-connection-timeout-ms-removed in conversions/registry.ts, wired into the step-18 chain.
  • ⭐ ADR-0087 residue stage acceptRetiredDefaultResidue on both carriers with { connectionTimeoutMs: 30000 }. A D2 does not discharge this: the ruled precedent 18.security__ObjectPermission__allowPurge carries both a D2 (permission-allow-restore-purge-removed) and the residue stage, so D2 coverage cannot be the discriminator. The discriminator is whether a released toolchain MATERIALIZED the default — a 17.x toolchain emits connectionTimeoutMs: 30000 into every connector entry, authored or not — and the second door is AutomationEngine.registerConnector, which parses ConnectorSchema for a def a plugin builds in code, where no conversion runs. Without the stage a 17.x connector package fails registration on a value its author never typed. Head now accepts-and-strips 30000 while still refusing 15000, 1000 and "30000". The preprocess pipe this introduces moves five ADR-0097 refinement sites onto its OUT side, so dropped-refinements.baseline.json moves with them — exactly the moves the build gate printed, no additions.
  • D3 semantic connector-provider-context-connection-timeout-ms-retired for the withdrawn ConnectorProviderContext member — a provider factory is code, so there is no authored source for a conversion to rewrite.
  • liveness/connector.json: the row stays dead with a REMOVED note, because retiredKey() keeps the key in the walked shape (the rls.priority precedent). Its stale 「every occurrence outside packages/spec is a WRITE」 claim is corrected there, with the reads named.
  • Baselines: authorable-surface/integration.json gains two [RETIRED] rows, authorable-defaults/integration.json loses the two = 30000 rows. api-surface/ and json-schema.manifest/ are byte-identical — the correct reading for a key-only tombstone that retires no def, not a missed regeneration.
  • Consumers cleaned: the four connector packages and service-automation (fingerprint, declared-item shape, context build, degraded husk).
  • Declared widening, round 4: packages/spec/liveness/README.md's connector row asserted, present tense, that the entry schema is ConnectorSchema.superRefine(...) — and rested its byte-identical-key-set conclusion on that attachment. Both halves are corrected: the mechanism is now the pipe's read-through shape, and the conclusion is re-measured rather than inherited (30 keys each carrier, byte-identical, zero entry-only, zero base-only). ⚠️ Hand-edited on purpose, ⛔ never regenerated: .gitattributes:71-77 splits liveness/state-counts.md (driver-managed numbers) from liveness/README.md (hand-written Notes prose), because 「regenerating a Note would fabricate a verdict」.
  • ⚠️ A SECOND, declared narrowing: the ZodObject combinators leave both published exports. Wrapping ConnectorSchema and DeclarativeConnectorEntrySchema in the residue stage makes them z.preprocess pipes, so .extend(), .omit(), .pick(), .partial(), .merge(), .strict(), .keyof() and .safeExtend() no longer exist on them. Build on the object and re-wrap — acceptRetiredDefaultResidue(<the extended object>, { connectionTimeoutMs: 30000 }), the EffectiveObjectPermissionSchema route. ⚠️ .superRefine() still exists on a pipe and is callable, but returns a schema with no read-through shape — which is exactly what the schema walkers duck-test — so refine before wrapping, never after. Parsing, z.input / z.infer and the read-through .shape are unchanged. The changeset's FROM → TO carries this row; the docblock at connector.zod.ts and the superseded sentence it replaces carry it in the source.
  • Changeset Clause-②: yes (narrowing), minor on @objectstack/spec (the launch-window gate refuses major), patch on the five consumer packages, with the FROM → TO table and the ADR-0087 disposition marker.

Tests and gates run locally

run verdict
pnpm --filter @objectstack/spec build exit 0
dependency-closure build of the five consumer packages exit 0
typecheck × 6 (spec, connector-rest, connector-openapi, connector-mcp, connector-slack, service-automation) exit 0
test × 5 consumer packages 1786 passed
spec src/integration src/conversions src/migrations + the migrate-sentence and cron pins 569 passed / 16 files
spec test:repo 600 passed
full pnpm build + the re-derived 112-command gate sweep on this head 112 / 112, and on the latest round with zero prerequisite failures, because the full build ran first (earlier rounds had three first-pass non-zeros, all PREREQUISITE NOT MET from unbuilt packages ⇒ read as NOT MEASURED, ⛔ never as failures, and re-run green after the build)
the new retirement pin under --project repo 15 passed
check:generated 15 of 15 green, check:docs and check:liveness included
the 14 source audits check:generated names as not run all exit 0, each captured separately
check:nul-bytes, check:cross-package-test-inputs, check:adr-0087-registration, check:changeset-no-major exit 0

Every exit code above was captured before any pipe. The repo-wide gate farm is CI's run, not this PR's local obligation.

Acceptance notes

  • Scope deviation, declared. The dispatch fenced content/docs/** off. content/docs/references/integration/connector.mdx is an auto-generated baseline whose gate (check:docs) is inside the required TypeScript Type Check job, and it goes stale on this change alone. Measured across all 19 open PRs (283 file rows, 0 unreadable): zero hold that path, so the fence's stated reason — "open PRs hold files there" — does not apply to it; the instrument discriminates, returning content/docs rows for seven other PRs. It is regenerated here, exactly as the sibling retirement fix(spec): retire tenancy.organizationField from the authorable surface (#19054) #19618 regenerates four of the same tree's pages. No hand-written content/docs/** prose is touched, and skills/** and .claude/** are untouched — this diff hits no governed surface.
  • Scope, mechanically forced. The tombstone types the key never, so the four connector packages and service-automation must stop writing it or the monorepo does not compile. Those paths are outside the claim's declared file surface and are held by zero open PRs on the same census.
  • The two-writer surface materialised as declared — see the report.
  • packages/spec/vitest.repo-tests.json gains one line: the new tree-scoped absence pin's walk radius, which check:cross-package-test-inputs demanded by name. No new glob; the radius was already declared for this package.
  • The connectionTimeoutMs-is-never-mapped pin in connector-fetch-policy.test.ts is kept after the retirement, deliberately: it is what makes a re-introduction as a silent alias onto timeoutMs fail.
  • health.circuitBreaker remains dead on this schema and is not touched here — a different set of rows on the same ADR-0049 worklist.

Generated by Claude Code

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 22, 2026
@github-actions

github-actions Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 6 package(s): @objectstack/connector-mcp, @objectstack/connector-openapi, @objectstack/connector-rest, @objectstack/connector-slack, @objectstack/service-automation, @objectstack/spec, touching 31 documentable anchor(s). ⚠️ 10 changed file(s) yielded no anchor (packages/spec/authorable-defaults/integration.json, packages/spec/authorable-surface/integration.json, packages/spec/docs/SYNC_ARCHITECTURE.md, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

⛔ 2 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via ConnectorSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-0.mdx (via ConnectorSchema (symbol, a top-level const), DeclarativeConnectorEntrySchema (symbol, a top-level const))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 10 changed file(s) yielded no anchor (packages/spec/authorable-defaults/integration.json, packages/spec/authorable-surface/integration.json, packages/spec/docs/SYNC_ARCHITECTURE.md, …) — pages documenting those are invisible to this run
  • 8 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 6eaa0f4a81a0146250f083df2b9d000b75dbec61 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 2b92990ed2f80d167a6def02a1968e5ef0e86ffd — the merge of head 1e5a44ab640141319360551b9207e711fee8fb47 into base 6eaa0f4a81a0146250f083df2b9d000b75dbec61, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 2b92990ed2f80d167a6def02a1968e5ef0e86ffd && git checkout 2b92990ed2f80d167a6def02a1968e5ef0e86ffd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6eaa0f4a81a0146250f083df2b9d000b75dbec61 1e5a44ab640141319360551b9207e711fee8fb47 && git checkout -B drift-repro 6eaa0f4a81a0146250f083df2b9d000b75dbec61 && git merge --no-ff 1e5a44ab640141319360551b9207e711fee8fb47

node scripts/docs-audit/affected-docs.mjs --json 6eaa0f4a81a0146250f083df2b9d000b75dbec61

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 6eaa0f4a81a0146250f083df2b9d000b75dbec61 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…eted card number

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
… correct the census note

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 (0870fb5)
and was superseded by b929e0a. 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
…s README

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
…head

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
…its census stale

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
…d 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 <noreply@anthropic.com>
…on claims

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
14fdebd on this checkout, 67 lines / 15 files / 77 occurrences.

Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
Co-authored-by: Claude <noreply@anthropic.com>
…tire-connector-connection-timeout

Conflict in packages/spec/src/migrations/registry.ts, step18.rationale (the
hand-written tail, outside the generated markers): main's paragraph for the
translation bundle split is kept whole after the shared closing line, and this
branch's connector.connectionTimeoutMs paragraph is appended after it. The
only byte changed in main's paragraph is its terminator (`.",` becomes `. "`)
so the concatenation continues. conversionIds merged as a set without
conflict: both ids present.

Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
Co-authored-by: Claude <noreply@anthropic.com>
…tire-connector-connection-timeout

Second sync: main advanced by six commits after the first merge, including
the tenancy.organizationField retirement, which also appends to
step18.rationale. Conflict in packages/spec/src/migrations/registry.ts,
step18.rationale (hand-written, outside the generated markers): the shared
closing line and the translation bundle paragraph stay once, the
organizationField paragraph is kept whole, and this branch's
connector.connectionTimeoutMs paragraph is appended after it. The only byte
changed in main's text is the organizationField paragraph's terminator
(`.',` becomes `. '`) so the concatenation continues. conversionIds merged as
a set without conflict: all three step-18 additions present.

Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1e5a44ab640141319360551b9207e711fee8fb47

Reviewed and posted 2026-09-23T11:03Z by the at-tier review subagent the domain:spec#5 seat spawned — read: card #19580 (all 38 comments; reports treated as claims), the PR body/commits/check-runs, the 31-file diff against merge-base 6eaa0f4a81, AGENTS.md, contract-review.md, the spec-property-retirement skill; ran, in a sibling worktree at the head: safeParse / conversion / registry probes on the head tree and on the pre-retirement base tree, the retirement pin under --project repo (15/15), the migrate-sentence pin (14/14), the connector + conversions + migrations suites (516/516), and the sweeps below; NOT MEASURED: the objectui SPA build against this head (no Console Pin Gate run exists at this head) and the derived gate family (read from CI, never re-run).

① Derived judgments

  • Applied nowhere — confirmed first-hand. At merge-base 6eaa0f4a81, git grep connectionTimeoutMs over non-spec, non-test code files: 13 occurrences / 7 files / 5 packages — 6 reads (plugin.ts:307 fingerprint, :1589 context build, rest-provider.ts:64 + openapi-provider.ts:193 ctx. pass-throughs, rest-connector.ts:134 + openapi-connector.ts:242 ?? 30000 onto the def), 4 type declarations, 3 literal writes. Every read terminates on the echoed def or the fingerprint; connectorFetchOptions() reads { retryConfig, requestTimeoutMs } only (control: requestTimeoutMs resolves to opts.timeoutMs, 22 non-spec code lines). At the card's 0870fb5418: exactly the five literal writes. objectui at .objectui-sha 87af769e9a3e…: 0 hits (exit 1); control DeclarativeConnectorEntrySchema 12 hits; no .extend/.omit/.pick/.partial/.merge/.strict/.keyof/.safeExtend/.superRefine/.shape call on either connector schema there. At head, no authoring survives outside the kit (tree-scoped pin green in my worktree; hand grep agrees — remaining hits are comments, prose and the pins).
  • What an author gets (ran, head). ConnectorSchema, DeclarativeConnectorEntrySchema, getMetadataTypeSchema('connector') and ObjectStackSchema each refuse 15000, 1000 and "30000" with code: invalid_type, path: ['connectionTimeoutMs'] (stack: connectors.0.connectionTimeoutMs) and the full prescription — fully-qualified key, removed in @objectstack/spec 17 (ADR-0049 …), names requestTimeoutMs, ends with the os migrate meta --from 17 sentence. 30000 is accepted and stripped on both carriers. Negative controls: the same objects without the key parse; an unknown key is stripped silently (non-strict, unchanged); requestTimeoutMs: 'x' fails at its own path. Base-tree control: the pre-retirement schema emits connectionTimeoutMs: 30000 for an entry authoring only name/label/type, and the write door retained 4321 — so the residue stage and the D2 are both owed, as the PR says. tsc channel: z.input is never (pin with @ts-expect-error green).
  • Registration. RETIRED_KEYS_BY_MAJOR[18] holds integration/Connector:connectionTimeoutMs and integration/DeclarativeConnectorEntry:connectionTimeoutMs once each, no other major. D2 connector-connection-timeout-ms-removed: toMajor: 18, retiredFromLoadPath: true, fixture disjoint, one attributed notice at connectors[0].connectionTimeoutMs, replay idempotent (0 notices, same reference), silent on the default load path, and applyConversionsToStoredItem('connector', …) strips the key with requestTimeoutMs intact. D3 connector-provider-context-connection-timeout-ms-retired present in step 18 with non-empty reason / acceptanceCriteria naming requestTimeoutMs. RETIRED_DEFS_BY_MAJOR[18] untouched (only the pre-existing integration/ConnectorErrorCategory).
  • ⭐ step18.rationale after two conflict resolutions — whole and once. All 86 of main's rationale lines appear in head byte-exact and exactly once, except main's last line whose closing quote-comma becomes a trailing space inside the string (the same edit fix(spec): retire tenancy.organizationField from the authorable surface (#19054) #19618 made to feat(spec)!: split the translation bundle type — settings is a platform group, not a per-app one (#15178) #19600's line); the feat(spec)!: split the translation bundle type — settings is a platform group, not a per-app one (#15178) #19600, fix(spec): retire tenancy.organizationField from the authorable surface (#19054) #19618 and this PR's paragraph heads each occur exactly once; exactly one quote-comma terminator; conversionIds = main's 33 in main's order plus connector-connection-timeout-ms-removed, no duplicates. The semantic[] and RETIRED_KEYS_BY_MAJOR regions differ from main only by this PR's entries.
  • Liveness ledger. Row stays dead, verifiedAt 2026-09-22, REMOVED note — correct for the tombstone route. README connector row: the 44-dead partition re-summed from connector.json (syncConfig 8, fieldMappings 7, health 15, triggers 6, webhooks 1, status 1, metadata 1, actions 2, three top-level tombstones 3 = 44); six tombstone rows by name; state-counts.md row 29/1/44 (unchanged, consistent). _note claims re-derived at head: fingerprint hashes exactly nine keys (has provider, no enabled); authentication declared .optional().default({ type: 'none' }) at :893, non-none value refused at :1168; the connector-rate-limit-config-removed fixture comment (:4503) and the field-mapping-transform-removed "Execution: none" line (:4544) are quoted as they read; ConnectorTrigger docblock verbatim; retryConfig at 14fdebd766 = 67 lines / 77 occurrences / 15 files; syncConfig outside spec = two comment lines in the dogfood ledger; objectui dda8f3815 previews = 22 distinct types / 20 components, connector absent; both carriers expose 30 identical keys. One overtaken count — non-blocking (iv).
  • Baselines and docs. authorable-surface/integration.json +2 [RETIRED]; authorable-defaults/integration.json −2 = 30000; api-surface/ and json-schema.manifest/ untouched — correct for a key-only tombstone (api-surface records ConnectorSchema (const) by name only); dropped-refinements.baseline.json moves exactly the 5 connector sites onto .out. content/docs/references/integration/connector.mdx is AUTO-GEN and mirrors the docblock; its currency rests on check:docs inside the green required TypeScript Type Check job (not re-run here). No hand-written content/docs, skills/, examples/, *.form.ts or i18n surface carries the key; packages/lint and packages/qa/dogfood have no row to move.
  • Console Pin Gate. Not affected by reading: objectui imports the two schemas only to safeParse. The SPA build against this head is NOT MEASURED (the gate was skipped, see CI). The sibling break is a structural test assertion, optOuts.test.ts:468 checks(DeclarativeConnectorEntrySchema): 1 on the base tree, 0 at head (both measured); objectui#10211 open, AGENTS.md §4 promises the pinned sibling is protected, but Console Pin Gate only BUILDS it — a break that reds objectui's tests is invisible to every gate in both repos #19692 closed not_planned.
  • Shipping-sentence sweep (normalised: */// stripped, backticks dropped, ' + ' seams joined). Verified: the thirteen/seven/five census and the five-writes census with their trees; the nine siblings (8 retryConfig + requestTimeoutMs); allowPurge carries D2 (:8595) and the residue stage (permission.zod.ts:563, the EffectiveObjectPermissionSchema route at :595); the two non-residue siblings are registered keys; combinators: eight gone on both carriers, superRefine survives and returns a schema with no shape (all measured against a plain-object control that keeps all nine); Clause-②: yes (narrowing) (the ruling wrote no; the claim corrected it to yes, which is what AGENTS.md's closed pair requires for a removal). Flagged — all non-blocking: (i) changeset: "a published interface member leaves ConnectorProviderContext" — the member entered with b929e0a662 (2026-09-20), after the last tag @objectstack/*@17.4.0 (2026-09-09; npm latest 17.4.0), so no release ever carried it; same for RestConnectorOptions.connectionTimeoutMs / OpenApiConnectorConfig.connectionTimeoutMs. (ii) "five sites … READ" (schema docblock, conversion docblock, pin header, rationale) vs "six reads" (changeset, entry, ledger row, PR body) — same set, the two ?? 30000 fallbacks counted as one site or two. (iii) changeset + entry: "the tombstoned build refuses that exact object at connectors.0.connectionTimeoutMs" — head accepts-and-strips it (residue stage); true only of the tombstone without the stage. (iv) ledger _note: "fieldMappings occurring on 14 lines over 6 files outside packages/spec" cites no tree; 14/6 at 9e2843a4d3, 15/7 at head after main's b22e3f7646 added skills/objectstack-upgrade/evals/protocol-major-upgrade.json (the gloss "prose, no read" still holds). (v) PR body: "objectui resolves @objectstack/spec from the registry at ^17.0.0, so main does not go red on merge" — the gate injects the local spec dist (OBJECTSTACK_SPEC_DIST); the real reasons are that the gate only builds (tsc && vite build) and its path filter does not fire on this diff. Not re-measured (historical censuses): the 19-open-PR / 283-row file census and the local gate tallies. Model identifiers: swept for every model-identifier spelling over the diff, the changeset, the PR body and all 19 commit messages — zero hits; control file 2 hits; commit trailers are the model-free pair.
  • CI at the head (35 check-runs, 35 distinct names, de-dup needed none; latest started_at 2026-09-23T08:38Z): 33 success, 2 skipped, 0 failure. Skips: Console Pin Gate — needs.filter.outputs.console == 'false' (its paths are .objectui-sha, the five console scripts and ci.yml; none in the diff); Packed-tarball smoke (opt-in) — opt-in label needs:pack-smoke absent. Check Changeset (runs check-changeset-no-major and check-adr-0087-registration), Spec property liveness, Lint & Repo Gates and TypeScript Type Check all green. mergeable_state: clean.

② Semver level

Correct. @objectstack/spec: minor (the launch-window gate refuses major; Check Changeset green) carrying the BREAKING banner, the FROM → TO table and the one-line fix, Clause-②: yes (narrowing), and exactly one ADR-0087 marker <!-- adr-0087: registered connector-connection-timeout-ms-removed, connector-provider-context-connection-timeout-ms-retired --> — both ids exist in the registries. patch on connector-rest, connector-openapi, connector-mcp, connector-slack, service-automation is defensible: nothing those packages published is removed (the option fields are post-17.4.0), and the one observable change — the def GET /connectors echoes no longer carries connectionTimeoutMs — is declared in the changeset.

③ Boundary flags

Dev flags, each answered on the card: content/docs/references/…/connector.mdx regenerated outside the fence (AUTO-GEN per the Documentation Guardrails; releases/ untouched) — accepted; the five consumer packages forced by tsc — accepted; the vitest.repo-tests.json line demanded by check:cross-package-test-inputs — accepted, and the pin's radius is declared (turbo @objectstack/spec#test:repo inputs carry packages/** by extension, examples/** non-code, content/**, skills/**, scripts/** — exactly the pin's roots and extension sets); dropped-refinements.baseline.json — accepted; README widening — accepted; PR-body edits deferred to the seat — present at head. Round-6 open question (two pending fragments) → option A, #19729 — closed via #19746, which is in this head. open_questions: [] on the merge-round report; the two merges verified above.

Blocking: none.
Non-blocking:

  1. ⚠️ Escalate before changeset version — unnamed by any round: .changeset/18975-connector-retry-config-and-request-timeout.md (pending, @objectstack/spec: minor) will publish into the same 17.5.0 CHANGELOG entry as this changeset the sentences "ConnectorProviderContext gains retryConfig, connectionTimeoutMs and requestTimeoutMs" and "connectionTimeoutMs … is carried onto ConnectorProviderContext (a custom provider … may honour it)". Unlike Two PENDING changesets ship false connector-ledger mechanism claims into CHANGELOG.md — and they are two different classes, only one of which may be corrected #19729's class 2, that state is never released, so the entry would describe a member 17.5.0 does not have while this entry says it was removed. Outside this diff by the seat's own Two PENDING changesets ship false connector-ledger mechanism claims into CHANGELOG.md — and they are two different classes, only one of which may be corrected #19729 rule; needs the docs(changeset): correct two false mechanism claims in a pending connector-ledger note #19746 route (docs-only correction, maintainer countersign) or an explicit seat authorisation to absorb those two clauses here. Seat's call.
  2. Changeset wording (i): say the ConnectorProviderContext member and the two options fields were added after 17.4.0 and never published; the Clause-② sentence should rest on the authorable key alone. Wording (iii): "the tombstone without the stage refuses …". Count (ii): pick "six reads" or "five sites" once, tree-wide.
  3. Ledger _note (iv): add the tree (9e2843a4d3) or re-cite 15 lines / 7 files at head — the row's own lesson.
  4. The merge-round report's "to file (a)" finding (check:type-check-debt --re-measure spawns an unlocked whole-repo turbo build that clobbers dist under os-verify-lock) has no card — only check:type-check-debt --re-measure trusts stale dist/: phantom upward drift for ledgered packages whose deps resolve to build artifacts, unless the caller builds the closure first #8271 (closed, a different defect). Seat to file.
  5. objectui#10211 (open) is the pin-bump-time break; no bump rides here, per the standing rule.

Implemented-by: claude/issue-19580-retire-connector-connection-timeout
Reviewed-by: session_01Sfe5YjBLwB9J3y8fvm2xq1 — at-tier review subagent spawned by the domain:spec#5 seat

VERDICT: PASS

@os-justin
os-justin marked this pull request as ready for review September 23, 2026 11:04
@os-justin
os-justin enabled auto-merge September 23, 2026 11:04
@os-justin
os-justin added this pull request to the merge queue Sep 23, 2026
Merged via the queue into main with commit fc29c74 Sep 23, 2026
40 checks passed
@os-justin
os-justin deleted the claude/issue-19580-retire-connector-connection-timeout branch September 23, 2026 11:29
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…ector-ledger note (objectstack-ai#19746)

Part of objectstack-ai#19729

## What this changes

Two sentences on
`.changeset/18582-connector-analytics-cube-liveness-ledgers.md:9` — the
card's **class 1**, the two statements that were **false when written**.
One file, one line, `+1 / -1`. No other sentence in that fragment, and
no other fragment, is touched.

## DELIBERATE CORRECTION — this is the written confirmation
`pr-automation.yml` route 0 requires, and `Check Changeset` is RED on
purpose

This PR **adds no changeset of its own**; it **changes a pending
changeset it did not add**. Route 0's discriminator, run against this
PR's merge base:

```
$ git diff --name-status 16d090e HEAD -- '.changeset/*.md'
M	.changeset/18582-connector-analytics-cube-liveness-ledgers.md
```

Every row is `M`, none is `A` ⇒ route 0. The class is **DELIBERATE
CORRECTION, not COLLISION**: this PR did not draw that filename, nothing
of its own was overwritten, and the base copy must **not** be restored —
restoring it republishes the false sentences.
`check-empty-changeset.mjs` reaches the same reading on its own and
prints it in the job log.

| Route 0 prescribes | Here |
|---|---|
| ⛔ do **not** apply `skip-changeset` | Not applied, and it must not be:
the note corrected below is a release that is still pending, so the
label would be a false declaration. |
| Write the confirmation on the PR, naming the note and what changed
under it | This section. |
| Leave `Check Changeset` **RED** | It is red, deliberately. It is not
one of the seven required contexts, so it blocks no merge. The red is
what puts this decision in front of a person. ⛔ Please do not turn it
green, and please do not read it as a failure — every *other* check
should be green. |

### The note

`.changeset/18582-connector-analytics-cube-liveness-ledgers.md` —
`"@objectstack/spec": patch`, **pending**, added by commit `559041d39d`.
`changeset version` deletes the fragment and publishes its text verbatim
into `packages/spec/CHANGELOG.md`, and the `chore: version packages` PR
that performs that is open right now. That is the window this correction
is inside.

### What changed under it — old and new, verbatim

**(a)** old:

> `authentication` is `planned`: refused outright by ADR-0097 §3, never
ignored.

**(a)** new:

> `authentication` is `planned` because the key is accepted and inert
rather than refused: the schema declares `authentication:
ConnectorAuthConfigSchema.optional().default({ type: 'none' })`, so it
parses and the accepted value reaches no consumer, while the objectstack-ai#7990
cross-field rule loudly rejects every non-`none` value and names `auth:
{ type, credentialRef }` as the mechanism to use instead. ADR-0097 §3
("Credentials are references") backs that refusal of inline secrets — it
does not refuse the key.

**(b)** old:

> The keys an authored entry can actually reach are the
`ConnectorProviderContext` fields plus `name` and `enabled`;

**(b)** new:

> The keys an authored entry can actually reach are the author-supplied
`ConnectorProviderContext` fields plus `provider` and `enabled` — `name`
is itself one of those fields, `loadPackageFile` is host-injected rather
than authored, and `provider` never reaches the context yet decides on
the authoring door whether the entry is materialized at all and which
factory does it;

### Why these two are defects in the record and not a dated reading

Every instrument below was read at **`559041d39d^` — the parent of the
commit that added the fragment** — so nothing that changed afterwards is
involved. The card's readings were treated as input and re-derived, not
quoted.

| The sentence's claim | Instrument at `559041d39d^` | Reading |
|---|---|---|
| `authentication` is "refused outright" |
`packages/spec/src/integration/connector.zod.ts:753` | `authentication:
ConnectorAuthConfigSchema.optional().default({ type: 'none' })` — the
key is accepted, and defaulted. |
| same | same file `:970` | `if (entry.authentication &&
entry.authentication.type !== 'none')` — only a non-`none` **value**
raises an issue, and that refusal's own message prescribes "drop
`authentication` (or set `{ type: 'none' }`)", which is only sayable if
the key is accepted. |
| "by ADR-0097 §3" | `docs/adr/0097-declarative-connector-instances.md`
§3, titled "Credentials are references" | "Inline secrets in stack
metadata are rejected at authoring/publish (lint + schema)." Inline
secrets — not the key. |
| "never ignored" | `packages/spec/liveness/connector.json`,
`props.authentication.note`, seeded by the **same commit** | "the only
value an author may write is `{ type: 'none' }` … Not `live`: the
accepted value does nothing". The same commit wrote the correct
statement in the ledger and the false one in the changeset. |
| "plus `name`" |
`packages/spec/src/integration/connector-provider.ts:58` | `readonly
name: string` is itself a `ConnectorProviderContext` field ⇒ the tail
double-counted it. |
| `loadPackageFile` included | same file `:77`;
`packages/services/service-automation/src/plugin.ts:1546` | On the
interface, but the materializer sets it to
`createPackageFileLoader(this.options.packageRoot)` — host-injected,
reached by no authored key ⇒ over-included. |
| `provider` absent | `plugin.ts:1451`, `:1494` | `if (typeof
entry.provider !== 'string' ...) continue` gates the desired set and
`const provider = entry.provider` then selects the factory, so
`provider` is read on the authoring door; it is on no field of
`ConnectorProviderContext` ⇒ omitted. |

Both replacement sentences are date-neutral: they name no count and no
enumeration, so they stay true at the seeding tree and at `origin/main`
alike.

### What deliberately did NOT change

The rest of line 9 is left byte-for-byte as written, because each of
these was **true when written** and has merely been overtaken. A dated
record's job is to say what was true when it was made, so overwriting it
would falsify history rather than correct a record:

- `74 properties: 20 live, 1 planned, 53 dead` — the seeding-time
measurement.
- `four declared subsystems with no engine — syncConfig, fieldMappings,
retryConfig, health` — `retryConfig` was genuinely dead when written.

And these carriers are not touched at all, for the same reason:

- `.changeset/18614-conversion-registry-retryconfig-liveness-claim.md`
- `.changeset/18983-connector-header-rate-limit-remedy.md`
- `packages/spec/src/conversions/registry.ts`

## Verification

Gate families derived from this worktree, never from the shared
checkout:

```
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands
```

It derived **19** families at commit `752992dd28`. All 19 were run, each
exit code captured **before any pipe**, and reconciled:

```
✓ dispatch-gates --ran: 19 derived famil(ies) accounted for — 19 run,
  0 NOT-MEASURED (a DERIVED zero — all 19 recorded an exit code and none of them is 3).
```

**18 of 19 exit 0.** The one non-zero is the expected one:

- `node scripts/check-empty-changeset.mjs --base origin/main` — **exit
1, the route-0 red**. Its output names this PR's class as DELIBERATE
CORRECTION unprompted and ends "this gate stays red either way, and
staying red is what puts the decision in front of a person instead of
routing around it."

Run in addition, because `dispatch-gates` flagged that its roster lives
under `.changeset`, which is where this PR's only path is:

- `node scripts/check-changeset-fixed.mjs` — exit 0,
"`.changeset/config.json` "fixed" group is in sync with 70 public
workspace packages" (a verdict over a real population, not a vacuous
green).

**Repo-wide `pnpm lint` narrowed to this diff, and the narrowing proven
rather than asserted** — all three readings, so the narrowing is a
measurement and not a skip:

1. **Population, read from eslint's own config**, not guessed:
`isPathIgnored('.changeset/18582-connector-analytics-cube-liveness-ledgers.md')`
is `true`; the positive control
`isPathIgnored('scripts/check-nul-bytes.mjs')` is `false` on the same
call, so the predicate can answer either way. Every `files` glob in
`eslint.config.mjs` names TS/JS extensions only, and the config contains
zero occurrences of `markdown` or the markdown extension.
2. **File count, read from `--format json`**: one result entry,
`errorCount` 0, and its only message is "File ignored because no
matching configuration was supplied" — zero rules evaluated. The same
command over the control path produces a genuinely linted entry.
3. **Invariance for untouched files**: the one changed path is in no
eslint population at all and no markdown processor is configured, so the
diff parses nothing and cannot move any untouched file's verdict.
Type-aware linting does not enter into it — the file is never handed to
a parser.

**No package build, test or typecheck is owed**: the diff touches one
`.changeset/*.md` file and no package source, so there is no
affected-package closure and no package's public surface moves.

Control characters: `grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'` over
the changed file reports nothing, with the same pattern firing on a
seeded control file in the same run.

## Acceptance notes

Observations found while verifying, deliberately **not** acted on in
this PR:

- **Two further carriers of the same class-1 false claims live in
in-repo source at `origin/main`**, both written by the same seeding
commit `559041d39d`: `packages/spec/liveness/README.md`'s `connector`
row (both claims) and `packages/spec/liveness/connector.json`'s `_note`
(claim (b) only). **Not filed and not edited** — both are already
corrected on the open PR objectstack-ai#19657's head, verified by reading that head
directly. Carrier: PR objectstack-ai#19657.
- **One input to this work did not survive re-measurement, in a way
worth recording**: the corrected wording was described as liftable from
`packages/spec/liveness/README.md:942`. At `origin/main` that line still
carries the **old, false** wording; the corrected text exists only on PR
objectstack-ai#19657's head, which is not merged. The wording used here was derived at
source instead, and it agrees with objectstack-ai#19657's.
- Source line numbers drift 7–8 lines between the card's citations and
`origin/main` (`connector-provider.ts` `:68` vs `:65`; `plugin.ts`
`:1478`/`:1533`/`:1601` vs `:1470`/`:1513` and `:1525`/`:1594`).
Substance is identical; noted only so a re-measurer does not read the
drift as disagreement.

---
_Generated by [Claude
Code](https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…tor at authoring time (objectstack-ai#19861)

Fixes objectstack-ai#19751

Clause-②: no (narrowing)

## What changes

`checkViewFilterRuleValueShape` (the value-shape refinement of
`ViewFilterRuleSchema`, `packages/spec/src/ui/view.zod.ts`) now refuses
a rule with NO `value` on every operator that takes one. Its scalar arm
returned early on `value === undefined` for every operator, so `{ field:
'name', operator: 'icontains' }` parsed green, while the key's published
description says every operator outside `in` / `not_in` / `between` and
the four unary operators takes a scalar, and the query path refuses the
lowered rule with `400 INVALID_FILTER`.

- The four unary operators (`is_empty`, `is_not_empty`, `is_null`,
`is_not_null`) are answered first and stay valueless, with or without a
value.
- `in` / `not_in` / `between` keep their own arms, which already refused
an absent value.
- The key stays `.optional()` on the shape; the coupling lives in the
refinement, like the other arms.
- The `value` `.describe()` is unchanged (it already declares this
contract), so no generated reference page moves.
- The code comment above the scalar arm, which named an absent value as
a carve-out "the query path itself makes", now says the opposite and
why. The docblock's "mirrors the query path" list and its
runtime-wording section name the new arm.

Refusal text, one issue at the rule's `value` path:

> Filter comparand for operator "icontains" on field "name" is
undefined. The rule carries no value, and "icontains" compares the field
against one — write the value to compare against, or, if the rule means
the field has no value, use an operator that takes none ("is_empty" /
"is_not_empty" / "is_null" / "is_not_null"), which reads its direction
from its name. This is refused at authoring time because the query path
refuses it too (400 INVALID_FILTER).

The leading sentence is the runtime's undefined-comparand sentence
("Filter comparand at PATH is undefined.") with the location named in
the view vocabulary: operator and field, the same substitution the list
and range arms already make. A view rule has no `where` path, and the
`$` spelling in that path is not one a view author can write. The unary
operator names in the tail come from the schema's own
`VIEW_FILTER_VALUELESS_OPERATORS`.

## Producer reading (step 1): objectui at the pinned `.objectui-sha`
`87af769e9a3ee28ace099fdd653d3ebd79fe82e2`

Read with `git show SHA:PATH` from a local clone at that sha, not from a
working tree.

**Does the console ever save a value-taking rule with no `value`? No.**

| writer | file at the pinned sha | what happens to a half-filled row |
|---|---|---|
| `foldFilterGroupToSpecRules`, the one fold every view-filter writer
shares | `packages/app-shell/src/views/viewFilterFold.ts` | a row whose
operator takes a value is dropped when `isFilterValueComplete(operator,
value)` is false (`if (takesValue && isMissingValue(...)) continue`) |
| `isFilterValueComplete` |
`packages/components/src/custom/filter-builder.tsx` | false for `value
== null` (also `''`, `[]`, a half-filled pair), so an absent value is
always incomplete |
| `FilterBuilderField` / `FilterBuilderWidget`: the `filter-builder`
widget that `view.form.ts` names for `filter` and `page.form.ts` for
`filterBy`, plus the per-tab filter editor |
`packages/app-shell/src/views/metadata-admin/widgets.tsx` | calls the
fold on every change; the runtime `ViewConfigPanel` hosts the same
inspector (`ViewConfigPanel.tsx`, `ViewVariantInspector`) |
| list toolbar | `packages/app-shell/src/views/ObjectView.tsx` | no
automatic write at all (its docblock: "There is deliberately NO
persistViewFilter"); explicit saves go through the fold |
| drill-down "Save as view", `foldUrlFilterTriplesToSpecRules` |
`packages/app-shell/src/views/ObjectDataPage.tsx` |
`ViewFilterRuleSchema.safeParse` per rule, refused rules dropped; the
URL triples (`drillUrlFilters.ts`, `parseUrlFilterTriples`) skip an
empty param and always carry a value |

One edge, stated rather than hidden: `handleViewConfigSave`
(`ObjectView.tsx`) persists the config draft whole. A view whose STORED
body already carries such a rule (hand-authored, or written by another
tool) and is re-saved through the panel without its filter being touched
now gets the refusal at save. That view already fails every query today
(next table).

**Does anything drop a valueless row between storage and the query?
No.**

| layer | file | reading |
|---|---|---|
| console lowering: `viewFilterRuleToNode`, behind `toFilterNode` /
`mergeFilterNodes` (plugin-list `buildEffectiveFilter`, plugin-view
`ObjectView`, `ObjectGrid`, `RelatedList`, `LineItemsPanel`) | objectui
`packages/core/src/utils/filter-converter.ts` | a rule without `value`
lowers to the 2-tuple `[field, operator]` and nothing skips it; its own
comment records the runtime throwing `INVALID_FILTER` / 400 for
`['name','icontains']` |
| REST lookup-picker route: `lowerViewFilterRule` | this repo,
`packages/rest/src/view-filter-rule-lowering.ts` | the same 2-tuple; the
module forwards and never drops |
| query normalizer | this repo,
`packages/metadata-protocol/src/protocol.ts` | `isFilterAST`, then
`parseFilterAST`, which throws |

Measured on this tree's spec source (4112752): `isFilterAST(['and',
['name','equals'], ['status','equals','open']])` is true, and
`parseFilterAST` of it throws `INVALID_FILTER` / 400, "Filter comparand
at where.$and[0].name is undefined". One valueless rule fails the WHOLE
view's query, its good rules included.

So no working flow saves or executes this shape, and refusing it at save
breaks nothing that works today.

## Today's behaviour for the whole class (step 2)

Measured at `origin/main` 4112752 by script. The operator list is
`VIEW_FILTER_OPERATORS` read at runtime; the unary set was derived by
behaviour from the schema's own scalar arm (an array is refused on every
non-list, non-range operator outside the private valueless set).

| operators | `ViewFilterRuleSchema`, `value` omitted, before this
change | `parseFilterAST([field, op])` |
|---|---|---|
| `equals`, `not_equals`, `contains`, `not_contains`, `icontains`,
`starts_with`, `ends_with`, `greater_than`, `less_than`,
`greater_than_or_equal`, `less_than_or_equal`, `before`, `after` (13) |
**ACCEPT** | throws `INVALID_FILTER` / 400, "Filter comparand at
where.name (or where.name.$op) is undefined" |
| `in`, `not_in` | refused by the list arm | throws, "requires an ARRAY
of values" |
| `between` | refused by the range arm | throws, "requires a [min, max]
value array" |
| `is_empty`, `is_not_empty`, `is_null`, `is_not_null` | accept | `{
"$null": true }` / `{ "$null": false }` |

After this change the 13 are refused. The other rows are unchanged.

## ADR-0087 reading (step 4)

- This narrows a published accept set. The repo's rule for that during
the launch window is in the header of
`scripts/check-changeset-no-major.mjs`: the level does not carry
breaking-ness, and "the mandatory information carriers for breaking-ness
in the meantime are the **BREAKING** banner the author writes in the
changeset body and the ADR-0087 migration-ledger disposition".
`scripts/check-adr-0087-registration.mjs` then requires a disposition on
the declared-breaking changeset.
- The disposition is `registered`, not `not-required`. The author has a
hand prescription (write the value, switch to a unary operator, or
delete an unfinished row), and `no-migration-prescription` is refused
for a body that carries one. None of the other categories fits: the
package publishes, no existing entry covers absence, and the surface is
a schema, not a runtime interface or a type surface.
- Precedents: `view-filter-rule-scalar-operator-array-refused` (the
sibling arm of this same check) and
`filter-preset-ordering-comparand-refused` (a shape that never executed
usefully) both registered a semantic entry under protocol major 18.
- Added:
`packages/spec/src/migrations/entries/semantic/18.view-filter-rule-absent-value-refused.ts`.
`packages/spec/src/migrations/registry.ts` was regenerated by `pnpm
--filter @objectstack/spec gen:migration-registry` and not hand-edited;
`check:migration-registry` is green. No D2 conversion: there is no value
to infer.
- `check-adr-0087-registration --base origin/main` reads the changeset
as `[BREAKING+clause-②-narrowing] registered
view-filter-rule-absent-value-refused (new here)`.
- `spec-changes.json` and `docs/protocol-upgrade-guide.md` did not move.
The protocol-18 step stays inert until the protocol major reaches 18,
and `check:spec-changes` / `check:upgrade-guide` are green without
regeneration.

## Changeset (step 7)

`.changeset/19751-view-filter-rule-absent-value-refused.md`, `minor` on
`@objectstack/spec`. `files[]` ships `dist` and `src/**/*.zod.ts`, and
both carry the refinement. Its summary is the user-visible change: a
stored view filter rule with no value on a value-taking operator is now
refused at save instead of failing every query. It carries the BREAKING
banner, a FROM/TO block, `Clause-②: no (narrowing)` and the registered
disposition marker. One sentence names that it reverses the carve-out
the still-pending objectstack-ai#19514 changeset records (an omitted value "still
parses", an absent comparand "is left unjudged"), so the two entries do
not contradict each other in the compiled CHANGELOG. The objectstack-ai#19514 file
itself is not edited.

Level: `minor`. An accept-set narrowing declared `(narrowing)` is
BREAKING (AGENTS.md, Post-Task Checklist step 3), and during the launch
window a breaking change ships as `minor`: the header of
`scripts/check-changeset-no-major.mjs` says "During the launch window we
ship breaking changes as `minor`", and that the BREAKING banner and the
ADR-0087 disposition carry the break, not the level. Both precedents
above shipped `minor` with the same banner. The first round graded this
`patch`; the at-tier contract review (record 5808364674) failed that,
and the patch-round commit ab0104d changes the frontmatter to `minor`
and rewrites the banner sentence to state the convention ("Shipped as
`minor` under the repo's launch-window convention for accept-set
narrowings"). That commit moves no package file.

The PR's `Clause-②: no (narrowing)` line is the claim's, copied
verbatim, and matches the changeset's line. With the arm present, the
level axis of `check-changeset-no-major.mjs` judges the level instead of
standing down. Measured offline with `--event` on this body, it refuses
the first round's `patch` head 22a14a1 (exit 1) and passes `minor` at
ab0104d (exit 0).

## Fixtures, examples and pins (step 5)

- An AST scan of every tracked `.ts` / `.tsx` / `.mts` / `.js` / `.mjs`
/ `.json` outside `content/docs/references/` (1,739 files mention
`operator`) found 311 object literals with `field` plus a string-literal
value-taking operator (aliases folded). 20 of them have no `value` key,
and none is a view filter rule in a shipped example or seed:
- 7 are QA assertions (`expectedValue`, a different schema) in
`examples/app-showcase/qa/platform-smoke.test.json`;
- 5 are QA assertions in `packages/core/src/qa/runner.test.ts` and
`packages/spec/src/qa/testing.test.ts`;
- 1 is a skill trigger condition in
`packages/spec/src/ai/skill-trigger-condition-value-shape.test.ts`;
- 4 are a structural walk with no schema in
`packages/metadata-protocol/src/protocol.graft-normalized-operators.test.ts`;
  - 3 are in `view-filter-rule-value-shape.test.ts`.
Markdown (`.md` / `.mdx`) has no match. No fixture was an authoring
mistake, so no fixture was edited.
- Pins that pinned the removed carve-out and moved with it:
- `packages/spec/src/ui/view-filter-rule-value-shape.test.ts`: `equals +
omitted` and `greater_than + omitted`, from accepted to refused.
- `packages/spec/src/data/filter-icontains-parse-door.test.ts`: "ABSENCE
is left unjudged" now asserts that absence is refused once, in the
absent-value arm's words and never in the conformance table's. ⚠️ This
file is outside the claim's declared file surface. It is a
view-filter-rule test that lives in `src/data/`, not beside
`view.zod.ts`, and it had to move with the carve-out it pinned.
- Carriers named in the docblock (`ListView.filter`, a tab filter,
`Page.filterBy`, a related-list filter, a lookup picker filter, plus
`ObjectGridProps.defaultFilters`) are all
`z.array(ViewFilterRuleSchema)`. The full spec suite is green, and a new
pin drives the refusal through `ListView.filter` at `filter.1.value`.

## Tests

- New pins, with operator lists derived at runtime: value-taking is
`VIEW_FILTER_OPERATORS` minus the four valueless operators. The
valueless set is module-private in `view.zod.ts` and deliberately not
exported, so the test reuses the file's existing transcription, and a
new two-way sweep holds that transcription equal to the private set by
behaviour: over every operator, an absent value is accepted exactly when
the operator is valueless.
- `vitest run --project local` on `view-filter-rule-value-shape.test.ts`
and `filter-icontains-parse-door.test.ts`: 104 passed.
- Firing control at 325052f, through `scripts/ablation-replace.mjs`:
the anchor `if (value === undefined) {` was replaced by `if (value ===
undefined) return;` followed by `if (false) {`, which is the base
behaviour (an absent value returns before any issue). The anchor went 1
to 0 and the blob b6b2f44 to 8c2839db. Result: **11 new pins red, 57
green**. After the restore, the blob equals HEAD and `git diff HEAD` is
empty. A first attempt was refused by the tool before anything ran,
because its replacement contained the anchor; nothing was measured on
that attempt.
- Full spec `local` project at 22a14a1: 522 files passed, 15,420
tests passed. One file skipped by its own stale-dist condition
(`scripts/root-entry-type-nameability.pin.test.ts`); after a rebuild at
the same head it ran with `OS_EXPECT_ROOT_NAMEABILITY=1`: 2 passed.
- Spec `repo` project at 22a14a1: 34 files, 587 tests passed.
- `pnpm --filter @objectstack/spec typecheck` at 22a14a1: exit 0
(tsc, scripts typecheck, `check:test-typecheck` OK).

## Gates

`node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` at 22a14a1 derived 86 commands. All were run and
reconciled with `--ran`: 84 exit 0, 2 NOT MEASURED, 0 unrun. The two NOT
MEASURED are `check:dual-build-cjs-loads` and `check:type-check-debt`,
both exit 3 PREREQUISITE NOT MET because they need the whole-workspace
build. They are left to CI. `check:generated` is 15/15 up to date after
a fresh build at that head.

Generated files that moved: `packages/spec/src/migrations/registry.ts`
only, via `gen:migration-registry`.

Patch round at ab0104d: `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` derived 86 commands from a tree
53 commits behind origin/main 2c1011b. origin/main's selector over the
same six paths adds one family, `check:migration-registry`, which was
run too (exit 0). The `--ran` reconciliation reads 84 run, all exit 0, 2
NOT MEASURED (`check:dual-build-cjs-loads`, `check:type-check-debt`:
exit 3 PREREQUISITE NOT MET, they need the whole-workspace build, left
to CI), and 0 unrun. `check-changeset-no-major --base origin/main`
prints "This diff introduces no `major` bump."
`check-adr-0087-registration --base origin/main` reads
`[BREAKING+clause-②-narrowing] registered
view-filter-rule-absent-value-refused`. CI at ab0104d: 35 check runs,
32 success, 3 skipped (Console Pin Gate, Build Docs, Packed-tarball
smoke), 0 failure.

## Scope held

- In `view.zod.ts`, only `checkViewFilterRuleValueShape` and its
docblock changed. The `ViewFilterRuleSchema` block is untouched: its
JSDoc and `.describe()` are still true. None of PR objectstack-ai#19809's regions is
touched.
- `FILTER_TEXT_CASES` gains no row. There are no objectui or runtime
(`parseFilterAST`) edits.
- `origin/main` has moved 8 commits past the branch point: objectstack-ai#19598
touches the `ListView` shape in `view.zod.ts`, and objectstack-ai#19657 touches
`registry.ts`. A driver-less `merge-tree` of HEAD onto `origin/main`
8cbc3c0 is clean. Main is not merged in.

## Acceptance notes

- The sibling entry `view-filter-rule-scalar-operator-array-refused`
says, in its replacement prose, "An omitted value is still an omitted
value". That was true of its own arm; after this change an omitted value
on a scalar operator is refused. Both entries sit in the uncut
protocol-18 step. The new entry's leading comment names the reversal,
and the sibling's text was left as it is (it is outside this card's file
surface).
- `checkViewFilterRuleTextComparand`'s docblock, carve-out 1, says an
omitted comparand "is left to whatever judges absence". That stays true:
the shape arm now judges it. Not edited.

---
_Generated by [Claude
Code](https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…when read as one release (objectstack-ai#19918)

Fixes objectstack-ai#19851

Clause-②: no

## What this changes

Changeset text only: two PENDING notes on `origin/main`,
`.changeset/18975-connector-retry-config-and-request-timeout.md` and
`.changeset/19580-retire-connector-connection-timeout-ms.md`, `+47 /
-29`. No code, no other file, and no frontmatter line: packages and
levels are byte-identical to the merge base.

Both notes go into the same next release. The last tag is
`@objectstack/*@17.4.0`. On npm, `latest` for `@objectstack/spec`,
`connector-rest`, `connector-openapi`, `connector-mcp`,
`connector-slack` and `service-automation` is `17.4.0`, published
2026-09-09, with no version after it. Read together as that one release,
the base text said that `ConnectorProviderContext` **gains**
`connectionTimeoutMs`, and also that the same member, called "a
published interface member", is **removed**. It also misstated what the
residue stage does with the default value, and it anchored its read
census to a moving ref. After this PR, every sentence the two notes
publish is true of what that release ships.

## DELIBERATE CORRECTION: this PR awaits the maintainer's written
confirmation, and `Check Changeset` is red on purpose

This PR adds no changeset of its own. It changes two pending changesets
it did not add. Route 0's discriminator against the merge base:

```
$ git diff --name-status 8490127 HEAD -- '.changeset/*.md'
M	.changeset/18975-connector-retry-config-and-request-timeout.md
M	.changeset/19580-retire-connector-connection-timeout-ms.md
```

Every row is `M` and none is `A`, so this is the DELIBERATE CORRECTION
class that `scripts/check-empty-changeset.mjs` names. It is not a
COLLISION: nothing of this PR's own was overwritten, and restoring the
base copies would republish the false sentences.

- ⛔ No `skip-changeset` label, and ⛔ no new changeset file (ruling D on
objectstack-ai#18375).
- `Check Changeset` stays **advisory red**. It is not one of the seven
required contexts. Every other check should be green.
- **This PR awaits the maintainer's one-sentence confirmation of the
correction.** The owning seat carries it to this PR with its provenance
(who, the words, where). The PR is not to be armed before that
confirmation is on it.

## Measurements (taken before editing a word)

The worktree was cut at merge base `8490127962`. The checkout is a full
clone (`git rev-parse --is-shallow-repository` answers `false`).

### M1: the three members entered after 17.4.0, and no release carried
them

| Question | Instrument | Reading |
|---|---|---|
| When did each member enter and leave? | `git log -G
'connectionTimeoutMs\?:' origin/main` on `connector-provider.ts`,
`rest-connector.ts` and `openapi-connector.ts` | For all three files, it
entered with `b929e0a662` (2026-09-20, objectstack-ai#19388) and left with
`fc29c74400` (2026-09-23, objectstack-ai#19657). |
| Is `b929e0a662` in any release? | `git tag --contains b929e0a` | 0
tags. |
| Same question, by ancestry | `git merge-base --is-ancestor b929e0a
'@objectstack/spec@17.4.0'` | **exit 1**. The control leg is
`@objectstack/spec@17.3.0` (`8a1bad8b8e`), in the same checkout, against
the same target, an older commit: **exit 0**. |
| Is 17.4.0 the newest release? | `git ls-remote --tags origin
'refs/tags/@objectstack/spec@*'` and `npm view @objectstack/spec
dist-tags time` | The newest tag is `17.4.0`, which peels to
`7e6337007f`. The `connector-rest`, `connector-openapi` and
`service-automation` 17.4.0 tags point at the same commit. npm has
`latest` `17.4.0` and `rc` `17.0.0-rc.6`, and nothing was published
after 2026-09-09T03:57Z. |
| What did 17.4.0 ship? | `npm install @objectstack/spec@17.4.0`, then
read `dist/integration/index.d.ts` | `interface
ConnectorProviderContext` has `name`, `label`, `description`, `icon`,
`type`, `providerConfig`, `auth` and `loadPackageFile`. It has no
`connectionTimeoutMs`, and no `retryConfig` or `requestTimeoutMs`
either. |
| same | `npm pack @objectstack/connector-rest@17.4.0` and
`@objectstack/connector-openapi@17.4.0`, then read every `.d.ts` |
`interface RestConnectorOptions` and `interface OpenApiConnectorConfig`
are present (the control), with **0** `connectionTimeoutMs` occurrences
in any `.d.ts`. |
| What does the next release ship? |
`packages/spec/src/integration/connector-provider.ts:67` at the merge
base | `ConnectorProviderContext` declares `retryConfig` and
`requestTimeoutMs`. `connectionTimeoutMs` appears only in a "REMOVED"
comment. `rest-connector.ts:42` and `openapi-connector.ts:130` have
comments only. |

The authorable key **was** published. In the 17.4.0 source,
`connector.zod.ts:839` reads `connectionTimeoutMs:
z.number().min(1000).max(300000).optional().default(30000)`, and
`DeclarativeConnectorEntrySchema` (`:955`) is built on
`ConnectorSchema`. The M2 probe of the released 17.4.0 package shows
`ConnectorSchema` accepting and keeping an authored `15000`. So "a
published authorable key is removed on two carriers" stays true, and it
is now the only thing the Clause-② sentence rests on.

### M2: parse probes, with the path each refusal is reported at

For today's tree, `tsx` ran against `packages/spec/src` at the merge
base, for four carriers. The entry is `{ name: 'ledger_api', label:
'Ledger API', type: 'api' }` plus the key:

| `connectionTimeoutMs` | `ConnectorSchema` |
`DeclarativeConnectorEntrySchema` | `getMetadataTypeSchema('connector')`
| `ObjectStackSchema` (`connectors: [entry]`) |
|---|---|---|---|---|
| absent (control) | accept, key absent | accept, key absent | accept,
key absent | accept, key absent |
| `30000` | **accept, key stripped** | **accept, key stripped** |
**accept, key stripped** | **accept, key stripped** |
| `15000` | refuse `invalid_type` @ `connectionTimeoutMs` | same | same
| refuse `invalid_type` @ `connectors.0.connectionTimeoutMs` |
| `1000` | same as `15000` | same | same | same |
| `"30000"` (string) | same as `15000` | same | same | same |

Every refusal message names `requestTimeoutMs`. In every accepted case
`requestTimeoutMs` still reads `30000`, the control that shows the stage
leaves the live sibling alone. `ConnectorSchema` is a `pipe` whose input
stage is a `transform`. The tombstone **without the stage** is the
pipe's inner object. It refuses `30000` at `connectionTimeoutMs`, and it
accepts the same entry with the key absent (the control).

Released side: `@objectstack/spec@17.4.0` from npm emits
`connectionTimeoutMs: 30000` for that entry on `ConnectorSchema`, on
`DeclarativeConnectorEntrySchema` and on `ObjectStackSchema`, and it
accepts and keeps an authored `15000`. `npm pack` of `connector-mcp`,
`connector-openapi`, `connector-rest` and `connector-slack` at `17.4.0`
each ship one literal `connectionTimeoutMs: 3e4` in their JS.

### M3: which sites READ the key (one number: **six**)

The instrument is `git grep -n connectionTimeoutMs SHA -- .
':!packages/spec'`, keeping non-test code files. It was run at
`e07843b5a6`, the parent of the landing commit `fc29c74400`, and gives
an identical result at `6eaa0f4a81`, the review's merge base. The result
is 13 occurrences, 7 files, 5 packages:

- **6 reads:** `openapi-connector.ts:242`, `openapi-provider.ts:193`,
`rest-connector.ts:134`, `rest-provider.ts:64`, `plugin.ts:307`,
`plugin.ts:1589`
- 4 type declarations: `openapi-connector.ts:135`,
`rest-connector.ts:47`, `plugin.ts:291`, `plugin.ts:339`
- 3 literal `30000` writes: `mcp-connector.ts:247`,
`slack-connector.ts:94`, `plugin.ts:1782`

Six read expressions sit at six file:line locations. The "five sites"
phrasing elsewhere counts the two `?? 30000` fallbacks as one site: it
is the same set in a different unit (see the acceptance notes). Within
the changesets, the one count used is **six reads**, and the tree it was
taken on is now named. `0870fb5418`, which the note cites for the
earlier census, re-measures at exactly five hits, all
`connectionTimeoutMs: 30000,`, as the note says.

## Old and new, per file

### `.changeset/18975-connector-retry-config-and-request-timeout.md`

**(a)** This release does not add a member that the same release removes
(M1).

> old: `ConnectorProviderContext` gains `retryConfig`,
`connectionTimeoutMs` and `requestTimeoutMs`, read-only and resolved
from the entry …

> new: `ConnectorProviderContext` gains `retryConfig` and
`requestTimeoutMs`, read-only and resolved from the entry …

**(b)** The key is no longer described as "carried onto
`ConnectorProviderContext`" and "owed a decision". The same release
retires it, and no release carries it on the context (M1). The mapping
and the ledger still record the reason: `connector-fetch-policy.ts:57`
and `liveness/connector.json` `props.connectionTimeoutMs.status: dead`,
both at the merge base.

> old: **⚠️ `connectionTimeoutMs` is NOT enforced, deliberately, …** …
So it is carried onto `ConnectorProviderContext` (a custom provider on a
transport that *can* separate the phases may honour it) and left
unenforced by the platform, with the reason recorded at the mapping and
in `packages/spec/liveness/connector.json`, which keeps that one row
`dead`. It is owed a second, narrower ADR-0049 decision: retire it, or
re-describe it as something the platform can enforce.

> new: **⚠️ `connectionTimeoutMs` is NOT made live, deliberately, …** …
So this change leaves it unenforced, with the reason recorded at the
mapping and in `packages/spec/liveness/connector.json`, whose row for it
stays `dead`. That left it owed a second, narrower ADR-0049 decision,
and this same release takes it: `connector.connectionTimeoutMs` is
**retired**, and its own entry in this release says what to write
instead. The key never reaches `ConnectorProviderContext` in any
release.

**(c)** The claim that the schema "keeps every key" is scoped to the
change it describes. Read as a claim about the release, it is false
twice. `connectionTimeoutMs` is retired by `fc29c74400`.
`syncConfig.schedule` is deleted by `929d9e3f20`: present at 17.4.0,
absent at the merge base, and `git merge-base --is-ancestor 929d9e3
'@objectstack/spec@17.4.0'` gives exit 1. As a claim about `b929e0a662`
itself it holds: that commit changes 0 non-comment lines of
`connector.zod.ts`, the file that also holds `RetryConfigSchema`.

> old: Nine of the ten ledger rows flip `dead` → `live` with the
consumer site named. No declaration moves: the connector schema keeps
every key, every bound and every default it had.

> new: Nine of the ten ledger rows flip `dead` → `live` with the
consumer site named; the tenth is `connectionTimeoutMs`, above. This
change itself moves no declaration: it leaves every key, every bound and
every default on the connector schema as it found them.

### `.changeset/19580-retire-connector-connection-timeout-ms.md`

**(d)** The never-released member is no longer called published. The
Clause-② sentence now rests on the authorable key alone (M1). The line's
leading token is unchanged, and `readClause2Line` reads it identically
at base and head.

> old: `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.

> new: `Clause-②: yes (narrowing)` — a published authorable key is
removed on two carriers, so the accept set a consumer writes against
narrows.

**(e)** The migration no longer tells a released-version factory to stop
reading something it never had (M1). One paragraph says which members
were never released and who could have read them.

> old: **The one-line fix: delete the key** — and, for a custom provider
factory, stop reading `ctx.connectionTimeoutMs`.

> new: **The one-line fix: delete the key.** … The three interface
members withdrawn with it were **never in a release**:
`ConnectorProviderContext.connectionTimeoutMs`,
`RestConnectorOptions.connectionTimeoutMs` and
`OpenApiConnectorConfig.connectionTimeoutMs` all entered with
`b929e0a662`, after the `@objectstack/*@17.4.0` tag, and leave in this
same release. A factory or caller built against a released version never
saw them; only code written against an unreleased `main` in between can
read them, and it stops.

Two sentences follow from the same reading. The FROM → TO row for
`ConnectorProviderContext.connectionTimeoutMs` gains "added after
`@objectstack/spec@17.4.0` and never in a release, see below". The D3
bullet now says the removal reaches "a factory author who read it —
possible only against an unreleased `main` —" rather than any factory
author.

**(f)** The residue stage is stated as measured (M2).

> old: … 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`.

> new: … measured on both sides of the retirement: the released
`@objectstack/spec@17.4.0` emits `connectionTimeoutMs: 30000` for an
entry that authored only `name`/`label`/`type`, and the tombstone
**without the stage** refuses that exact object at
`connectionTimeoutMs`. With the stage, as it ships, that object is
**accepted and the key stripped** before the tombstone reads it — on
`ConnectorSchema`, `DeclarativeConnectorEntrySchema`, the
`/meta/connector` schema and `stack.connectors[]` alike.

> old: … 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, …

> new: … and in 17.4.0 all four shipped connector packages put that
`30000` straight into the def literal. So the emitted `30000` is
accepted-and-stripped, while every other value (`15000`, `1000`, the
string `"30000"`) keeps the tombstone's refusal — at
`connectionTimeoutMs`, or at `connectors.0.connectionTimeoutMs` inside a
stack — …

The tombstone bullet's "`stack.connectors[]` and the `/meta/connector`
door refuse it too" gains "every value but the retired default `30000`,
which the residue stage below strips first". Without that clause, the
bullet contradicted the residue bullet.

**(g)** The read census names its tree (M3).

> old: Measured with `git grep -n connectionTimeoutMs SHA -- .
':!packages/spec'` at `origin/main`: **thirteen** …

> new: Measured with `git grep -n connectionTimeoutMs SHA -- .
':!packages/spec'` at `e07843b5a6`, the tree this retirement landed on:
**thirteen** …

### What deliberately did not change

- **Frontmatter, both files.** No level is wrong after the correction:
- 18975's `minor`s cover real widenings: `ConnectorProviderContext`
gains `retryConfig` and `requestTimeoutMs`, and the provider options and
`resilientFetch` gain knobs.
- 19580 stays `@objectstack/spec: minor` with the **BREAKING** banner,
because the launch window refuses `major`.
- 19580's `patch` for the connector and service packages is now better
supported: the correction states outright that the option fields those
packages withdraw were never released.
- **The ADR-0087 marker and the remaining sentences.** The marker line
is unchanged, and so is every sentence not quoted above.
- **What the changeset gates read.** Base and head get the same reading
from `check-adr-0087-registration` and `readClause2Line`:
- 19580: `breaking true ["BREAKING","bang"]`, disposition `registered`
with the same two ids, migration prescription found.
- 18975: non-breaking, no migration prescription, `Clause-②: yes
(widening)` declared.
- An intermediate wording (`654adaba14`) mentioned the retirement
entry's migration table by its house label. The detector's label branch
read that as a migration prescription on 18975. It was reworded in
`baf93b20fd`, and the parity above was re-measured on `f4fbb2203e`.

## Local gates, on `f4fbb2203e`

The gate list is derived by `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands`: 19 commands. Reconciled with
`--ran` and exit codes recorded: `19 derived, 19 run, 0 NOT-MEASURED, 0
UNRUN`. Every exit code was captured before any pipe.

| Command | Exit |
|---|---|
| `node scripts/check-empty-changeset.mjs --base origin/main` | **1**,
as intended: `✓ No empty-frontmatter changeset introduced`, then "This
PR changes a changeset it did not add" naming both files, with the
DELIBERATE CORRECTION remedy |
| `node scripts/check-adr-0087-registration.mjs --base origin/main` | 0
(`✓ … this PR adds no declared-breaking changeset (2 non-breaking
changeset(s) seen)`) |
| `node scripts/check-changeset-no-major.mjs --base origin/main` | 0 (`✓
This diff introduces no major bump.`; the LEVEL axis is not applicable
without a `pull_request` payload) |
| `node scripts/check-changeset-fixed.mjs` (not derived, run as a
`check-changeset*` gate) | 0 (`✓ … "fixed" group is in sync with 70
public workspace packages.`) |
| `pnpm check:changeset-gate-self-tests` | 0 (159, 441 and 339
assertions) |
| `pnpm check:nul-bytes` | 0 (`OK … no raw ASCII control bytes`) |
| the `--self-test` of `check-adr-0087-registration`,
`check-changeset-no-major` and `check-empty-changeset`, plus
`check-closing-keyword-parity` (both), `check-comment-mask-corpus`,
`pm/release-rehearsal-clone --self-test`, `check:driver-memory-census`,
`check:gitlink-declared`, `check:objectui-changeset`,
`check:pm-changeset-deadline-census`, `check:published-files`,
`check:refd-timer-probe`, `check:watch-hint-literal` | 0 each |

NOT MEASURED: the four type-check programs the derivation lists outside
the derived total, which are CI's whole-workspace lanes. Reason: the
diff touches no TypeScript, and no workspace build was bought for a
text-only diff. Also scanned for control bytes (none, with a lit
positive control) and for model identifiers in the diff (0 hits).

## Acceptance notes

- **"five sites … READ" is still the wording in four code docblocks.**
`packages/spec/src/integration/connector.zod.ts:565`,
`packages/spec/src/conversions/registry.ts:9078`,
`packages/spec/src/integration/connector-connection-timeout-retirement.test.ts:15`
and `packages/spec/src/migrations/registry.ts:5255` say that. The
retired-key entry
(`18.integration__Connector__connectionTimeoutMs.ts:14`,
`migrations/registry.ts:14678`), the liveness row and the 19580
changeset say "six reads". It is the same set, with the two `?? 30000`
fallbacks counted as one site or two. Not touched here, because this PR
is changeset text only. Carrier: none.
- **The retired-key entry and the liveness `_note` still anchor the read
census "at `origin/main`" without a sha.** This is the moving-ref
reading corrected in the changeset by (g). Code and data, not changeset
text, so not touched here. Carrier: none.
- **The 19580 note's `Clause-②` sentence reads as `near-miss` (reason
`describing`) in `readClause2Line`.** That is so at base and at head,
because the token sits in backticks with prose after it.
`check-adr-0087-registration` therefore classifies the note as breaking
through the banner and the `!`, not through signal (4). It is left as it
was: changing the line would change what the gate reads, which is
outside a correction of false sentences.

---
_Generated by [Claude
Code](https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

3 participants