Skip to content

[decision] A non-object metadata name shared by two installed packages often resolves to one package silently: which naming contract replaces ADR-0048 §3.4? #30798

Closed

Description

@objectstack-fleet

epic:#3600#1 · hotlong · session_01Q4EjjPXwX1YfHyKJUh4agu

Filing-gate category: ② decision the maintainer directed. Maintainer, verbatim (Claude Code session, 2026-11-06), on the seat's bare-name audit: 「裸名的问题立决策卡」. Origin: the scenario-library epic objectstack-ai/cloud#3600 (gallery apps installed side by side in one environment).

Maintainer brief

  • The problem in one sentence: when two installed apps each ship, say, an automation or a button with the same name, the platform often runs only one of them — usually silently, and which one depends on install order.
  • What you decide: how a non-object metadata name stays unambiguous across installed packages. Options A, B-lite, B, C, D below; the seat recommends C, phased.
  • If nothing is decided: every new marketplace or gallery app that reuses a common name (send_email, approve, sales_rep) can silently disable part of another app; nobody sees an error.

Background

Only object names must carry the package namespace (<ns>_<short>, ADR-0129). Every other metadata type — flows, actions, hooks, picklists, pages, dashboards, reports, apps, datasets, api items, email templates, agents, tools, skills, jobs, connectors, webhooks, … — has a bare name. ADR-0048 decided that two packages MAY ship the same bare name: storage keeps both (unique on type, name, package_id, bundle_member), §3.3 says "each caller resolves to its own package's item", and §3.4 says two distinct packages "coexist on the same bare name by construction". The 2026-10-08 narrowing of §3.4 states its premise, "§3.4 rests on every caller carrying its package id", and took the security catalog out because assignments carry no package context. For action the ADR never claimed package scoping (§6 lists it as intentionally global; same-name actions on different objects are keyed objectName:name and do not collide).

Governing text

Analysis re-read: an isolated reviewer at the contract-review tier re-verified every cited line and answered PASS WITH FIXES; all ten fixes are applied in this text.

Protocol statement: this card changes ADR-0048 §3.4's scope (every option but D) and, under C, ADR-0129's rule. No fleet protocol change.

Premise (re-check commands)

The seat's audit (read-only, 2026-11-06, origin/main 802c98c; re-verified by an isolated reviewer at 04f36b6) finds §3.4's premise false for most behaviour types: their runtime consumers resolve by bare name with no package context. Re-check:

grep -n "register(" packages/metadata/src/metadata-manager.ts | head      # kernel 'metadata' service keyed (type, name); second register overwrites (~853-858); list dedups by name (~1407-1412)
sed -n 340,400p packages/services/service-automation/src/flow-precedence.ts   # one flow armed per name, ranked by package id alphabetically, warning logged
sed -n 4970,4990p packages/runtime/src/action-execution.ts                   # action dispatch: by-name read with no package
sed -n 415,425p ../cloud/packages/service-ai/src/tools/action-tools.ts        # AI tool name action_<bare name>; second skipped (~1014-1017)
sed -n 250,260p ../cloud/packages/service-ai/src/tools/tool-registry.ts       # ToolRegistry.register overwrites silently
sed -n 100,110p packages/objectql/src/engine-hooks-and-functions.ts           # function registry keyed by bare name, last wins
sed -n 7900,7945p packages/objectql/src/registry.ts                           # managed install refuses only namespace / object / security-catalog / default-page clashes

Measured outcome per type (non-security types only):

