Skip to content

feat(spec)!: retire the connector resilience family — health (probe + breaker), status and nested webhooks, sixteen keys nothing read (#20273) - #20350

Merged
objectstack-fleet[bot] merged 23 commits into
mainfrom
claude/issue-20273-connector-resilience-keys-retired
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 23 commits into
mainfrom
claude/issue-20273-connector-resilience-keys-retired

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20273

Clause-②: no (narrowing)

Retires the connector resilience family under ADR-0049 enforce-or-remove, one batch, by the triage verdict RETIRE (comment 5858520070) under the maintainer's criterion on #18900: connector.health (the healthCheck probe, eight keys, and the circuitBreaker, six keys), connector.status and the connector-nested webhooks — sixteen authorable keys that nothing read. Authoring any of them is now a tsc error and a parse error that carries the prescription; no alias window.

Census first (origin/main 3f86dc5, each zero beside a lit control)

family readers outside packages/spec lit control, same scan
the fourteen health.healthCheck.* / health.circuitBreaker.* leaves 0 (circuitBreaker, fallbackStrategy, halfOpenMaxRequests, unhealthyThreshold, healthyThreshold, monitoringWindowMs, resetTimeoutMs outside generated docs; healthCheck only as the kernel plugin-health contract; no .health. read in packages/connectors or service-automation) retryConfig read 17 times in packages/connectors + service-automation
status 0 connector reads: one member-read pattern over connectors, service-automation, rest, runtime, metadata and objectql finds 38 .status reads, every one on an HTTP answer, an error case or a flow-run entry the same pattern finds requestTimeoutMs read off a connector entry / provider context 5 times
nested webhooks 0 reads of a connector's own array (the one test that authors one pins that it is NOT hoisted) stack.webhooks read 5 times

objectui: nothing imports a removed name at the pinned .objectui-sha f8a9d0fb (one comment mentions WebhookEventSchema), and no connector status / health / webhooks reader at objectui main 610819c40. No key had a live reader, so no premise_still_valid fork.

What changed

  • Tombstones. health, status, webhooks are retiredKey() tombstones on the private ConnectorBaseSchema that both published carriers wrap (ConnectorSchema, DeclarativeConnectorEntrySchema), so defineConnector, registerConnector, stack.connectors[] and PUT /api/v1/meta/connector/:name all refuse them. Six RETIRED_KEYS_BY_MAJOR[18] rows.
  • Residue. status was .default('inactive'), emitted by every 17.x parse into every connector; status: 'inactive' joins connectionTimeoutMs: 30000 in CONNECTOR_RETIRED_KEY_RESIDUE (accepted and stripped). Every other value is refused.
  • Seven defs leave whole (RETIRED_DEFS_BY_MAJOR[18]): ConnectorHealth, HealthCheckConfig, CircuitBreakerConfig, ConnectorStatus, WebhookConfig, WebhookEvent, WebhookSignatureAlgorithm. The manifest keys and baseline rows were deleted deliberately after the build named them.
  • D2 conversion connector-resilience-keys-removed (step 18, retired from the load path): strips the three keys from connectors[] and from stored rows, one notice per key; nested webhooks are stripped, never moved.
  • D3 entry connector-resilience-keys-retired (one per family, ruling B on [Decision] 一次退役,要写一条记录还是两条?—— 迁移条目的 D2/D3 约定,两处成文相互矛盾 #17152), naming the D2 and the chain below.
  • Writers deleted. status: 'active' in the four shipped connector packages and status: 'error' on the automation service's degraded husk were writes nothing read back; tsc found the husk's test fixture too.
  • packages/spec/docs/SYNC_ARCHITECTURE.md: the "Monitoring: Health checks" tick is gone, and so are the ticks and example lines this retirement made false (connector webhooks, circuit breaker, status: 'active'); the doc's compile gate (connector-author-shape.test.ts) holds the example.
  • automation/webhook.zod.ts: its connector-webhook note is corrected, and the extraKeys: ['signatureAlgorithm'] suggestion is dropped (the only surface accepting that key is gone, so a typo on the delivered webhook would have been pointed at a refused key).
  • Ledger: status and webhooks stay one dead row each, now tombstones; connector/webhooks left the undrilled baseline (the gate called it stale); state-counts.md and the README notes cell regenerated / corrected (dead 44 to 30).
  • Projections regenerated by their generators only: migrations/registry.ts, api-surface/, declaration-map/, export-origins/, authorable-surface/, authorable-defaults/, json-schema.manifest/, content/docs/references/**, strictness counts, test-typecheck-debt.json (shrink only). spec-changes.json and docs/protocol-upgrade-guide.md do not move: they fold majors up to PROTOCOL_MAJOR 17, and this is step 18 (check:spec-changes / check:upgrade-guide green).
  • docs/adr/0122-...md is NOT edited: no gate forced it. type-alias-convention.pin.test.ts loses the three isomorphic pins of the removed enums (783 to 780 on the merged tree; [finding] four more exported spec types resolve to unknown while their TSDoc promises a shape — ViewMetadataParsed, InlineAction, AssembledViewArtifact, JoinedReportBlock (the #19871 class, other sites) #19920 landed first with its own 786 to 783), the way the error-mapping precedent did.

Deviations from the dispatch's mechanism assumptions (measured)

  1. Per-key tombstones vs. defs leaving. The dispatch asked for one tombstone per key AND for ConnectorHealth / ConnectorStatus to leave via RETIRED_DEFS_BY_MAJOR. Both cannot hold for the fourteen health.* leaves: once ConnectorHealth leaves, its leaves have no shape to carry a tombstone. I followed the error-mapping precedent (13c48c2): one carrier tombstone per key the walk still reaches (health, status, webhooks), defs whole.
  2. "The liveness rows stay dead." The fourteen drilled health.* rows cannot stay: with health a leaf, check:liveness refuses them (measured: connector/health (declared children but property is not a container)). health is one dead tombstone row whose note carries the fourteen verdicts and their census.
  3. "Rename, then removal." Keeping the breaker half of connector-health-and-trigger-durations-unit-in-key is impossible under the conversion table's disjoint-fixture contract: the rename's own fixture carries a health block that the removal strips. Per spec-property-retirement §0 (same unreleased step) the breaker half is ABSORBED: the rename now carries only triggers[].interval, and the removal serves an author holding either spelling (a pin replays monitoringWindow plus a trigger interval and gets exactly one rename notice and one removal notice).
  4. plugin.ts:1792 was not comment-only. The line under that comment WROTE status: 'error' into the husk def, which the tombstone makes a tsc error and a registration-time parse refusal; the write is deleted and the comment corrected.
  5. File surface grew beyond the claim, each forced by the retirement: the four connector packages and one service-automation test (writers), automation/webhook.zod.ts (orphaned extraKeys), plugin-webhooks' docblock and its pin test's docblock (they quoted the retired spec prose), rest-server.test.ts (a stale comment), connector-author-shape.test.ts, type-alias-convention.pin.test.ts.

Acceptance notes

  • PR fix(spec): one D3 entry per major-18 retirement family — the census and the 25 missing entries (#20201) #20255 merged before this PR was finished, so main was merged here and its D3 entry connector-resilience-durations-unit-in-key is reconciled in this PR: it now prescribes only triggers[].interval to intervalSeconds and tells an author holding monitoringWindow / monitoringWindowMs that the renamed key is itself retired (delete the health block). triggers[].interval is untouched otherwise.
  • The out-of-repo consumer population of @objectstack/spec is not measured.

Evidence (head 153f652)

  • pnpm --filter @objectstack/spec build green (manifest and baseline gates fired on the seven defs first, as they must on a whole-def removal, then passed once the rows were deleted deliberately).
  • check:generated: all 15 artifacts current. check:liveness, check:migration-registry, check:spec-changes, check:upgrade-guide green.
  • Gate union from node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands at 153f652, reconciled with --ran: 118 derived, 113 run with exit 0, 5 NOT MEASURED — check:skill-examples, check:dual-build-cjs-loads, check:i18n, check:type-check-debt refused with exit 3 (PREREQUISITE NOT MET: builds outside this diff's closure — client-react, the CLI plugin set, the whole monorepo), check:query-options-erasure (repo-wide ESLint scan; self-test passed, the ratchet outran a 300s per-gate bound — exit 124). CI runs all five.
  • Tests at 153f652: spec --project local over src/migrations, src/conversions, src/integration, the ADR-0122 pin, rest-server, webhook, stack — 18 files, 777 passed; spec --project repo (this PR's pin, the connectionTimeoutMs pin, the migrate-sentence pin, two sibling tree-scoped pins, three reference-tree scripts) — 8 files, 221 passed.
  • Earlier on this branch (pre-second-merge heads): full spec --project local 552 files / 16265 passed; service-automation 146 files / 1757 passed; connector-mcp / -openapi / -rest / -slack and plugin-webhooks 27 files / 255 passed; typecheck of those six packages green; dogfood expression-conformance 7 passed.
  • ESLint over the 32 changed lintable files (--no-inline-config --format json): 32 files, 0 errors, 0 warnings. Population: eslint.config.mjs flat config over **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}; the config enables no type-aware linting (no parserOptions.project), so this diff cannot move a verdict in an untouched file. The repo-wide pnpm lint is CI's.

Ablation (one per closed door)

Via node scripts/ablation-replace.mjs (anchor must hit, restore proven by blob hash equal to HEAD and an empty git diff HEAD), on committed head 849fb62, running connector-resilience-keys-retirement.test.ts:

tombstone deleted (bare strip) result
health 4 failed / 17 passed — REJECTS health, the three-door refusal, the either-spelling breaker refusal, the walked-shape pin
status 4 failed / 17 passed — REJECTS status, the three-door refusal, the non-default-value refusal, the walked-shape pin
webhooks 3 failed / 18 passed — REJECTS webhooks, the three-door refusal, the walked-shape pin

Each leg restored to blob 704684dc6f0c (HEAD's). No permanent ablation test is left.


Generated by Claude Code

… nested webhooks tombstones, defs, D2, D3, pins

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
…; tombstone rows regenerated

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
…eference docs, strictness counts and the shrunk test-typecheck debt

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
…n no longer spells the sibling retirement's key

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
…nnector-resilience-keys-retired

# Conflicts:
#	packages/spec/src/migrations/registry.ts
…hors the retired status

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
…re note name the resilience removal

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
…nnector-resilience-keys-retired

# Conflicts:
#	packages/spec/src/migrations/registry.ts
… the merged tree; reconcile the duration-rename D3 entry with the absorbed breaker half

The rename family's D3 entry (landed from the D3-per-family census) still
prescribed `monitoringWindow` -> `monitoringWindowMs`; that renamed key is
itself retired with the whole `health` block, so the entry now prescribes the
trigger rename only and sends the breaker spelling to the removal.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: 62/62 CONTRACT_REVIEW_TIER
Head-sha: 153f652c278a573f06f7b40cd19acdcaa307ba2d

① Derived judgments

  1. Premise holds. Re-grepped origin/main (a36a691) outside packages/spec, non-md: circuitBreaker / fallbackStrategy / halfOpenMaxRequests / unhealthyThreshold / healthyThreshold / monitoringWindow(Ms) / resetTimeoutMs = 0; healthCheck 28 and failureThreshold 25 are all the kernel plugin-health contract, knowledge adapters and the messaging outbox (different subjects); lit control retryConfig = 48 in connectors + service-automation. No def.status read anywhere; engine.ts#getConnectorDescriptors publishes state only. No read of a connector's nested webhooks; control stack.webhooks read by lint + plugin-webhooks. examples/, skills/: no connector authors health/status/webhooks (showcase status: { type: 'string' } is an action outputSchema property). objectui at pinned f8a9d0fb: 0 imports of the seven names (one comment mention), 0 connector status/health/webhooks reads; it consumes DeclarativeConnectorEntrySchema whole, so the tombstone travels with it.
  2. Refusal shape correct. Three retiredKey() tombstones on the shared private ConnectorBaseSchema; stack.zod.ts:712 uses DeclarativeConnectorEntrySchema, /meta/connector binds the same, registerConnector parses ConnectorSchema (engine.ts:3706). Pin test drives all four doors plus @ts-expect-error on each key and controls without the keys. Prescriptions verified true: GET /api/v1/automation/connectors exists (dispatcher-plugin.ts:1506, route ledger) and reports state (ready/degraded, connector-descriptor.ts:42); enabled: false withdraws materialization (plugin.ts:1488) and marks catalog-only (Declarative connectors: stack entries are inert — bridge them to the automation connector registry or document as descriptor-only #2612); top-level webhooks: is materialized to sys_webhook and delivered on data.record.* (auto-enqueuer). Nothing inert is named. status: 'inactive' residue is strict-equality strip on both carriers; all other values refused (pinned).
  3. Nested webhooks. Lossless in the ADR-0087 sense: never registered as a webhook item, never delivered (ledger + connector-nested.test.ts pin). The webhooks prescription, the D3 reason/replacement and the changeset all tell the author to re-author in top-level webhooks:, warn that doing so starts deliveries, and state events/signatureAlgorithm have no counterpart. Not silently dropped.
  4. Chain. Pin replays monitoringWindow + trigger interval: exactly [interval→intervalSeconds, health→(removed)]; removal is ordered after the rename in conversionIds. No prescription anywhere ends at monitoringWindowMs: the health tombstone names both spellings, the D3 connector-resilience-durations-unit-in-key says "delete the block". Editing the landed entry is acceptable: step 18 is unreleased (PROTOCOL_VERSION = '17.0.0'; spec-changes.json / upgrade guide carry no step-18 id), the skill §0 prescribes same-major absorption, registry.ts header states step rationale/conversionIds are hand-written and semantic regions regenerate from one-file-per-entry, and check-adr-0087-registration.mjs reads no entry file; the changeset marker names only the two ids this diff adds, as registered requires.
  5. Out-of-spec edits. Zero readers of the deleted writes: descriptor payload has no status, objectui reads name/label/origin/actions, the only test fixture (degraded-register-cause.test.ts) is updated. extraKeys: ['signatureAlgorithm'] drop is correct and complete: the only acceptor (WebhookConfigSchema) is gone; alias-integrity.test.ts only audits alias targets, none of which is signatureAlgorithm; no other pin names it.
  6. Ledger/baselines. retiredKey() is z.never().optional(), not a container, so children under health is refused by the walk — the single dead tombstone row carrying the fourteen verdicts is the forced shape, in the file's own errorMapping house style. connector/webhooks leaving the undrilled baseline is forced (leaf now). test-typecheck-debt.json −2 (removed unused imports), pin test 786→783 = exactly the three enum pins; the four Parsed-bearing defs were never listed. Nit, non-blocking: the baseline's _containers description prose still narrates connector/webhooks as a recorded row.
  7. Projections arithmetically exact: api-surface/export-origins −18 names, declaration-map −14, manifest −7, docs 1523→1516 and 24→17, authorable-surface 6 rows → [RETIRED] plus CBC 7 / ConnectorHealth 2 / HCC 8 / WebhookConfig 21 gone, defaults −15, strictness 8→5 (three non-strict objects). Generated registry regions match the entry files verbatim. Generators not re-run (derived gate).
  8. Changeset. Every sentence checked true; FROM→TO table, one-line fix, BREAKING banner, registered marker; Clause-②: no (narrowing) stands alone on its own line in both changeset and body.

② Semver level

PASS. @objectstack/spec: minor is the launch-window rule (check-changeset-no-major.mjs refuses major; AGENTS.md: (narrowing) is BREAKING). The four connector packages and service-automation take patch for deleting an inert write. plugin-webhooks is docblock-only, no changeset owed. no (narrowing) is one of the four spellings clause2-line.mjs reads; no (widening) would be the malformed one.

③ Boundary flags

Implemented-by: claude/issue-20273-connector-resilience-keys-retired
Reviewed-by: session_01QcAS3qiYYZNezaxZxaUdMV

VERDICT: PASS

Main moved the action and translation rows; the connector row keeps this
branch's retirement (dead 30, total 60). Regenerated with gen:liveness-counts,
never hand-merged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
…ooks as a row that has left

Review nit on the retirement: the `_containers` prose still described
`connector/webhooks` as a recorded row after the row was deleted.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Sep 28, 2026
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 7 package(s): @objectstack/connector-mcp, @objectstack/connector-openapi, @objectstack/connector-rest, @objectstack/connector-slack, @objectstack/plugin-webhooks, @objectstack/service-automation, @objectstack/spec, touching 60 documentable anchor(s). ⚠️ 23 changed file(s) yielded no anchor (packages/plugins/plugin-webhooks/src/bootstrap-declared-webhooks.ts, packages/spec/api-surface/integration.json, packages/spec/authorable-defaults/integration.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/connectors.mdx (via /api/v1/automation/connectors (route, a path literal in HEALTH_RETIRED; a path literal in STATUS_RETIRED; a path literal in a comment in ConnectorBaseSchema; a path literal in a comment in RETIRED_KEYS_BY_MAJOR; a path literal in a comment on a changed line; a path literal in acceptanceCriteria; a path literal in replacement; a path literal in semantic))
  • content/docs/automation/flows.mdx (via AutomationServicePlugin (symbol, a top-level class))
  • content/docs/automation/webhooks.mdx (via WebhookSchema (symbol, a top-level const))
  • content/docs/kernel/cluster.mdx (via RETIRED_DEFS_BY_MAJOR (symbol, a top-level const object))

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

  • content/docs/releases/v15.mdx (via /api/v1/automation/connectors (route, a path literal in HEALTH_RETIRED; a path literal in STATUS_RETIRED; a path literal in a comment in ConnectorBaseSchema; a path literal in a comment in RETIRED_KEYS_BY_MAJOR; a path literal in a comment on a changed line; a path literal in acceptanceCriteria; a path literal in replacement; a path literal in semantic))
  • content/docs/releases/v17/17-0.mdx (via WebhookConfig (symbol, a top-level type), WebhookEvent (symbol, a top-level type))
  • content/docs/releases/v17/17-4.mdx (via intervalSeconds (literal, a string literal in summary))

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
  • 23 changed file(s) yielded no anchor (packages/plugins/plugin-webhooks/src/bootstrap-declared-webhooks.ts, packages/spec/api-surface/integration.json, packages/spec/authorable-defaults/integration.json, …) — 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 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 2c310705f7c7b6b073b3ada0a1eaff48ff9078f4 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 407b8d42e6e392211a6e6165653c45a8c6fe0829 — the merge of head e54b124b82f4ad3fe88cc240807335028a5ffa49 into base 2c310705f7c7b6b073b3ada0a1eaff48ff9078f4, 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 407b8d42e6e392211a6e6165653c45a8c6fe0829 && git checkout 407b8d42e6e392211a6e6165653c45a8c6fe0829
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2c310705f7c7b6b073b3ada0a1eaff48ff9078f4 e54b124b82f4ad3fe88cc240807335028a5ffa49 && git checkout -B drift-repro 2c310705f7c7b6b073b3ada0a1eaff48ff9078f4 && git merge --no-ff e54b124b82f4ad3fe88cc240807335028a5ffa49

node scripts/docs-audit/affected-docs.mjs --json 2c310705f7c7b6b073b3ada0a1eaff48ff9078f4

⚠️ 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 2c310705f7c7b6b073b3ada0a1eaff48ff9078f4 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: 79/79 CONTRACT_REVIEW_TIER
Head-sha: 597e867f4d1c1575b22be23eb337ca84283cdad4

① Derived judgments

  1. Delta is exactly the three reported commits. New merge base f39ea95961; commits since 153f652: 0185f00a66 (merge of main), 18cd526317 (state-counts regen), 597e867f4d (baseline prose). Same 50-file set. Per-file comparison of 21ab410417..153f652c27 vs f39ea95961..597e867f4d (index and hunk offsets stripped) differs in exactly two files: liveness/state-counts.md and scripts/liveness/undrilled-containers.baseline.json. No other difference. The merge commit's tree equals a driver-free text merge of 153f652c27 + f39ea95961 except state-counts.md (the driver-managed file, then regenerated) — no other text conflict.
  2. state-counts.md is the merged-tree output. Against the new base the diff is only the connector row (44 → 30 dead, 74 → 60) and the total row (168 → 154 dead, 1119 → 1105); main's moved rows (action 46/0/0/3/0/49, translation 23/0/0/0/1/24, total 936/5/1/…/9) are carried verbatim. Independent arithmetic: all 40 rows sum to their classified, and column sums equal the stated totals. CI Spec property liveness (which asserts the gate's byStatus equals this table) is green on this head.
  3. Prose nit is now true. The _containers sentence reads in the past tense and closes with "That row has since LEFT by the third route above, its property going away: connector.webhooks … a retiredKey() tombstone rather than a container, so the gate reported the row stale and it was deleted (the connector ledger's webhooks row carries the census)". Matches the ledger row and the baseline diff.
  4. Nothing main brought in touches the family. git diff 21ab410417 f39ea95961 adds no line naming circuitBreaker/healthCheck/fallbackStrategy/monitoringWindow*/signatureAlgorithm, a connector status/health/webhooks read, or any of the seven removed defs. On the merged tree 597e867f4d, outside packages/spec (non-md): all eight breaker/probe leaves = 0; signatureAlgorithm = 1 (plugin-webhooks docblock); the seven names appear only in this PR's own comments; no def.status or nested webhooks read; lit control retryConfig = 48. Premise stands.
  5. CI on 597e867: 35 check runs, 33 success, 2 skipped (Console Pin Gate, Packed-tarball smoke (opt-in)), 0 failures — including Check Changeset, Lint & Repo Gates, Spec property liveness, Type Check · debt ledger, Governed Surface Queue Guard, the three Type Check gates and Build Docs.

② Semver level

Unchanged: @objectstack/spec: minor under the launch-window rule with Clause-②: no (narrowing); five runtime packages patch. The delta publishes nothing new.

③ Boundary flags

Implemented-by: claude/issue-20273-connector-resilience-keys-retired
Reviewed-by: session_01QcAS3qiYYZNezaxZxaUdMV

VERDICT: PASS

…the connector reference page on the merged tree

Main moved the rest_api liveness row, the api/ strictness count and the
connector page's front-matter description; each regenerated with its own
generator (gen:liveness-counts, gen:strictness-ledger, gen:docs on a build of
the merged tree), never hand-merged. The retirement's rows are unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: 93/93 CONTRACT_REVIEW_TIER
Head-sha: 0c522e3fc10dc1c6d4e478fdc54f6c54bd593a41

① Derived judgments

  1. Delta is the two reported commits. New merge base 29720975b6; since 597e867: f8c077a4fc (merge of main) and 0c522e3fc1 (regen of exactly state-counts.md, strictness-ledger.counts.md, connector.mdx: 4 lines). Same 50-file set. Per-file comparison of f39ea95961..597e867f4d vs 29720975b6..0c522e3fc1 differs only in state-counts.md, strictness-ledger.counts.md (regenerated) and liveness/README.md, where the PR's own change is the identical connector Notes cell at both bases and the difference is main's moved rest_api row carried through the text merge. connector.mdx compares identical. The merge commit equals a driver-free text merge of 597e867f4d + 29720975b6 except the three generator-owned files: no other text conflict.
  2. Exact generator output on the merged tree. state-counts.md vs base: only the connector row (44 → 30 dead, 74 → 60) and totals (166 → 152 dead, 1117 → 1103); main's moved rest_api 12/0/0/12/0/24 and totals carried; all 40 rows sum to classified and columns sum to the stated totals. strictness-ledger.counts.md vs base: only integration/ 8 → 5; main's api/ 432 → 431 carried. connector.mdx: the regen changed only the front-matter description, which docs(content,spec): search-ready page descriptions — 52 authored rewrites and derived reference descriptions #20258's build-docs.ts now derives from the module doc block (connector.zod.ts:12, a line this PR does not touch), so it equals main's; the six [REMOVED] tombstone rows (health/status/webhooks on both carriers) are byte-identical to the judged text.
  3. Nothing main brought in touches the family. git diff f39ea95961 29720975b6 adds no line naming the seven defs, a breaker/probe leaf, signatureAlgorithm, a connector status/health/webhooks read, or ConnectorSchema/DeclarativeConnectorEntrySchema/ConnectorBaseSchema; its spec touches are spec(api): retire api.responseFormat and api.documentation.enabled (4 keys); the envelope is fixed and enableOpenApi already decides the document #20295's RestApiConfig entries and the regenerated registry. On 0c522e3fc1 outside packages/spec (non-md): all eight leaves = 0; signatureAlgorithm = 1 (plugin-webhooks docblock); the seven names = 4, all comments; def.status / nested webhooks reads = 0 (the only entry.status hits are flow-run entries); lit control retryConfig = 48.
  4. CI on 0c522e3: still running. 32 check runs at two reads 45 s apart: 14 success, 2 skipped, 16 in_progress — Test Core (6 shards), Dogfood Regression Gate (3), Dogfood Verify CLI, Build Core, Temporal Conformance, Type Check · consumer gates / debt ledger / workspace, Lint & Repo Gates. No failure so far. The previous head 597e867 finished 33 success / 2 skipped.

② Semver level

Unchanged: @objectstack/spec: minor with Clause-②: no (narrowing); five runtime packages patch. The delta publishes nothing new.

③ Boundary flags

Implemented-by: claude/issue-20273-connector-resilience-keys-retired
Reviewed-by: session_01QcAS3qiYYZNezaxZxaUdMV

VERDICT: PASS

…nnector-resilience-keys-retired

# Conflicts:
#	packages/spec/src/migrations/registry.ts
Main moved the qa row (and so the total); the connector row keeps this
branch's retirement. Regenerated with gen:liveness-counts, never hand-merged;
gen:migration-registry and gen:strictness-ledger produced no diff.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
…nnector-resilience-keys-retired

# Conflicts:
#	packages/spec/src/migrations/registry.ts
…nnector-resilience-keys-retired

# Conflicts:
#	packages/spec/src/migrations/registry.ts
#	packages/spec/vitest.repo-tests.json
…nnector-resilience-keys-retired

# Conflicts:
#	packages/spec/src/type-alias-convention.pin.test.ts
…nnector-resilience-keys-retired

# Conflicts:
#	packages/spec/src/migrations/registry.ts
Main's action.aria retirement and this branch's connector retirement each
moved a row of state-counts.md; the merge took main's side, and this
regeneration re-derives both rows from the merged ledger.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit 40b315b Sep 28, 2026
43 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20273-connector-resilience-keys-retired branch September 28, 2026 09:26
os-warren pushed a commit that referenced this pull request Sep 28, 2026
Landing lap 2: origin/main 40b315b, 6 commits past dcd3bce. #20350
(`40b315b0`) appended its connector-resilience sentence to step 18's
`rationale`, the one conflict region (conversions/registry.ts and step
18's `conversionIds` merged cleanly). Resolved per the seat's answer B
on #15429 (5865957805, re-applied by 5867190364), and nowhere else:
main's text kept whole (the action-aria sentence, then the
connector-resilience sentence), this branch's sentence appended
verbatim; the one string join at the seam makes main's closing literal
end in a space so the concatenation continues.

Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH
Co-authored-by: Claude <noreply@anthropic.com>
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
…retiredAfter; the artifact door opens its window per entry (objectstack-ai#20390) (objectstack-ai#20435)

Fixes objectstack-ai#20390

Clause-②: yes

Implements ruling `5865890672` (batch objectstack-ai#235 item 1, letter **A**,
maintainer 「同意 A」; maintainer record `5865873150`, route `5866178043`):
every retired entry in the ADR-0087 conversion registry carries a
REQUIRED `retiredAfter`, and the artifact forward-conversion window
decides per entry. It is one vertical PR across `packages/spec`,
`packages/metadata-core` and the artifact door in `packages/metadata`.

## Spec half

- **`MetadataConversion` is a live-or-retired union**
(`packages/spec/src/conversions/types.ts`). An entry with
`retiredFromLoadPath: true` must also carry `retiredAfter`, typed as a
stable `x.y.z` template-literal string; a live entry carries neither.
tsc refuses an unstamped retirement (the reverse verification is below).
The type moves from an interface to a type alias, so `gen:api-surface`
and `gen:export-origins` each rewrite one row: `MetadataConversion
(interface)` becomes `MetadataConversion (type)`.
- **Backfill, from the published tarballs.** Each published entry's
value is the stable release just before the first tarball that carries
it retired. Each entry in no published tarball carries the current
`package.json` label, `17.4.0`.
- **Census test.** `src/conversions/retired-after.census.json` holds raw
facts per stable release since the registry first shipped (14.8.0
through 17.4.0): the tarball integrity and the ids its `ALL_CONVERSIONS`
marks retired. `src/conversions/retired-after.census.test.ts` pins every
entry's value against it, offline, in the `local` tier. It pins that
every entry absent from the last published tarball carries the label,
and that no value is malformed or above the label.
`scripts/build-retired-after-census.ts` re-derives the census from
registry.npmjs.org. It checks each tarball's integrity, imports each
release's `dist/index.mjs`, and writes the census, or compares it with
`--check`.

## metadata-core half

`applyArtifactForwardConversions` replays entry E when the artifact's
floor is below the runtime label OR at or below `E.retiredAfter`.
`DEFAULT_FLIPS_NOT_REPLAYED_HERE` is still read first. Its membership is
unchanged; `flow-decision-mode-inclusive-explicit` came in with the
merge of objectstack-ai#20344. When the floor is at or above the label, only the
entries the floor predates are replayed. The rest reach the strict parse
and their tombstones through the existing `excludeConversionIds` seam,
computed per entry from the registry. There is no second table.
`ArtifactForwardConversionVerdict` gains `'converted-retired-after'` for
that case. `ArtifactForwardConversionResult` gains `replayedRetirements`
(element type `ArtifactReplayedRetirement`): under that verdict, each
retirement this runtime enforces past the artifact's floor, with its
`retiredAfter`; it is empty for every other verdict. The module
docblock's two policy sentences still hold: "a key retired at version V
stays a loud refusal for anything authored at ≥ V" (the
floor-at-or-above-label bullet), and "Not a second conversion table".

## The door's consumer arm (`packages/metadata/src/plugin.ts`)

The verdict has one in-tree consumer that branches on it, and the new
arm is added there.

- **Which verdicts open the window** is now one total table,
`FORWARD_WINDOW_OPENED` (a readonly `Record` keyed by every
`ArtifactForwardConversionVerdict` member, valued `boolean`), with
`'converted-retired-after'` on the open side.
`_warnUnboundFormPredicateRoots` (the objectstack-ai#12915 scope-C notice) returns on
`!FORWARD_WINDOW_OPENED[result.verdict]`. That makes its docblock
sentence true again: the notice is "read off that pass's own verdict
rather than recomputed, so the two can never disagree", and it no longer
depends on the label. On `main` today, a 17.4.0-built artifact with a
bare-root form predicate is announced now, not once the label reaches
17.5.0.
- **Why the table, not the inverted guard.** The two forms the order
offered have opposite defaults for a verdict that does not exist yet.
Adding the arm to the old hand-written guard defaults a future verdict
to "closed", which is how this defect arose. Inverting the guard (return
only on `'authored-current'` / `'runtime-version-unknown'`) defaults it
to "open", and it would also admit `'not-an-object'`. A total `Record`
over the verdict union has no default: a new member is a compile error
until someone places it. This is the "add the arm" route, spelled so
that tsc forces the next decision. Reverse-verified below.
- **The warn lines under the new verdict** no longer say the artifact
"predates this runtime's spec" beside a runtime version equal to its
floor. The conversion summary names the retirement this runtime enforces
past the artifact's floor, with the release that last accepted the shape
(from `replayedRetirements`). It then says the artifact converts again
on every boot until it is rebuilt with tooling from a release that ships
the retirement. The objectstack-ai#12915 notice opens with the same verdict-aware
clause. Every other verdict keeps its existing wording.
- `plugin-unbound-form-predicate-roots.test.ts`'s "current surface"
silence pin had derived that surface as a caret range on the installed
label. That spelling is itself the label-dependence this change removes:
on `main` it names an artifact built BY the last release. It now derives
the first `x.y.z` past both the label and every `retiredAfter`.

## The four pins

| Pin | Where | Asserts |
|:--|:--|:--|
| (1) a 17.4.0-CLI-built artifact with dashboard charts and page
`assignedProfiles` boots on `main` and logs the notices |
`packages/metadata/src/plugin-artifact-forward-conversion-retired-after.test.ts`,
on a REAL fixture: `dist/objectstack.json` built verbatim by the
published `@objectstack/cli` 17.4.0 | the dashboard and page register
with `chartConfig.type`/`xAxis`/`yAxis` and `assignedProfiles` converted
away; one warn line each for
`dashboard-widget-chart-config-structure-removed` (3 sites) and
`page-assigned-profiles-removed` (1 site) |
| (2) newly authored sources using the retired keys are still refused
loudly | same file | `defineStack` refuses with `code:
'STACK_SCHEMA_INVALID'`, `status: 422`, and one issue per retired site
(4 paths) |
| (3) floor exactly 17.5.0 on a 17.5.0-labelled runtime is refused, not
converted |
`packages/metadata-core/src/artifact-forward-conversion.test.ts` |
verdict `authored-current`, zero notices, and the strict parse refuses
the same 4 paths |
| (4) unreleased `main` (label 17.4.0), artifact at the last release
(`^17.4.0`) | same file | verdict `converted-retired-after`, notices by
id and path, and the strict parse passes |

Beside pin (1), **the objectstack-ai#12915 pin**
(`plugin-artifact-forward-conversion-retired-after.test.ts`, "announces
a bare-root form predicate once"): the `^17.4.0` fixture with one
bare-root form predicate (`stage == "won"`) on the 17.4.0 runtime logs
the unbound-root line exactly once, including across a second ingestion.
It is red under the old guard and green now (below).

Three companions sit beside the pins. After the release (label 17.5.0)
the same artifact converts through the label half, with
`replayedRetirements` empty. A 17.2.0 retirement still meets its
tombstone inside the open per-entry window.
`flow-decision-mode-inclusive-explicit` stays refused inside its own
per-entry window. Pin (4) also asserts `replayedRetirements`: both
retirements at `17.4.0`, and never the default flip.

## Census (re-derived on this tree, npm `latest` = `17.4.0`, label =
`17.4.0`)

94 retired entries: **73 published** and **21 unpublished**. The ruling
counted 91 retired with 18 unpublished at `df3ba164`. Three unpublished
entries landed since then: `action-aria-removed`,
`connector-resilience-keys-removed` (objectstack-ai#20350) and
`flow-decision-mode-inclusive-explicit` (objectstack-ai#20344, merged into this
branch).

| first published retirement | entries | `retiredAfter` |
|:--|--:|:--|
| 15.1.0 | 5 | 15.0.0 |
| 17.0.0 | 45 | 16.1.0 |
| 17.1.0 | 5 | 17.0.0 |
| 17.2.0 | 2 | 17.1.0 |
| 17.3.0 | 8 | 17.2.0 |
| 17.4.0 | 8 | 17.3.0 |
| none (unpublished) | 21 | 17.4.0 |

The ruling's census bucket of 50 entries "first retired in 17.0.0" is 45
+ 5. The engine seat's census started at the 17.0.0 tarball. Those 5
entries (`object-compactLayout-to-highlightFields`,
`stack-roles-to-positions`, `owd-legacy-read-aliases`,
`sharing-recipient-role-to-position`,
`book-audience-profile-to-permission-set`) are already retired in the
15.1.0, 15.1.1, 16.0.0 and 16.1.0 tarballs, so the ruling's own
principle gives them `15.0.0`.

## Verification (final HEAD `2c537b7e`)

- Derived gates: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` gave 90 commands at `2c537b7e` (16 files,
+1667/−72), and all 90 exit 0 on that head. The `--ran` reconciliation
(each line carrying its exit code) reads: "90 derived, 90 run, 0
NOT-MEASURED, 0 UNRUN". The full package closure was rebuilt first
(turbo 71/71).
- `@objectstack/spec` `test` (`--project local`): Test Files 565 passed
(565), Tests 16645 passed, 1 todo. `test:repo` (`--project repo`, run in
two halves of 18 files each to fit the foreground cap): 18 files / 460
tests and 18 files / 195 tests, together Test Files 36 passed (36),
Tests 655 passed.
- `@objectstack/metadata-core` `test`: Test Files 16 passed (16), Tests
295 passed (295). `typecheck` exit 0.
- `@objectstack/metadata` `test`: Test Files 55 passed (55), Tests 826
passed (826). `typecheck` exit 0.
- eslint `--no-inline-config --format json` on the 10 changed source
files: 10 files linted, 0 errors, 0 warnings. `eslint.config.mjs` never
enables type-aware linting, so this diff cannot move the verdict on any
untouched file.
- Main was merged three times, all through
`scripts/pm/os-regen-merge.sh`. None of this round's incoming commits
touch `packages/spec/src/conversions`, `packages/metadata-core` or
`packages/metadata`, and none adds a retired entry: every one of the 94
carries `retiredAfter`.

## Ablation and reverse verification (from committed state, through
`scripts/ablation-replace.mjs`)

- **Guard ablation (this round).** In `plugin.ts`, `if
(!FORWARD_WINDOW_OPENED[result.verdict]) return;` was put back to the
old guard, `if (result.verdict !== 'converted-forward' && result.verdict
!== 'converted-undeclared') return;`, with a marker comment. On-disk
count: marker 1, new guard 0. Across the three door suites (21 tests),
exactly one went red, the objectstack-ai#12915 pin ("announces a bare-root form
predicate once"). The rest stayed green, including the updated
current-surface silence pin. Restore: blob `8f43972c` equals HEAD, `git
diff HEAD` is empty, `git status --porcelain` has 0 lines, and all 21
tests pass again. The suites import `plugin.ts` from source, so no build
sits between the mutation and the run.
- **tsc forces the next verdict decision.** With the
`'converted-retired-after': true` row removed from
`FORWARD_WINDOW_OPENED`, `tsc --noEmit` in `packages/metadata` exits 2
with `error TS2741: Property '"converted-retired-after"' is missing`.
Restored to the HEAD blob.
- **Window ablation (round 0, at `87da6b88`).** The per-entry branch was
replaced with the old label-only verdict, and `metadata-core` was
rebuilt, with the marker present in 2 built files. Pin (4), pin (1) boot
and pin (1) notices went red, along with both per-entry companions. Pins
(2) and (3) stayed green. The restore was proven (blob equals HEAD, 0
porcelain lines, and the marker absent from the rebuilt dist).
- **tsc refuses an unstamped retirement.** With `retiredAfter` removed
from `page-assigned-profiles-removed`, spec `tsc --noEmit` exits 2 with
exactly one `error TS2322`.
- **The census test fails when it should.** A published entry stamped
low reds the PUBLISHED test, and an unpublished entry stamped low reds
the UNPUBLISHED test. `build-retired-after-census.ts --check` passes
against npm (11 releases), and exits 1 on a tampered census.

## Deviations from the ruling text, and why

1. **The rule for unpublished entries has one tolerance.** While the
label is AHEAD of the census's last release, an unpublished entry may
carry any version from that release up to the label. Taken literally
("carries the current label"), the rule turns the Version Packages PR
red. That PR bumps the label to 17.5.0 before 17.5.0 is published, while
the 17.5.0 entries correctly carry 17.4.0. The tolerance closes again
once the census records the new tarball. The seat confirmed this reading
(`5869635456`). The refresh is now a written step of the GA release
flow: `docs/releases-maintenance.md`, under "Cutting a GA release — the
Version Packages PR flow", says to run
`scripts/build-retired-after-census.ts` after a stable
`@objectstack/spec` publish and commit the refreshed census. The seat
answered the refresh question with A; no workflow and no gate are added.
2. **The network half is a script, not a repo-tier test** (accepted by
the seat, `5869635456`). `vitest.repo-tests.json` is held equal to the
set of tests that read outside the package
(`check:cross-package-test-inputs`), so a network-only test cannot be
listed there. Reading the tarballs means downloading every stable
release since 14.8.0 (about 11 tarballs, over 250 MB), so no per-run
suite does it. So CI pins the committed census offline, and
`scripts/build-retired-after-census.ts` re-derives it. The script
refuses loudly when offline; it never skips. It is not a `package.json`
script and not wired into CI, so no gate is added.
3. **Stable releases only.** The census and the rule skip `-rc`
versions: a caret floor never names a prerelease, and the door compares
`x.y.z` triples.
4. **Counts.** See the Census section: 73 published, 21 unpublished, and
a 15.1.0 bucket the ruling's counts did not have.

## Acceptance notes

- `packages/metadata` now carries a `patch` changeset entry for the door
change. It changes no public API; the objectstack-ai#12915 notice and the conversion
summary wording follow the per-entry window.
- `field-required-notnull-explicit` appears retired in the 17.0.0
through 17.3.0 tarballs and is gone from 17.4.0 and `main`, withdrawn by
objectstack-ai#16693. The census test ignores ids not on `main`.
- The seat files two follow-ups at landing, as governed surfaces outside
this PR (per `5869635456`): ADR-0087's objectstack-ai#12772 addendum sentence that a
floor at or above the runtime "replays nothing", and the retirement kit
in `.claude/skills/spec-property-retirement/SKILL.md`.
- Once this lands, any open PR that adds a retired entry fails typecheck
until it stamps `retiredAfter`. That is the designed loud direction.
- `main` narrowed `manifest.id` (underscores refused, objectstack-ai#17534). So a
17.4.0-built artifact whose id has an underscore is refused whatever
this window does. The pin fixture uses a reverse-domain id for that
reason.

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

---------

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

Development

Successfully merging this pull request may close these issues.

spec(integration): retire the connector health-probe, circuit-breaker, authored status and nested webhooks keys (16), which nothing enforces

2 participants