From 61ef9be23116afee2c82f8e489f3a721f0808558 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 05:31:44 +0000 Subject: [PATCH 1/2] docs(cli,examples): name the live owner-key helper in three stale comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #14667 (#14422) collapsed the standalone-action owner-key ladder onto one implementation: `standaloneActionOwnerKey` in `packages/objectql/src/action-governance.ts`, exported from `@objectstack/objectql`. The private `ObjectQLPlugin.actionObjectKey` copy is gone (0 occurrences in `packages/objectql/src/plugin.ts` at `origin/main` f3ae441; positive controls on the same class hit: `standaloneActionOwnerKey` 5, `registerAction` 4, `isArtifactShippedAction` 2, `class ObjectQLPlugin` 1). Three comments outside that PR's face still named the deleted method. Each explained something that still exists, so each now names its replacement rather than dropping the sentence: - `packages/cli/src/commands/lint.ts` — the composite-key rationale behind `PREFIXED_TYPES`' actions entry. - `packages/cli/test/lint-namespace-prefix.test.ts` — the same rationale for the #5510 dedup suite. - `examples/app-showcase/test/actions.test.ts` — the object-less specimen's mirror of the engine key. Also corrected in the same stroke: `standaloneActionObjectName` is no longer "kept in lockstep with" a sibling copy — it is a delegating alias that returns `standaloneActionOwnerKey(action)`. Comments-only; no behaviour changes. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza --- examples/app-showcase/test/actions.test.ts | 3 ++- packages/cli/src/commands/lint.ts | 20 ++++++++++--------- .../cli/test/lint-namespace-prefix.test.ts | 9 +++++---- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/examples/app-showcase/test/actions.test.ts b/examples/app-showcase/test/actions.test.ts index 4e26bac856..d39ecf96de 100644 --- a/examples/app-showcase/test/actions.test.ts +++ b/examples/app-showcase/test/actions.test.ts @@ -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(); diff --git a/packages/cli/src/commands/lint.ts b/packages/cli/src/commands/lint.ts index 5690b98f61..97334818ac 100644 --- a/packages/cli/src/commands/lint.ts +++ b/packages/cli/src/commands/lint.ts @@ -295,15 +295,17 @@ export function lintConfig(config: any, opts: LintConfigOptions = {}): LintIssue { key: 'dashboards', label: 'Dashboard' }, { key: 'flows', label: 'Flow' }, // An action's engine registration key is `:`, 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 diff --git a/packages/cli/test/lint-namespace-prefix.test.ts b/packages/cli/test/lint-namespace-prefix.test.ts index 7c570629e8..2143737c73 100644 --- a/packages/cli/test/lint-namespace-prefix.test.ts +++ b/packages/cli/test/lint-namespace-prefix.test.ts @@ -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 `:` - // (`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']; From c550b205e2137e3c7a8d796c9a336c1d82f5fe54 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 06:25:35 +0000 Subject: [PATCH 2/2] refactor(cli): converge the action-dedup object-less key onto GLOBAL_ACTION_OBJECT_KEY MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Folded in on a PM hand-off from #14678, whose seat deliberately left this one site: it sits inside the same docblock this PR was already editing, five lines below it, so for any other seat it is a merge conflict rather than a two-line change. Verified here rather than taken on report: - the ladder is real — `PREFIXED_TYPES`' `actions` entry built its key half with `typeof item?.objectName === 'string' && item.objectName ? item.objectName : 'global'`; - the role is the right one — that value becomes the object half of the `:` engine registration key `executeAction` looks up, which is the discriminator #14678 established (626 occurrences across 160 files share the token across at least five unrelated vocabularies; an `ownerLabel` that becomes a handler-name prefix is NOT this class); - the constant is reachable with no new dependency — `GLOBAL_ACTION_OBJECT_KEY` is exported from `@objectstack/objectql`'s entry (`src/index.ts`) and `@objectstack/cli` already declares that dependency. No behaviour moves: the constant is `'global'`, so every key this table builds is byte-identical. The #5510 dedup suite passes unchanged (24 tests over lint-namespace-prefix + the validate-build-gate-parity source pin). A changeset rides with it. The diff is no longer comments-only: it changes an expression that compiles into the published `dist/`, so the `skip-changeset` closed list no longer covers this shape. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza --- .../cli-action-dedup-global-key-constant.md | 22 +++++++++++++++++++ packages/cli/src/commands/lint.ts | 22 ++++++++++++++----- 2 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 .changeset/cli-action-dedup-global-key-constant.md diff --git a/.changeset/cli-action-dedup-global-key-constant.md b/.changeset/cli-action-dedup-global-key-constant.md new file mode 100644 index 0000000000..aca3ef454c --- /dev/null +++ b/.changeset/cli-action-dedup-global-key-constant.md @@ -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 +(`:`), 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. diff --git a/packages/cli/src/commands/lint.ts b/packages/cli/src/commands/lint.ts index 97334818ac..85e8018828 100644 --- a/packages/cli/src/commands/lint.ts +++ b/packages/cli/src/commands/lint.ts @@ -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'; @@ -307,10 +308,14 @@ export function lintConfig(config: any, opts: LintConfigOptions = {}): LintIssue // 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 @@ -319,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' },