Skip to content

fix(federation): distinct unreachable SchemaDiffEntry kind — an unreachable remote is no longer reported as missing_table or aborted on at boot - #11358

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-11166-unreachable-diff-kind
Aug 23, 2026
Merged

fix(federation): distinct unreachable SchemaDiffEntry kind — an unreachable remote is no longer reported as missing_table or aborted on at boot#11358
os-sam merged 1 commit into
mainfrom
claude/issue-11166-unreachable-diff-kind

Conversation

@claude

@claudeclaudeBot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Fixes#11166

Implements the maintainer ruling recorded on the card (comment 5383569537, 2026-08-23, quote kept verbatim there: 「其他同意你的意见」 — adopting recommendation A): SchemaDiffEntry gains a distinct "could not be read" kind (unreachable); the boot gate stops treating an unreachable remote as a schema mismatch — no onMismatch: 'fail' abort for unreachable, loud logging instead; drift ticks report it under the new kind, not as missing_table.

Anchors re-verified on the merged ref (d39569239)

The dispatch named packages/spec/src/contracts/schema-diff-service.ts as the contract surface. Re-verified on the merged ref: SchemaDiffEntry / SchemaDiffEntryKind live in packages/spec/src/shared/external-errors.ts (the ADR-0015 federation error contract); schema-diff-service.ts (the file #11270 edited) declares the introspection types and carries neither symbol. The spec edit therefore lands in shared/external-errors.ts; schema-diff-service.ts is untouched. The defect site (validateEach, packages/services/service-datasource/src/external-datasource-service.ts) and both runtime consumer sites were re-verified as the card describes.

Reproduction

Pinned as a unit test with a throwing introspector — the card's measured shape verbatim (connect ECONNREFUSED 10.0.0.5:5432): before this change the row came back { kind: 'missing_table', severity: 'error' }; the new pin (classifies a per-object throw as 'unreachable', never 'missing_table' (#11166)) asserts the full row shape including the error text in actual.

What changed

  • packages/spec/src/shared/external-errors.tsSchemaDiffEntryKind adds 'unreachable', appended to the union, with a docblock carrying the transient-vs-fact distinction: it is the one kind that asserts nothing about the remote schema — validation was indeterminate; every other kind is a measured fact about a schema that was successfully read.
  • packages/services/service-datasourcevalidateEach's per-object catch produces kind: 'unreachable' (rows stay ok: false, severity: 'error', error text in actual). missing_table is still produced — only from its measured branch (!table after a successful introspection).
  • packages/runtimerunValidation partitions each failed row: unreachable entries are logged loudly (datasource + object + underlying error + "unverified for this boot") and never feed onMismatch; only measured diffs reach the policy, so a mismatch row still aborts under fail even beside an unreachable row. runDriftCheck still emits external.schema.drift for unreachable rows (consumers discriminate on kind in the payload's diffs), but the operator-facing summary now says "could not read the remote", never "drift detected", for them.

Design decisions (measured, then decided within the ruling)

  • Kind name unreachable — the ruling's example fits the existing vocabulary (snake_case single-fact names) and the cloud-side seam measurement was taken against this spelling family.
  • Classification is structural; no error-signature test — the ruling requires the READ_FAILURE_DISCRIMINATORS precedent (packages/metadata/src/utils/schema-sync-errors.ts), not a hand-rolled err.code test. That precedent's binding principle is a fact verdict must be positively earned, never defaulted to. Here the discrimination "connection failure or schema fact?" is answered by structure: every schema fact this service reports is derived from an introspection that returned; a throw means the comparison never ran, so no signature test on the thrown value can earn a claim about a schema nobody read. A hand-rolled ECONNREFUSED-style allowlist (the prohibited shape) would misclassify every unrecognised connection error back into a fact; a missing-table-shaped rescue from the throw would mislabel in a second direction — on this path a "no such table" throw is about the metadata store or the introspection machinery, not the remote table the row names. So every catch-path throw is unreachable, and the precedent module is cited at the classification site. (This is stated in the code docblock; if the PM intended a literal shared-predicate call here, say so and it forks back — no such call can produce a sound fact from this catch.)
  • Severity stays 'error' — decided from consumer measurement: ok must stay false (silently-ok would be the invent-an-answer failure), and the interactive consumers (CLI os datasource validate exits 1 on severity: 'error'; Studio renders a red badge) should treat "could not validate" as attention-level, not success. The transient-vs-fact distinction consumers act on is the kind axis; severity is the attention axis. The ruling fixed the kind, not the severity.
  • Boot-gate log level warn, outside the onMismatch resolution — per AGENTS.md's degradation table this is a functional degradation (a check did not run, and says so; nothing claims to have persisted). Logged under every onMismatch value: that policy governs measured mismatches, and an outage is a different condition — even an ignore datasource's operator is told their boot ran unverified. Message names the consequence and the fix per the discipline.
  • No new config invented — the ruling's contingency (onUnreachable-style knob) proved unnecessary: unconditional loud-log-and-continue implements the ruling with no new authorable surface.

Consumer posture toward the new kind (measured; controls make the zeros valid)

Enumeration: grep -rn "SchemaDiffEntry|missing_table|schema\.drift|SchemaValidationResult" over packages/ at d39569239 (control: 2305 from '@objectstack/spec import sites in-repo — the scan surface is live). All non-test hits:

consumerposture toward unreachable
runtimerunValidation (boot gate)was the defect — changed per the ruling (see above)
runtimerunDriftCheck (drift tick)kind flows through the event payload's diffs; loud pass-through + summary-log split
specrenderDiffEntry / ExternalSchemaMismatchErrorprints the raw kind + actual text — loud pass-through (pinned)
clios datasource validateprints raw d.kind; severity: 'error' → exit 1 — loud, correct (an unvalidatable object is not a passing validation)
restexternal-datasource-routesserializes the report envelope; no branching on kind values — pass-through
driver-sqlschema-drift.tsproducer of its own SchemaDiffEntry values (managed drift); consumes no service rows; d.kind appears only in its own dedup key — unaffected
objectuiValidationPanel (app-shell, measured @ ad404e0)runtime: DIFF_LABEL[diff.kind] ?? diff.kind — renders the raw kind, loud; compile: DIFF_LABEL is Record<SchemaDiffEntry['kind'], string>total on purpose — its own docblock declares that a kind added upstream "fails THIS map to compile instead of rendering a blank cell" (their designed arrival mechanism; the label lands objectui-side at their next spec sync). Not a silent break. Heads-up filed: objectstack-ai/objectui#5831 (unassigned, recording only).
cloudcited from seam card #11232 reading (b): `git grep -nE 'SchemaDiffEntry

No consumer breaks silently on the new kind → nothing forks back to the PM.

Console Pin Gate safety (additions can break the pinned sibling too): verified not here — the pinned console build (apps/consolebuild = tsc && vite build) type-checks against @objectstack/spec from objectui's own lockfile (the published spec); the OBJECTSTACK_SPEC_DIST injection is honored only in vite.config.ts (bundle resolution), which does not type-check. So the total DIFF_LABEL record at the pinned SHA (190fbd01d) stays consistent with the spec tsc sees, and the gate is unaffected by this addition.

Verification

All commands at final commit 84854c5fb (clean tree), through scripts/pm/os-verify-lock.sh; exit codes captured before any pipe.

  • Tests: @objectstack/spec 419 files / 11136 passed (run on the byte-identical tree the commit was made from); @objectstack/service-datasource 26 files / 572 passed and @objectstack/runtime 184 files / 2715 passed — both re-run as the union at 84854c5fb (Test Files 26 passed / Tests 572 passed; Test Files 184 passed / Tests 2715 passed, VERDICT command-exit 0).
  • Typecheck: turbo typecheck for spec, service-datasource, runtime + the measured consumers (cli, rest, driver-sql): Tasks: 61 successful, 61 total.
  • Spec generated artifacts: pnpm --filter @objectstack/spec check:generated✓ All 14 generated artifacts are up to date (the api-surface snapshot records export existence, not union membership — no regen owed; verified by the gate, not assumed).
  • Derived gate families (node scripts/pm/dispatch-gates.mjs, no hand-fed paths; derivation header named this repo @ 84854c5fb): all 22 derived commands + check:where-matcher + check:nul-bytes green (OVERALL_FAIL=0 after a full turbo run build cleared check-dev-prereqs's workspace-not-built precondition — a worktree fact, not a diff fact).
  • Reverse verification (predicted direction first, from the committed state): mutating the catch back to kind: 'missing_table' (mutation confirmed on disk by anchored grep at the catch site, line 732) turned exactly the two predicted pins red — classifies a per-object throw as 'unreachable' … (#11166) and validateDatasource › keeps the per-object failure row2 failed | 570 passed; restore via git checkout HEAD confirmed by grep (the test imports the service by in-package relative path, so vitest ran the mutated source directly; no dist leg involved).
  • Local narrowing declared: full repo-wide pnpm lint/full pnpm test sweep left to CI (this PR's local runs are the affected packages + measured consumers + the derived gate families); CI runs the full farm regardless.

Process notes

  • Clause-② path limb fires (packages/spec/src/** touched) — expected; content-limb self-grade: yes (contract vocabulary addition, per the recorded ruling).
  • Changeset: @objectstack/specminor (additive public union member), @objectstack/service-datasource / @objectstack/runtimepatch (behavior fix). Not declared-breaking → no ADR-0087 disposition marker owed (check-adr-0087-registration green; check-changeset-no-major green).
  • content/docs/releases/ untouched; no gate weakened; no ledger raised.

Generated by Claude Code


Generated by Claude Code

…le' diff kind, not missing_table (#11166)
- spec: SchemaDiffEntryKind gains 'unreachable' — the one kind that states
validation was indeterminate rather than a measured schema fact
- service-datasource: validateEach's per-object catch produces the new kind
(rows stay ok:false, severity error, error text in 'actual')
- runtime: the boot gate no longer feeds unreachable rows to onMismatch —
loud warn + continue under every policy value; drift tick summary says
'could not read', never 'drift detected', for unreachable rows
Maintainer ruling 2026-08-23 (issue comment, verbatim quote recorded there).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RadETjNRLALFLhFA3xehZP
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

7 anchor(s) derived from 3 changed package(s); no hand-written page names any of them. ✅

What this run could not see
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 132 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 09db64a486a84456d5b753cdccaec995b28fbc9cpackageMentionDocs.

Which tree this was computed on

This run read content/docs from d911fb24f1a16ea9a7b8d9d7b7c09db984ae8747 — the merge of head 84854c5fbdca41e1cdfa7e7130441d0dad2e9ab8 into base 09db64a486a84456d5b753cdccaec995b28fbc9c, 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 d911fb24f1a16ea9a7b8d9d7b7c09db984ae8747 && git checkout d911fb24f1a16ea9a7b8d9d7b7c09db984ae8747
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 09db64a486a84456d5b753cdccaec995b28fbc9c 84854c5fbdca41e1cdfa7e7130441d0dad2e9ab8 && git checkout -B drift-repro 09db64a486a84456d5b753cdccaec995b28fbc9c && git merge --no-ff 84854c5fbdca41e1cdfa7e7130441d0dad2e9ab8
node scripts/docs-audit/affected-docs.mjs --json 09db64a486a84456d5b753cdccaec995b28fbc9c

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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-sam@claude