Skip to content

fix(spec)!: refuse an object's external binding on a managed datasource (ADR-0015 §4.2) - #25192

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-5h4pzn
Oct 22, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-5h4pzn

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

Fixes #25168

Clause-②: no (narrowing)
Metadata-protocol: narrowed

What

ADR-0015 §4.2 allows object.external only on a datasource whose schemaMode is not 'managed', and declares the rule "enforced at metadata-load time". No load door enforced it. This PR makes the spec judge it, once, through one predicate (packages/spec/src/data/external-binding-datasource.ts, internal), at each door that can see the halves it needs. This follows the route in triage comment 6333828054: a declaration rule at the parse, inherited by the save door and os validate, and no second check in packages/objectql.

The object's datasource Where it is judged Answer now
omitted or 'default' (the primary, managed by construction) ObjectSchema's parse (superRefine), at external ObjectSchema.create throws at module load. defineStack and os validate give STACK_SCHEMA_INVALID (422) at objects.N.external. PUT /api/v1/meta/object/NAME gives 422 INVALID_METADATA with the finding at external, and nothing is stored.
a datasource the same stack declares with schemaMode: 'managed' or with no schemaMode defineStack's cross-reference pass (validateCrossReferences) STACK_CROSS_REFERENCE_INVALID (422), one finding per bound object. os validate and os build inherit it.
a datasource declared schemaMode: 'external' or 'validate-only' (both doors) accepted, as before
a datasource the stack does not declare no door can see its schemaMode no verdict, as before (see Acceptance notes)

A package defaultDatasource or a datasourceMapping rule does not count as the object's datasource. They route queries, while the boot validation (validateObjectUsing) and the write gate (assertWriteAllowed) read only the object's own datasource. The refusal message says so.

ADR-0087 kit: the semantic entry object-external-binding-managed-datasource-refused (packages/spec/src/migrations/entries/semantic/), and a REFUSED_STORED_SHAPES detector that lists each stored object in the refused shape for os migrate meta --stored (no lossless rewrite: dropping external would turn the remote table into one ObjectStack creates). The changeset carries the registered disposition, minor, BREAKING with FROM and TO, and the one-line fix.

Measured on the base (b6e8707), before any edit

  • defineStack accepted all six probe stacks: datasource omitted, 'default', declared managed, declared with no schemaMode, declared external (control), and undeclared.
  • os validate answered valid: true, exit 0, for both a declared-managed and a no-schemaMode datasource. This was the card's first NOT MEASURED reading.
  • The object save door stored all three probes (datasource omitted, 'default', and a stored managed datasource named wh) and answered success. Each save logged the platform's own "published but its storage was NOT created" warning. This was the card's second NOT MEASURED reading.

After this PR (same probes)

  • defineStack: managed and no-schemaMode give STACK_CROSS_REFERENCE_INVALID 422. Omitted and 'default' give STACK_SCHEMA_INVALID 422. The external control and the undeclared datasource are accepted.
  • os validate: exit 1, valid: false, with those codes and messages.
  • Save door: 422 INVALID_METADATA at external for omitted and 'default', and nothing is stored. A named stored datasource still saves; see Acceptance notes.

Producers (the stop condition)

No producer was found, so no conversion question applies.

Tests

  • packages/spec/src/data/external-binding-datasource.test.ts: the parse refuses at ['external'] (code custom) for datasource omitted, 'default', and an empty binding. ObjectSchema.create throws with the same issue. Controls and the predicate's unit cases.
  • packages/spec/src/stack-external-binding-datasource.test.ts: code + status for both stack verdicts, one finding per bound object, the external and validate-only controls, an undeclared datasource accepted, strict: false skips, and a manifest defaultDatasource does not count.
  • packages/spec/src/conversions/refused-shapes.test.ts: the stored-row detector lists each object.
  • packages/metadata-protocol/src/protocol.external-binding-primary-datasource-refusal.test.ts: the save door answers INVALID_METADATA 422, issues at external, and stores nothing; controls save.
  • packages/spec/src/data/object-strictness-batch20.test.ts: four fixtures bound external with no datasource. They now name their federated datasource (remote_crm), per fixture triage (add the missing declaration).

