Skip to content

feat(service-automation)!: two packages of two namespaces may each declare a connector of the same name (ADR-0150 child B, the narrowing) - #33431

Draft
objectstack-fleet[bot] wants to merge 1 commit into
mainfrom
claude/project-thread-y6vjs1
Draft

objectstack-fleet[bot] wants to merge 1 commit into
mainfrom
claude/project-thread-y6vjs1

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

domain:server#7 · os-tesla · session_01L9qibzKCovWpJoMZD6U3NT

Requested by Tesla · project thread
Fixes #31699

Clause-②: yes (narrowing)
Metadata-protocol: added

维护者速读

  • 改了什么:连接器(objectql: narrow the phase-1 one-holder refusal from environment to namespace for webhook, connector and email_template (ADR-0150 child B, webhook family) #31699 的最后一个类型)把「一个名字一个包」收窄到「一个命名空间一个」。两个不同命名空间的包可以各自声明同名连接器,各自物化、连接、派发。
  • 怎么做到的:connector 加入 core / objectql / metadata 三个命名空间类型集合;协调器拆掉一持有者兜底,物化器的各个表和 OAuth 授权句柄按 (包, 名) 建键;引擎对每个调用方(包括无包的调用方)都问解析器,解析到的持有者没有实例时回答 none,只有插件代码注册的连接器例外;拉取按映射所在的包解析连接器;带命名空间地址的连接门(/automation/connectors/:ns/:name/connection)连接该命名空间那个包的实例。
  • 仍然拒绝:同一命名空间的共有者、没有命名空间的包与他包同名、一个包内重名、两个持有者下没有自己包戳的条目(点名两个持有者,谁都不物化)。没有包作用域注册表的组合仍然一个名字一个实例。
  • 答复变化:changeset 的 Clause-② 列了十条 FROM → TO,标为 BREAKING。单持有者部署只变三处:3b 的回退分支(解析到的包没有实例时不再用名字的唯一实例,插件代码注册的除外)、只有停用包持有的名字(停用包以外的调用方不再派发;停用包自己的流程照旧读自己的实例)、无包戳条目换持有者后到下次协调之间不派发。凭据列表、重载合并、惰性描述符审计在单持有者下都与 main 相同。
  • 与交接清单的偏差:交接 6871869590 写的是裸连接门和 GET /connectors 加 ?package=。本 PR 用已有的命名空间地址代替第二种写法,GET /connectors 不加过滤,每个描述符带 packageId。认领修订 6882996364 事先说明了这一点。
  • 先看:packages/services/service-automation/src/engine.ts(resolveConnectorInstanceFor)、src/plugin.ts(协调器的期望集合与 judgedByPackage、connectionDoorSubject)、src/connector-pull.ts(第 2 步)、packages/runtime/src/domains/automation.ts(准入块)。

What changes

  • Type sets. connector joins NAMESPACE_SCOPED_BARE_NAME_TYPES (core), PACKAGE_SCOPED_READ_TYPES (objectql) and PACKAGE_SLOTTED_STORE_TYPES (metadata). Every covered type has now narrowed.
  • Reconcile (plugin.ts):
    • The backstop is gone (refusedDeclaredConnectorNames, and refusedConnectorNames in connector-package-scope.ts).
    • Each entry is the instance of its own package. An entry with no package of its own is the instance of the package its name resolves to over every holder. Under two holders it is refused naming both and materialized as neither (connectorInstanceRefusal).
    • desired, the materialized, degraded and credential-refusal maps, retry, dematerialize and grantHandles are keyed by instance (connectorInstanceKey: name, or PACKAGE/NAME).
    • A name repeated within one package is a duplicate naming the package. A composition with no package-scoped registry keeps one instance per name (judgedByPackage, red line 1).
    • A reloaded artifact (finding: dev metadata reload does not re-ingest connector items — reconcileDeclaredConnectors reads a registry the artifact reload never refreshes for connectors #7742) supersedes the registry's entries by (package, name): an unstamped artifact item is stamped with its manifest id at capture, so a reload of one package leaves the other package's instance of the name live. With no manifest id, or no package-scoped registry, it supersedes by name, as before.
    • An OAuth2 grant handle reads its instance's fixed package. The inert-descriptor audit judges each entry by the instance a caller of its package would be served. Instance log lines name the package.
  • Dispatch (engine.ts, resolveConnectorInstanceFor):
    • Every caller asks the resolver over the served holders, a caller with no package included, so a two-holder name is refused naming both rather than answered none.
    • A resolved holder with no instance answers none, unless the name has a connector a plugin registers in code (package null, origin plugin), which is served as before. That keeps a package's catalog descriptor plus a plugin's connector working.
    • A name only disabled packages hold answers the same way (notes 6816295828 and 6820178514).
    • getConnectorDescriptors sets packageId.
  • Pull (connector-pull.ts): the mapping's connector is resolved from the mapping's holder package through the registry's optional resolveConnectorInstanceFor, and its origin, degraded state, provider (materializedProviderOf), action and inputSchema are read from that instance. New reason connector_name_refused (409).
  • Connection door: ConnectorConnectionRequest.package (spec, optional). The runtime admits a namespace address of a two-holder connector name to the addressed holder (admitAutomationAddress answers a slot, as for a flow) and passes the package. The bare door keeps its 409.
  • Credential listing: each declaring entry is matched to its own instance's state, by the instance's package (an environment row or a compatibility entry is its holder's instance); the reported packageId is the entry's stamp, as on main.
  • feat(service-automation): the connector connection store, the connect door and the OAuth callback read and write one instance by its package and name #33218 review N6: judged, not restamped. A row a moved declaration left stamped with its old package is that package's own again once it declares the name beside the new holder. Pinned.
  • Docs: content/docs/automation/connectors.mdx gains "Two packages, one connector name" and two failure-mode rows. The objectql, core and metadata module docs say the connector narrowed.

Verification

  • New pins:
    • connector-connection-door.test.ts (7): both instances materialize; the bare door 409 and the addressed begin, read and revoke per package, each with its own row and token; revoke one, the other keeps serving; an unowned entry under two holders refused; a duplicate within a package; the no-registry CONTROL; N6.
    • flow-package-scoped-resolution.test.ts: the narrowing none pin (FROM the fallback arm); the plugin catalog CONTROL; the disabled holder in both registration orders (A's caller never served B's or A's instance; a caller with no package served B; CONTROL both enabled) and a lone disabled holder; the null caller refused naming both; the no-registry CONTROL.
    • connector-pull.test.ts (4) and connector-pull-service-door.test.ts (1): each package's mapping pulls its own instance; the provider read per instance; holder with no instance is connector_not_registered; an environment mapping over two holders is 409 connector_name_refused.
    • connector-materialization.test.ts: two holders of different namespaces both materialize (FROM the boot refused); a reload of one holder's artifact leaves the other's instance live; the credential listing reports each of two instances' states, and a sys_metadata or __runtime__ entry over a one-holder name reads the holder's state.
    • connector-connection-door.test.ts (review round): a disabled holder at the bare door and at its own namespace address (404, nothing written), both orders; a revoked instance is held degraded; the unstamped entry's handle reads its fixed package.
    • flow-package-scoped-resolution.test.ts (review round): a caller inside the disabled package reads its own instance (both orders, and alone); two catalog holders beside a plugin connector (each package served the plugin's, the null caller refused naming both).
    • connector-pull.test.ts (review round): one of two holders disabled, the environment's mapping pulls through the served holder.
    • connector-descriptor-audit.test.ts: a package's catalog entry is not silenced by another package's live instance of its name.
    • runtime automation-addressed-route.test.ts: each namespace address reads its own package's instance (FROM 409).
    • marketplace-install-local-bare-name-conflict.test.ts: the refusal case is now a same-namespace co-owner; a cross-namespace connector install is admitted (FROM 422).
  • Updated pins: the one-holder type-set pins in core, objectql and metadata; the CLI stored-references report (no sealed clash left).
  • Ablations A1 to A14 (scripts/ablation-replace.mjs, each restored with an empty git diff HEAD), each red on its pins: the fallback arm back (3), the disabled branch off (1), the null caller not asking the resolver (8), no plugin instance (1), the unowned entry not refused (1), no registry judged (6), grant handles by name (5), the pull by bare name (4), the door ignoring the package (3), the provider by name (1), no connector slot at the runtime address (1); the reload merge by bare name (1, A12); the credential listing by stamp (1, A13); the inert audit by name (1, A14). Record: server-seat-7/r2026-11-14/31699-narrow-ablations.txt.
  • Full suites before the rebase: service-automation 3923, core 3480, objectql 12967, metadata 2479, metadata-protocol 22848 (43 skipped), cloud-connection 1207. After the rebase on df9fe946: service-automation 3941, runtime 12060 (19 skipped), core 3480, objectql 12976, metadata 2479, metadata-protocol 22848 (43 skipped), cloud-connection 1210, CLI stored-references 13. All passed. Rebased again on 4ace1fa9 over fix(metadata-protocol)!: history and rollback doors refuse a bare two-holder name and address one holder by ?package= #33193 (head 1530146d): metadata 2490, metadata-protocol 22865 (43 skipped), service-automation 3941, runtime 12074 (19 skipped), spec 28422 (1 todo), spec check:generated green.
  • Gates: tsc --noEmit (service-automation, runtime), ESLint on every changed file, tenant-audit, system-context, route-ledger and engine-double censuses, changeset no-major / empty / fixed, doc-authoring, doc-anchors, spec check:generated (its test-typecheck leg needs a larger Node heap here: NODE_OPTIONS=--max-old-space-size=12288; then OK, no debt moved). dispatch-gates --ran: 124 derived families accounted for, 122 run green, 2 NOT-MEASURED for prerequisites this checkout lacks and the diff does not touch: check:dual-build-cjs-loads (no studio / embedder-openai dist) and the check-plugin-teardown-shape self-test (shallow clone). check:dts-closure was red once in the sweep and green when re-run alone (311/311 declaration files present).

Contract review

An isolated contract reviewer read the card, ADR-0150, the handoff and the diff with no anchor on mine.

  • r1 on 4a9d32b9: PASS-WITH-CHANGES. Two must-fix items: M1, the credential listing lost an environment row's state; M2, a reload of one package's artifact tore down the other package's instance. Five should-fix items (S1 to S5) and four nits.
  • Every item was taken: fixed in code (M1, M2, the nits), or declared in the changeset and docs with FROM and TO (S1 to S4), with pins for S5. The ablations were extended to A12 to A14.
  • delta-1 on de90aedb: PASS. Three notes; I acted on all three: no-registry reloads supersede by name as on main, a changeset clause, and a __runtime__ pin.
  • delta-2 on 510a8400: PASS.
  • delta-3 on e467615b, the rebase over fix(metadata-protocol)!: history and rollback doors refuse a bare two-holder name and address one holder by ?package= #33193: PASS. connector joins PACKAGE_SLOTTED_METADATA_TYPES (@objectstack/spec), the one list the history and rollback doors and the metadata store read; the changeset says what moves on those doors.

The brief, the r1 record and the delta record are posted as comments on this PR.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (Two optional members on TypeScript interfaces; no schema.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes on the changed files.
  • 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: check:generated is green for its artifacts.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01L9qibzKCovWpJoMZD6U3NT

…clare a connector of the same name, and a flow, a pull and the connection door address one by package

The connector, the last type of ADR-0150 child B, narrows its one-holder
rule to the namespace: it joins the namespace-scoped type sets (core,
objectql, metadata), the declared-connector reconcile materializes each
package's instance under its package (the backstop is lifted), the engine
resolves a bare name for every caller over the served holders, a pull
resolves its connector from the mapping's package, and the namespace
connection door connects the addressed package's instance.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibzKCovWpJoMZD6U3NT
@os-tesla
os-tesla force-pushed the claude/project-thread-y6vjs1 branch from 510a840 to 1530146 Compare November 14, 2026 21:22
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 7 package(s): @objectstack/core, @objectstack/metadata-protocol, @objectstack/metadata, @objectstack/objectql, @objectstack/runtime, @objectstack/service-automation, @objectstack/spec, touching 68 documentable anchor(s). ⚠️ 4 changed file(s) yielded no anchor (packages/metadata/src/package-slotted-store.ts, packages/objectql/src/bare-name-one-holder.ts, packages/services/service-automation/src/connector-instances.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

33 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json ba35dfd78283d5f839eaaaab24764322ae3e263a.

⛔ 15 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 4 changed file(s) yielded no anchor (packages/metadata/src/package-slotted-store.ts, packages/objectql/src/bare-name-one-holder.ts, packages/services/service-automation/src/connector-instances.ts, …) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 65 of 247 client-bound route-ledger rows — the other 182 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 182: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 123 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 — 186 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 ba35dfd78283d5f839eaaaab24764322ae3e263a → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json ba35dfd78283d5f839eaaaab24764322ae3e263a

⚠️ 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 ba35dfd78283d5f839eaaaab24764322ae3e263a → 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

domain:server#7 · os-tesla · session_01L9qibzKCovWpJoMZD6U3NT

Contract review brief — #31699 narrowing PR (connector, last PR of ADR-0150 child B)

Reviewer: an isolated agent with no part in writing the change. Read the code, not the author's summary, and judge it yourself.

What to read

  • The change: git -C /home/user/objectstack-issue-31699 show 4a9d32b9 (one commit on origin/main df9fe94). Read every hunk, and the surrounding code where a hunk's correctness depends on it.
  • The changeset in that commit: .changeset/31699-connector-names-per-namespace.md. Its Clause-② line must list every answer that moves for a caller that worked before, each with FROM and TO.
  • The PR body draft: /mnt/project-files/server-seat-7/r2026-11-14/31699-narrow-body.md.
  • The card's requirements: GitHub issue objectql: narrow the phase-1 one-holder refusal from environment to namespace for webhook, connector and email_template (ADR-0150 child B, webhook family) #31699, comments 6871869590 (handover, "The narrowing PR" list), 6879929803 (addendum), 6816295828 and 6820178514 (a disabled holder at the connector dispatch door: "resolve and count over one holder set, the served set"), 6882996364 (this PR's file surface and its one stated deviation).
  • ADR-0150 (docs/adr/0150-*.md in the worktree), especially D2, D3, D4, D9, D10 and the epic red lines:
    1. Resolution never loosens: no caller is served an item it could not reach before, and no other package's body is served or sent.
    2. Ambiguity is reported, never guessed: two candidates are refused naming both, never one picked by order.
    3. Each PR is independently releasable: a one-holder deployment's answers do not move unless the changeset declares it.
  • The precedent narrowing for webhook (merged as e55347d, changeset .changeset/31699-webhook-names-per-namespace.md in git history) for the expected shape of the declaration.

What to judge

  1. Red lines 1 to 3 across every reader that names a connector: the reconcile (materialization, refusal of an entry with no package of its own, duplicate checks, the no-registry composition), the engine's resolveConnectorInstanceFor (every arm, including the plugin code-registered exception and the disabled-only arm), the connector_action dispatch, the connector pull (connector resolved from the mapping's package; provider, origin, degraded state, handler and inputSchema read from that one instance), the connection door (bare and namespace address; request.package; the recorded package of a begun connect), the OAuth2 grant handles keyed by instance, the credential listing, descriptors.
  2. A disabled holder, in both registration orders, at every door that serves or dispatches.
  3. Whether anything still keyed by bare name can mix two packages' instances (maps, caches, log keys, the materializer's retry timers, the credential-refusal map, grantHandles).
  4. The completeness and truth of the changeset's FROM → TO list and its one-holder sentence, against the code.
  5. Whether the tests pin what they claim (look for vacuous pins), and whether an important path has no pin.
  6. The stated deviation (namespace address instead of ?package=): acceptable or not, and why.

Output

Write the report to /mnt/project-files/server-seat-7/r2026-11-14/31699-narrow-review-r1.md and return it. Format:

  • Verdict: PASS, PASS-WITH-CHANGES, or FAIL.
  • Must-fix (blocks landing), should-fix, nits: each with file:line, the concrete failing scenario (inputs → wrong answer), and the fix.
  • A short Clause-② judgement: is yes (narrowing) right, and is every moved answer listed.

Do not edit the worktree, push, or post anywhere. Running tests read-only is fine (cd packages/services/service-automation && npx vitest run <file>), but do not leave files changed.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#7 · os-tesla · session_01L9qibzKCovWpJoMZD6U3NT

Contract review r1 — #31699 narrowing PR (connector), commit 4a9d32b9

Reviewer: isolated agent, no part in the change. Read: every hunk of 4a9d32b9, the changeset, the PR body draft, issue comments 6871869590, 6879929803, 6816295828, 6820178514, 6882996364, and the surrounding code the hunks depend on. Five claims below were measured with probe tests run from a scratch directory against the worktree's source (nothing in the worktree was written; git status is clean at 4a9d32b9). Each finding says whether it was run or only read.

Verdict: PASS-WITH-CHANGES

Red lines 1 and 2 hold on every path I traced: I found no caller served or sent another package's instance, and no instance picked by order. Two defects block landing: one answer moves for a one-holder deployment without being declared (red line 3), and one bare-name key still mixes two packages' instances.

Must-fix

M1. The credential listing loses state and degradedReason for a one-holder instance whose entry carries a sentinel stamp (red line 3, undeclared)

  • Where: packages/services/service-automation/src/plugin.ts:5159-5163 (declaredCredentialRefs keeps the raw _packageId) and plugin.ts:5196-5200 (descriptorOf matches d.packageId === c.packageId).
  • Scenario (run): one declared provider-bound entry billing with _packageId: 'sys_metadata' (the environment's stored row) or '__runtime__' (the compatibility default), credential missing. The instance registers under the package connectorInstancePackage gives it (none, or the name's holder), so its descriptor carries no packageId, or the holder's.
    • FROM (main): listConnectorCredentialRefs() answers connectors: [{ name: 'billing', packageId: 'sys_metadata', state: 'degraded', degradedReason: … }] (the descriptor was found by name).
    • TO (this commit, measured): connectors: [{ name: 'billing', packageId: 'sys_metadata' }]. Same for '__runtime__'.
  • This is a one-holder answer that moves, and the changeset's one-holder sentence does not list it.
  • Fix: look the descriptor up by the instance's package, not the raw stamp: connectorInstancePackage(resolve, entry) (or at least declaredEntryPackage), null matching a descriptor with no packageId. Add a pin with a sys_metadata entry, and one with two packages' instances in different states (the changeset claims the per-instance listing and nothing pins it).

M2. The reload merge supersedes by bare name, so a reload of one package's artifact tears down another package's instance of the name

  • Where: plugin.ts:1329-1335 (mergeDeclaredConnectorSources): a registry entry is dropped when declaredNames.has(name), whatever its package.
  • Scenario (the merge was run; the consequences below are read from the reconcile): host package com.acme.host and installed package com.acme.beta (another namespace) each declare a provider-bound billing; both are materialized. A metadata:reloaded payload of the host's artifact carries connectors: [billing].
    • Stamped payload item: the merge returns only the host's entry. com.acme.beta/billing is no longer desired, so the reconcile dematerializes it, and reloadedConnectors persists, so every later reconcile and the connection door (readDeclaredConnectorItems) omit it until restart. Beta's flows fail "no handler", and its namespace door answers 404.
    • Unstamped payload item (the existing control at connector-materialization.test.ts:1115 shows this shape is supported): the merge returns one entry with no package of its own. Under two holders connectorInstanceRefusal refuses it, so both instances are gone after the reload.
  • It fails closed (nothing is served to the wrong caller), but it is exactly the bare-name mixing the brief asks about, and the two-holder state is opened by this PR.
  • Fix: supersede by (package, name): drop a registry entry only when the artifact re-declares its name under the same package, or under one of owners for an unstamped item; treat an unstamped artifact item as its manifest's package (owners) rather than as an entry with no package. Pin: two holders, reload of one package's artifact, the other's instance still dispatches.

Should-fix

S1. A caller inside the disabled package is still served its own instance; the changeset, docblock, docs and one test title say "no caller"

  • Where: engine.ts:5946-5959. The registry keeps the caller's own holding in the served set (objectql/src/registry.ts:9535-9537), so resolver(name, A) is resolved (own) for a disabled A, and the resolved arm returns A's instance while it is registered.
  • Scenario (run): A and B hold billing, both instances registered, A disabled: resolveConnectorInstanceFor('billing', A) is { status: 'instance', packageId: A }. Same with A the lone holder.
  • This is main's answer, and it is the package's own body, so no red line breaks. But it is the exact case note 6816295828 names ("a flow of a disabled package"), and nothing pins it: the tests at flow-package-scoped-resolution.test.ts:932 and :961 use P (a dependent of A) as "A's own caller", and the second is titled "served to no caller".
  • Not true as written: changeset item (7) "for every caller"; the docblock "a disabled package's instance is never served"; the docs line "its instance is never served".
  • Fix: either answer codeRegisteredInstance when the resolved package holder is disabled (then every statement is true), or keep the answer, say so in the three places, and pin caller = A in both registration orders.

S2. Changeset item (5) gives the wrong FROM for catalog holders beside a plugin's connector

  • Scenario (run): A and B each declare a catalog entry slack (no provider), a plugin registers slack in code. resolveConnectorInstanceFor('slack', null) is refused; for caller A it is the plugin's instance.
  • FROM (main, a boot-sealed clash): an environment-authored flow dispatched the plugin's connector (the one instance of the name). TO: refused naming both. Item (5) says FROM "not dispatched as not registered".
  • Fix: add the row to the changeset. Or decide that a name whose every candidate has no instance of its own answers the plugin's connector for a caller with no package too, since every package caller already gets it.

S3. Holders the namespace address cannot tell apart are now materialized, and no HTTP door reaches them

  • The reconcile no longer refuses two stamped entries of one name, whatever their namespaces (plugin.ts:3402-3432). The install door still refuses a same-namespace co-owner and a namespace-less package, but a cold boot only warns (BARE_NAME_COLD_BOOT_MODE 'warn').
  • Scenario (read, not run): two code-declared packages of one namespace both declare provider-bound crm with authorization_code. FROM: boot refused naming both. TO: both materialized; the bare door is 409, and /connectors/NS/crm/connection is 409 ambiguous-in-namespace (automation-address.ts:300-302). Neither can be connected over HTTP. ?package= would have reached them.
  • The docs say such holders "still may not" coexist, which is true only of the install door.
  • Fix: either keep the reconcile's refusal for holders the namespace rule does not admit (the boot answer then does not move for them), or state in the changeset and docs that they materialize and are connectable only in process (request.package).

S4. "The reconcile drops a disabled package's instance on its next run" does not cover an entry with no package of its own (read, not run)

  • connectorInstancePackage judges over every holder (includeDisabledPackages), and listItems hides only items stamped with the disabled package. An unstamped entry whose name only a disabled package D holds stays declared, is registered under D on every reconcile, and the disabled arm answers none for every caller, permanently rather than "until the next reconcile".
  • FROM: a caller with no package dispatched it (the one instance of the name). The changeset's (7) and its BREAKING paragraph describe this as transient.
  • Fix: word (7) for this case, or have the reconcile not materialize an entry whose instance package is disabled.

S5. Missing pins on paths the brief names

  • A disabled holder at the connection door under two holders: the namespace address of the disabled package (read: slot A, then 404 because A's entry is not listed), and the service's bare door (read: resolves to the enabled holder). Neither is pinned in either registration order.
  • A pull whose connector has two holders, one disabled.
  • The credential listing with two instances (see M1), and the reload with two holders (see M2).

Nits

  • plugin.ts:4271: the grant handle is keyed by the instance but reads its connection by a package re-resolved on every read. For an unstamped entry, between a second holder's install and the next reconcile, the handle of P/name reads as package null. It fails closed (read: not connected). Pass the instance's fixed packageId.
  • connector-connection-door.test.ts:1265: if (revoked) await expect(...) asserts nothing when the handler is absent. Assert the state explicitly (not connected or degraded for CRM).
  • plugin.ts:3177: the inert-descriptor audit builds live by name, so package A's catalog entry is silenced by package B's live instance of the name.
  • plugin.ts:3558 and the connect, revoke and grant-refusal log lines name the instance without its package (inPackage exists).
  • PR body: RESULTS_AFTER_REBASE, GATES_LINE and REVIEW_LINE are unfilled, and "单持有者部署只变三处" needs M1's outcome.

What I checked and found sound

  • Reconcile: each stamped entry is its own instance; an entry with no package under two holders is refused naming both and registered as neither; a duplicate within a package is refused; the no-registry composition keeps one instance per name. desired, materialized, degraded, credential-refusal attempts and grantHandles are keyed by instance; unregister is by (name, package). The retry timer is one global timer over all degraded instances, and credentialRefusals is keyed by credential ref: neither mixes instances.
  • Engine: refused is passed through; resolved reads exactly the holder's instance; the plugin exception cannot return a declarative instance (assertSameOriginOrFree keeps the two origins off one name); the no-resolver and not-found arms answer the sole instance or none.
  • connector_action: handler, degraded reason, hold, effect and inputSchema are all read by the resolved package.
  • Pull: origin, degraded state, provider, handler and inputSchema are read from the one resolved instance; refused is 409 naming both; none is connector_not_registered. The four pins are not vacuous.
  • Connection door: the recorded package wins over the addressed one; the addressed package is read exactly; grantHandles seed and forget by instance; the two-instance test proves two rows and two distinct tokens. N6 is judged and pinned.
  • Disabled holder in both orders for a dependent caller and for a caller with no package: pinned and correct.

The deviation (namespace address instead of ?package=)

Acceptable. The address already exists (child G1), it is the ADR's D5 spelling, and for the state D9 admits (two holders of different namespaces) it names exactly one holder; a second spelling would add a door to keep consistent. GET /connectors unfiltered with packageId on each descriptor is additive. Its one cost is S3: holders that share a namespace or have none cannot be addressed, so the choice should come with a decision on S3.

Clause-② judgement

yes (narrowing) is right. The FROM → TO list is not complete or fully true:

  • Missing: M1 (a one-holder answer, so the one-holder sentence is also incomplete).
  • Wrong FROM: (5) for catalog holders beside a plugin's connector (S2).
  • Overstated: (7) "for every caller" (S1), and the "next run" claim in the BREAKING paragraph (S4).
  • Items (1) to (4), (6), (8), (9) and (10) match the code as I read it.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#7 · os-tesla · session_01L9qibzKCovWpJoMZD6U3NT

Contract review, delta after r1 — #31699 narrowing PR (connector), head de90aedb

Scope: git diff 4a9d32b9 de90aedb only (9 files; code changes in service-automation/src/plugin.ts, a docblock in engine.ts, tests, changeset, docs), judged against the r1 findings and for anything the delta itself breaks. Worktree not modified (git status clean at de90aedb).

Verdict: PASS

No must-fix item remains, and the delta introduces none that I found.

What was run

  • The six touched or adjacent test files on de90aedb: connector-connection-door, connector-materialization, connector-descriptor-audit, connector-pull, flow-package-scoped-resolution, connector-credential-door: 329 passed, 0 failed.
  • My r1 probes again, from a scratch directory:
    • M1: an entry stamped sys_metadata with no package holder, and one stamped __runtime__, each with a missing credential. The listing now answers state: 'degraded' and the degradedReason for both, with the public packageId still the raw stamp (main's answer).
    • M2: mergeDeclaredConnectorSources with a host item stamped com.acme.host over registry entries of com.acme.host and com.acme.beta: both entries are returned, beta's kept.
  • I did not re-run the full service-automation suite or ablations A12 to A14; those are the author's figures.

Per finding

  • M1, closed. declaredCredentialRefs carries the instance's package and descriptorOf matches on it (plugin.ts, (d.packageId ?? null) === c.instance). The instance field does not reach the public answer (the mapping builds name, packageId, state, degradedReason explicitly). Dedupe by name and instance gives main's answer where two entries of one name are one instance. Both new pins assert state, not just presence.
  • M2, closed. The merge supersedes by (package, name), and the capture stamps an unstamped item with the manifest id. I checked that this id is the one the registry files the same items under: the residual rule uses manifest.id ?? stack.id (spec/src/stack.zod.ts:9035), the same expression as the capture, so a one-holder reload still replaces its own entry through owners. The pin proves one teardown (A's) and one materialization of B.
  • S1, closed by wording and pins. Main's answer is kept; the changeset (7), the engine docblock and the docs now say "a caller outside the package", and caller = A is pinned in both orders and as the lone holder.
  • S2, closed. Changeset (5) carries the plugin FROM; the new pin covers both package callers and the caller with no package.
  • S3, closed by declaration. Changeset (2) and the docs state that a pair the install door refuses is materialized and is connectable only in process. This is a decision recorded, not a refusal; I accept it as declared.
  • S4, closed by wording. Changeset (7), the BREAKING paragraph and the docs say the unstamped entry over a lone disabled holder is not dispatched until the package is enabled.
  • S5, closed. New pins: bare door and CRM-addressed door with CRM disabled, both orders (404, no row written); a pull with one holder disabled; plus the M1 and M2 pins.
  • Nits, closed. The grant handle takes the instance's fixed package (the rewritten 3b test pins that the registered instance still reads its own row and that the reconcile then materializes neither). The revoke test asserts state unconditionally. The audit judges each entry through resolveConnectorInstanceFor for its own package; I traced the one-holder cases (catalog entry beside a plugin connector, no plugin, no registry, an environment row) and each answers as main did. Log lines carry the package.

Notes, none blocking

  • An artifact with no manifest id and unstamped items still supersedes every entry of the name, and under two holders the reconcile then refuses that item, so both instances go on that reload. The docblock says so; the changeset bullet says only "an unstamped item is its manifest's". Worth one clause in the changeset.
  • In a composition whose registry offers no package-scoped reads, a reloaded item of package H beside a registry entry of package B under one name now leaves both declared, and the reconcile refuses the second as a duplicate (main: the artifact's superseded B's). Read, not run; it needs two packages on one name with no judging registry.
  • The M1 pin for sys_metadata covers the holder case; the no-holder and __runtime__ cases pass in my probe but have no pin of their own.
  • PR body: the three placeholders (RESULTS_AFTER_REBASE, GATES_LINE, REVIEW_LINE) and the one-holder count line still need filling against this head.

Clause-② judgement

yes (narrowing) stands. With the reworded (2), (5) and (7), the BREAKING paragraph and the new reload and listing bullet, every moved answer I found in r1 is now either fixed back to main's answer (M1) or declared with its FROM and TO.


delta-2 — head 510a840 (git diff de90aedb 510a8400)

Verdict: PASS

No must-fix item. Scope: this delta only (changeset, plugin.ts, one test).

  • Note 2, fixed. mergeDeclaredConnectorSources supersedes by (package, name) only with byPackage: true; otherwise by name, as main did. readDeclaredConnectorItems computes byPackage with packageScopedRegistryOf(ql.registry) !== undefined, the same predicate the reconcile uses for judgedByPackage, so the merge and the reconcile agree on when two instances of a name may live. The function's only caller is that one site, and the default is the main behaviour (false).
  • Note 1, declared. The changeset says an artifact with no manifest id, or a composition with no package-scoped registry, still supersedes by name.
  • Note 3, pinned. The M1 environment-row pin runs for sys_metadata and __runtime__.

Run by me on 510a840: connector-materialization.test.ts and connector-descriptor-audit.test.ts, 124 passed, 0 failed. Not re-run: the full suite (the author reports 3941) and tsc. The by-name arm for a registry with no package-scoped reads has no pin of its own; it is main's behaviour and not blocking. Worktree untouched.

Still open from the earlier record, outside this delta: the PR body placeholders and its one-holder count line.


delta-3 — head e467615b, rebased on main 4ace1fa (conflict with #33193, b462444)

Verdict: PASS

No must-fix item. Scope: the conflict resolution and the new clause in changeset row (3).

The resolution is right. connector is in PACKAGE_SLOTTED_METADATA_TYPES (packages/spec/src/api/protocol.zod.ts:2337), and PACKAGE_SLOTTED_STORE_TYPES is built from it (packages/metadata/src/package-slotted-store.ts:147), so there is one list and the metadata hunk is docblock only. The branch's diff to packages/spec is that one line plus the two interface members already reviewed.

The clause is right, read against historySlotOf:

  • ?package= on a connector: FROM refusePackageSelectorOutsideSlottedTypes throwing 400 VALIDATION_ERROR (connector not in the list on main), TO the slot of the row a read naming the package serves, or the package's own slot when it ships the name.
  • A bare connector name two packages hold: FROM historySlotOf returning undefined for a type outside the list (the door served the name's whole log), TO 409. Two paths reach it, and both hold for connector now: two row slots (ambiguous-in-environment), and refuseWhereBareReadRefuses, which fires because this PR also puts connector in PACKAGE_SCOPED_READ_TYPES, so two holders in code are refused even with one stored row.
  • A one-holder name, bare: one slot, sole: true, the whole log and any version restorable, as on main.

Nothing else in #33193 needs connector handling that I found. The type list appears once in that commit; every other site reads PACKAGE_SLOTTED_STORE_TYPES or isPackageSlottedMetaType, so the REST routes, the repository's version attribution, the Studio history page and the metadata client pick connector up from the list. No generated spec artefact or reference doc embeds the list's values (searched packages/spec/*/api.json and content/docs/references/api/protocol.mdx).

One wording note, not blocking. The bare door also refuses a name whose rows sit in a package's slot and in the package-less slot (one package holder, two row slots). That is the rule every slotted type already has, and it needs a row saved under the package beside an unbound environment row, so existing one-row connector data is unaffected. The clause could add "or a package's row beside the package-less row" for completeness.

Run by me on e467615b: packages/metadata-protocol/src/protocol-history-slots.test.ts, 15 passed, 0 failed. Not run by me: the other suites and spec check:generated, which the author has in progress; this PASS assumes they come back green. Worktree untouched.


Generated by Claude Code

This branch has not been deployed

No deployments
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/xl tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants