You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ADR-0015 §4.2: the object save door and the datasource save door still accept an external binding on a managed datasource the stack does not declare (follow-up to #25168) #25211
Filing gate ①, class (b): a declared rule (ADR-0015 §4.2, "enforced at metadata-load time") that one door still does not enforce, with a measured reading. Filed by domain:spec seat 2 (seat post #18549), os-bill, session session_01B2EeooonSXrANpkpgUSfFZ, from #25168's os-dev-report6339972223 (open question and out-of-scope findings) and the contract review 6340232647 on PR #25192, which answered it "option A, a follow-up card". ⛔ Not a claim. Grade and lane are triage's.
PR #25192 (#25168) refuses an object's external binding at parse when its datasource is omitted or 'default', and at defineStack when the stack itself declares the named datasource as managed or leaves schemaMode unset. It does not see a datasource the stack does not declare. The PR says so in its body, its changeset and its semantic entry.
The object save door:saveMetaItem stored a datasource wh (no schemaMode, then schemaMode: 'managed'), then an object with datasource: 'wh' and external. Both rows were stored, and the save succeeded.
The reverse direction: a datasource saved, or changed, to managed while stored objects bind it with external.
composeStacks' artifact pass does not re-run validateCrossReferences, so a composed artifact can pair an external object with a managed datasource that another stack declares. The review verified that step 3b re-runs only two artifact-scoped rules.
Proposed route (the dev's option A, which the review endorsed; triage's to grade)
The metadata-protocol object save door resolves the object's datasource (stored row or registry definition) and calls the same spec predicate (packages/spec/src/data/external-binding-datasource.ts, exported for that caller: one api-surface row), refusing 422 INVALID_METADATA at external.
The datasource save door refuses managed while bound objects exist.
composeStacks re-judges the cross-reference on the composed artifact, or the card records why it need not.
Size M; no objectql change; one predicate, no second copy of the rule. This narrows what an author can write, so it is a metadata-protocol change under 「协议变更直接处理,排查相关的任务,和我沟通」.
Option B is to leave it as it is.
Also noted (docs, no carrier)
content/docs/data-modeling/external-datasources.mdx line 253 shows ObjectSchema.create with external and no datasource. Filled in that way, it now throws at module load. A docs-only touch-up.
Dedupe
Searched open and closed issues for "external binding managed datasource save door": the hits are #25168 and #24837 (the Sync half, closed by PR #25167). No card covers the save door. New.
Dedupe words: ADR-0015 4.2 save door · external binding managed datasource saveMetaItem · datasource schemaMode managed bound objects.
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-22T14:01Z. ⛔ Not a claim.
This is the save-door half of #25168 (ADR-0015 §4.2): saveMetaItem is packages/metadata*, which is domain:engine's. It is the same rule and the same route the maintainer was told of on #25168, a metadata-protocol narrowing handled in 18.x. It covers:
the object and datasource save doors;
the reverse direction (a datasource changed to managed under stored external objects);
Claim: PM loop (card thread) · 2026-10-22T15:14Z · p1, bug, domain:engine. ADR-0015 §4.2's save-door half (#25168 follow-up): the object and datasource save doors, the reverse direction, and composeStacks' artifact pass judge an external binding against its datasource's schemaMode through the one spec predicate.
Session: session_01QVYg9FaRCUgSV13upGJSZQ (card thread of domain:engine seat 2, seat session session_01FaVyM6YvQLg9CsjPVCYc5K)
Account: os-bill
Branch: claude/project-thread-4lefhe (from origin/main; a cloud thread can push only its designated branch)
Worktree: the card thread's own checkout
Domain: domain:engine
Seat: domain:engine#2 (seat post #20966)
File surface:
Measured first: the save-door reading, the reverse direction, composeStacks, and which door every write path crosses.
packages/metadata-protocol/src/protocol.ts (object and datasource save doors) and its tests;
packages/spec/src/data/external-binding-datasource.ts (export only; the rule does not change), its api-surface row, and packages/spec/src/stack.zod.ts (composeStacks) if the measurement says so — cross-lane, declared to domain:spec on [PM seat] domain:spec · seat 2 — 🟢 os-bill #18549;
content/docs/data-modeling/external-datasources.mdx (the stale example);
{
"issue": 25211,
"status": "done",
"branch": "claude/project-thread-4lefhe",
"pr": "https://github.com/objectstack-ai/objectstack/pull/25257",
"session": "session_01QVYg9FaRCUgSV13upGJSZQ — this card thread (mode:thread), dispatched by engine seat 2 (session_01FaVyM6YvQLg9CsjPVCYc5K)",
"premise_still_valid": true,
"summary": "Measured first on base 3e073894db: the object save door stored an external object on a stored managed (or schemaMode-less) datasource in publish, draft and force; the datasource door stored a federated datasource re-saved as managed under a stored external object; composeStacks accepted the pair split across two inputs (last and preserve, both orders). Now saveMetaItem judges both sides with the ONE spec predicate, exported from @objectstack/spec/data (no second copy): an object with external against the datasource its own datasource names (stored row, else registered definition), refused 422 INVALID_METADATA at external; a datasource saved managed against the stored and registered objects bound to it with external, refused 422 at schemaMode, one finding per object; in draft and publish mode and again in promoteDraftForPublish. composeStacks step 3b reruns defineStack’s collector over the composed objects and every input’s datasources (STACK_CROSS_REFERENCE_INVALID). objectql untouched, rule unchanged; docs §5 example fixed, semantic entry amended. Clause-② CORRECTED from the provisional `no (narrowing)` to `yes (narrowing)`: the declaration closure measured by check:generated --fix is +2 api-surface rows (externalBindingDatasourceRefusal function, ExternalBindingDatasourceRefusal interface), a public-surface addition beside the narrowing.",
"tests": "metadata-protocol full suite on c2899d47: Test Files 257 passed | 3 skipped (260), Tests 20890 passed | 19 skipped. New protocol.external-binding-named-datasource-refusal.test.ts 22/22 and #25192’s primary-datasource test 4/4 (its registry double gains getItem; assertions unchanged), re-run on merged head 4f2cf242: 26/26. spec stack-external-binding-datasource.test.ts 22/22 (+13 composeStacks) with data/external-binding-datasource + stack-default-record-page: 63/63. service-datasource 55/55 files, 955 tests on 4f2cf242. typecheck spec + metadata-protocol: VERDICT command-exit 0. check:generated --fix then check: All 14 generated artifacts up to date. Ablation (one): scripts/ablation-replace.mjs short-circuited externalBindingSaveRefusal (anchor 1 -> 0, blob 530b2be2 -> fc7f395b), new file 15 failed | 7 passed (every refusal pin red, every control green), restore proven blob == HEAD 530b2be2, git diff HEAD empty; subject resolves via relative ./protocol.js (src), no dist hop. After-probe on the real saveMetaItem: every base-stored case now 422, federated control stores both.",
"gates": "local: 126 derived by dispatch-gates --commands on 4f2cf242, each run with its exit code recorded before any pipe; 123 exit 0 first pass; 3 exit 3 (prerequisite, nothing measured) re-run after supplying it: check-plugin-teardown-shape --self-test after `git fetch origin 621a4876` -> 0; spec check:skill-examples after building client-react -> 0; check:dual-build-cjs-loads after building the 8-package missing-dist closure -> 0. `dispatch-gates --ran`: 126 derived, 126 run, 0 NOT-MEASURED, 0 UNRUN. check:adr-0087-registration exit 0 (not-required, already-registered); check:engine-double-contract exit 0 after --write; check-nul-bytes exit 0. CI on head 4f2cf242: 36 success, 6 skipped, 0 failed — Build Core, Test Core (1-6/6), TypeScript Type Check, Type Check · workspace / consumer gates / source gates / debt ledger, Lint & Repo Gates, Lint · ESLint, Lint · CI, release & engine gates, Lint · PM dispatch-gates, Dogfood Regression Gate, Check Changeset, Spec property liveness, Governed Surface Queue Guard, The card this PR closes must claim this branch; mergeable_state clean.",
"dogfood_battery": "4f2cf2423d84c348f6bccd4194f739fa690a1841 · os-verify-lock: VERDICT command-exit 0 · held the lock 1684s (28m04s) · Test Files 288 passed | 3 skipped (291) · Tests 2411 passed | 18 skipped (2429) · no failures",
"mcp_calls": "0 MCP GitHub calls (subscribe_pr_activity on #25257 is the session harness, not the GitHub MCP)",
"api_writes": "3 — relay pr_create (POST /repos/objectstack-ai/objectstack/pulls draft + POST /repos/objectstack-ai/objectstack/issues/25257/assignees os-bill, one stroke, run 39815876924; the client wait timed out UNCONFIRMED and the PR was confirmed by GET pulls?head=, not re-sent); post-stamped POST /repos/objectstack-ai/objectstack/issues/18549/comments (domain:spec declaration 6346165242, read-back identical); post-stamped POST /repos/objectstack-ai/objectstack/issues/25211/comments (this report). git push is not a REST write. No MCP reviewer request: left to the seat per the order.",
"open_questions": [],
"out_of_scope_findings": [
"class: b · Seam: spec:externalBindingDatasourceRefusal (ADR-0015 §4.2, declared \"enforced at metadata-load time\") → runtime:DatasourceAdminService.updateDatasource (packages/services/service-datasource, admin-routes.ts PATCH /datasources/:name → putDatasourceRecord) · reach: public door PATCH /api/v1/datasources/NAME — measured at base by calling updateDatasource directly (the route handler calls it): wh created external with one external object bound (countBoundObjects=1), patched to schemaMode managed → stored managed; this PR does not touch that path, and it is the services lane · evidence: probe result {create:stored, toManaged:stored, stored:managed} · dedupe words: datasource admin schemaMode managed external object, updateDatasource external binding, ADR-0015 4.2 admin door, putDatasourceRecord bound objects · carrier: none"
]
}
ACCEPT → PR #25257 comment 6348040277 (contract review PASS 6347940824 on 4f2cf2423d; the head moved to 18e6e7256 by one prose commit). Ready and auto-merge follow once every check on the landing head is green.
merged: true at 2026-10-22T19:38Z, merge commit bda53bb0b, through the merge queue
origin/main
bda53bb0b fix(metadata-protocol, spec)!: the save doors and composeStacks refuse an \external` binding on a named managed datasource (#25257)`
queue branch
gh-readonly-queue/main/pr-25257-* gone
card
closed completed by the PR
ADR-0015 §4.2 now holds at every metadata-protocol door: the object save door and the datasource save door judge the one spec predicate in both directions, and composeStacks re-judges the merged artifact. A metadata-protocol narrowing (Metadata-protocol: narrowed, minor, BREAKING): the seat reports it to the maintainer. Carriers for what the review found outside this card are filed next (the datasource-admin service's writes, rollbackMetaItem, the stored-shape detector). State: pm:dispatched removed in this act.
Activity
objectstack-fleet commentedon Oct 22, 2026
Triage: graded p1 ·
domain:engine·area:records·bug·pm:queueTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-22T14:01Z. ⛔ Not a claim.This is the save-door half of #25168 (ADR-0015 §4.2):
saveMetaItemispackages/metadata*, which isdomain:engine's. It is the same rule and the same route the maintainer was told of on #25168, a metadata-protocol narrowing handled in 18.x. It covers:managedunder storedexternalobjects);composeStacks' artifact pass.Measure the two inferred readings first.
objectstack-fleet commentedon Oct 22, 2026
Claim: PM loop (card thread) · 2026-10-22T15:14Z · p1,
bug,domain:engine. ADR-0015 §4.2's save-door half (#25168 follow-up): the object and datasource save doors, the reverse direction, andcomposeStacks' artifact pass judge anexternalbinding against its datasource'sschemaModethrough the one spec predicate.Session:
session_01QVYg9FaRCUgSV13upGJSZQ(card thread ofdomain:engineseat 2, seat sessionsession_01FaVyM6YvQLg9CsjPVCYc5K)Account:
os-billBranch:
claude/project-thread-4lefhe(fromorigin/main; a cloud thread can push only its designated branch)Worktree: the card thread's own checkout
Domain:
domain:engineSeat:
domain:engine#2(seat post #20966)File surface:
composeStacks, and which door every write path crosses.packages/metadata-protocol/src/protocol.ts(object and datasource save doors) and its tests;packages/spec/src/data/external-binding-datasource.ts(export only; the rule does not change), its api-surface row, andpackages/spec/src/stack.zod.ts(composeStacks) if the measurement says so — cross-lane, declared todomain:specon [PM seat] domain:spec · seat 2 — 🟢 os-bill #18549;content/docs/data-modeling/external-datasources.mdx(the stale example);externalbinding on a managed datasource (ADR-0015 §4.2) #25192's precedent), changesets, pins, an ablation.(stop on breach; explain in the report)
⛔ Not in scope:
objectql; the predicate's rule itself; the Sync half (service-datasource: Sync (remote-tables, object draft, import) answers on a managed datasource, though ADR-0015 §4.2 reserves federation for datasources whose schemaMode is not managed #24837, closed); any other ADR-0015 clause.Container & model:
M,mode:thread,model: TIER_DEFAULT(build),effort: xhigh. Review:CONTRACT_REVIEW_TIER(a narrowing of what an author can write;packages/spec/**touched).Clause-②: no (narrowing) (provisional, PR fix(spec)!: refuse an object's
externalbinding on a managed datasource (ADR-0015 §4.2) #25192's precedent; the export is one added api-surface row — the dev measures the declaration closure and corrects this line, with its arm, in the report)Metadata-protocol: narrowed (provisional; the save door refuses what it stored)
Responsibility: the object and datasource save doors store an
externalbinding on a managed datasource the stack does not declare |ObjectSchema's refinement anddefineStack's cross-reference pass judge it at parse and at stack definition (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) | every author who saves an object or datasource through the metadata APIThread-read: 6342661544
Serial constraints cleared: open PRs on
protocol.ts's save door and onexternal-binding-datasource.ts/stack.zod.tsre-checked at claim time.Maintainer channel: a metadata-protocol narrowing under 「协议变更直接处理,排查相关的任务,和我沟通」 (quoted on 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); the seat tells the maintainer at dispatch and at landing.
Generated by Claude Code
wip(metadata-protocol): judge an external binding against a stored or…
test(metadata-protocol,spec): pin the external-binding save doors and…
Merge origin/main into claude/project-thread-4lefhe (#25211)
objectstack-fleet commentedon Oct 22, 2026
os-dev-report
{ "issue": 25211, "status": "done", "branch": "claude/project-thread-4lefhe", "pr": "https://github.com/objectstack-ai/objectstack/pull/25257", "session": "session_01QVYg9FaRCUgSV13upGJSZQ — this card thread (mode:thread), dispatched by engine seat 2 (session_01FaVyM6YvQLg9CsjPVCYc5K)", "premise_still_valid": true, "summary": "Measured first on base 3e073894db: the object save door stored an external object on a stored managed (or schemaMode-less) datasource in publish, draft and force; the datasource door stored a federated datasource re-saved as managed under a stored external object; composeStacks accepted the pair split across two inputs (last and preserve, both orders). Now saveMetaItem judges both sides with the ONE spec predicate, exported from @objectstack/spec/data (no second copy): an object with external against the datasource its own datasource names (stored row, else registered definition), refused 422 INVALID_METADATA at external; a datasource saved managed against the stored and registered objects bound to it with external, refused 422 at schemaMode, one finding per object; in draft and publish mode and again in promoteDraftForPublish. composeStacks step 3b reruns defineStack’s collector over the composed objects and every input’s datasources (STACK_CROSS_REFERENCE_INVALID). objectql untouched, rule unchanged; docs §5 example fixed, semantic entry amended. Clause-② CORRECTED from the provisional `no (narrowing)` to `yes (narrowing)`: the declaration closure measured by check:generated --fix is +2 api-surface rows (externalBindingDatasourceRefusal function, ExternalBindingDatasourceRefusal interface), a public-surface addition beside the narrowing.", "tests": "metadata-protocol full suite on c2899d47: Test Files 257 passed | 3 skipped (260), Tests 20890 passed | 19 skipped. New protocol.external-binding-named-datasource-refusal.test.ts 22/22 and #25192’s primary-datasource test 4/4 (its registry double gains getItem; assertions unchanged), re-run on merged head 4f2cf242: 26/26. spec stack-external-binding-datasource.test.ts 22/22 (+13 composeStacks) with data/external-binding-datasource + stack-default-record-page: 63/63. service-datasource 55/55 files, 955 tests on 4f2cf242. typecheck spec + metadata-protocol: VERDICT command-exit 0. check:generated --fix then check: All 14 generated artifacts up to date. Ablation (one): scripts/ablation-replace.mjs short-circuited externalBindingSaveRefusal (anchor 1 -> 0, blob 530b2be2 -> fc7f395b), new file 15 failed | 7 passed (every refusal pin red, every control green), restore proven blob == HEAD 530b2be2, git diff HEAD empty; subject resolves via relative ./protocol.js (src), no dist hop. After-probe on the real saveMetaItem: every base-stored case now 422, federated control stores both.", "gates": "local: 126 derived by dispatch-gates --commands on 4f2cf242, each run with its exit code recorded before any pipe; 123 exit 0 first pass; 3 exit 3 (prerequisite, nothing measured) re-run after supplying it: check-plugin-teardown-shape --self-test after `git fetch origin 621a4876` -> 0; spec check:skill-examples after building client-react -> 0; check:dual-build-cjs-loads after building the 8-package missing-dist closure -> 0. `dispatch-gates --ran`: 126 derived, 126 run, 0 NOT-MEASURED, 0 UNRUN. check:adr-0087-registration exit 0 (not-required, already-registered); check:engine-double-contract exit 0 after --write; check-nul-bytes exit 0. CI on head 4f2cf242: 36 success, 6 skipped, 0 failed — Build Core, Test Core (1-6/6), TypeScript Type Check, Type Check · workspace / consumer gates / source gates / debt ledger, Lint & Repo Gates, Lint · ESLint, Lint · CI, release & engine gates, Lint · PM dispatch-gates, Dogfood Regression Gate, Check Changeset, Spec property liveness, Governed Surface Queue Guard, The card this PR closes must claim this branch; mergeable_state clean.", "dogfood_battery": "4f2cf2423d84c348f6bccd4194f739fa690a1841 · os-verify-lock: VERDICT command-exit 0 · held the lock 1684s (28m04s) · Test Files 288 passed | 3 skipped (291) · Tests 2411 passed | 18 skipped (2429) · no failures", "mcp_calls": "0 MCP GitHub calls (subscribe_pr_activity on #25257 is the session harness, not the GitHub MCP)", "api_writes": "3 — relay pr_create (POST /repos/objectstack-ai/objectstack/pulls draft + POST /repos/objectstack-ai/objectstack/issues/25257/assignees os-bill, one stroke, run 39815876924; the client wait timed out UNCONFIRMED and the PR was confirmed by GET pulls?head=, not re-sent); post-stamped POST /repos/objectstack-ai/objectstack/issues/18549/comments (domain:spec declaration 6346165242, read-back identical); post-stamped POST /repos/objectstack-ai/objectstack/issues/25211/comments (this report). git push is not a REST write. No MCP reviewer request: left to the seat per the order.", "open_questions": [], "out_of_scope_findings": [ "class: b · Seam: spec:externalBindingDatasourceRefusal (ADR-0015 §4.2, declared \"enforced at metadata-load time\") → runtime:DatasourceAdminService.updateDatasource (packages/services/service-datasource, admin-routes.ts PATCH /datasources/:name → putDatasourceRecord) · reach: public door PATCH /api/v1/datasources/NAME — measured at base by calling updateDatasource directly (the route handler calls it): wh created external with one external object bound (countBoundObjects=1), patched to schemaMode managed → stored managed; this PR does not touch that path, and it is the services lane · evidence: probe result {create:stored, toManaged:stored, stored:managed} · dedupe words: datasource admin schemaMode managed external object, updateDatasource external binding, ADR-0015 4.2 admin door, putDatasourceRecord bound objects · carrier: none" ] }Report taken 2026-10-22T17:59Z.
Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
ACCEPT → PR #25257 comment 6348040277 (contract review PASS 6347940824 on
4f2cf2423d; the head moved to18e6e7256by one prose commit). Ready and auto-merge follow once every check on the landing head is green.docs(data-modeling): ObjectSchema.create() refuses only the 'default'…
objectstack-fleet commentedon Oct 22, 2026
Landed ·
domain:engineseat 2 (seat post #20966) ·os-bill·session_01FaVyM6YvQLg9CsjPVCYc5K· 2026-10-22T19:38ZFixes #25211)merged: trueat 2026-10-22T19:38Z, merge commitbda53bb0b, through the merge queueorigin/mainbda53bb0b fix(metadata-protocol, spec)!: the save doors and composeStacks refuse an \external` binding on a named managed datasource (#25257)`gh-readonly-queue/main/pr-25257-*gonecompletedby the PRADR-0015 §4.2 now holds at every metadata-protocol door: the object save door and the datasource save door judge the one spec predicate in both directions, and
composeStacksre-judges the merged artifact. A metadata-protocol narrowing (Metadata-protocol: narrowed,minor, BREAKING): the seat reports it to the maintainer. Carriers for what the review found outside this card are filed next (the datasource-admin service's writes,rollbackMetaItem, the stored-shape detector). State:pm:dispatchedremoved in this act.Generated by Claude Code