Skip to content

Commit 2511a98

Browse files
xuyushun441-sysos-zhuangclaude
authored
feat(cli): ADR-0048 follow-up — namespace-prefix lint warning (#1804)
Shifts the cross-package metadata collision detection (ADR-0048 runtime MetadataCollisionError) left to authoring time. `os lint` now emits a non-fatal `naming/namespace-prefix` warning when a bare-named UI/automation item (app/page/dashboard/flow/action/report/dataset) is not prefixed with the package namespace — the clash these names risk fails loudly at registration; this nudges authors to prefix before it ever gets there. - Exempts an app named after the namespace (ADR-0019 single-app convention, e.g. `crm`) and `sys_`-reserved names. - Objects (already prefix-enforced as an error) and object-derived views are untouched; `doc` keeps its own build-time strict lint. - Warning-only: never fails `os lint` (only errors exit non-zero) and `os build` does not run lintConfig, so example builds are unaffected. - Marks the ADR-0048 §4 lint follow-up as implemented. Tests: 7 cases (warn/accept/app-exemption/sys_/all-types/no-namespace/ objects-views-untouched). Full lint suite green (38). Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 05717ed commit 2511a98

4 files changed

Lines changed: 128 additions & 3 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@objectstack/cli": minor
3+
---
4+
5+
ADR-0048 follow-up: `os lint` now emits a `naming/namespace-prefix` **warning** when a bare-named UI/automation item is not namespace-prefixed. This shifts the cross-package collision detection (ADR-0048, runtime `MetadataCollisionError`) left to authoring time — a soft nudge to prefix `app`/`page`/`dashboard`/`flow`/`action`/`report`/`dataset` names with the package namespace, so a clash with another package is unlikely to ever reach install.
6+
7+
Warning-only and never fatal (only errors fail the lint). An app named after the namespace (ADR-0019 single-app convention, e.g. `crm`) and `sys_`-reserved names are exempt; objects (already prefix-enforced as an error) and object-derived views are untouched.

docs/adr/0048-cross-package-metadata-collision.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,9 +199,13 @@ detect.**
199199
an error; the fix (rename with a namespace prefix, or `warn` during
200200
migration) is in the message.
201201
- No change to the key shape, the overlay model, or object/nav paths.
202-
- Follow-ups (not in this ADR): a CLI lint that flags non-prefixed
203-
bare-named metadata as a warning; prefix-strict spec validation for the
204-
next net-new bare-named type.
202+
- Follow-ups: a CLI lint that flags non-prefixed bare-named metadata as a
203+
warning — **implemented** as the `naming/namespace-prefix` rule in
204+
`os lint` (covers `app`/`page`/`dashboard`/`flow`/`action`/`report`/
205+
`dataset`; exempts an app named after the namespace per ADR-0019 and
206+
`sys_` names; warning-only, never fatal). This shifts detection left from
207+
the registration-time guard to authoring time. Still open: prefix-strict
208+
spec validation for the next net-new bare-named type.
205209

206210
## 5. Implementation notes
207211

packages/cli/src/commands/lint.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,51 @@ export function lintConfig(config: any): LintIssue[] {
216216
}
217217
}
218218