Ablation, one time, on 488c2fe:

  • The mutation made the predicate answer nothing, which drops all three halves at once. It was checked on disk (grep of the injected text in the source, and in 12 built .mjs files), and @objectstack/spec was rebuilt.
  • Under the mutation, the three spec files went red, 16 failed of 76, and the save-door file went red, 2 failed of 4 (exactly the two refusal cases).
  • The source was restored with git checkout HEAD, and the restore was proved by comparing blob hashes (equal to HEAD) and by an empty git diff HEAD. @objectstack/spec was then rebuilt with declarations, and the mutation is in 0 dist files.
  • After the restore, the spec files pass 76 of 76 and the save-door file passes 4 of 4.

Full @objectstack/spec suite on 03234e2: 780 files passed and 1 skipped; 22737 tests passed, 1 skipped, 1 todo. The next commit, 488c2fe, only narrows the assertions in two of the new test files, and those files pass 76 of 76 on it.

Acceptance notes

  • The save door cannot see a named datasource's schemaMode. PUT /api/v1/meta/object/NAME with datasource: 'wh', where wh is a stored managed datasource, plus external, still saves (measured on this head). The spec parse sees only the object. The minimal place is the metadata-protocol object save door, calling this same predicate with the datasource it resolves, so the rule is not copied. That needs the predicate exported and is outside this card's file surface. Raised in the report as an open question.
  • Not covered here, also noted in the report: the composeStacks artifact pass (it does not re-run validateCrossReferences); the reverse direction (saving a datasource as managed while objects bind it with external); and the /* ... */ example in content/docs external-datasources §5, which should show the object's datasource.
  • Runtime fixtures not rewritten. 62 fixtures in 37 test files across 12 packages hand an object with external and no datasource straight to a driver, the engine or a lint rule, without the parse. Those suites are green on this head. They describe a shape the parse now refuses, but they test runtime behaviour that a stored legacy row can still reach: such a row still registers under [metadata_spec_invalid]. They were left as they are.
  • File surface. scripts/engine-double-contract.pinned.json gains 15 rows from node scripts/check-engine-double-contract.mjs --write, for the save-door test's engine double. It is a mechanical ledger row, as in feat(spec)!: the parse refuses a controlled_by_parent detail whose one required master reference ADR-0055 §1 does not admit #25181 and fix(spec)!: the Field.select string shorthand keeps what it does not document transforming, so a hyphenated option is refused instead of declared as another value #25122.
  • Branch name. The branch is the session's designated claude/project-thread-5h4pzn, not claude/issue-25168-…. The claim names it.

Generated by Claude Code

… parse, defineStack, the stored-row listing and the save door

Claude-Session: https://claude.ai/code/session_013dX1BHfcVSJNSkfA6cvwJN
Co-authored-by: Claude <noreply@anthropic.com>
…ss fixtures name their federated datasource

Claude-Session: https://claude.ai/code/session_013dX1BHfcVSJNSkfA6cvwJN
Co-authored-by: Claude <noreply@anthropic.com>
…d the named subject, not the remedy's wording

Claude-Session: https://claude.ai/code/session_013dX1BHfcVSJNSkfA6cvwJN
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 14 documentable anchor(s).

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

  • content/docs/concepts/metadata-driven.mdx (via ObjectSchemaBase (symbol, a top-level const object))
  • content/docs/data-modeling/drivers.mdx (via /api/v1/datasources (route, a path literal in acceptanceCriteria))
  • content/docs/data-modeling/external-datasources.mdx (via /api/v1/datasources (route, a path literal in acceptanceCriteria))
  • content/docs/kernel/cluster.mdx (via /api/v1/datasources (route, a path literal in acceptanceCriteria))
  • content/docs/protocol/kernel/error-handling.mdx (via /api/v1/datasources (route, a path literal in acceptanceCriteria))

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

  • content/docs/releases/v17/17-1.mdx (via /api/v1/datasources (route, a path literal in acceptanceCriteria))
  • content/docs/releases/v17/17-6.mdx (via /api/v1/datasources (route, a path literal in acceptanceCriteria))
  • content/docs/releases/v17/17-7.mdx (via /api/v1/datasources (route, a path literal in acceptanceCriteria))

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
  • 6 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 — 144 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 2069b81c266836fd6d7b52fc5e6679c734bdcd3c → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 2069b81c266836fd6d7b52fc5e6679c734bdcd3c

