Skip to content

fix(objectql): a connector a composed stack declares only on its top level, beside package bodies, lends its credential at boot - #28046

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-r6cu87
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-r6cu87

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by 田騏 · project thread

Fixes #28032

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么: 合成栈里,某个输入带 packages[] 包体、又只在顶层声明了一个连接器实例(没有包体声明同名)时,ObjectQL 的 manifest 服务现在会把这个"残留"连接器注册进 ObjectQL 注册表,归属栈自己的 manifest.id,和残留对象(ADR-0130 D4)已有的做法一样。
  • 为什么改: 凭据出借方和连接器 reconcile 只读 ObjectQL 注册表。实测修复前:构建期 composeStacks(…, { manifest: 'preserve' }) 接受这个借用,开机时数据源借用却被拒,报"is not declared in this stack's connectors"。修复后开机即可借到。
  • 风险与回滚: 只多注册一种原本漏掉的残留项;包体重复声明的连接器、单栈、常规合成都不变。不改 spec,不改作者能写什么。回滚即 revert。
  • 需要您做的: 无(非治理面,合入走常规流程)。

What changes

  • packages/objectql/src/plugin.ts. A new unclaimedTopLevelConnectors(stack) reads the residual rule's own answer (unclaimedTopLevel, @objectstack/metadata) for connector items, as unclaimedTopLevelObjects does for objects. The manifest service registers them after every body and after the residual objects, under the residual rule's id, through registry.registerItem('connector', …), the verb a body's connectors take. Each is stamped with the stack's version on a shallow copy, so the artifact's own entry is left as it arrived (the residual-object precedent). The "empty packages: []" warning no longer fires for a stack whose only residual is a connector.
  • packages/services/service-automation/src/connector-credential-residual-connector.test.ts (new). A real kernel (ObjectKernel, ObjectQLPlugin over SQLite, AutomationServicePlugin) boots composeStacks([mixed(instance), looseBorrower], { manifest: 'preserve' }) through the manifest service and borrows through CONNECTOR_CREDENTIAL_SOURCE_SERVICE. Pins: the composed check accepts the shape; the residual instance lends at boot and is owned by the stack's id; a residual descriptor (no provider) is refused at boot as a descriptor (declares no provider), not as undeclared; CONTROL: an instance a package body declares lends, owned by the body.
  • packages/objectql/src/manifest-service-residual-objects.test.ts. Three engine-half pins: a residual connector registers under the stack's id and version beside a body's own; it registers when the stack has no residual object; CONTROL: a top-level connector a body repeats is the body's, registered once.
  • .changeset/28032-residual-connector-lends-at-boot.md. @objectstack/objectql patch.

Why

The measurement (before the fix, at origin/main 13d85bb)

The new service-automation test, run against the unfixed @objectstack/objectql build:

  • registry.listItems('connector') was [] after boot, for the residual instance and the residual descriptor alike.
  • source.borrow('sap_s4') was refused: "connector instance 'sap_s4' (named by this datasource's external.connector) is not declared in this stack's connectors."
  • The CONTROL (the instance declared in a package body) lent, as did the composed check.

So the card's premise held: the build accepts a borrow the boot refuses. The reload half ("lends after a reload") was read off captureReloadedConnectors, not measured.

Why option (a)

Option (a) makes the boot agree with what the composed check already accepts, and changes nothing an author can write. The residual rule already decides which items are residual and under which id, and the engine already registers that answer's objects; connectors are the second type a boot-time reader takes from the registry alone. Option (b) (refuse the borrow in stack.zod.ts) would narrow the accept set and was not claimed.