219+
// ── Namespace-prefix advisory for bare-named metadata (ADR-0048 §3.3) ──
220+
// Cross-package collision detection (ADR-0048) makes a clash between two
221+
// packages' bare-named items (`page`/`flow`/`action`/…) a loud error at
222+
// registration time. This shifts that left: a non-fatal nudge to prefix
223+
// the name with the package namespace at authoring time, so the clash is
224+
// unlikely to ever happen. Objects are already prefix-*enforced* (error)
225+
// in defineStack; views are object-derived; `doc` has its own build lint
226+
// — so they are excluded here. Warning only — prefix stays a recommended
227+
// convention for legacy types, not a retroactive requirement.
228+
const ns: string | undefined = config.manifest?.namespace;
229+
if (ns) {
230+
// A name is namespace-safe if it IS the namespace (ADR-0019: a package's
231+
// single app is conventionally named after the namespace, e.g. `crm`),
232+
// is prefixed with it (`crm_lead`), or is a platform-reserved `sys_` name.
233+
const isNamespaceSafe = (name: string) =>
234+
name === ns || name.startsWith(`${ns}_`) || name.startsWith('sys_');
235+
236+
// Bare-named UI/automation types subject to the cross-package collision
237+
// (ADR-0048 §1.1). Data-driven so a new bare-named type is one line.
238+
const PREFIXED_TYPES: Array<{ key: string; label: string }> = [
239+
{ key: 'apps', label: 'App' },
240+
{ key: 'pages', label: 'Page' },
241+
{ key: 'dashboards', label: 'Dashboard' },
242+
{ key: 'flows', label: 'Flow' },
243+
{ key: 'actions', label: 'Action' },
244+
{ key: 'reports', label: 'Report' },
245+
{ key: 'datasets', label: 'Dataset' },
246+
];
247+
248+
for (const { key, label } of PREFIXED_TYPES) {
249+
const items: any[] = Array.isArray(config[key]) ? config[key] : [];
250+
for (let i = 0; i < items.length; i++) {
251+
const name = items[i]?.name;
252+
if (typeof name !== 'string' || !name || isNamespaceSafe(name)) continue;
253+
issues.push({
254+
severity: 'warning',
255+
rule: 'naming/namespace-prefix',
256+
message: `${label} "${name}" is not namespace-prefixed. Two packages defining "${name}" collide on the registry key and fail at install (ADR-0048). Rename to "${ns}_${name}".`,
257+
path: `${key}[${i}].name`,
258+
fix: `${ns}_${name}`,
259+
});
260+
}
261+
}
262+
}
263+
219264
// ── Data-model best practices (relationships / master-detail / roll-ups) ──
220265
// Cross-object rules that encode the conventions in ADR-0035 and the
221266
// objectstack-data/-ui skills. These double as the eval rubric (see score.ts).
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { describe, expect, it } from 'vitest';
4+
import { lintConfig } from '../src/commands/lint';
5+
6+
const RULE = 'naming/namespace-prefix';
7+
const prefixIssues = (config: any) => lintConfig(config).filter((i) => i.rule === RULE);
8+
9+
describe('lint — namespace-prefix advisory (ADR-0048 §3.3)', () => {
10+
it('warns on a bare-named page without the namespace prefix', () => {
11+
const issues = prefixIssues({
12+
manifest: { namespace: 'crm' },
13+
pages: [{ name: 'home', label: 'Home' }],
14+
});
15+
expect(issues).toHaveLength(1);
16+
expect(issues[0].severity).toBe('warning');
17+
expect(issues[0].path).toBe('pages[0].name');
18+
expect(issues[0].fix).toBe('crm_home');
19+
expect(issues[0].message).toContain('ADR-0048');
20+
});
21+
22+
it('accepts a namespace-prefixed name', () => {
23+
expect(
24+
prefixIssues({ manifest: { namespace: 'crm' }, flows: [{ name: 'crm_onboarding' }] }),
25+
).toHaveLength(0);
26+
});
27+
28+
it('exempts an app named after the namespace (ADR-0019 single-app convention)', () => {
29+
// `defineApp({ name: 'crm' })` in namespace `crm` must NOT warn.
30+
expect(
31+
prefixIssues({ manifest: { namespace: 'crm' }, apps: [{ name: 'crm' }] }),
32+
).toHaveLength(0);
33+
});
34+
35+
it('exempts platform-reserved sys_ names', () => {
36+
expect(
37+
prefixIssues({ manifest: { namespace: 'crm' }, pages: [{ name: 'sys_admin' }] }),
38+
).toHaveLength(0);
39+
});
40+
41+
it('covers every bare-named UI/automation type', () => {
42+
const issues = prefixIssues({
43+
manifest: { namespace: 'crm' },
44+
apps: [{ name: 'other' }],
45+
pages: [{ name: 'home' }],
46+
dashboards: [{ name: 'overview' }],
47+
flows: [{ name: 'onboard' }],
48+
actions: [{ name: 'send' }],
49+
reports: [{ name: 'pipeline' }],
50+
datasets: [{ name: 'sales' }],
51+
});
52+
expect(issues).toHaveLength(7);
53+
expect(new Set(issues.map((i) => i.severity))).toEqual(new Set(['warning']));
54+
});
55+
56+
it('is silent when the package declares no namespace', () => {
57+
// No manifest.namespace → nothing to prefix against; stays quiet.
58+
expect(prefixIssues({ pages: [{ name: 'home' }] })).toHaveLength(0);
59+
});
60+
61+
it('does not touch objects (already prefix-enforced as an error) or views', () => {
62+
const issues = prefixIssues({
63+
manifest: { namespace: 'crm' },
64+
objects: [{ name: 'lead', fields: { id: { type: 'text' } } }],
65+
views: [{ name: 'lead.all' }],
66+
});
67+
expect(issues).toHaveLength(0);
68+
});
69+
});

0 commit comments

Comments
 (0)