⚠️ 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 2069b81c266836fd6d7b52fc5e6679c734bdcd3c → 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: CONTRACT_REVIEW_TIER
Head-sha: 488c2fe49d4fbb2786a141e2628bc6e99ffbf402
Local-runs: none
Scope: packages/spec/src/data/external-binding-datasource.ts, packages/spec/src/data/object.zod.ts, packages/spec/src/stack.zod.ts, packages/spec/src/conversions/refused-shapes.ts, packages/spec/src/migrations/entries/semantic/18.object-external-binding-managed-datasource-refused.ts + docs/adr/0015-external-datasource-federation.md, packages/spec/src/data/datasource.zod.ts, packages/spec/src/kernel/metadata-type-schemas.ts, packages/spec/src/conversions/apply.ts, packages/spec/src/migrations/types.ts, packages/metadata-protocol/src/protocol.ts, packages/objectql/src/engine.ts, packages/objectql/src/federated-object.ts, packages/cli/src/commands/validate.ts, examples/app-showcase/src/system/datasources/showcase-external.datasource.ts, .changeset/25168-external-binding-managed-datasource-refused.md, scripts/check-adr-0087-registration.mjs, packages/spec/src/data/external-binding-datasource.test.ts, packages/spec/src/stack-external-binding-datasource.test.ts, packages/spec/src/conversions/refused-shapes.test.ts, packages/spec/src/data/object-strictness-batch20.test.ts, packages/metadata-protocol/src/protocol.external-binding-primary-datasource-refusal.test.ts

Inputs: card #25168 (body, triage 6333828054, claim 6335118058, dev report 6339972223), PR #25192 (body, 12 files, the diff against main), and the 42 check-runs on this head: 35 success, 7 skipped by path selection (18.x upgrade harness, Build Docs, Console Pin Gate, the Dogfood shards, Dogfood Verify CLI, Packed-tarball smoke, Temporal Conformance), 0 failed, 0 pending. Those conclusions are the gate verdicts here: Check Changeset (the job that runs check-adr-0087-registration), Lint & Repo Gates, Lint · PM dispatch-gates, Governed Surface Queue Guard, Test Core 1/6 to 6/6 and the aggregate, Type Check · workspace, TypeScript Type Check, Build Core, Spec property liveness, Dogfood Regression Gate (aggregate success; its three shards path-skipped, so the dev's local battery is the dogfood reading and is not re-run here), and the three card/branch guards — all success. Nothing built, run or re-run locally; committed blobs read with git show after a fetch.

