Repository navigation
fix(service-datasource)!: Sync refuses a managed datasource with the existing 400 (ADR-0015 §4.2) - #25167
Conversation
…5 §4.2) Claude-Session: https://claude.ai/code/session_01UnHcTADjeg3VwxF21QHj7W Co-authored-by: Claude <noreply@anthropic.com>
…wo managed-Sync pins Claude-Session: https://claude.ai/code/session_01UnHcTADjeg3VwxF21QHj7W Co-authored-by: Claude <noreply@anthropic.com>
…tch, Metadata-protocol: none) Claude-Session: https://claude.ai/code/session_01UnHcTADjeg3VwxF21QHj7W Co-authored-by: Claude <noreply@anthropic.com>
… (door kind, not a code parameter) check:dispatcher-error-vocabulary reported both refusal helpers as unresolved once the shared guard forwarded a code variable to them. Claude-Session: https://claude.ai/code/session_01UnHcTADjeg3VwxF21QHj7W Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 2 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 2 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c646f942b89c5fb88062ba85657064c4f10773b1 && git checkout c646f942b89c5fb88062ba85657064c4f10773b1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 425c743fd9b80005d83e528dfdc8a5f8c68c8b30 658d9d0b54fb68201710b21904e6adf9e8b03159 && git checkout -B drift-repro 425c743fd9b80005d83e528dfdc8a5f8c68c8b30 && git merge --no-ff 658d9d0b54fb68201710b21904e6adf9e8b03159
node scripts/docs-audit/affected-docs.mjs --json 425c743fd9b80005d83e528dfdc8a5f8c68c8b30
|
…e 'external' The fixture datasource declared no schemaMode, so it is managed by the spec default and both Sync twins now refuse it (ADR-0015 §4.2). The twin-equivalence cases compare listings of a federation source. Claude-Session: https://claude.ai/code/session_01UnHcTADjeg3VwxF21QHj7W Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #24837 (body and comments ① Derived judgments(a) The accept set of the six Sync doors, each read to its producer and its catch — right. The one producer is
(b) (c) (d) Decided before any introspection — right. All four methods (e) The seat's answer A (refuse a name with no definition) — consistent, a narrowing within ADR-0015, not a widening beyond it. The spec default is Public-surface changes implied by the diff, each named: the four REST doors' and two admin doors' accept set narrows on the three shapes above (right, the card's route B); ② Semver levelRight. The changeset is The ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对
Landing: ordinary queue landing. The seat sets ready and auto-merge through the relay in this act. After the merge, #24837 closes by Changeset sentences checked against the diff:
All hold. Generated by Claude Code |
Fixes #24837
Clause-②: no (narrowing)
Metadata-protocol: none
What changes
ADR-0015 §1 puts a federated object on a datasource whose
schemaModeis notmanaged, and §4.2 allowsobject.externalonly there;managedis the spec's default. The Sync doors (list remote tables, draft an object, import one, refresh the catalog) refused only the platform's primary datasource (default, the route-D ruling on #24772). Every other managed datasource answered, including the runtime datasources Setup creates, which are saved with noschemaModeand so are managed (the HotCRM tester'sqa_memlisted its tables200).Now one guard in the federation service,
ExternalDatasourceService.assertFederationSource, runs first in all four Sync methods and refuses, before anything is introspected:default, by name, with its existing message (unchanged);schemaMode: 'managed'declared, noschemaMode(the default), or no datasource definition at all.Each refusal is the existing
400with the code that door already serves:EXTERNAL_DATASOURCE_ERROR, andEXTERNAL_IMPORT_ERRORon the import. The message names ADR-0015's rule and the remedy (sync from a datasource declaredexternalorvalidate-only). No status code or error code is minted. The governing text is ADR-0015 §1 and §4.2; retiring the rule instead would be an ADR amendment, which this does not take.Patch round 1 (
658d9d0b5) adds one test-fixture line inpackages/rest, declared cross-lane on #22648 (6332535245):remote-tables-twin.equivalence.test.tsnow declares its fixture datasourceschemaMode: 'external', because a datasource with noschemaModeis managed and both Sync twins now refuse it; nopackages/restsource line changes.Where the refusal lives (measured)
The one producer behind every door is
ExternalDatasourceServiceinpackages/services/service-datasource/src/external-datasource-service.ts:GET /api/v1/datasources/:name/remote-tablesservice-datasourceadmin-routes.tslistRemoteTablesPOST /api/v1/datasources/:name/object-draftservice-datasourceadmin-routes.tsgenerateObjectDraftGET /api/v1/datasources/:name/external/tablespackages/restexternal-datasource-routes.tslistRemoteTablesPOST /api/v1/datasources/:name/external/tables/:remote/draftpackages/restexternal-datasource-routes.tsgenerateObjectDraftPOST /api/v1/datasources/:name/external/tables/:remote/importpackages/restexternal-datasource-routes.tsimportObjectPOST /api/v1/datasources/:name/external/refresh-catalogpackages/restexternal-datasource-routes.tsrefreshCatalogos datasource list-tables,os datasource introspectand@objectstack/clientreach those HTTP doors. So nopackages/restsource line changes.importObjectnow drafts through a privatedraftObject(the oldgenerateObjectDraftbody, unchanged), so an import is judged once, under its own code.validateObjectasks the same predicate (isFederationSource) for its existing "not federated, nothing to validate" skip: no behaviour change there, and what Sync refuses is now exactly what validation skips.Measured answers on head
b4fb8b8fe(demo_extdeclares noschemaMode;lake_extdeclaresexternal). The fourpackages/restrows came from an uncommitted scratch harness built fromremote-tables-twin.equivalence.test.ts's fixture, deleted after the run. The final head,145e52859, only replaces the guard's code parameter with a door kind, so thatcheck:dispatcher-error-vocabularyreads both codes as literals; the codes per door are pinned inservice-datasource.demo_ext)external(lake_ext)GET …/external/tables400 EXTERNAL_DATASOURCE_ERROR200POST …/external/tables/customers/draft400 EXTERNAL_DATASOURCE_ERROR200POST …/external/tables/customers/import400 EXTERNAL_IMPORT_ERROR201POST …/external/refresh-catalog400 EXTERNAL_DATASOURCE_ERROR200GET …/remote-tables(admin)400 EXTERNAL_DATASOURCE_ERROR200POST …/object-draft(admin)400 EXTERNAL_DATASOURCE_ERROR200Readings taken within the ruling (each stated, none new)
EXTERNAL_IMPORT_ERROR. The card says "the existing 400EXTERNAL_DATASOURCE_ERROR" for Sync's routes. The import route's catch serves400 EXTERNAL_IMPORT_ERRORfor every refusal, whatever the thrown code, anddefault's refusal already declares that code there. So the import answers its existing code; nothing is minted or moved.refresh-catalogis refused too. It is the fourth method behind the same producer, and thedefaultrefusal already covered it. One guard, one fact, so no door stays open.schemaMode, so it is managed. It is also how validation already read it ("not federated"), so an import can no longer bind an object the sweep never checks. Its message says "has no datasource definition".Pins reversed (not deleted)
primary-datasource-test-and-sync.test.ts› "a managed runtime datasource tests and syncs exactly as before" (remote-tables200on a managed runtime SQLite datasource) is now "a managed runtime datasource tests exactly as before, and its Sync refuses". The Test half is unchanged.primary-datasource-test-and-sync.test.ts› "Sync on a driver that cannot introspect still refuses with the engine reason": the managedmemorydatasource now gets the managed refusal (asserted), and the engine reason is asserted on a federatedmemorydatasource (stream). The harness now has the'metadata'service the plugin reads, serving the same records the admin list reads.external-metadata-read-at-use.test.ts› "validate and the sweep answer an empty report, the draft a bare name, the catalog an unpersisted snapshot" (the draft, refresh and listing answering with no datasource definition readable) is now "validate and the sweep answer an empty report, and the Sync doors refuse an undeclared datasource". The validate half is unchanged.New:
managed-datasource-sync-refusal.test.ts, over the real composition (realObjectQL, connection service, driver factory,ExternalDatasourceServicePluginwiring, admin routes on Hono). It covers each door refusingwarehouse(noschemaMode),ledger(managed) andqa_mem(runtimememory) withcode+statusand nothing saved or introspected; a name with no definition;lake(external) andmirror(validate-only) still listing, drafting, importing and refreshing;defaultunchanged; and Setup's list (schemaMode: 'managed'for the tester's runtime datasources) agreeing with the refusal row by row.Known gap: one
packages/restfixture line needs the seat's declaration first (closed)Closed in patch round 1 (
658d9d0b5): the seat declared the edit on #22648 (6332535245), the line below is committed, andTest Coreis green on that head. The paragraph below is the record of the gap as it stood on145e52859.packages/rest/src/remote-tables-twin.equivalence.test.tsbuilds its datasource as{ name }. That is managed by default, so 5 of its cases now go red (Test Core): both twins answer the refusal where the cases expect200. It is an under-declared fixture, not a managed-Sync pin: the twin-equivalence cases need a federation source to compare listings. The fix is one line,getDatasource: async (name: string) => ({ name, schemaMode: 'external' as const }). Probed uncommitted on145e52859throughscripts/ablation-replace.mjs: with that line the file passes 10 of 10, and the tool restored the file (blob equal to HEAD,git diff HEADempty). This PR does not make the edit:packages/restisdomain:cli's, and the dispatch requires a seat declaration before an edit there.Tests
All on the final head
145e52859, under the shared verify lock:pnpm --filter @objectstack/service-datasource exec vitest run --maxWorkers=2:Test Files 54 passed (54),Tests 945 passed (945).pnpm --filter @objectstack/service-datasource typecheck: exit 0.tsc --listFilesincludes all three touched test files.pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/remote-tables-twin.equivalence.test.ts src/external-datasource-validate-scope.test.ts src/external-datasource-envelope.conformance.test.ts: 5 failed, 27 passed. All 5 failures are the twin-fixture cases named above.external-import-saves-like-meta,external-import-code-datasource-namespace,external-import-destructive-remedy,external-validate-sees-runtime-save,external-validate-start-ordering) on the booted showcase, whoseshowcase_externaldeclaresschemaMode: 'external': 5 files, 20 tests passed.!): the dependency closure was built, thenpnpm --filter @objectstack/dogfood testran under the lock with no shard and no path.VERDICT command-exit 0·Test Files 287 passed | 3 skipped (290)·Tests 2395 passed | 18 skipped (2413).Ablation (one-off, not kept). The fix was committed first. Then, through
scripts/ablation-replace.mjs, the guard's managed line on145e52859was replaced with a no-op (anchor hit 1 time, blobad6c9f16563fbecamea2e8fe2b78ac) and the three files were run: 13 failed, 19 passed. Every managed cell, the no-definition cells, the row-by-row list agreement and the three reversed pins turned red.default's refusal and the federated cells stayed green. The tool restored the file (blob back toad6c9f16563f, equal to HEAD;git diff HEADempty). The subject is imported by relative path fromsrc/, so nodist/leg applies. A first ablation onb4fb8b8fegave the same 13 and 19.Gates. The derived list on
145e52859wasnode scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, 68 commands. All 68 exited 0, each code captured before any pipe.--ranreconciliation: 68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. The no-major level axis was also run against this body as the event payload:LEVEL AXIS: … clause-② no (narrowing) and metadata-protocol none … a non-protocol change at patch, exit 0. Every other family is CI's, on this PR's check runs.Patch round 1, on
658d9d0b5(under the verify lock, dependency closures rebuilt):remote-tables-twin.equivalence.test.ts10 passed (10), the 5 cases red on145e52859now green; the threeservice-datasourcetest files 32 passed (32); the 68 derived gates re-run and reconciled (68 derived, 68 run, 0 NOT-MEASURED). The full dogfood battery above stands for this head: the round changes one test-fixture line inpackages/restand no runtime code, and CI'sDogfood Regression Gateis green on658d9d0b5.Acceptance notes
origin/main56cf4d8,DatasourceResourcePage.tsxrenders the "Sync objects" button on every row, whatever itsschemaMode(which is already in the list payload). After this PR, the button on a managed row opens the dialog with the refusal message (objectui#13207 draws refusals there). Not changed here; reported to the seat for an objectui card. Carrier: an objectui card the seat files when this PR lands.defineStack()on the built spec, atb4fb8b8fe, accepted an object withexternal: { remoteName: 'orders' }bound to aschemaMode: 'managed'datasource. This PR closes the Sync producer of such objects only. Reported to the seat; not fixed here. Carrier: ADR-0015 §4.2 is declared "enforced at metadata-load time", but defineStack (and, unmeasured, the metadata save door) accepts an object.external binding on a datasource whose schemaMode is managed #25168, filed for triage.scripts/adr-anchors/is outside the claim's declared file surface; the ADR id is in the code and in the refusal text. The contract review (6333095709) found it not blocking (check:adr-anchorsis green and no ADR-0015 anchor names this service) and named it a lane follow-up. Carrier: none (no user-visible defect).packages/restdoors (draft, import, refresh) belong inpackages/rest. They were measured once here (table above) and not committed. The contract review (6333095709) found it not blocking (each REST catch maps every service throw to that door's fixed code) and named apackages/restpin of the four doors as adomain:clifollow-up. Carrier: none (no user-visible defect).Generated by Claude Code