Verification

  • connector-credential-residual-connector.test.ts: red before the fix (2 of 4: both residual cases), green after (4 of 4), against a rebuilt @objectstack/objectql.
  • manifest-service-residual-objects.test.ts: 12 passed. Reverse check from the committed fix: restoring plugin.ts from the parent commit fails exactly the two residual-connector pins; the CONTROL stays green.
  • pnpm --filter @objectstack/objectql typecheck and pnpm --filter @objectstack/service-automation typecheck: exit 0. Full suites: @objectstack/objectql 493 files, 9595 passed; @objectstack/service-automation 224 files, 2845 passed (1 expected fail).
  • dispatch-gates --commands derived 63 families; all run with exit codes recorded (after building ./packages/**). One red on the first pass: check:slot-lookup (an untyped getService<any>('automation') in the new test), fixed in 3ab79536. --ran: 63 derived, 63 run, 0 NOT-MEASURED (a derived zero). origin/main merged at a5d4ed3f (5 commits; cc5586da touched the lender's plugin.ts); the derived list is unchanged after the merge, and the borrow and lender tests re-run green on the merge (21 passed).

Acceptance notes

  • Other residual types. The manifest service still registers only the residual's objects and, now, connectors. Other registry-read collections a residual can carry (flows, permissions and the rest of METADATA_ARRAY_KEYS) are still metadata-service only. Not measured and not widened here; named in the os-dev-report for the PM.
  • Both doors. The test boots the config door's hand-off to the manifest service. The artifact door hands the engine the same stack through the same service, so the fix covers both; the artifact door was not booted separately.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (Not touched.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran (pnpm lint is CI's).
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: not touched.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 block is the repo's own Chinese reader section).
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_017ihL2dhraZ7BwSanQPGEAb

…ends at boot

The manifest service registered a multi-package stack's residual objects
under the stack's manifest id, but not its connectors. The credential lender
and the connector reconcile read connectors from the ObjectQL registry only,
so a connector instance declared only on the top level of a mixed composed
input was refused as undeclared at boot, although the composed check accepts
a borrow of it. The residual's connectors now register the way its objects
do: after every body, under the residual rule's id, on a stamped copy.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2dhraZ7BwSanQPGEAb
…connector borrow test

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2dhraZ7BwSanQPGEAb
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/data-modeling/objects.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/kernel/services.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/permissions/authentication.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/plugins/packages.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/protocol/kernel/index.mdx (via ObjectQLPlugin (symbol, a top-level class))

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

  • content/docs/releases/v17/17-0.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/releases/v17/17-1.mdx (via ObjectQLPlugin (symbol, a top-level class))

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
  • the SDK route bridge reached 58 of 210 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 — 20 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 5aee206543c87fab6c555d5fdfc19b9d29c428b2 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 5aee206543c87fab6c555d5fdfc19b9d29c428b2

⚠️ 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 5aee206543c87fab6c555d5fdfc19b9d29c428b2 → 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

epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
ACCEPT · PR #28046 @ a5d4ed3f · 2026-10-30T10:58Z

维护者速读

  • 改了什么: 多包合成栈里,有些连接器实例只在顶层声明、没有包体声明,这叫「残留」。现在 ObjectQL 的 manifest 服务会把这些残留连接器注册进引擎注册表,用合成栈自己的 id,做法与残留对象相同。以前它们只进元数据服务,凭据出借方开机时看不到,数据源借用就会被当成「未声明」拒收,要等一次开发态重载才能借到。
  • 为什么改: fix(spec): composeStacks says why a top-level connector beside a package body's same-named entry is ambiguous #28026 的契约复核读出这个问题,开发实测复现:注册表里没有这个连接器,借用返回 422。构建期的合成检查放行,运行时却拒收,两者不一致。
  • 风险与回滚: 只补注册,不改 spec,也不改作者能写什么(Clause-②: no)。残留的定义就是没有包体声明的名字,所以不会遮盖包体条目。单包栈和顶层重复包体的栈不受影响。还原即可回滚。
  • 先看: packages/objectql/src/plugin.ts 的 unclaimedTopLevelConnectors 和注册处。
  • 两分钟验证: 在 @objectstack/service-automation 跑 connector-credential-residual-connector.test.ts:真内核开机后能借到凭据,修复前 2 个失败。
  • 需要你做的: 无,席位落地。

席位意见:ACCEPT。不碰契约四面:Clause-②: no,没有 packages/spec/src/** 改动,也不是 governed 路径。按 contract-review.md,这种情况的复核记录就是 CI 加席位自读,本条即席位自读。无 must-fix。

席位核对

  1. Shape: +297/−1 across 4 files. plugin.ts gets unclaimedTopLevelConnectors and the registration after the bodies (registry.registerItem('connector', …, ownerId), version-stamped on a copy, as residual objects are). Also an engine pin test, a real-kernel borrow test, and an @objectstack/objectql patch changeset.
  2. Premise measured: at 13d85bb3, unfixed, a real kernel booted the card's preserve shape with registry.listItems('connector') returning [], and the borrow was refused with 422 "not declared". The body-declared control lent (os-dev-report 6520658127). With the fix, the new kernel test goes from 2/4 to 4/4.
  3. Gate check-runs on a5d4ed3f: 48 runs: 35 success, 13 skipped, 0 failed. Local: 63 derived gates, all exit 0.
  4. Seat read: the reader is the same unclaimedTopLevel rule the metadata door uses, so it cannot claim an entry a body declares. Two residual entries of one name share the composed id's slot, and the registry keeps the last. That is the case fix(spec): composeStacks says why a top-level connector beside a package body's same-named entry is ambiguous #28026's third ambiguity sentence refuses, and that sentence still holds, because neither entry registers as a package body's.
  5. Out of scope, routed: other residual registry collections still reach only the metadata service. Measured for flows (getFlow returns null) and filed as A flow a composed stack declares only on its top level, beside package bodies, is never registered with the automation engine at boot — the manifest service registers the residual's objects and connectors only #28048 (finding).
  6. Landing: not governed (check-governed-merges --pr 28046; 298 lines ≤ 3000). No contract face is owed. The seat lands it through the relay.

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/m tests tooling

Projects

None yet

2 participants