Runtime behaviour on a second same-name package item Types
Both served correctly job (a second holder is scheduled under appId:jobName, #21602), assignment rules (both evaluated)
One armed, warning logged, deterministic flow
One served, silently everything read through the kernel metadata service (agents, skills, external catalogs, api items via listForIndex), dispatch of an object-less action, AI tools (action_<name>, ToolRegistry), function handlers behind hooks, picklists, mappings, datasets/cubes, email templates, webhooks, two declared connectors
Package-scoped when the caller names the package, first match otherwise (reads only: a package-less save or delete of an ambiguous name already answers 409) app, page, dashboard, report, standalone view, doc/book

A security-relevant subset of the audit is with the maintainer separately and is not described here.

The specific question

§3.4 promised coexistence; most execution-time consumers cannot honour it. Which contract replaces it?

Options

Option What it does What a customer sees Workload
A. One name, one holder, everywhere Extend the security catalog's refusal to every type whose consumers resolve without package context: install and package registration refuse a held name, naming both holders The second app's install is refused with "rename X or uninstall Y"; nothing breaks silently, but two unrelated vendors' apps that both use send_email cannot coexist domain:engine · 1–2 PRs · precedent #22197 (+1080/−103) and #22365 (+705/−43) for three types
B-lite. Package-scope only the execution-time consumers Flow engine, object-less action dispatch, function registry and AI tool names resolve by (package, name); everything else stays as is Fewer silent cases, but a cross-package trigger or action must still name a package, and every new consumer must remember to several lanes (automation, runtime, ai) · ~4 PRs · no precedent
B. Make every consumer package-aware Honour §3.4: each runtime consumer resolves by (package, name); cross-package references use the qualified <packageId>:<name> form Same-name apps coexist; authors and AI must choose between a bare and a qualified reference at every cross-package site many lanes (automation, runtime, ai, mcp, metadata, analytics, email, webhooks) · ~10+ PRs · no precedent
C. Every name carries the namespace Extend ADR-0129's literal prefix to every package-authored top-level metadata name (crm_send_email), except names already derived from a prefixed object (fields, object extensions, container views) or already namespaced (translations <ns>_<locale>); enforced at defineStack/os validate and the save door; same-name clashes become impossible by construction Names read crm_send_email; two apps never collide; one naming rule for everything domain:engine + domain:devx · 3–4 PRs (rule + ADR amendment, load-time conversion or migration of first-party packages/templates/examples, lint) · no measured precedent for a cross-type naming rule; nearest landed: #22197 (+1080/−103) and #22365 (+705/−43), three types
D. Status quo, fix consumers one by one Keep §3.4; file each silent consumer as found Silent partial breakage continues until each is found open-ended; no precedent

Business meaning, one line each:

  • A = "first come, first served" for names — like a shared drive that refuses a second file of the same name.
  • B-lite = folders only for the files that run, links into them must still say which folder.
  • B = every app gets its own folder, but every link must say which folder — the ADR-0028 shape.
  • C = every app's files start with the app's code. Salesforce managed packages show every custom component as ns__Name; ServiceNow scoped apps prefix tables, fields, properties and script-include API names x_<co>_<app>_ and scope the rest by application scope. Both derive the prefix; ObjectStack authors it literally (ADR-0129, for the one-writing-style reason).
  • D = keep the bug, chase symptoms.

Four axes (business stance)

Recommendation

C, phased. End state, two years out: every package-authored name carries its namespace, as every custom component in a Salesforce managed package does; a collision cannot be authored by a namespaced package, and the per-type refusal list is a backstop only, not the contract.

Phasing (② and workload adjust the order only; the letter does not change):

  1. Now: extend the existing registration refusal (A's mechanism) to the silent types. It closes silent mis-resolution for packages already in the field. After phase 3 it remains only as the backstop for the cases C cannot reach: packages that declare no namespace, the shared base/system/sys namespaces, and third-party packages inside the deprecation window.
  2. Next: the existing naming/namespace-prefix lint rule (today intra-package duplicates only, its message calling the prefix optional) is re-widened to warn on every bare non-object name, and ADR-0048 §5's lint bullet is corrected in the same PR; scenario/gallery cards and first-party templates adopt the prefix (already ruled for the gallery; live card Scenario cards carry an app code; the runner and gallery use the demo<code> namespace and refuse a bare non-object item name #30795).
  3. Then: defineStack/os validate and the save door refuse a bare name in a package that declares a namespace, with the rename in the message; first-party packages migrate in the same release, and third-party packages get a load-time conversion or a deprecation window (the lane's design call).

Confidence gaps: the per-type list is an audit reading, not a booted two-package measurement; some names may be referenced by external callers (inbound webhook ids, connector ids), where a rename is a breaking change — phase 3 must list them. The migration size of first-party packages (hotcrm, templates, examples) is not measured.

Not chosen: A alone keeps two naming rules and a list that grows with each new type. B-lite leaves every new execution-time consumer to remember package scoping, and adds the qualified form. B is the ADR-0028 direction the maintainer rejected for AI-error reasons. D leaves silent breakage in place.

Self-check: on ① alone C; do ② ③ ④ flip it: no.

Execution after the ruling

  • C: the seat files three domain:engine / domain:devx cards (phase 1 refusal backstop; phase 2 lint + templates; phase 3 rule + ADR-0129 / ADR-0048 §3.4 amendment + migration), serial in that order, and records the ruling on objectstack-ai/cloud#3600.
  • A: one domain:engine card (the phase-1 card alone) plus the ADR-0048 §3.4 narrowing.
  • B or D: the seat files per-consumer cards from the table above.

os-decision-facets

Prior rulings read: "one name, one holder" → 10 hits (security catalog, #28337); "name collision" package metadata → 193 hits (relevant: #21602, #28337, #22307 A); "needs-user-decision prefix name" → #29594 (a different question); ADR-0048 §3.3–§3.5, §5, §6 + 2026-10-08 addendum, ADR-0129 D2–D4, ADR-0028 (Deferred), ADR-0146 D2/D8; thread: none

Related


Generated by Claude Code

Activity

  1. added
    priority:p1High: required for production / M2
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they land
    area:devpathThe road — create, dev, verify, publish/install, connect an agent, iterate
    on Nov 6, 2026
  2. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: audit · domain:engine → domain:server · p1 kept · needs-user-decision · 2026-11-06T03:53Z

    ⛔ Not a claim.

    • domain:engine is a retired alias of domain:server. A decision card on the naming contract that replaces ADR-0048 for a non-object name shared by two installed packages. It waits for the maintainer.

    Generated by Claude Code

  3. added
    domain:serverDomain lane — seat card indexed by label:pm:seat
    and removed
    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they land
    on Nov 6, 2026
  4. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#3600#1 · hotlong · session_01Q4EjjPXwX1YfHyKJUh4agu

    Ruled: C

    Maintainer, verbatim (Claude Code session, 2026-11-06): 「同意 c」, after the seat's multi-dimension analysis in session. Every package-authored metadata name carries its namespace, phased.

    Execution, all filed and handed to lanes (maintainer, verbatim: 「能交给车队开发的你不要自己开发」):

    The security-relevant subset stays with the security seat. Closing this decision card as completed; the phase cards carry the work.

  5. removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Nov 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratedomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p1High: required for production / M2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions