Skip to content

os lint's naming/namespace-prefix reports a legitimate cross-package name reuse as an intra-package duplicate, contradicting the ADR-0048 §3.4 sentence in its own message #17821

Description

@claude

Symptom

os lint's intra-package duplicate advisory (naming/namespace-prefix, packages/cli/src/commands/lint.ts) reads ONE flattened array per collection key and has no idea which package each item came from. On a composed multi-package stack the flattened array carries both packages' items, so two packages that each legitimately declare home are reported as one package declaring it twice — and the warning's own last sentence says the opposite.

The rule's in-code comment states the contract it is implementing:

ADR-0048 §3.4 retired the per-item CROSS-package throw: package ids are globally unique, so two installed packages shipping the same bare name (e.g. page/home) legitimately COEXIST under distinct composite keys and each caller resolves to its own via package-scoped resolution. A bare name is therefore NOT a collision risk and must not warn on its own.
[…] We only see one package's config here, so the legitimate signal is a genuine duplicate (type, name) pair within it — never a unique bare name.

"We only see one package's config here" is false for a composed multi-package project, which is exactly the shape composeStacks(…, { manifest: 'preserve' }) emits.

Minimal repro — two packages, one bare name each

objectstack.config.ts, today's additive shape (flattened top level PLUS packages[], i.e. what the platform emits):

const appA = { name: 'home', label: 'Home A' };
const appB = { name: 'home', label: 'Home B' };
const obj = { name: 'ob_order', label: 'Order', sharingModel: 'private', nameField: 'number',
              fields: { number: { type: 'text', label: 'Number' } } };

export default {
  manifest: { id: 'com.example.a', name: 'a', version: '1.0.0', type: 'app', namespace: 'alpha', engines: { protocol: '^17' } },
  apps: [appA, appB], objects: [obj],
  packages: [
    { manifest: { id: 'com.example.a', name: 'a', version: '1.0.0', type: 'app', namespace: 'alpha', apps: [appA], objects: [obj] } },
    { manifest: { id: 'com.example.b', name: 'b', version: '1.0.0', type: 'module', namespace: 'beta',
                  dependencies: { 'com.example.a': '^1.0.0' }, apps: [appB] } },
  ],
};

Measured through the real binary on origin/main c9eb773ba, os lint, exit 0:

  Warnings (1)
  ⚠ App "home" is declared more than once in this package (also at apps[0].name). Two items of the
    same type sharing a bare name within one package shadow each other on the registry key
    (ADR-0048 §3.4) — rename one, e.g. "alpha_home". Distinct packages may reuse the same name
    freely; the namespace prefix is an optional convention, not a collision-avoidance requirement.
    naming/namespace-prefix  at apps[1].name

The two declarations are in com.example.a and com.example.b. Nothing shadows anything: they occupy distinct composite registry keys, which is precisely what the message's own final sentence says. The prescribed remedy (rename one, e.g. "alpha_home") asks the author to rename a name that is already correct, and alpha_ is the namespace of the OTHER package.

Cause, located

packages/cli/src/commands/lint.ts, the PREFIXED_TYPES loop:

const items: any[] = Array.isArray(stack[key]) ? stack[key] : [];

One array, one firstSeen map, no package boundary. The reader never sees which packages[i] an item came from, so "within ONE package" cannot be evaluated at all — it is asserted by the comment, not computed.

Scope note

Measured in both ADR-0130 D4 shapes, with and without #17528's fold (which moves that read from config to the union-folded stack):

stack shape before #17528 after #17528
additive (today's emitted shape) 1 false positive 1 false positive
packages[]-only (option B) 0 (the family read an empty stack) 1 false positive

So this is pre-existing on the shape the platform emits today and independent of that card: #17528 only brings the option-B spelling into parity with the additive one, which is its acceptance. Filed separately rather than folded in, because the fix is not mechanical — the advisory needs per-item package attribution (or to run per package body, as compile.ts's step 3b-ii rule run already does), and which of those is right is a design decision, not a wiring detail.

Found while driving #17528's parity controls.

⛔ Deduplication: REST /search/* is refused by this session's egress proxy (HTTP 403, sessions are bound to their configured repositories — verified this session), so one targeted MCP search_issues over open and closed, channel switch declared. 4 results, all read. Positive control fired: #5510 and #17069 both returned. Nearest neighbours rejected: #5510 (closed — the SAME rule false-positiving, but on actions whose dedup key omitted objectName; an intra-package, per-object axis, and its fix is the registryKey hook that is still in the code), #17069 (closed — which stack the rule table is handed, not how this advisory interprets it), #16565 (open — docs about one app per type: 'app' package), #4698 (closed — declared-but-never-read metadata). No card names the cross-package attribution axis.


Generated by Claude Code

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions