Skip to content

feat(objectql)!: a flow, a webhook, a connector and an email template hold one name per package (bare-name phase 1, 3/4) - #31093

Merged
os-zhuang merged 5 commits into
mainfrom
claude/issue-30803-bare-name-pr3-flow-family
Nov 8, 2026
Merged

os-zhuang merged 5 commits into
mainfrom
claude/issue-30803-bare-name-pr3-flow-family

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Refs #30803 (PR 3 of 4 — flow, webhook, connector, email_template)

Opened by domain:server#1 (session_017xRBcTJTE7kSbcMoJQ4Txf); taken over by domain:server#5 (claim 6713676993, card-thread session_01FEkQ5pr7Ua2kPRCMZNupZg) on triage's hand-over 6713649116.

PR 1 was #30836 (the data family), PR 2 #31006 (the request family); the last PR of the series carries the card's closing line.

Clause-②: no (narrowing) — the engine's package door (SchemaRegistry.installPackage) and item seam (SchemaRegistry.registerItem) refuse, after the cold boot, a package registration they accepted before: one whose flow, webhook, connector or email template bears a name another installed package holds. Nothing is widened.
Metadata-protocol: narrowed
Skills-docs: none — skills/** does not teach that two packages may share a flow, webhook, connector or email template name, and the naming rule itself is phase 2's (#30805); the prescription ships in the refusal and warning text and in ADR-0048's 2026-11-06 addendum (P1.8).

⚠️ Tier H: this diff touches docs/adr/0048-cross-package-metadata-collision.md (header, §3.4 note, P1.5, P1.6 and the new P1.8). The seat routes the approver.

维护者速读

  • 改了什么:裁决 C 第一阶段的第三族。冷启动结束后,flow、webhook、connector、email template 这四类名字,一个部署只认一个包:安装门和注册口拒绝第二个包,422 NAMESPACE_CONFLICT,点出两个持有包和改名建议(与 feat(objectql)!: a picklist, mapping or dataset name holds one package (bare-name phase 1, 1/4) #30836、feat(objectql)!: an API, an object-less action and an engine function hold one name per package (bare-name phase 1, 2/4) #31006 同一机制,不另起一套)。
  • 为什么改:实测两包同名——自动化引擎只武装一个 flow(包 id 字典序靠前者,有告警),/meta/flow/NAME 却给先注册包的声明;webhook 派发只订阅先注册包的声明,另一个永不触发,无告警;/meta/connector/NAME(设计器的连接器选择器读的门)给先注册包的描述;邮件发送 template: NAME 解析先注册包的模板。启动后再装一个同名包,今天照常 200 并被静默遮蔽。
  • 风险与回滚:只收窄接受集,不改任何已存数据;平台内置邮件模板包(com.objectstack.service.email)按既有契约(ADR-0131 D6,应用同名模板覆盖内置)被排除为"可覆盖基线",既不持有名字也不被拒。基线常量只在引擎内部,不进 @objectstack/objectql 的公开导出。回滚即 revert 本 PR。
  • 先看:packages/objectql/src/bare-name-one-holder.ts(四类加入列表、OVERRIDABLE_BASELINE_PACKAGES、declaredBareNames 按门的包 id 读基线)、registry.ts 的 bareNamePackageHolders / sealBareNameHolders(邮件模板 pkg:name@locale 键的成员剥离,是最有风险的 hunk)、ADR-0048 P1.8。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run src/registry-bare-name-one-holder-flow-family.test.ts ——预期 54 passed;或看 dogfood 文件 bare-name-one-holder-flow-family.dogfood.test.ts 的四个用例。
  • 需要你做的:ADR 改动是 Tier H,需要审批人批准后由席位落地。

What changes

  • packages/objectql/src/bare-name-one-holder.ts — the covered types grow from six to ten: the registry types flow, webhook, connector and email_template, listed after the request family (BARE_NAME_ONE_HOLDER_TYPES is now the one ordered list, BARE_NAME_REGISTRY_TYPES derived from it); their collection keys, labels (email template), consumer clauses and nouns, so a refusal's reason names what each consumer resolves. New OVERRIDABLE_BASELINE_PACKAGES / isOverridableBaseline(type, packageId) (module-level, not on the package index): per type, the packages whose items a consumer serves any other package's same-named item over by contract — today the email service's built-in templates (com.objectstack.service.email, EmailServicePlugin.init). declaredBareNames reads the manifest's package id the way the package door does (artifactPackageId: id, else name) and declares nothing of a type the package is the baseline of. Module doc: the four measurements, the baseline, what is not judged.
  • packages/objectql/src/registry.ts — bareNamePackageHolders and sealBareNameHolders read a bundled type's key through bundleBaseKey (pkg:name@locale holds the NAME), and the holder list drops a type's baseline package; the item seam skips a baseline package's own registration. Comments name the ten types.
  • packages/objectql/src/index.ts — unchanged: @objectstack/objectql's published surface does not widen. The booted pin holds the baseline's spelling by behaviour instead (see Verification).
  • packages/objectql/src/plugin.ts — the seal's comment names the ten types; no code change.
  • docs/adr/0048-cross-package-metadata-collision.md (Tier H) — the header's Addenda entry and §3.4's 2026-11-06 note name the ten types and the baseline; P1.5 keeps agent and skill as the last family; P1.6 names the baseline; new P1.8 — the measurement, what is refused (the bundle, the connector kinds), the overridable baseline, what is not judged (the ADR-0146 declared override, ADR-0097's duplicate at boot, the kernel metadata service), the cold boot.
  • scripts/adr-anchors/packages__objectql__src__bare-name-one-holder.ts.json — the invariant names the third family and its served holder, the bundle reading and the baseline exclusion. Same ADR citations.
  • .changeset/30803-bare-name-flow-family.md — @objectstack/objectql minor, both declaration lines, BREAKING Before / Now / The overridable baseline / The cold boot / Who moves / Unchanged, adr-0087: not-required (no-migration-prescription), measured producers.

Why (measured)

Booted showcase + two AppPlugin packages (com.qa.bare.alpha, com.qa.bare.beta) sharing one name per type, in both registration orders, on 24bc2593; then POST /marketplace/install-local of a third package (gamma) declaring all four held names:

Type Consumer Served Warned Post-boot third package today
flow automation engine (getFlow, the bare-name flow map via resolveFlowPrecedence) alpha in BOTH orders (lexicographically-first package id) yes, naming both 200, shadowed
flow GET /meta/flow/NAME first-registered (alpha in alpha→beta, beta in beta→alpha) — so in one order the designer's body and the running body come from two packages no —
webhook declaredWebhooks.declaredWebhookNamed (the dispatcher's reader) and GET /meta/webhook/NAME first-registered; the other is never subscribed no 200, never subscribed
connector (descriptor) GET /meta/connector/NAME (the picker's read); listItems returns both first-registered no 200, shadowed
connector (provider-bound instance) automation service reconcile the boot is REFUSED: duplicate declarative connector instance name 'qa_shared_conn_live' — connector names must be unique (ADR-0097) loud (fatal) after the boot: logged at error, skipped (code reading)
email_template email.renderTemplate({ template }) and GET /meta/email_template/NAME first-registered no 200, shadowed

The showcase boot also showed the built-in auth.password_reset bundle registered under com.objectstack.service.email only (four locales) — the showcase itself declares no override; the email plugin's loader serves any other package's same-named member over the built-in by contract, which is why that package is the overridable baseline rather than a holder.

Verification

Stamped on head c05d912a = e01df46c (this PR's two original commits) + merge of origin/main b3bd75d8 (6bbbb4a7, 93 commits, no conflict; the only main change in these files is registry.ts's ADR-0146 pair lookup, pairOf → pairsOf, no hunk shared) + c05d912a (should-fix 1 and the nit of contract review 6703490135).

  • packages/objectql/src/registry-bare-name-one-holder-flow-family.test.ts (54 cells, real ObjectKernel + ObjectQLPlugin): for each of the four types the data family's matrix — package door (both holders, rename, no record left), ABLATION before the seal, item seam, nested plugin, same-package reload (CONTROL), uninstall releases, environment registration never judged and never a holder (CONTROL), the cold boot (warns once naming both and the rename; reload keeps both, a third refused naming both; 'refuse' refuses with the doors' envelope). Family-specific: the type list and its order; declaredBareNames over the four collections and a nested plugin, a bundle read once; a descriptor and a provider-bound connector instance hold the name alike; an email template's bundle is one holder and another package's member of that name is one conflict on the name; the item seam judges every member of a bundle; a holder is read from its member key alone (no claim) and the cold boot reads a bundled clash from the member keys; the baseline vocabulary, now also read by the door's package id (a manifest with no id whose name is the baseline declares nothing; one named for an app declares its template); CONTROLs — an app overriding a built-in template after the boot installs and a second app is refused naming the first app only, the baseline package is refused nothing at either door, the cold boot of the baseline beside an app's override is no clash, two apps overriding one built-in name warn naming the two apps and never the baseline; the reason names only the types it carries; a built-in flow name is refused at the package door. With the data- and request-family pins and hook-binder.test.ts: 4 files / 154 passed on c05d912a.

  • packages/qa/dogfood/test/bare-name-one-holder-flow-family.dogfood.test.ts (booted showcase + two AppPlugin packages + MarketplaceInstallLocalPlugin): the cold boot boots and logs ONE WARN line naming the flow, the webhook, the connector and the email template, both packages and the renames, never the built-in bundle or its package, while the automation engine's own flow-collision warning still arms alpha; POST /marketplace/install-local of a third package declaring the held flow name is 422 PLUGIN_REGISTER_FAILED naming both holders and gamma_…, the package list unchanged and the armed flow untouched; a package declaring the held webhook name plus the showcase's own flow and email template names is refused naming the showcase; CONTROL: fresh names install, and so does a package overriding the built-in auth.password_reset — the send then renders the app's subject, the built-in bundle still registered beside it. The spelling pin, without the export: the package the email plugin registered the built-ins under is read back from the registry, and the override's install is what holds the engine's spelling of it — were the vocabulary to spell that package any other way, the package would hold auth.password_reset and the install would be refused naming it (ablation below). On c05d912a: this file and the data and request families' dogfood files plus email-template-materialization.dogfood.test.ts → 4 files / 16 passed.

  • Ablations on c05d912a, each through ablation-replace.mjs or a trapped restore, restored with blob == HEAD and git diff HEAD empty:

    Leg Mutation Result
    src (new, the nit) declaredBareNames reads the baseline by manifest.id again unit file 1 of 54 red: the vocabulary cell, expected [ { type: 'email_template', … } ] to deeply equal []
    dist (new, should-fix 1) OVERRIDABLE_BASELINE_PACKAGES.email_template misspelt (marker in 4 built files), objectql rebuilt dogfood pin 1 of 4 red: the CONTROL, PLUGIN_REGISTER_FAILED … declares the email template "auth.password_reset" naming com.objectstack.service.email; restored, rebuilt, marker absent from all 14 built files
    dist (re-run, the dogfood file moved) objectql's bare-name-one-holder.ts, registry.ts, plugin.ts at the merge base b3bd75d8, rebuilt (head marker absent from all 14 built files) dogfood pin 3 of 4 red (no warning line; gamma and delta installed), the CONTROL green — the CONTROL no longer imports anything from objectql, so on the base it is a control; rebuilt from HEAD
    src (round 1 at e01df46c, code unmoved) the four types removed from BARE_NAME_ONE_HOLDER_TYPES / the baseline exclusion removed / the bundle member not stripped 39 / 3 / 2 of 54 red — not re-run: the merge and c05d912a leave those hunks byte-identical
  • pnpm --filter @objectstack/objectql typecheck exit 0 (incl. check:test-typecheck: 39 files / 233 errors / 64 pinned signatures held, unchanged); pnpm --filter @objectstack/dogfood exec tsc --noEmit exit 0; eslint (--no-inline-config) over packages/objectql/src and the dogfood file exit 0.

  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands at c05d912a (merge base b3bd75d8, 8 paths): 84 derived families, every one run with its exit code captured before any pipe; --ran: "84 derived famil(ies) accounted for — 84 run, 0 NOT-MEASURED" (exit 0). One rerun after a prerequisite: pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (@objectstack/scenarios had no dist/ on this worktree; the seven packages round 1 named were built first) → built, rerun exit 0 (122 entry points / 72 packages load). check:type-check-debt exit 0 on the first run.

  • Full dogfood battery (pnpm --filter @objectstack/dogfood test, under os-verify-lock) at c05d912a: VERDICT command-exit 0 · Test Files 436 passed | 3 skipped (439) · Tests 3614 passed | 41 skipped (3655) · no failures (held the lock 1567s on a shared box). Ran because the title carries ! and Clause-② carries the (narrowing) arm.

  • Not re-run on the merged head (round 1 at e01df46c, VERDICT command-exit 0 both): the full @objectstack/objectql suite (565 files / 11476 passed) and @objectstack/runtime (553 files / 8882 passed, 19 skipped); CI's Test Core runs both on this head.

  • CI on c05d912a, read once after the push: 35 success, 13 skipped, 2 failure — Test Core (3/8) and its aggregate Test Core, one cell: @objectstack/scenarios test/runner/external-driver.test.ts › "a full crm run through the command…" timed out at 5044 ms against vitest's 5000 ms default (no assertion failed; 572 of 573 passed). Not this diff: the test arrived with feat(scenarios): run-scenarios --driver codex and --driver external:COMMAND, with the driver on each gallery app page #31265 and drives a scripted CRM run through a spawned command; on this head it passed 4 of 4 locally (2973 ms when timed). Left for the seat to judge; nothing pushed to re-run it.

Kept

  • Same-name actions on different objects (objectName:name), jobs keyed appId:jobName, the unmanaged import's all-type refusal, the environment catalog (ADR-0005 overlay) and the ADR-0146 declared override of a base flow (a stored row); the automation service's own ADR-0097 duplicate refusal at boot; every data- and request-family pin green unchanged (registry-bare-name-one-holder.test.ts, …-request-family.test.ts, hook-binder.test.ts — 100 cells).

Acceptance notes

  • The measurement per type is the table above; the card's audit row for flow ("one armed, warning logged") holds, and the second consumer (/meta/flow/NAME) adds a silent split in one order.
  • Not judged (stated in the module doc and P1.8): the kernel metadata service's (type, name) overwrite — on the composed runtime none of the four is read through it (the webhook reader falls back to it only with no registry).
  • GET /connectors answered 404 on the showcase composition during the measurement (the route is not mounted there); the connector reading used /meta/connector/NAME and the registry list instead. Observation only.
  • Round 2 (take-over): contract review 6703490135's should-fix 1 is closed by its route (b) — OVERRIDABLE_BASELINE_PACKAGES stays off packages/objectql/src/index.ts, so the file leaves this PR's diff and the changeset no longer calls the constant exported; Clause-②: no (narrowing) now reads true as written. Its nit (declaredBareNames read the baseline by manifest.id while the door judges artifactPackageId) is fixed in the same reader line. The ADR-0131 D6/D10 citation nit needed no change (the review said so).

Series and out of scope

  • PR4 (agent, skill, the card's closing line) follows this one; ADR-0048 P1.5 names it.
  • A second app overriding a built-in template beside a first app's override is refused by this PR; whether the first app's override should itself be reported at boot (today: no warning, by the loader's contract) is left as the email plugin's own question.

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 — see Verification.
  • 4. One issue, one branch off an up-to-date main, one pull request (third of a series by type family, per the seat).
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the maintainer brief is in Chinese by the seat's convention).
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FEkQ5pr7Ua2kPRCMZNupZg

… hold one name per package (bare-name phase 1, 3/4)

The flow family of maintainer ruling C on #30798, phase 1: `flow`, `webhook`,
`connector` and `email_template` join the bare-name registry types. Measured
on a booted showcase with two packages sharing one name per type, in both
orders: the automation engine arms one flow per name (lexicographically-first
package, with a warning) while `/meta/flow/NAME` serves the first-registered
declaration; the webhook dispatcher's declared reader keeps the first-registered
webhook and never subscribes the other; `/meta/connector/NAME` serves the
first-registered descriptor (two provider-bound instances already refuse the
boot, ADR-0097); a send's `template` resolves the first-registered email
template. A third package installed after the boot under any of those names
installed (200) and was shadowed.

- `bare-name-one-holder.ts`: the four types, in family order, with their
  collection keys, labels, consumer clauses and nouns; the holder reading
  excludes a type's OVERRIDABLE BASELINE (the email service's built-in
  templates, which an app's same-named template overrides by contract,
  ADR-0131 D6): that package neither holds a template name nor is refused one.
- `registry.ts`: the holder reading and the cold-boot seal strip a bundled
  type's registry-key member (`pkg:name@locale`), so an email template's NAME
  holds its bundle; the item seam skips the baseline.
- New `registry-bare-name-one-holder-flow-family.test.ts` (52 cells) and the
  booted dogfood pin `bare-name-one-holder-flow-family.dogfood.test.ts`.
- ADR-0048: header, §3.4 note, P1.5, P1.6 and the new P1.8; the anchor's
  invariant names the third family and the baseline; one changeset.

Claude-Session: https://claude.ai/code/session_017xRBcTJTE7kSbcMoJQ4Txf
Co-authored-by: Claude <noreply@anthropic.com>
… showcase and the bundled holder reading

- `OVERRIDABLE_BASELINE_PACKAGES` is exported from `@objectstack/objectql`, so
  the dogfood pin can hold its spelling of the email service's package equal to
  the package the email plugin registers the built-in templates under on this
  boot (read back from the registry: the two spellings are pinned together
  across the package boundary without either package importing the other).
- Two unit cells read an email template's holder from its member key alone:
  a package registering a bundle member through the registry directly (no
  install claim) holds the name, and the cold boot reads such a clash from
  the member keys as one conflict on the name.

Claude-Session: https://claude.ai/code/session_017xRBcTJTE7kSbcMoJQ4Txf
Co-authored-by: Claude <noreply@anthropic.com>
@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 7, 2026
@github-actions

github-actions Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql, touching 19 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/objectql/src/plugin.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

22 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 7c523aa108650e48d19419ae9b76924c64707741.

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

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/objectql/src/plugin.ts) — pages documenting those are invisible to this run
  • 8 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 63 of 226 client-bound route-ledger rows — the other 163 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 163: 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; 104 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 — 21 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 7c523aa108650e48d19419ae9b76924c64707741 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 7c523aa108650e48d19419ae9b76924c64707741

⚠️ 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 7c523aa108650e48d19419ae9b76924c64707741 → 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#1 · sunhaolin · session_017xRBcTJTE7kSbcMoJQ4Txf

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e01df46cce1db092238882a4625ec556e3f0baf3
Local-runs: none
Scope: docs/adr/0048-cross-package-metadata-collision.md, packages/objectql/src/bare-name-one-holder.ts, packages/objectql/src/registry.ts, packages/objectql/src/plugin.ts, packages/objectql/src/index.ts, packages/objectql/src/registry-bare-name-one-holder-flow-family.test.ts, packages/qa/dogfood/test/bare-name-one-holder-flow-family.dogfood.test.ts, scripts/adr-anchors/packages__objectql__src__bare-name-one-holder.ts.json + packages/plugins/plugin-email/src/template-loader.ts, packages/plugins/plugin-email/src/email-plugin.ts, packages/services/service-automation/src/flow-precedence.ts, packages/services/service-automation/src/plugin.ts, packages/plugins/plugin-webhooks/src/declared-webhooks.ts, packages/metadata-protocol/src/protocol-registry-hydration.ts, packages/metadata-protocol/src/stored-row-binding.ts, packages/metadata-protocol/src/protocol-package-install.ts, packages/metadata-protocol/src/protocol-customization-package.ts, packages/cloud-connection/src/marketplace-install-local-plugin.ts, packages/runtime/src/domains/packages.ts, packages/objectql/src/engine-registration.ts, packages/objectql/src/engine-package-registration.ts, packages/objectql/src/engine-top-level.ts, packages/spec/src/meta-spelling/manifest-collection-spelling.ts, packages/spec/src/kernel/manifest.zod.ts, packages/core/src/artifact-packages.ts, docs/adr/0146-studio-designs-setup-administers-one-editor-per-item-paired-customize-package.md, docs/adr/0131-total-organization-ownership-no-null-organization-id.md

Written 2026-11-07T06:41Z. Inputs: card #30803 (body + all 15 comments, rulings 6687840782 and 6688041914 and the dev report 6703300597 included), PR #31093 (body, 9-file list, git diff origin/main...e01df46c, merge base 24bc2593), the check-runs on the head. Every cited file read with git show e01df46c:PATH. Nothing built, run or re-run. The dispatch order and the dispatching seat's conclusions were not inputs.

① Derived judgments

Accept-set and public-surface changes the diff implies, each judged against the code it reaches:

  1. Package door narrows (SchemaRegistry.installPackage → refuseBareNameConflicts): after the seal, a manifest whose flows[], webhooks[], connectors[] or emailTemplates[] (top level and nested plugins[], one level) carry a name another installed package holds is refused ahead of every mutation. Right. declaredBareNames reads the same four collection keys the engine registers (engine-top-level.ts collection list; pluralToSingular: flows→flow, webhooks→webhook, connectors→connector, emailTemplates→email_template, manifest-collection-spelling.ts:74/89/98/101), and a nested plugin registers under its parent id (engine-package-registration.ts:171), so the door and the seam judge the same holder.
  2. Item seam narrows (SchemaRegistry.registerItem, the four types, package-bound, after the seal). Right. A registration with no packageId stays unjudged: the protocol's hydration registers every stored row bare (protocol-registry-hydration.ts:521, registerItem(type, registered, 'name')), so the ADR-0005 overlay is never a holder, as PR 1 set it.
  3. email_template is read by NAME: bareNamePackageHolders and sealBareNameHolders strip the member through bundleBaseKey (pkg:name@locale → pkg:name; the search starts after the last :, so a scoped id's own @ is intact), and the seam judges by the item's name (baseName), so another package's member in another locale is one conflict on the name. Right, and deliberately conservative: a second package cannot ship the missing locale of another package's bundle; the loader's locale ladder serves the holder's other locales (template-loader.ts module doc). Applying bundleBaseKey to every registry type's key is harmless: no undiscriminated covered type carries @ after its last :.
  4. The overridable baseline (OVERRIDABLE_BASELINE_PACKAGES.email_template = {com.objectstack.service.email}): that package holds nothing and is refused nothing at the holder reading, the seam guard and declaredBareNames (by the manifest's own id). Right against the live loader: template-loader.ts:159 picks, per (name, locale) slot, slot.find(item ⇒ item._packageId !== BUILTIN_EMAIL_TEMPLATES_PACKAGE_ID) ?? slot[0], and email-plugin.ts:555 registers the built-ins at init under exactly that id (namespace: 'sys', before the seal). The spelling is pinned across the package boundary by the dogfood CONTROL reading the built-in bundle's _packageId back from the registry — no import of plugin-email. BUILT_IN_BARE_NAMES stays empty for the four: right, since a built-in template is a name apps MAY take by contract, not a platform-held name.
  5. Cold boot: candidates strip the member, one conflict per name, BARE_NAME_COLD_BOOT_MODE untouched ('warn') — ruling B. Right.
  6. Public surface widens by one symbol: packages/objectql/src/index.ts:106 exports OVERRIDABLE_BASELINE_PACKAGES. At the merge base the index exported nothing of bare-name-one-holder.ts; this is the rule's first published symbol. It is a widening of @objectstack/objectql's published surface, test-motivated (the dogfood pin) — judged in ② (should-fix 1). isOverridableBaseline and the four new BareNameRegistryType members stay module-level (not on the index).
  7. Refusal/warning text per type (CONSUMER_CLAUSE, TYPE_NOUN, bareNameTypeLabel('email_template') = 'email template'): right; the connector clause's connector_action node is a reader beyond the measured /meta/connector/NAME, wording only.
  8. Kept: the flow precedence warning still fires and arms one (dogfood cell 1 pins Flow name collision); object-bound actions, appId:jobName, the unmanaged import, the environment catalog — untouched by this diff.

Half-right (nit, no change owed): declaredBareNames reads the baseline by manifest.id while the door's selfId is artifactPackageId(manifest) = id || name (artifact-packages.ts:178). A manifest with no id whose name is the baseline id is judged at the door and exempt at the seam. Direction conservative; unreachable for the email plugin's own manifest, which carries id.

② Semver level

  • Changeset .changeset/30803-bare-name-flow-family.md: '@objectstack/objectql': minor. The diff moves objectql's published source (bare-name-one-holder.ts, registry.ts, index.ts); Metadata-protocol: narrowed requires at least minor on a moved package (check-changeset-no-major.mjs, level axis; the launch window ships breaking as minor). Level matches. Check Changeset on the head: success.
  • Metadata-protocol: narrowed — right: a requirement tightened on what a package author can write (four more types must bear unheld names).
  • Clause-②: no (narrowing) on the PR body and the changeset, read with clause2-line.mjs: the ARM is right — the accept set narrows, this is BREAKING, the **BREAKING** banner and the adr-0087: not-required (no-migration-prescription) marker are present, and the migration ("Who moves", the rename) is in the body. The VALUE no is half-true: the diff also widens the published surface by one export (① item 6). The closed set spells exactly this case yes (narrowing) — "a diff that widens one surface and narrows another; both facts are true and both are read" (clause2-line.mjs, the four combinations). Neither level nor breaking-ness nor the ADR-0087 reading changes between the two spellings, so no gate reads differently; what is off is the record of what the PR publishes.
    should-fix 1 — one of: (a) spell Clause-②: yes (narrowing) on the PR body and the changeset; or (b) keep the export off the published index — the dogfood CONTROL can hold the spelling from the registry read it already makes (the built-in bundle's _packageId) against the refusal/warning text, or the vocabulary cell can stay a unit cell beside the module. The changeset body already tells consumers the constant is exported, so the prose half is honest either way.
  • Skills-docs: none — acceptable: skills/** teaches no two-package sharing of these names, and the rename prescription ships in the refusal, the warning and P1.8.

③ Boundary flags

  • Reviewed-by isolation. This record is rendered by an isolated subagent running inside session_017xRBcTJTE7kSbcMoJQ4Txf — the dev's own session. The brief was adversarial by design: the inputs were the card, the PR, the diff and the check-runs, and the dev's conclusions were read as claims to test, never adopted. The identity line names the session because that is the session that renders the verdict.
  • Tier. docs/adr/0048-cross-package-metadata-collision.md is a Tier H row (header Addenda, §3.4 note, P1.5, P1.6, new P1.8). This record is the contract-tier review the narrowing face owes; it does NOT lift Tier H — landing needs an authorized APPROVED review by a GOVERNED_APPROVERS account, and the owning seat then lands it. The PR body says so. Head repo = base repo (not a fork); author objectstack-fleet[bot]; draft; mergeable: true, state blocked.
  • Rulings carried. Cold boot B (6687840782): 'warn' untouched, the doors refuse only a new holder — held. Series (6688041914): PR 3 = flow, webhook, connector, email_template, Refs not Fixes — held (PR body "Refs Bare-name phase 1: refuse a second package holding a non-object name whose consumers resolve without package context (ruling C on #30798) #30803 (PR 3 of 4)"); agent/skill left to PR 4 (P1.5).
  • Card asks. 1 measured (the per-type table, both orders, post-boot third package); 2 extended at managed install (install-local 422 PLUGIN_REGISTER_FAILED, dogfood cells 2–3), package registration and cold boot, naming both holders and the rename; 3 kept (holdsBareName for object-bound actions, jobs, the unmanaged import — none touched); 4 ADR-0048 §3.4 note + P1.8.
  • Dev report 6703300597: open_questions: []. Its three out-of-scope findings, answered here: (1) GET /connectors 404 on the showcase composition — the route is not mounted there; an observation, not this card's; (2) a first app's override of a built-in template is served silently — that is the loader's own contract (template-loader.ts, plugin-email CHANGELOG: "an app that declares the same (name, locale) wins over the built-in"); the one-holder rule's baseline is not a holder, so no change is owed here, the email plugin's question; (3) exit-3 prerequisites on a fresh worktree — harness facts, not findings. The PR body's own open point (whether the first override should be reported at boot) is the same question as (2).
  • Scope. All nine files sit inside the claim's file surface (6702457694); no packages/spec/**, no skills/**, no agent/skill; the environment-registration and object-bound-action controls are untouched.
  • Check-runs on e01df46c, read 2026-11-07T06:23:13Z (47 runs, 18 in progress, 0 failure) and again 2026-11-07T06:40:27Z: 34 success, 13 skipped, 2 in progress (Test Core (1/8), Test Core (4/8)), 0 failure. Green among them: Build Core, Check Changeset, Governed Surface Queue Guard, Lint & Repo Gates (ESLint, docs & code guards incl. check:adr-anchors, repo & tooling, PM dispatch-gates, CI/release/engine, the three scoped), TypeScript Type Check (workspace, consumer, source, debt ledger), Dogfood Regression Gate, Test Core 2/8, 3/8, 5/8–8/8, Live E2E UI, Spec property liveness, PR-body guards. The two shards were unresolved at write time; the owning seat reads them before landing.
  • Not in scope / not read: origin/main has moved past the merge base (24bc2593 → 51992dff); the diff is the three-dot diff against the merge base, as the brief fixes it.

④ Design judgment

Each scoped file against the code it cites and the scenario it is accepted against; cited code READ, never reviewed.

docs/adr/0048 (P1.8, §3.4 note, header, P1.5, P1.6) — against its first consumer (bare-name-one-holder.ts + registry.ts) and its execution plan (rulings B and the series): the ten types, the bundle-is-the-name reading, the baseline and the cold-boot sentence all match the code. The "Not judged" list, each verified in the tree: (a) ADR-0146 D9's declared override of a base flow IS a stored row — flow-precedence.ts#classifyContender admits override only by storedRowBindingOf (the package_id column, written by bindStoredRow at hydration, a non-enumerable symbol), and the hydration registers that row bare; the sanctioned pair paths never reach the package door (install-local's pair limb writes each item through saveMetaItem with the pair as its package — "a pair is never registered as code", marketplace-install-local-plugin.ts:4170–4200; a Studio save is the same door). So the override path stays open, as P1.8 says. (b) ADR-0097's duplicate provider-bound instance: service-automation/src/plugin.ts:2913, at boot, before the seal. (c) The kernel metadata service: the webhook reader falls back to it only with no registry (declared-webhooks.ts:143–155). P1.8's served-holder sentences match the consumers: flow — resolveFlowPrecedence ranks package contenders lexicographically and warns; /meta/flow/NAME — SchemaRegistry.getItem precedence 3 "first composite match (legacy first-registered-wins)"; webhook — declared-webhooks.ts:225 "Two registrations of one name: the first wins"; email template — the layered list through the loader. Nit: P1.8 cites "ADR-0131 D6/D10" for app-over-built-in; ADR-0131 D6 states the environment overlay (the O rule), and the app-over-built-in precedence is the email plugin's loader contract, which cites D6/D10 the same way in code (email-plugin.ts:545–549) — the chain exists; no change owed.

bare-name-one-holder.ts — the four consumer clauses in the module doc hold against the cited readers (above). The baseline, tried hard: (i) Can a package claim to be the baseline? By shape, yes: manifestIdRefusal reserves no prefix and sys is a shareable namespace, so a manifest with id: com.objectstack.service.email passes the namespace gate as a same-id reload, declares nothing, holds nothing and is refused nothing. Does that open a hole in the one-holder rule? No: its items wear _packageId = com.objectstack.service.email, which the loader's pick demotes below every other package's item in the slot, so it can never be served over a holder's template through the consumer the rule protects; /meta/email_template/NAME keeps the first-registered composite entry (a Map.set on the built-in's existing key keeps the built-in's insertion position; for a name an app holds, the app's entry is earlier). What such a package CAN do is replace the platform's own built-in bodies under the same keys — the pre-existing same-id reload property of every package id (nothing anywhere reserves com.objectstack.*), not a hole this PR opens. Observation for the seat, not this PR's: a reserved-prefix refusal at the managed doors would close id impersonation generally. (ii) A nested plugin claiming the id: nested items register under the parent id and declaredBareNames reads the top-level id only — no bypass. (iii) The baseline registering a name an app holds (the seam skips it): its item loses at the loader (demoted) and at /meta (later entry) — no consumer flips; the unit CONTROL covers it. (iv) The built-in four-locale bundle beside an app overriding en-US only: holders = {app}; the built-in keeps serving the other locales through the loader's ladder (the CHANGELOG contract), and a second app's zh-CN is refused naming the first app — the rule's design, one holder per NAME, and P1.8 says so. (v) BARE_NAME_REGISTRY_TYPES derived by filter keeps the family order; BUILT_IN_BARE_NAMES extended empty — right (built-in templates are not platform-held names).

registry.ts — bareNamePackageHolders: baseline filter applied after the claims are merged, so a claim could never resurrect the baseline (and declaredBareNames records none for it anyway). sealBareNameHolders: candidate = bundleBaseKey(key) after its last : — right for pkg:name@locale, unchanged for the rest. Item seam: the baseline guard sits before bareNameHoldersOtherThan, baseName is the item's locale-less name, nothing stored before the refusal. recordBareNameClaims runs after the mutations; forgetBareNameClaims at uninstall releases the name (cell "uninstalling the holder releases the name"). Comments name the ten types. No hole found.

plugin.ts — comment only; the seal call and BARE_NAME_COLD_BOOT_MODE import unchanged.

index.ts — the export: should-fix 1 (②). The comment states its purpose truthfully.

registry-bare-name-one-holder-flow-family.test.ts — 54 cells counted (2 + 4 types × 10 + 1 connector + 9 email + 2 reason), real ObjectKernel + ObjectQLPlugin, every refusal asserted by envelope (NAMESPACE_CONFLICT, 422), both holders and the rename; the ABLATION cells show the same registrations coexist before the seal; the baseline CONTROLs register the email service's package through the manifest service with the plugin's real id and namespace. Holds against the scenario. Not pinned (and not owed, a pre-existing property): an impostor package under the baseline id (probe (i) above).

bare-name-one-holder-flow-family.dogfood.test.ts — booted showcase + two AppPlugins + install-local: one WARN line naming all four names, both packages and every rename, never the built-in; the precedence warning still arms alpha; install-local 422 naming both holders, the package list unchanged; the showcase's own flow and template names refused naming the showcase; CONTROL installs fresh names plus an override of auth.password_reset, and renderTemplate renders the app's subject with the built-in still registered beside it — the loader contract proven live. Its spelling pin depends on the index export (should-fix 1's alternative (b) rewrites that one assertion).

scripts/adr-anchors/packages__objectql__src__bare-name-one-holder.ts.json — the invariant names the third family with its served holder (first-registered; the flow lexicographic-first with a warning), the bundle reading and the baseline exclusion; ADR ids unchanged; check:adr-anchors green under "Lint · docs & code guards".

Observation routed to the seat (outside this card): POST /packages (protocol.installPackage → SchemaRegistry.installPackage) of an EXPORTED pair bundle — assemblePackageManifest folds a runtime package's own stored items into the manifest's collection keys — will be refused post-seal for a base-named flow/email_template (and, since PR 1, dataset/mapping) with a rename prescription. Nothing that worked stops working: through that door the re-registered items were code-registered composite entries, package contenders that classifyContender never served as the override; the sanctioned pair path is install-local's pair limb and the Studio save, which bypass the package door. The prescription on that door belongs with ADR-0146's export/import work, not here.

must-fix: none. should-fix: 1 (the Clause-② value or the index export, ②). Nits: declaredBareNames baseline by manifest.id vs artifactPackageId; the ADR-0131 D6/D10 citation wording.

Implemented-by: claude/issue-30803-bare-name-pr3-flow-family
Reviewed-by: session_017xRBcTJTE7kSbcMoJQ4Txf

VERDICT: PASS

… read it by the door's package id

The booted flow-family pin now holds the baseline's spelling by behaviour:
it reads the package the email plugin registered the built-in templates
under from the registry, and an app overriding a built-in template must
install, which it cannot if the engine's vocabulary spells that package
any other way. OVERRIDABLE_BASELINE_PACKAGES leaves the objectql index,
so the package's published surface does not widen, and the changeset no
longer says it is exported.

declaredBareNames reads the baseline by artifactPackageId (id, else
name), the identity the package door judges, so the door and the seam
agree on a manifest with no id.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5pr7Ua2kPRCMZNupZg
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-project-manager · session_01FEkQ5o8PE7ZtVjcor51wBZ

Contract-review-claim: session_01FEkQ5o8PE7ZtVjcor51wBZ · head c05d912 · 2026-11-07T23:35Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-project-manager · session_01FEkQ5o8PE7ZtVjcor51wBZ

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c05d912a85ea0369cadcae4f7992921fd0e3afc8
Local-runs: none
Scope: docs/adr/0048-cross-package-metadata-collision.md, packages/objectql/src/bare-name-one-holder.ts, packages/objectql/src/plugin.ts, packages/objectql/src/registry.ts, packages/objectql/src/registry-bare-name-one-holder-flow-family.test.ts, packages/qa/dogfood/test/bare-name-one-holder-flow-family.dogfood.test.ts, scripts/adr-anchors/packages__objectql__src__bare-name-one-holder.ts.json + packages/objectql/src/index.ts (unchanged, read for ① and ②), packages/core/src/artifact-packages.ts, packages/core/src/index.ts, packages/services/service-automation/src/flow-precedence.ts, packages/services/service-automation/src/plugin.ts, packages/plugins/plugin-webhooks/src/declared-webhooks.ts, packages/plugins/plugin-email/src/template-loader.ts, packages/plugins/plugin-email/src/email-plugin.ts, packages/metadata-protocol/src/protocol-registry-hydration.ts, packages/metadata-core/src/item-key-discriminators.ts, packages/cloud-connection/src/marketplace-install-local-plugin.ts, packages/objectql/src/engine-registration.ts, packages/objectql/src/engine-top-level.ts, packages/spec/src/meta-spelling/manifest-collection-spelling.ts, packages/spec/src/automation/flow.zod.ts, packages/spec/src/automation/webhook.zod.ts, packages/spec/src/integration/connector.zod.ts, packages/spec/src/system/email-template.zod.ts, scripts/pm/clause2-line.mjs, scripts/pm/check-governed-merges.mjs

Written 2026-11-07, outside the dev's session. Inputs: card #30803 (body and all 18 comments — rulings 6687840782 and 6688041914, the claims, os-dev-reports 6703300597 and 6714512451, the earlier records); PR #31093 (body, 8-file list, 4 commits, git diff b3bd75d8..c05d912a — identical to the three-dot diff against the merge base); PRs #30836 and #31006 (merged, the mechanism); the check-runs on c05d912a. Every cited file read with git show c05d912a:PATH; nothing checked out, built, run or re-run. The dispatch order and the dispatching seat's conclusions were not inputs. The earlier record 6703490135 (PASS on e01df46c, rendered inside the dev's own session) was tested, never adopted.

① Derived judgments

Every accept-set and public-surface change the diff implies, judged against the code it reaches:

  1. Package door narrows (SchemaRegistry.installPackage → refuseBareNameConflicts, registry.ts 8690): after the seal, a manifest whose flows[], webhooks[], connectors[] or emailTemplates[] (top level and nested plugins[], one level) carry a name another installed package holds is refused ahead of every mutation. Right. declaredBareNames reads the four collection keys the engine registers (COLLECTION_KEY; pluralToSingular in manifest-collection-spelling.ts: flows→flow, webhooks→webhook, connectors→connector, emailTemplates→email_template; engine-top-level.ts lists all four in the registered collections, engine-registration.ts:428 registers each through registerItem(pluralToSingular(key), stampPackage(…, ownerId), 'name', ownerId)), so door and seam judge the same holder. No collisionPolicy read on this path: OS_METADATA_COLLISION=warn does not downgrade it.
  2. Item seam narrows (registerItem, registry.ts 7641, the four types, package-bound, after the seal, !isOverridableBaseline). Right. A registration with no packageId stays unjudged; the protocol's hydration registers every stored row bare (protocol-registry-hydration.ts:527, registerItem(type, registered, 'name')), so the ADR-0005 overlay and an ADR-0146 pair's stored flow row are never holders.
  3. email_template is held by NAME, the bundle as one holder: bareNamePackageHolders and sealBareNameHolders read keys through bundleBaseKey (pkg:name@locale → pkg:name; the @ is searched only after the last :, so a scoped id's own @ is intact), and the seam judges baseName = item.name (locale-less). Right, and safe for the other nine: ITEM_KEY_DISCRIMINATORS holds email_template alone, and no covered type's name can carry @ (flow and connector ^[a-z_][a-z0-9_]*$, webhook SnakeCaseIdentifierSchema, email template dotted snake_case). The env slot's key name@locale strips to name and is skipped (base === name); the seal's candidate scan skips it too (no :). Deliberately conservative: another package's member in a missing locale is one conflict on the name (P1.8 says so).
  4. The overridable baseline (OVERRIDABLE_BASELINE_PACKAGES.email_template = {com.objectstack.service.email}): the holder reading filters it after the claims are merged, the seam skips it, declaredBareNames declares nothing of that type for it, so it holds nothing and is refused nothing. Right against the live loader: template-loader.ts:159 picks per (name, locale) slot slot.find(item ⇒ item._packageId !== BUILTIN_EMAIL_TEMPLATES_PACKAGE_ID) ?? slot[0], and email-plugin.ts:555 registers the built-ins at init under exactly that id (namespace: 'sys', through the manifest service, before the seal). BUILT_IN_BARE_NAMES stays empty for the four — right: a built-in template is a name apps may take by contract, not a platform-held name.
  5. Cold boot: the seal strips the member, one conflict per name, BARE_NAME_COLD_BOOT_MODE untouched ('warn'), the warning sentence "changes nothing about which holder each caller is served" — ruling B. Right.
  6. Public surface of @objectstack/objectql: unchanged. packages/objectql/src/index.ts at c05d912a is byte-identical to the merge base b3bd75d8 and to current origin/main (git diff --quiet both ways); the file left the diff with c05d912a. OVERRIDABLE_BASELINE_PACKAGES, isOverridableBaseline and the four new BareNameRegistryType members live on the module only, and the index exports nothing of bare-name-one-holder.ts (it re-exports NAMESPACE_CONFLICT_CODE from registry.js, as before). The earlier should-fix 1 is gone, by route (b).
  7. The dogfood CONTROL pins the spelling without a second copy of the constant. The dogfood file imports nothing from @objectstack/objectql and carries no com.objectstack.service.email literal: it reads the package the email plugin registered auth.password_reset under back from the registry (builtInTemplatePackage()), then requires an app's override of that name to install (a 2xx status) and the send to render the app's subject. Were the engine's vocabulary to spell that package any other way, the built-in's slot would be a holder and the install refused naming it (dev ablation A2 shows exactly that red). The pin holds in both directions across the package boundary. The unit file's EMAIL_SERVICE_PKG literal is a same-package fixture, not a cross-boundary copy. Right.
  8. The nit (baseline read by manifest.id vs artifactPackageId) is closed right: declaredBareNames now reads artifactPackageId(manifest) (id || name, artifact-packages.ts:178, re-exported by core's index), the same identity installPackage computes as selfId (registry.ts, const selfId = artifactPackageId(manifest)), so the door's exemption and the seam's agree on a manifest with no id; the vocabulary unit cell pins both spellings ({ name: baseline } declares nothing; { name: 'com.acme.alpha' } declares its template). @objectstack/core is already a workspace:* dependency of objectql and imported by nine other source files there — no new edge.
  9. Refusal and warning text per type (CONSUMER_CLAUSE, TYPE_NOUN, bareNameTypeLabel('email_template') = 'email template', BARE_NAME_ONE_HOLDER_TYPES as the one ordered list with BARE_NAME_REGISTRY_TYPES derived): right; the order pin in the new unit file and the data-family pin on conflicts[] order still agree (CI shards green).
  10. Kept: object-bound actions (holdsBareName), jobs appId:jobName, the unmanaged import, the environment catalog, the flow precedence warning (dogfood cell 1 pins Flow name collision still arming alpha), ADR-0097's duplicate refusal — untouched by the diff.

② Semver level

  • .changeset/30803-bare-name-flow-family.md: '@objectstack/objectql': minor. The only published package the diff moves is objectql (@objectstack/dogfood is private: true). Metadata-protocol: narrowed (a requirement tightened on what a package author can write) takes at least minor on the moved package, and the launch window ships breaking as minor (check-changeset-no-major.mjs). Level matches; the ! title and the **BREAKING** banner, the migration ("Who moves", the rename) and exactly one adr-0087: not-required (no-migration-prescription) marker are present. Check Changeset on the head: success (both runs).
  • Clause-②: no (narrowing) on the PR body and the changeset: now right as written. The arm is right (the accept set narrows, breaking); the value no is true because nothing is widened — ① item 6: the index is unchanged, no published symbol is added, SchemaRegistry's signatures are unchanged and only its behaviour narrows. The yes (narrowing) spelling the earlier record offered as route (a) would now be wrong (clause2-line.mjs: that spelling is for a diff that widens one surface and narrows another).
  • Metadata-protocol: narrowed — right. Skills-docs: none — acceptable: skills/** is not in the diff and teaches no two-package sharing of these names; the prescription ships in the refusal, the warning and P1.8. The changeset's prose now says the constant is "private to the engine", which matches the tree.

③ Boundary flags

  • Tier H. docs/adr/0048-cross-package-metadata-collision.md is the adr row of GOVERNED_SURFACES (scripts/pm/check-governed-merges.mjs:1208, tier: GOVERNED_TIER_H). This record is the contract-tier review the narrowing face owes; it does NOT lift Tier H. Landing needs an authorized APPROVED review by a GOVERNED_APPROVERS account on any commit, not dismissed, then the owning seat lands it; no AI seat merges, queues or arms auto-merge before that. State read: draft, mergeable: true, mergeable_state: blocked, zero reviews, head repo = base repo (not a fork), author objectstack-fleet[bot], 986/65 = 1051 changed lines (under the 3,000 human-merge line).
  • Rulings carried. Cold boot B (6687840782): 'warn' untouched, only a new holder refused — held. Series (6688041914): PR 3 = flow, webhook, connector, email_template, Refs not Fixes ("Refs Bare-name phase 1: refuse a second package holding a non-object name whose consumers resolve without package context (ruling C on #30798) #30803 (PR 3 of 4)"), agent/skill left to PR 4 (P1.5) — held. Claim 6702457694's file surface (packages/objectql/src/, the dogfood pin, the ADR, the anchor, one changeset; ⛔ packages/spec/**, skills/**, agent/skill) — held; take-over claim 6713676993 names the same nine files, of which index.ts left the diff.
  • Card asks. 1 measured (the per-type table, both orders, post-boot third package); 2 extended at managed install (install-local inline door 422 PLUGIN_REGISTER_FAILED, dogfood cells 2–3), package registration and cold boot, naming both holders and the rename; 3 kept; 4 ADR-0048 §3.4 note + P1.8.
  • os-dev-report 6703300597 (round 1): open_questions: []. Its three out-of-scope findings, answered: (1) GET /connectors 404 on the showcase composition — the route is not mounted there; an observation for the seat, not this card's. (2) A first app's override of a built-in template served silently — the loader's own contract (template-loader.ts header: "an app that declares auth.password_reset overrides the platform's copy"); the rule's baseline is not a holder, no change owed here; the email plugin's question, as the report says. (3) exit-3 prerequisites on a fresh worktree — harness facts, not findings.
  • os-dev-report 6714512451 (round 2): open_questions: []. Deviations: effort below the xhigh floor and the thread's configured model — recorded; this record is the CONTRACT_REVIEW_TIER review the claim owes, rendered outside that session. PR body patch handed to the seat by relay file — the body read on GitHub already carries the refreshed round-2 text (Verification stamped on c05d912a, the index.ts "unchanged" bullet, the route-(b) acceptance note), so no body drift remains to judge. Out-of-scope finding: the Test Core (3/8) timeout — below.
  • Check-runs on c05d912a, read 2026-11-07T23:37Z and again at the end of this review: 53 runs, 37 success, 15 skipped, 1 in progress. Green: Build Core, Check Changeset ×2, Governed Surface Queue Guard, Lint & Repo Gates (ESLint, docs & code guards incl. check:adr-anchors, repo & tooling, PM dispatch-gates, CI/release/engine, the three scoped), TypeScript Type Check (workspace, consumer, source, debt ledger), Dogfood Regression Gate, Test Core 1/8, 2/8, 4/8–8/8, Test UI, Live E2E UI, Spec property liveness, PR-body guards ×2, Check Documentation Links. Test Core (3/8) is the seat's re-run, in progress at write time (started 23:35:46Z); its first run was red on @objectstack/scenarios test/runner/external-driver.test.ts timing out at vitest's 5000 ms default — a package this diff does not touch, fixed on main by test(scenarios): give the external-driver crm run a measured 30s budget and a lighter stand-in #31284 (59d9815a), which is in origin/main and NOT in this head (merge base b3bd75d8 predates it). I read nothing red as this PR's; the re-run may still time out on a loaded runner for the same reason, and the owning seat reads its final conclusion (and the Test Core aggregate) before landing — in_progress is not a pass.
  • Scope. All eight files sit inside the claim's file surface; no packages/spec/**, no skills/**, no agent/skill. The earlier record's ADR-0131 D6/D10 citation nit needed no change, and none was made.
  • Escalated — should-fix 1, routed to the series, not this PR's diff (see ④, install-local): the cloud (control-plane download) branch of POST /marketplace/install-local swallows the registry's bare-name refusal and persists the package for the next boot; this predates the PR (PR 1's mechanism, every covered type) and sits in @objectstack/cloud-connection, outside the claim's file surface. The seat files it as its own card.

④ Design judgment

Each scoped file against the code it cites and the scenario it is accepted against; cited code READ, never reviewed.

docs/adr/0048 (header Addenda, §3.4 note, P1.5, P1.6, new P1.8) — against its first consumer (bare-name-one-holder.ts + registry.ts) and its execution plan (ruling B, the remaining-types ruling, the series order): the ten types, the bundle-is-the-name reading, the baseline and the cold-boot sentence all match the code. P1.5 now names only agent and skill as the last change and points the flow family at P1.8; P1.6 adds the baseline to the "where implemented" list; the status blockquote cites P1.8 beside P1.2 and P1.7. P1.8's served-holder sentences hold against the consumers: flow — flow-precedence.ts ranks package contenders lexicographically and warns, /meta/flow/NAME is getItem precedence 3 "first composite match (legacy first-registered-wins)"; webhook — declared-webhooks.ts:225 "Two registrations of one name: the first wins", the kernel metadata service read only when the registry holds no webhook (readDeclaredWebhookItems); email template — the loader's slot pick over the layered list. P1.8's "Not judged" list, each verified: (a) the ADR-0146 D9 override is a stored row — classifyContender admits override only by storedRowBindingOf (the package_id column written by bindStoredRow at hydration), and the hydration registers the row bare; (b) ADR-0097's duplicate at boot — service-automation/src/plugin.ts:2913, fail(...) throws in fatal (boot) mode and logs at error and skips in the reload mode, as P1.8 says; (c) the kernel metadata service is not a consumer of the four on a composed runtime. The P1.2 heading still enumerates the data family while P1.7 and P1.8 "join P1.2's list" — the addendum's chosen structure since PR 2, readable; no change owed.

bare-name-one-holder.ts — the four consumer clauses hold against the cited readers (above). The one ordered list with the registry types derived keeps the family order a refusal and the seal list. The baseline, tried hard: (i) An impostor under the baseline id. Nothing reserves com.objectstack.* and sys is a shareable namespace, so a package with that id (or, now, a name of that spelling and no id) passes the namespace gate as a same-id reload, declares no template, holds none and is refused none; for every other type it is judged like any package. Does that open a hole in the one-holder rule? No: its items wear _packageId = com.objectstack.service.email, which the loader demotes below every other package's item in the slot, so it can never be served over a holder's template through the consumer the rule protects. What it can do — replace the platform's own built-in bodies under the same keys — is the pre-existing same-id reload property of every package id on main, not something this PR opens or widens (on main the type was not covered at all). Observation for the seat, outside this card: a reserved-prefix refusal at the managed doors would close id impersonation generally. (ii) A nested plugin claiming the baseline id declares under its parent — no bypass. (iii) The baseline registering a name an app holds (the seam skips it): its item loses at the loader and keeps its earlier /meta position only when it registered first — no consumer flips; the unit CONTROLs cover both doors and the cold boot. (iv) Two apps overriding one built-in name: the first holds it, the second is refused naming the first, never the baseline (unit cells and the cold-boot cell); the built-in's other locales keep serving through the loader's ladder. (v) isOverridableBaseline(type, undefined) is false, so an id-less manifest is judged, not exempted.

registry.ts — bareNamePackageHolders: bundleBaseKey before the name/suffix tests, the baseline filter after the claims are merged, so a claim could never resurrect the baseline (and declaredBareNames records none for it). sealBareNameHolders: candidate = bundleBaseKey(key) after its last : — right for pkg:name@locale, unchanged for the rest, the bare name@locale slot never a candidate. Item seam: the baseline guard sits in the predicate before bareNameHoldersOtherThan, baseName is the item's locale-less name, nothing stored before the refusal. recordBareNameClaims after the mutations, forgetBareNameClaims at uninstall. The only main movement in this file since the merge base (the ADR-0146 pairsOf lookup) shares no hunk. No hole found.

plugin.ts — comment only; the seal call at kernel:bootstrapped and the BARE_NAME_COLD_BOOT_MODE import unchanged.

The narrowing itself against the published spec on main — does it take a declared capability away at the consumer, or make the spec's own refusal real? On main, two packages may declare one flow, webhook, connector or email template name and both register; at the consumer only ONE is served (the flow precedence arms one and warns; the webhook reader subscribes the first and never the second; /meta/connector/NAME serves the first; the send resolves the first), so the second package's declaration was never a working capability — ADR-0048 §3.4's coexistence premise (every caller carries its package) does not hold for these readers. The refusal makes real at the door what the consumer already refused silently, with the rename the author needs. Both orders: the unit matrix refuses the second declarer whichever registered first, and the cold boot admits a clash that exists with one warning (ruling B). Nothing the spec declares as working is taken away: the ADR-0146 pair override stays a stored row, the environment overlay stays unjudged, the ADR-0131 D6 app-over-built-in override still installs and renders (dogfood CONTROL), same-package reload stays one holder.

The package door (POST /packages) — protocol.installPackage → SchemaRegistry.installPackage: refused post-seal with the same envelope. Observation carried from the earlier record, outside this card: re-registering an EXPORTED pair bundle through that door (which folds stored items into the manifest's collections) will now be refused for a base-named flow or email template too; nothing that worked stops working (those re-registered items were never the served override), and the prescription belongs with ADR-0146's export/import work.

Install-local — tried to break it, one finding. The inline branch (the dogfood's door, controlPlaneUrl: 'off'): manifestService.register throws BareNameConflictError → 422 PLUGIN_REGISTER_FAILED with the registry's sentence, before the ledger write (step 4) — nothing persisted, the package list unchanged, the armed flow and the dispatcher's webhook untouched (dogfood cells 2–3). The cloud branch (inlineManifest false, a control-plane download): the same throw is tested only by refusesEnvironmentHeldCatalogName, which recognises existingHolder.kind === 'environment'; a bare-name refusal carries kind: 'package' or 'built-in', so it takes the lenient path of #22695 — hotLoadError recorded, the ledger entry WRITTEN, the answer success with hotLoaded: false "loads at the next restart" — and at that restart the rehydrate registers the package before the seal, so the cold boot admits the clash with one warning (ruling B) and the consumers mis-resolve exactly as the card describes. A restart does not heal a package-held clash any more than it heals the environment-held catalog case #25366 carved out, so the lenient answer is false for this refusal too. should-fix 1 — not a FAIL of this PR: the path predates the PR (PR 1's mechanism, every covered type since 2a6a28db, PR 2 extended it with PASS records), this diff neither opens nor widens it, it lies in @objectstack/cloud-connection outside the claim's file surface, and the PR body claims the inline door only. The fix is one predicate: have install-local refuse the registry's BareNameConflictError (NAMESPACE_CONFLICT_CODE with a conflicts[] whose holders are packages or built-ins) the way #25366 refuses the environment-held catalog refusal, with its own pin on that branch. The seat files it as its own card under the series; PR 4 may carry it if the seat so routes.

registry-bare-name-one-holder-flow-family.test.ts — 54 cells counted (2 + 4 types × 10 + 1 connector + 9 email template + 2 reason), real ObjectKernel + ObjectQLPlugin, every refusal asserted by envelope (NAMESPACE_CONFLICT, 422), both holders and the rename; ABLATION cells show the same registrations coexist before the seal, so every refusal is the seal's; the vocabulary cell now pins the artifactPackageId reading in both directions. Holds against the scenario. Not pinned, and not owed (pre-existing property): an impostor under the baseline id.

bare-name-one-holder-flow-family.dogfood.test.ts — booted showcase + two AppPlugins + install-local: one WARN line naming all four names, both packages and every rename, never the built-in bundle or its package; the flow precedence warning still arms alpha, the dispatcher's reader serves alpha; install-local 422 naming both holders, the package list unchanged; the showcase's own flow and template names refused naming the showcase; CONTROL installs fresh names plus an override of auth.password_reset with the spelling pin by behaviour (① item 7) and renderTemplate renders the app's subject with the built-in still registered beside it — the loader contract proven live. Imports nothing from objectql.

scripts/adr-anchors/packages__objectql__src__bare-name-one-holder.ts.json — the invariant names the third family with its served holder (first-registered; the flow lexicographic-first with a warning), the bundle reading and the baseline exclusion, ADR ids unchanged (ADR-0048, ADR-0005, both cited in the anchored file); check:adr-anchors green under Lint & Repo Gates.

Out-of-scope observation (not this PR's, for the seat): on main today, /meta/email_template/auth.password_reset (first composite match) serves the platform's built-in while the send serves an app's override — the designer's body and the sent body come from two packages, the ADR-0131 D6 override's own split; this PR neither causes nor claims otherwise.

must-fix: none. should-fix: 1 (install-local's cloud branch swallows the bare-name refusal into a next-boot clash — the series' door in cloud-connection, filed as its own card, outside this diff). Observations: id impersonation under the baseline package id (pre-existing); the exported-pair bundle through POST /packages; the /meta/email_template first-registered split.

Implemented-by: claude/issue-30803-bare-name-pr3-flow-family
Reviewed-by: session_01FEkQ5o8PE7ZtVjcor51wBZ

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-project-manager · session_01FEkQ5o8PE7ZtVjcor51wBZ

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 212583ddbc47ce4c7e4bee0862cd8a7e78dc474a
Local-runs: none
Scope: docs/adr/0048-cross-package-metadata-collision.md, packages/objectql/src/bare-name-one-holder.ts, packages/objectql/src/plugin.ts, packages/objectql/src/registry.ts, packages/objectql/src/registry-bare-name-one-holder-flow-family.test.ts, packages/qa/dogfood/test/bare-name-one-holder-flow-family.dogfood.test.ts, scripts/adr-anchors/packages__objectql__src__bare-name-one-holder.ts.json + packages/objectql/src/index.ts, packages/core/src/artifact-packages.ts, packages/core/src/index.ts, packages/services/service-automation/src/flow-precedence.ts, packages/services/service-automation/src/plugin.ts, packages/plugins/plugin-webhooks/src/declared-webhooks.ts, packages/plugins/plugin-email/src/template-loader.ts, packages/plugins/plugin-email/src/email-plugin.ts, packages/metadata-protocol/src/protocol-registry-hydration.ts, packages/metadata-core/src/item-key-discriminators.ts, packages/cloud-connection/src/marketplace-install-local-plugin.ts, packages/objectql/src/engine-registration.ts, packages/objectql/src/engine-top-level.ts, packages/spec/src/meta-spelling/manifest-collection-spelling.ts, packages/spec/src/automation/flow.zod.ts, packages/spec/src/automation/webhook.zod.ts, packages/spec/src/integration/connector.zod.ts, packages/spec/src/system/email-template.zod.ts, scripts/pm/clause2-line.mjs, scripts/pm/check-governed-merges.mjs

Delta record on the new head, 2026-11-07T23:56Z. It stands on its own and carries forward record 6714650391 (PASS on c05d912a85ea0369cadcae4f7992921fd0e3afc8, rendered by this session outside the dev's, read back from the PR thread with its six carrier lines intact). The new head 212583dd is one commit on c05d912a: a merge of origin/main at 7c523aa1 (commit 212583dd, parents c05d912a and 7c523aa1, author and committer the fleet's, SSH-signed), bringing in the seven commits main added since b3bd75d8, among them #31284 (59d9815a, the scenarios external-driver budget). Inputs: the same as 6714650391 (card #30803 and every comment, PR #31093's body, 8-file list, now 5 commits, PRs #30836 and #31006) plus git fetch origin pull/31093/head, git diff origin/main 212583dd, git merge-tree --write-tree c05d912a 7c523aa1 (a read of what a clean merge would produce, no checkout), blob ids of the 8 paths at both heads and on main, and the check-runs on 212583dd as they stood at 23:56Z. Nothing checked out, built, run or re-run. The dispatch order and the dispatching seat's conclusions were not inputs.

① Derived judgments

Three confirmations, each a reading, then the carried judgments.

  1. The merge resolved nothing by hand in the PR's 8 paths. Every one of the eight blobs at 212583dd is byte-identical to its blob at c05d912a (.changeset/30803-bare-name-flow-family.md 9d0bb1d7, docs/adr/0048 ac7d06e0, bare-name-one-holder.ts 4abd9e88, plugin.ts 4a947430, the unit file a3afc431, registry.ts e74130b9, the dogfood file 6328cb55, the anchor d898b307), and main (b3bd75d8..7c523aa1) touched none of the eight (git diff --stat over those paths: empty). Stronger: git merge-tree --write-tree of the two parents yields tree d222716bb7d868c23ab0dc56a91557021d538846, the merge commit's own tree — the whole commit is exactly git's clean merge, no hand resolution anywhere, in the eight paths or outside them.
  2. The net diff against main is unchanged in its added and removed lines. git diff 7c523aa1 212583dd (the merge base is now 7c523aa1 = current origin/main) is 8 files, +986/−65, 1051 changed lines; the patch is byte-identical to git diff b3bd75d8 c05d912a except for the index blob-hash header lines, and the 1051 +/− lines compare equal line for line. The PR's file list reads the same 8 files with the same counts.
  3. main moved none of the cited code the judgments rest on. git diff --stat b3bd75d8 7c523aa1 over every file the Scope line cites beyond the diff (the four consumers' files, artifact-packages.ts and core's index, the hydration, the discriminator table, install-local, the engine registration seams, the collection spelling, the four schemas, clause2-line.mjs, check-governed-merges.mjs, check-changeset-no-major.mjs), plus the data- and request-family pins and hook-binder.ts, plus the dogfood harness (packages/qa/dogfood, @objectstack/verify, the runtime AppPlugin): empty. The seven incoming commits (feat(console): filter and page the marketplace catalog through the list door #31288, fix(mcp): a coded sandboxed hook crash is a fault on MCP, not a refusal at its declared status #31290, feat(console): the sharing-rule object picker lists only shareable objects, business objects first #31274, fix(pm): dispatch-gates derives fifteen whole-root lint gates from the diffs that can move them #31291, test(scenarios): give the external-driver crm run a measured 30s budget and a lighter stand-in #31284, fix(cli): os build leaves devPlugins out of the artifact; os validate --target stops reading it as code #31276, feat(scenarios)!: cards declare user-handle fields and persona records; seed dependence is decided per turn #31275) touch the console, the MCP plugin, pm dispatch-gates, scenarios and the CLI — none of objectql, plugin-email, plugin-webhooks, service-automation, cloud-connection, metadata-protocol, metadata-core, core or packages/spec/src.

So every judgment of 6714650391 ① carries forward unchanged: the package door and item seam narrow for the four types (right); email_template held by NAME through bundleBaseKey (right, safe for the other nine); the overridable baseline against the live loader (right); cold boot ruling B (right); packages/objectql/src/index.ts equal to main — now equal to 7c523aa1 as well as to b3bd75d8 — so the published surface does not widen and the earlier should-fix 1 stays closed by route (b); the dogfood CONTROL's spelling pin without a copy of the constant (right); the artifactPackageId nit closed (right); refusal text and the one ordered list (right); the kept set untouched.

② Semver level

Unchanged from 6714650391: '@objectstack/objectql': minor with Metadata-protocol: narrowed on the one published package the diff moves (dogfood is private), the ! title, the BREAKING banner, the migration and one adr-0087: not-required (no-migration-prescription) marker; Clause-②: no (narrowing) reads true as written (the accept set narrows, nothing is widened, no published symbol added); Skills-docs: none acceptable. The changeset blob is the same (9d0bb1d7). Check Changeset on 212583dd is in progress at write time; it was success on both runs at c05d912a over the identical changeset.

③ Boundary flags

  • Tier H. Unchanged: docs/adr/0048 is the adr row of GOVERNED_SURFACES, GOVERNED_TIER_H. This record is the contract-tier review the narrowing face owes and does NOT lift Tier H: landing needs an authorized APPROVED review by a GOVERNED_APPROVERS account on any commit, not dismissed, then the owning seat lands it; no AI seat merges, queues or arms auto-merge before that. State read: draft, mergeable: true, mergeable_state: blocked, base sha 7c523aa1, head repo = base repo, 1051 changed lines (under 3,000).
  • Rulings carried. Cold boot B and the series order (PR 3 = the four types, Refs not Fixes, agent/skill to PR 4) — held, as before; the merge adds no PR content.
  • Check-runs on 212583dd, read 2026-11-07T23:56Z and again at 23:57:50Z, as they stand: 47 runs at the second read — 13 success (Governed Surface Queue Guard, PR-body guards, Check PR Size, Check Documentation Links, Test UI, filter, Plan Test Core shards and the first of the lint and type-check lanes to finish), 13 skipped (the UI, docs, temporal and opt-in lanes, no UI path in the diff), 21 in progress (Build Core, Check Changeset, Dogfood Regression Gate, the remaining Lint · lanes incl. Lint · docs & code guards which carries check:adr-anchors, the remaining Type Check · lanes, Live E2E UI, Spec property liveness, and all eight Test Core shards, now planned and running). No run on this head is red; the required set is not green yet — in_progress is not a pass. At c05d912a, over the identical diff, every required job read success except Test Core (3/8), whose first run timed out in @objectstack/scenarios test/runner/external-driver.test.ts at vitest's 5000 ms default — a package the diff does not touch; its re-run was still in progress when 6714650391 was written. test(scenarios): give the external-driver crm run a measured 30s budget and a lighter stand-in #31284 (59d9815a), which gives that run a measured 30 s budget, is an ancestor of 212583dd (git merge-base --is-ancestor: yes), so the known red's cause is in this head's tree. I read nothing red as this PR's on either head. The owning seat reads the final conclusions on 212583dd — the eleven required contexts, Test Core (3/8) and the Test Core aggregate among them — before landing.
  • os-dev-reports 6703300597 and 6714512451: answered in 6714650391; nothing new arrived on the card or the PR thread for this head (the PR thread's newest comments are the claim 6714539606 and the record 6714650391).
  • Escalated, carried forward — should-fix 1 (the series' door, not this diff): install-local's cloud branch swallows the registry's bare-name refusal into a next-boot clash (④). marketplace-install-local-plugin.ts is unmoved by main, so the finding stands as written; the seat files it as its own card.

④ Design judgment

Each scoped file against the code it cites and the scenario it is accepted against; cited code READ, never reviewed — and, on this head, unmoved: the eight blobs and every cited file compare equal to what 6714650391 judged, so each judgment there holds here verbatim. In brief:

docs/adr/0048 (header Addenda, §3.4 note, P1.5, P1.6, P1.8) — against its first consumer (bare-name-one-holder.ts + registry.ts, same blobs) and its execution plan (ruling B, the remaining-types ruling, the series order): the ten types, the bundle-is-the-name reading, the baseline and the cold-boot sentence match the code; the "Not judged" list (ADR-0146 D9 stored row, ADR-0097's duplicate at boot, the kernel metadata service) verified against unmoved consumers; the served-holder sentences match flow-precedence.ts, getItem precedence 3, declared-webhooks.ts and the template loader's slot pick.

bare-name-one-holder.ts, registry.ts, plugin.ts — the one ordered list with the registry types derived; declaredBareNames by artifactPackageId (core's export, unmoved); bundleBaseKey before the name/suffix tests and the baseline filter after the claims merge; the seam's guard inside the predicate, baseName locale-less, nothing stored before a refusal; the seal's candidate scan skipping the bare slot; the seal call and the 'warn' switch unchanged. The baseline tried hard (impostor under its id, a nested plugin claiming it, the baseline over an app's name, two apps over one built-in name, an id-less manifest): no hole this PR opens; the impostor case is main's pre-existing same-id reload property.

The narrowing against the published spec on main (7c523aa1) — main moved none of the four consumers, so the answer is the same: on main the second declarer of a flow, webhook, connector or email template name was never served (one flow armed with a warning, the first webhook subscribed, the first connector descriptor served, the first template resolved), so the refusal makes the consumers' silent refusal real at the door with the rename, and takes no declared capability away — the ADR-0146 pair override stays a stored row, the ADR-0005 overlay stays unjudged, the ADR-0131 D6 app-over-built-in override still installs and renders, same-package reload is one holder.

Install-local — should-fix 1, carried forward, not a FAIL of this PR. The inline branch refuses 422 PLUGIN_REGISTER_FAILED before the ledger write (the dogfood's door). The cloud branch recognises only existingHolder.kind === 'environment' (refusesEnvironmentHeldCatalogName), so a bare-name refusal (kind: 'package' or 'built-in') takes #22695's lenient path — ledger entry written, success with hotLoaded: false — and the next boot rehydrates the package before the seal, so the cold boot admits the clash with one warning. Predates this PR (PR 1's mechanism, every covered type), outside the claim's file surface, neither opened nor widened here; the fix is one predicate on that branch plus a pin, filed as its own card under the series.

The two test files and the anchor — same blobs: 54 unit cells on a real kernel, every refusal by envelope with both holders and the rename, ABLATION cells before the seal, the vocabulary cell pinning both artifactPackageId spellings; the booted dogfood pin (one WARN line, install-local 422s, the showcase's names, the CONTROL with the spelling pin by behaviour and the app's subject rendered) importing nothing from objectql, its harness unmoved by main; the anchor's invariant naming the third family, the bundle reading and the baseline, ADR ids unchanged.

Observations carried (not this PR's): id impersonation under the baseline package id; the exported pair bundle through POST /packages; the /meta/email_template first-registered split beside the send's override.

must-fix: none. should-fix: 1 (carried forward from 6714650391 — install-local's cloud branch, the series' door in cloud-connection, filed as its own card, outside this diff). The merge itself adds nothing to judge: a clean merge of main, no hand resolution, an identical net diff, no cited code moved.

Implemented-by: claude/issue-30803-bare-name-pr3-flow-family
Reviewed-by: session_01FEkQ5o8PE7ZtVjcor51wBZ

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-project-manager · session_01FEkQ5o8PE7ZtVjcor51wBZ

维护者速读

  • 改了什么:同名「一个包只能持有一个」规则(Bare-name phase 1: refuse a second package holding a non-object name whose consumers resolve without package context (ruling C on #30798) #30803 第 3/4 步)扩到流程(flow)、Webhook、连接器(connector)和邮件模板(email_template):启动封印后,第二个包再注册同名的这四类元数据会被拒(422 NAMESPACE_CONFLICT,点名两个持有者并给出改名);冷启动遇到已有冲突只告警(裁决 B)。邮件服务的内置模板是可覆盖的基线,不占名字。ADR-0048 补 P1.8 等段。
  • 为什么改:这四类按名字解析、不带包上下文,第二个同名包之前会被静默遮蔽(实测:流程按字典序、Webhook/连接器/邮件模板按先注册者)。
  • 风险与回滚:@objectstack/objectql minor,标题带 !。会被新拒绝的只有「封印后第二个包声明已被持有的这四类名字」;一个应用覆盖内置邮件模板照旧可以。回滚即 revert。
  • 先看:packages/objectql/src/bare-name-one-holder.ts(十类的一张有序表、基线排除、email_template 成员键);docs/adr/0048-cross-package-metadata-collision.md 的 P1.8。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run src/registry-bare-name-one-holder-flow-family.test.ts,预期 54 个用例全绿。
  • 需要你做的:批准或直接合并本 PR(Tier H,docs/adr/**)。契约复审 PASS 6714650391,增量复审 PASS 6714753251,Head-sha 212583dd。

席位意见:建议批准。四类的拒绝与前两步同一机制,ADR 与代码、四个消费方一致;上一轮复审的发布面问题已按方案 (b) 关闭(index.ts 与 main 相同,不新增导出)。

席位核对 — ACCEPT · domain:server#5 · Head-sha 212583ddbc47ce4c7e4bee0862cd8a7e78dc474a · 2026-11-08T00:26Z


Generated by Claude Code

@objectstack-fleet objectstack-fleet Bot added the pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue label Nov 8, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review November 8, 2026 02:27
@os-zhuang
os-zhuang added this pull request to the merge queue Nov 8, 2026
Merged via the queue into main with commit a2826a1 Nov 8, 2026
79 of 81 checks passed
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 pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue 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

Development

Successfully merging this pull request may close these issues.

2 participants