Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .changeset/cli-action-dedup-global-key-constant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
"@objectstack/cli": patch
---

refactor(cli): spell the action-dedup object-less key as `GLOBAL_ACTION_OBJECT_KEY` (#14669)

`os lint` dedups action declarations on the engine's composite registration key
(`<objectName>:<name>`), and the object half of that key terminated on a bare
`'global'` string literal in `lintConfig`'s `PREFIXED_TYPES` table. The engine's
own writers stopped spelling the literal: PR #14667 converged
`ObjectQLPlugin.actionObjectKey` onto the shared `GLOBAL_ACTION_OBJECT_KEY`
constant for exactly this reason — a copy that agrees by value today is the one
that parts from the writer in silence the day the constant moves, with no test
in the repo able to see it. This reader now imports the constant from
`@objectstack/objectql`, which `@objectstack/cli` already depends on.

**No behaviour moves.** `GLOBAL_ACTION_OBJECT_KEY` is `'global'`, so every key
this table builds is byte-identical to the one it built before; the #5510 dedup
suite (`lint-namespace-prefix.test.ts`, 15 declarations over 5 objects) passes
unchanged. Only `objectName` is read, exactly as before — the `object`/`entity`
aliases are still rejected upstream by `ActionSchema`'s strict shape and are
deliberately not admitted here.
3 changes: 2 additions & 1 deletion examples/app-showcase/test/actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ describe('showcase actions — the object-less (`global`) specimen', () => {
const runner = new QuickJSScriptRunner();

it('declares no object, so it keys at `global` (framework#3913)', () => {
// This mirrors ObjectQLPlugin.actionObjectKey / AppPlugin's
// This mirrors `standaloneActionOwnerKey` (`@objectstack/objectql`, the
// helper the ObjectQL plugin calls) / AppPlugin's
// `action.object || 'global'`: neither field set → the 'global' bucket.
const a = PortfolioSnapshotAction as { objectName?: string; object?: string };
expect(a.objectName).toBeUndefined();
Expand Down
42 changes: 27 additions & 15 deletions packages/cli/src/commands/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import chalk from 'chalk';
import { bundleRequire } from 'bundle-require';
import { normalizeStackInput, type ConversionNotice } from '@objectstack/spec';
import { PROTOCOL_MAJOR } from '@objectstack/spec/kernel';
import { GLOBAL_ACTION_OBJECT_KEY } from '@objectstack/objectql';
import { loadConfig, BUNDLE_REQUIRE_EXTERNALS } from '../utils/config.js';
import { computeI18nCoverage, type CoverageIssue } from '../utils/i18n-coverage.js';
import { lintDataModel, runAuthoringRules } from '@objectstack/lint';
Expand Down Expand Up @@ -295,20 +296,26 @@ export function lintConfig(config: any, opts: LintConfigOptions = {}): LintIssue
{ key: 'dashboards', label: 'Dashboard' },
{ key: 'flows', label: 'Flow' },
// An action's engine registration key is `<objectName>:<name>`, NOT the
// bare name: `ObjectQLPlugin.actionObjectKey` (and the runtime's
// `standaloneActionObjectName`, kept in lockstep with it) resolve the
// object half to `objectName`, falling back to the canonical object-less
// key `'global'` (#3913). So one package legitimately declaring
// `log_call` on each of five objects occupies five distinct keys and
// nothing shadows anything — deduping those on the bare name produced 12
// fixed false positives per `objectstack lint` run on HotCRM, growing
// linearly with the object count (#5510), and "just rename one" would have
// broken the shared i18n keys that shape depends on (#592).
// bare name: `standaloneActionOwnerKey` in `@objectstack/objectql` — the
// single implementation, called directly by the ObjectQL plugin and
// re-exported by the runtime, whose `standaloneActionObjectName` is now a
// delegating alias for it — resolves the object half to `objectName`,
// falling back to the canonical object-less key `GLOBAL_ACTION_OBJECT_KEY`
// (`'global'`, #3913). So one package legitimately declaring `log_call` on
// each of five objects occupies five distinct keys and nothing shadows
// anything — deduping those on the bare name produced 12 fixed false
// positives per `objectstack lint` run on HotCRM, growing linearly with the
// object count (#5510), and "just rename one" would have broken the shared
// i18n keys that shape depends on (#592).
//
// `'global'` rather than an inert sentinel like `''` is deliberate: it is
// the literal the engine really registers under, so an action declared on
// an object actually NAMED `global` and an object-less action of the same
// name collide for real — and are reported, as they must be.
// `GLOBAL_ACTION_OBJECT_KEY` rather than an inert sentinel like `''` is
// deliberate: it is the key the engine really registers under, so an action
// declared on an object actually NAMED `global` and an object-less action
// of the same name collide for real — and are reported, as they must be.
// It is spelled as the imported constant rather than a bare `'global'`
// literal so this reader cannot part from the engine's writer in silence
// the day the constant moves — the same divergence #14667 removed from the
// plugin's own copy.
//
// Only `objectName` is read. `object`/`entity` are rejected outright by
// `ActionSchema`'s strict shape with a rename prescription, so they never
Expand All @@ -317,8 +324,13 @@ export function lintConfig(config: any, opts: LintConfigOptions = {}): LintIssue
{
key: 'actions',
label: 'Action',
registryKey: (item, name) =>
`${typeof item?.objectName === 'string' && item.objectName ? item.objectName : 'global'}:${name}`,
registryKey: (item, name) => {
const objectKey =
typeof item?.objectName === 'string' && item.objectName
? item.objectName
: GLOBAL_ACTION_OBJECT_KEY;
return `${objectKey}:${name}`;
},
},
{ key: 'reports', label: 'Report' },
{ key: 'datasets', label: 'Dataset' },
Expand Down
9 changes: 5 additions & 4 deletions packages/cli/test/lint-namespace-prefix.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,11 @@ describe('lint — intra-package duplicate-name advisory (ADR-0048 §3.4)', () =

describe('lint — actions dedup on the composite engine key, not the bare name (#5510)', () => {
// The engine registers an action under `<objectName>:<name>`
// (`ObjectQLPlugin.actionObjectKey`; the runtime's
// `standaloneActionObjectName` is kept in lockstep with it), with the
// canonical object-less key `'global'` (#3913). Deduping on the bare name
// asked a question the registry never asks.
// (`standaloneActionOwnerKey` in `@objectstack/objectql`, which the ObjectQL
// plugin calls directly and the runtime's `standaloneActionObjectName` now
// delegates to), with the canonical object-less key `'global'`
// (`GLOBAL_ACTION_OBJECT_KEY`, #3913). Deduping on the bare name asked a
// question the registry never asks.
const ACTIVITY_ACTIONS = ['log_call', 'log_meeting', 'schedule_meeting'];
const CRM_OBJECTS = ['crm_lead', 'crm_contact', 'crm_account', 'crm_opportunity', 'crm_case'];

Expand Down
Loading