Face: schema — the five packages/spec/src/** hits decide it. The Clause-②: no (narrowing) line is also present, at the start of a line in the PR body and in the changeset, and reads as a declared narrowing; that face maps to the same tier, so CONTRACT_REVIEW_TIER stands on either reading.

① Derived judgments

  1. ObjectSchema's accept set narrows — RIGHT. An object carrying external (an empty binding included) whose datasource is omitted or 'default' is refused at ['external'], code custom, by refuseExternalBindingOnPrimaryDatasource inside the existing .superRefine of object.zod.ts. The parsed datasource is .optional().default('default') (object.zod.ts:2625) and the predicate reads a missing or empty string as the primary too, so the parsed and the stored readings agree. 'default' is managed by construction: stack.zod.ts:548 refuses a stack datasource of that name as host-reserved, so no artefact can re-declare it external, and ADR-0015 §1 makes federation a property of the datasource. Every object door inherits the refusal with no second check: ObjectSchema.create (throws at module load — pinned), defineStack (STACK_SCHEMA_INVALID 422 at objects.N.external — pinned), os validate (loadConfig loads the default export, which is defineStack's result), the object save door (kernel/metadata-type-schemas.ts:91 maps object to ObjectSchema; 422 INVALID_METADATA, the issue at external, nothing stored — pinned in the metadata-protocol test), and the stored-row load, which registers the row anyway under [metadata_spec_invalid] (protocol.ts:31601, objectql registry.ts:5124), so metadata at rest keeps loading exactly as the changeset's "Unchanged" bullet says.

  2. defineStack's accept set narrows — RIGHT. collectExternalBindingDatasourceErrors runs inside validateCrossReferences on result.data, the strict parse's output (stack.zod.ts:5088), so a declared datasource's schemaMode already carries the spec default 'managed' (datasource.zod.ts:673) and the mode / schema_mode aliases are already folded. An object naming a datasource this stack declares with schemaMode: 'managed' or none → STACK_CROSS_REFERENCE_INVALID 422, one finding per bound object; 'external' and 'validate-only' accepted; an undeclared name gets no verdict (absence of a definition is not evidence about its schemaMode, and the host's and other packages' datasources are not visible); strict: false returns before the parse and the pass, by the existing contract. The primary half never reaches this pass and 'default' cannot be in the declared map, so no object is found twice. Placement after the objectNames.size === 0 return is right: no objects, no bindings. os validate and os build inherit it through defineStack.

  3. What stays accepted — RIGHT, and stated in every carrier. A binding on a datasource the door cannot see: the save door with a named stored datasource (wh, managed or unset) still saves; a stack naming an undeclared datasource still passes; and the composeStacks artifact pass, whose step 3b re-runs two artifact-scoped rules only, never pairs an external object of one input with a managed datasource another input declares. The PR body's table (row 4), its Acceptance notes, the changeset's "Unchanged" bullet, the entry's reason, the predicate's docblock and the save-door test's header all say so. Judged in ③.

  4. "A package defaultDatasource or a datasourceMapping rule does not count" — RIGHT. assertWriteAllowed (objectql/src/engine.ts:9702) reads object.datasource, returns on 'default' and on a datasource with no or 'managed' schemaMode; isFederatedObject (federated-object.ts:33) is a presence test on external. An object routed only by a mapping is exactly the split the card describes, so the binding has to be on the object's own datasource, and the refusal message says so.

  5. ADR-0087 kit — RIGHT. The D3 entry 18.object-external-binding-managed-datasource-refused.ts has the SemanticMigration shape of its siblings (id, surface, replacement, reason, acceptanceCriteria); no conversionIds, right for a refusal nothing rewrites losslessly; relevantWhen omitted, which the type allows — the entry is then always listed by os migrate meta, a noise cost only. The directory listing is the registry (gen:migration-registry). The REFUSED_STORED_SHAPES detector carries the same id and toMajor: 18, lists one TODO per refused object with from and a reason naming the owner's three choices, runs on the converted stack (conversions/apply.ts:188), and the registration test enumerates it. Listing-only note: the detector reads a stored datasource row's schemaMode key raw, so a stored row written under the mode / schema_mode alias would list as managed — a TODO, never a refusal; the parse's own verdict is unaffected.

  6. Public surface — RIGHT. externalBindingDatasourceRefusal is not re-exported from data/index.ts (no api-surface row; Lint & Repo Gates green). ObjectExtensionSchema (object.zod.ts:4018) exposes no external key, so an extension cannot add a binding past the refinement. The .describe() strings of external are unchanged, so the generated JSON Schema and references are unmoved (the dev's check:generated 14/14 reading, Lint green). No consumer package changes source; the engine-double ledger and one test are the only non-spec files.

  7. Fixtures — RIGHT. The four batch-20 fixtures now name remote_crm (undeclared → no verdict), so their unknown-key assertions still pin what they pinned. The 62 runtime fixtures that hand external without datasource straight to drivers, the engine or lint bypass the parse and model what a legacy stored row still reaches; green on this head; answered in ③.

  8. Producers (the stop condition) — RIGHT. Read on the head tree: the only object with external outside tests is the showcase's, bound to showcase_external, declared schemaMode: 'external' (showcase-external.datasource.ts); every other remoteName hit is a driver or CLI struct field, not object metadata; the CLI zoo fixture names its federated datasource. HotCRM (0 producers) is the dev's reading, not re-read here. No conversion question applies.

  9. Remainder (the lane seat's ACCEPT at TIER_DEFAULT): scripts/engine-double-contract.pinned.json, three rows for the new test's engine double, a mechanical --write output named in the PR body.

② Semver level

  • .changeset/25168-external-binding-managed-datasource-refused.md: '@objectstack/spec': minor — RIGHT. Metadata-protocol: narrowed is the kind (a value refused: external on the primary or on a managed declared datasource), a kind takes at least minor on the package the diff moves, and (narrowing) is BREAKING: the body carries the banner, FROM, TO in its three arms, the unchanged set and the one-line fix (datasource: 'warehouse' beside a schemaMode: 'external' declaration). minor rather than major is the launch-window convention the changeset names.
  • Clause-②: no (narrowing) — TRUE, line-initial in both the PR body and the changeset, the exact spelling the fleet's reader keys on. Metadata-protocol: narrowed — TRUE in both.
  • ADR-0087 disposition: exactly one marker, adr-0087: registered object-external-binding-managed-datasource-refused, naming the D3 entry this same diff adds (the gate's resolve-at-HEAD and added-by-this-diff facts); Check Changeset success.
  • No changeset is owed by @objectstack/metadata-protocol: its door changes behaviour through the spec dependency alone; the diff adds it a test and no source. skip-changeset is not claimed; content/docs/releases is untouched.

③ Boundary flags

Dev deviations (six), each answered:

  1. Branch claude/project-thread-5h4pzn, not an issue-25168 slug — the claim names it and "The card this PR closes must claim this branch" is success. Accepted.
  2. First ablation attempt killed mid-build with an EXIT-only trap — the restore is proven on the committed bytes: the diff's index line for external-binding-datasource.ts is blob 8a978cd3, the hash the dev compared to HEAD, and the three gates overlapping the window were rerun. Accepted as a process note; the head is what CI judged.
  3. First dogfood run interrupted, rerun in full on this head — accepted as the dev's reading; the CI shards are path-skipped and nothing is re-run here.
  4. scripts/engine-double-contract.pinned.json outside the claimed surface — a mechanical ledger for the new test's double, named in the PR body, with the precedent the dev cites; the lane seat's remainder. Accepted.
  5. Tests narrowed from message wording to code, status, path and the subject-naming first sentence — right under the os-dev rule; the anchors still pin which object and which datasource is refused.
  6. Worktree kept for the review round — procedural; accepted.

The open question (dev report 6339972223: the object save door cannot see a named datasource's schemaMode; a stored wh, managed or unset, plus an object binding it with external still saves; A a follow-up card, B leave it; recommendation A):

  • Is the PR's claim of what it enforces honest about the gap? YES. The gap is declared in the PR body's table and Acceptance notes ("still saves, measured on this head"), in the changeset's "Unchanged" bullet, in the entry's reason, in the predicate's docblock and in the save-door test's header. The PR claims the parse half and the same-stack half, and only those.
  • Does the gap block this PR? NO — it belongs to a follow-up card (A), which the seat files before this PR lands and links from the PR body. The route (triage 6333828054) put the rule at the parse, inherited by the save door, with no second check in objectql, and the claim's surface is packages/spec/src/** with stop-on-breach. Closing the named-datasource half at the save door needs a datasource lookup inside saveMetaItem (metadata-protocol) and the predicate exported (an api-surface row): a different door and a different surface. No producer was measured (0 in the repo, 0 in HotCRM), and the gap is loud. Against the card's "done" (every authoring and load door refuses), this PR closes the measured defect (defineStack), both NOT-MEASURED readings for the primary, and leaves one authoring half open and named — Fixes #25168 stands on the triage route, with the remainder on its own card. That card should carry, on the same predicate: (a) the object save door resolving the object's datasource (stored row or registry definition) and refusing 422 at external; (b) the reverse direction, a datasource saved or changed to managed while stored objects bind it with external; (c) the composeStacks artifact pass, which today does not re-run validateCrossReferences across inputs (read at step 3b) — the one load door that can see both halves across packages. Option B is wrong: it keeps the split runtime answer the card was filed on.

Out-of-scope findings (four): items 1 to 3 are the follow-up card above, which gives them the carrier the report left blank. Item 4 (content/docs/data-modeling/external-datasources.mdx §5 shows ObjectSchema.create with external and no datasource, a shape that now throws at module load) is a docs-only touch-up, not governed, non-blocking; the seat files or folds it.

Acceptance note, 62 runtime fixtures not rewritten — accepted: they bypass the parse and test what a legacy stored row still reaches (it registers under [metadata_spec_invalid]); green on this head. They now describe a shape the contract refuses, so a later sweep may re-home them under a stored-row harness; not this PR's.

Escalated, not blocking:

  • ADR-0015 §4.2's mechanism sentence is now imprecise. The record says the invariant is "enforced at metadata-load time, not in the Zod schema, because the datasource may live in another artefact". This PR keeps the half that needs the other artefact out of the Zod schema (defineStack's cross-reference pass) and puts the self-contained half, the primary, into ObjectSchema's refinement: the ADR's reason is honoured and its decision, the invariant, is implemented, not reversed, so Prime Directive [WIP] Add Chinese version of the documentation #13 is not engaged. docs/adr/** is Tier H and rightly untouched here; the maintainer amends §4.2's sentence by hand (judged at the parse for 'default', at defineStack for a declared datasource, and at the save door for a stored one once the follow-up lands). The object.zod.ts docblocks already describe the split.
  • Triage's protocol note — Metadata-protocol: narrowed is "reported to the maintainer in this round's record" — is the seat's round record to carry, together with the ADR amendment above.
  • No scripts/adr-anchors/ entry anchors external-binding-datasource.ts to ADR-0015; the file cites the ADR and its section in its docblock, and the anchor README makes one optional. Note only.

Implemented-by: claude/project-thread-5h4pzn
Reviewed-by: session_01B2EeooonSXrANpkpgUSfFZ

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:spec seat 2 (#18549) · os-bill · session session_01B2EeooonSXrANpkpgUSfFZ · 2026-10-22T12:03Z · head 488c2fe49d4fbb2786a141e2628bc6e99ffbf402

  1. Shape: draft, base main, one closing keyword (Fixes #25168, line 4). check-governed-merges.mjs --pr 25192: 0 of 12 paths governed, 726 changed lines ≤ 3000, so ordinary queue landing. mergeable_state: clean.
  2. Premise measured: zero producers in-repo and in HotCRM (the dev's census; the review re-swept the tree: only the showcase datasource, schemaMode: 'external'). The route is triage's 6333828054: a spec declaration rule, with no second check in objectql.
  3. Gates (check-runs on this head): 35 success, 7 path-skipped, 0 failed or pending. Local: 104 dispatch-gates families accounted for, 104 run (--ran: 0 NOT-MEASURED, 0 UNRUN); check:generated 14/14; dogfood on this head, VERDICT exit 0 (2395 passed, 18 skipped); ablation red then green.
  4. Contract review: PASS at CONTRACT_REVIEW_TIER on this head, record 6340232647.
  5. Accepted deviations: this is a card thread of this seat on its own designated branch claude/project-thread-5h4pzn, as claim 6335118058 names it. The thread posted the claim from the seat's text. Other deviations are as listed in report 6339972223 and accepted by the review.
  6. Prose faces (the seat's own read): the changeset (minor, BREAKING, FROM → TO, the one-line fix, both declaration lines, one ADR-0087 registered marker); the new step-18 entry; the predicate's docblock. Each states the save-door gap honestly. Each matches the code at this head.
  7. Out of scope: the save-door half, the reverse direction, the composeStacks pass and the docs line are filed as 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 (the dev's option A, which the review endorsed). The review's note that ADR-0015 §4.2's "not in the Zod schema" sentence is now imprecise goes to the maintainer: an ADR amendment is Tier H, by his hand.
  8. Landing: pr_ready + automerge_enable through the relay now. The seat follows it to MERGED.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 22, 2026 12:05
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 22, 2026 12:05
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 22, 2026
Merged via the queue into main with commit fe0ec2b Oct 22, 2026
43 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-5h4pzn branch October 22, 2026 12:42
os-bill pushed a commit that referenced this pull request Oct 22, 2026
… composeStacks, with docs and changeset (#25211)

- the object and datasource save doors, draft and publish, force, the
  publish re-judge, and the controls
- composeStacks: the cross-input pair in both orders under last and preserve
- #25192's harness gains the registry read the object door now makes
- docs: the write example names its datasource; section 2 states the rule
- api-surface and export-origins rows for the exported predicate

Claude-Session: https://claude.ai/code/session_01QVYg9FaRCUgSV13upGJSZQ
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 protocol:data size/l tests tooling

Projects

None yet

2 participants