Skip to content

Commit 11137d4

Browse files
fix(lint): the dataset save door's object-reference-unknown hint names the dependency declaration under a package closure (#27937)
<!-- claude-projects-attribution: {"github_login":"marchtian"} --> _Requested by **田騏** · [project thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSiEosMC3zDfHaauURHFNq1GJ)_ Fixes #27827 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:运行时保存门在软件包闭包下拒收一个 dataset(其基础对象不在闭包内)时,`object-reference-unknown` 的修复提示改为"在 manifest 的 `dependencies` 里声明对定义该对象的软件包的依赖";三扇门(flow 触发、列表视图、dataset)共用同一个提示构造函数。 - **为什么改**:原提示叫作者"指向本 stack 的对象",把人和 AI 都引离真正有效的修法(ADR-0141 D1:声明依赖);另两扇门早已给出声明依赖的处方,dataset 门是唯一的例外。 - **风险与回滚**:只动提示文字,判定、严重级别、message 都不变;无闭包(CLI、未指明软件包的写入)时提示与今天逐字相同。flow 门的提示措辞随共用形式微调(引号与语序),回滚即 revert 本 PR。 - **先看**:`packages/lint/src/object-graph.ts`(`closureDependencyRemedy`)、`packages/lint/src/validate-object-references.ts`(闭包分支)、`packages/lint/src/reference-integrity-suite.ts`(把 `packageScope` 交给该规则)。 - **两分钟验证**:`pnpm --filter @objectstack/lint exec vitest run src/runtime-gate.dataset-writes.test.ts src/runtime-gate.package-closure.test.ts`,预期全绿,其中 #27827 两个 describe 块覆盖有闭包/无闭包两向。 - **需要你做的**:无,席位落地。 ## What changes At the runtime save door a dataset written into a package is judged against that package's closure (`narrowObjectsToPackageClosure`). A base object outside it lands on rung ② of `validateObjectReferences` and is refused `422` with `object-reference-unknown`. Under a closure the fix line now names the manifest `dependencies` declaration, the prescription the flow trigger and list view doors print for the same closure. - **The fact, handed over the same way.** `validateObjectReferences` takes `options.packageScope`, the shape `validateFlowTriggerReadiness` and `validateListViewFieldRefs` take. Its reference-integrity member passes `ctx.packageScope` through, as `validateListViewFieldRefs`' member does. The gate states `packageScope` only when it narrowed the snapshot (`packageScopeInForce`), so the rule is never told "closure" about an unnarrowed snapshot. - **One shared prescription, not a third copy.** `closureDependencyRemedy(object, packageId, alternative)` sits in `object-graph.ts`, the module all three doors already import (`suggestName` lives there). The flow door (`flow-trigger-unknown-object`), the list view door (`list-view-object-outside-closure`) and the dataset door (`object-reference-unknown`) build their line from it; each passes its own second remedy. - **Only the hint moves.** The ladder, severity, message and `where`/`path` are identical with and without a closure. A site's own sharper remedy (the view container's namespace prescription) still wins, and the platform rung ④ is untouched (narrowing keeps platform objects, so a prefixed miss is never a closure case). - **`os explain object-reference-unknown`** gains a paragraph for the closure case, so it no longer contradicts the new fix line. ### The hint, before and after | Door | Before | After | |:--|:--|:--| | dataset base object, closure in force | `Point it at one of this stack's objects, or at a platform object by its full name (the platform user object is "sys_user", not "user")` | `` Declare a dependency on the package that defines "OBJECT" in the `dependencies` of "PACKAGE"'s manifest, or point it at an object inside that closure `` | | dataset base object, no closure (CLI, unpackaged write) | the `Point it at …` line | unchanged | | flow trigger (both positions) | `` Declare a dependency on the package that defines 'OBJECT' in the manifest `dependencies` of package 'PACKAGE', or set KEY to an object inside that closure `` | `` Declare a dependency on the package that defines "OBJECT" in the `dependencies` of "PACKAGE"'s manifest, or set KEY to an object inside that closure `` | | list view | `` Declare a dependency on the package that defines "OBJECT" in the `dependencies` of "PACKAGE"'s manifest, or bind the view to an object inside that closure `` | unchanged | The shared form is the list view door's wording, because the runtime package-dependency door comments and ADR-0141 already quote it ("declare a dependency … in the `dependencies` of P's manifest"). Unifying changes the flow door's printed words: double quotes in place of single, and "PACKAGE's manifest" in place of "the manifest `dependencies` of package 'PACKAGE'". No pin asserted the flow door's exact sentence. Its pins (the hint contains the package id, `dependencies` and the re-pointed key) hold as written, and the new shared-prescription pin asserts its full new text. ## Tests - `runtime-gate.dataset-writes.test.ts`, new block "the base-object fix line names the dependency declaration under a package closure (#27827)". Through the real door, both directions: - closure in force: the full new hint for an undeclared package's object, and the same fix for a name that exists nowhere. - the own package and a declared dependency are admitted. - no closure: the stranger's object resolves, and a ghost gets today's hint byte for byte. An empty package id states no closure. The direct `validateObjectReferences` call (the CLI reading) gives the same rule, severity, message and today's hint. - the verdict is identical both ways; only the hint differs. - `runtime-gate.package-closure.test.ts`, new block: the dataset door under the shared `packagedTenant` closure. It also pins that the three doors print one prescription: the same declaration clause, each with its own alternative, asserted in full. - `validate-object-references.test.ts`: the unit-level pins. These cover the closure hint, the unchanged verdict, a site's sharper remedy winning under a closure, and an untouched platform rung. The one-line-fix coverage control now also requires the closure line to have fired (at most 200 chars). The `os explain` pin requires the closure paragraph. - **Pin flipped:** `reference-integrity-suite.test.ts` "wires the same function the package exports" asserted `run === validateObjectReferences` by identity. The member is now an adapter that hands over `ctx.packageScope`, so the pin asserts what the member answers instead: the exported function's answer without a closure and with the context's closure, on a stack that carries a real miss, and the two answers differ. <details><summary>Local runs (head 45b851c)</summary> - `pnpm --filter @objectstack/lint test`: Test Files 178 passed (178), Tests 7639 passed, 5 skipped (7644). Lock VERDICT command-exit 0. - `pnpm --filter @objectstack/lint typecheck`: exit 0 (`check:test-typecheck: OK`). - Targeted run, the 7 touched suites: Test Files 7 passed, Tests 428 passed. </details> <details><summary>Ablation (one, on the tested behaviour)</summary> The closure branch was disabled through `scripts/ablation-replace.mjs` on the committed head, with anchor `(closure` replaced by `(false && closure` in `validate-object-references.ts` (anchor 1 → 0, blob `38f25ec47981` → `f71aad39f462`). The four suites that pin it ran under that mutation: Test Files 4 failed (4), Tests 8 failed / 130 passed. Every #27827 closure-direction pin went red, plus the flipped member pin and the one-line coverage control. The no-closure pins stayed green, as they should, since they assert today's hint. The mutation was restored: blob after restore `38f25ec47981` == HEAD, `git diff HEAD` empty. The tests import `src/` directly, so no `dist/` leg applies. </details> ## Acceptance notes - **Not changed, on purpose:** the verdict sentence ("resolves to no object defined in this stack. The reference is inert at runtime — nothing reports the miss.") still reads stack-relative under a closure, while the flow and view doors' messages name the closure. The order fixed severity and verdict as out of scope; this is noted only. - **Related observation, from the card, not this PR's scope:** an `object` write's field lookup to a name that exists nowhere is judged by no door (`validateObjectReferences` is not dispatched on an `object` write). This belongs with the #25805 reopen. ## Verification Independent review: none run (quota); the seat records the contract review. Gates: the derived local family list (`dispatch-gates.mjs --commands`) was run on the final diff, with each exit code recorded before any pipe and `--ran` reconciled; the readings are in the os-dev-report on the card. CI runs the full farm. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9900366 commit 11137d4

11 files changed

Lines changed: 319 additions & 16 deletions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/lint': patch
3+
---
4+
5+
fix(lint): a dataset refused at the save door for a base object outside its package's closure prints the dependency declaration as the fix
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
At the runtime save door a dataset written into a package is judged against that package's closure: its own objects, the objects of the packages its manifest declares in `dependencies`, platform objects, and tenant objects that belong to no package. A base object outside it is refused `422` with `object-reference-unknown`, and its fix line used to read "Point it at one of this stack's objects, or at a platform object by its full name", which sends the author away from the fix that works. Under a closure it now reads "Declare a dependency on the package that defines "OBJECT" in the `dependencies` of "PACKAGE"'s manifest, or point it at an object inside that closure", the prescription the flow trigger and list view doors print for the same closure (ADR-0141 D1).
11+
12+
- **One prescription.** The three doors build that line from one helper, so they cannot drift apart again. The list view door's line is unchanged. The flow trigger door's line now quotes the object and package with double quotes and names "PACKAGE's manifest" instead of "the manifest `dependencies` of package 'PACKAGE'"; its second remedy (`set config.objectName …` / `set config.timeRelative.object …`) is unchanged.
13+
- **Only the fix line moves.** The verdict, the severity and the message are the same with or without a closure.
14+
- **Unchanged without a closure.** A write that names no package, and `os validate`, `os build` and `os lint`, print the hint they printed before.
15+
- `os explain object-reference-unknown` describes the closure case.

‎packages/lint/src/object-graph.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,29 @@ export function suggestName(target: string, known: Iterable<string>): string {
612612
return best ? ` Did you mean "${best}"?` : '';
613613
}
614614

615+
/**
616+
* The fix line for an object reference outside a package closure — ONE
617+
* prescription, printed by every save-door rule that judges an object name
618+
* under a stated `packageScope` (#27827): the flow trigger door
619+
* (`flow-trigger-unknown-object`), the list view door
620+
* (`list-view-object-outside-closure`) and the dataset base-object door
621+
* (`object-reference-unknown`). The closure is the written package's own
622+
* objects, the objects of the packages its manifest declares in
623+
* `dependencies`, and platform objects, so the declaration is the fix that
624+
* puts another package's object in reach (ADR-0141 D1). Three hand-written
625+
* copies are how one door came to print a remedy pointing away from it.
626+
*
627+
* `alternative` is the door's own second remedy — the key it re-points, as a
628+
* lower-case verb phrase with no trailing period. Like {@link suggestName},
629+
* this decides no severity and holds no rule id; the caller's verdict does.
630+
*/
631+
export function closureDependencyRemedy(object: string, packageId: string, alternative: string): string {
632+
return (
633+
`Declare a dependency on the package that defines "${object}" in the \`dependencies\` of ` +
634+
`"${packageId}"'s manifest, or ${alternative}`
635+
);
636+
}
637+
615638
function distance(a: string, b: string): number {
616639
const m = a.length;
617640
const n = b.length;

‎packages/lint/src/reference-integrity-suite.test.ts‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,21 @@ describe('reference-integrity suite — membership', () => {
9797

9898
it('wires the same function the package exports', () => {
9999
const byName = new Map(REFERENCE_INTEGRITY_RULES.map((r) => [r.name, r.run]));
100-
expect(byName.get('validateObjectReferences')).toBe(validateObjectReferences);
101100
expect(byName.get('validateTranslationReferences')).toBe(validateTranslationReferences);
101+
// [#27827] `validateObjectReferences` is wired through an adapter that hands
102+
// it `ctx.packageScope`, the way `validateListViewFieldRefs` is, so the pin
103+
// is on what the member ANSWERS: the exported function's answer, with the
104+
// context's closure and without it. The stack carries a real miss, so the
105+
// two equalities cannot hold by both sides being empty.
106+
const member = byName.get('validateObjectReferences')!;
107+
const stack = { objects: [{ name: 'crm_lead', fields: {} }], datasets: [{ name: 'd', object: 'crm_ghost' }] };
108+
const scope = { packageId: 'com.acme.crm', dependencies: [] };
109+
expect(validateObjectReferences(stack)).toHaveLength(1);
110+
expect(member(stack)).toEqual(validateObjectReferences(stack));
111+
expect(member(stack, { runtimeWriteType: 'dataset', packageScope: scope })).toEqual(
112+
validateObjectReferences(stack, { packageScope: scope }),
113+
);
114+
expect(member(stack, { runtimeWriteType: 'dataset', packageScope: scope })).not.toEqual(member(stack));
102115
});
103116
});
104117

‎packages/lint/src/reference-integrity-suite.ts‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,20 @@ export const REFERENCE_INTEGRITY_RULES: readonly ReferenceIntegrityRule[] = [
224224
// — 0 findings, precision 1.0. The zero is a fact about the corpus AND the
225225
// door: `rulesRun` read 2 on every one, and the same harness's synthetic
226226
// probe is refused (`runtime-gate.dataset-writes.test.ts` makes it permanent).
227-
{ name: 'validateObjectReferences', runtimeTypes: ['flow', 'dataset'], run: validateObjectReferences },
227+
//
228+
// [#27827] It reads `packageScope` the way `validateListViewFieldRefs` below
229+
// does: under a stated closure the snapshot's `objects` are that closure, so
230+
// a base object missing from it is outside what the package declared it may
231+
// reference, and the `object-reference-unknown` fix line is the closure
232+
// prescription the flow and view doors print (declare the dependency,
233+
// ADR-0141 D1). Absent — the CLI, or a write that names no package — the
234+
// rule prints the hint it always did. The verdict and severity do not move.
235+
{
236+
name: 'validateObjectReferences',
237+
runtimeTypes: ['flow', 'dataset'],
238+
run: (stack, ctx) =>
239+
validateObjectReferences(stack, ctx?.packageScope ? { packageScope: ctx.packageScope } : {}),
240+
},
228241
// [#9313] `runtimeTypes` gains `view` on this member and its sort sibling:
229242
// both judge a LIST VIEW's field references, and a standalone list view is
230243
// written through `PUT /api/v1/meta/view` — the only door a Studio tenant or
@@ -310,7 +323,8 @@ export const REFERENCE_INTEGRITY_RULES: readonly ReferenceIntegrityRule[] = [
310323
// crossing is its own adjudication with its own non-vacuity control, and
311324
// this card was scoped to the two axes its title names.
312325
//
313-
// [#24563] The one member that reads `packageScope`. Under a package
326+
// [#24563] A member that reads `packageScope` (#27827: so does
327+
// `validateObjectReferences` above, for its fix line only). Under a package
314328
// closure the snapshot's `objects` are that closure (ruling #9612), so a
315329
// binding missing from it is outside what the package declared it may
316330
// reference, or nowhere at all — and the member's skip 1 used to read that as
@@ -769,7 +783,9 @@ export interface ReferenceIntegrityRunOptions {
769783
* force; ABSENT on the CLI commands and on a write that names no package.
770784
* Read by `validateListViewFieldRefs` and `validateRelatedListRefs`, which
771785
* read its absence beside a set `runtimeWriteType` as the whole collection
772-
* (#24668).
786+
* (#24668), and by `validateObjectReferences`, whose
787+
* `object-reference-unknown` fix line names the dependency declaration
788+
* under it (#27827).
773789
*/
774790
packageScope?: { readonly packageId: string; readonly dependencies: readonly string[] };
775791
}

‎packages/lint/src/rule-explanations.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3961,6 +3961,17 @@ const OBJECT_REFERENCE_UNKNOWN_EXPLANATION: RuleExplanation = {
39613961
'object by its full name (the platform user object is `sys_user`, not `user`). The verdict names ' +
39623962
'the nearest defined object when one is close; a view container written with an object\'s short ' +
39633963
'name in a namespaced package is told the prefixed spelling.',
3964+
// [#27827] The closure case, so the explanation does not contradict the
3965+
// fix line the save door prints under one (ADR-0141 D1).
3966+
'A dataset written into a package at the runtime save door is judged against that package\'s ' +
3967+
'closure instead of one stack: the package\'s own objects, the objects of the packages its ' +
3968+
'manifest declares in `dependencies`, platform objects, and tenant-authored objects that carry ' +
3969+
'no package. A base object outside it, whether it belongs to a package the manifest never ' +
3970+
'declared or exists nowhere at all, is refused with the fix the flow and view doors print for ' +
3971+
'the same closure: declare a dependency on the package that defines the object in the ' +
3972+
'`dependencies` of the written package\'s manifest, or point the dataset at an object inside the ' +
3973+
'closure. A write that names no package, and `os validate`, `os build` and `os lint`, state no ' +
3974+
'closure and print the fix above.',
39643975
],
39653976
};
39663977

‎packages/lint/src/runtime-gate.dataset-writes.test.ts‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import { describe, expect, it } from 'vitest';
3939

4040
import { REFERENCE_INTEGRITY_RULES } from './reference-integrity-suite.js';
41+
import { validateObjectReferences } from './validate-object-references.js';
4142
import {
4243
runRuntimeAuthoringRules,
4344
runtimeAuthoringRulesFor,
@@ -242,6 +243,95 @@ describe('the door refuses each crossed rule s own defect (#19143)', () => {
242243
});
243244
});
244245

246+
describe('the base-object fix line names the dependency declaration under a package closure (#27827)', () => {
247+
// ADR-0141 D1: at the save door a dataset in a package may name its own
248+
// objects, its declared dependencies' objects and platform objects. Under a
249+
// stated closure the snapshot's `objects` ARE that closure, so a base object
250+
// another package defines lands on rung ② — and the fix that works is the
251+
// declaration, the line the flow and view doors print for the same closure.
252+
const OWN = 'com.acme.billing';
253+
const DEP = 'com.acme.core';
254+
const STRANGER = 'com.other.analytics';
255+
const owned = (name: string, pkg: string) => ({
256+
name,
257+
label: name,
258+
sharingModel: 'private',
259+
fields: { name: { type: 'text', label: 'Name' } },
260+
_packageId: pkg,
261+
});
262+
const objects = [owned('acme_invoice', OWN), owned('acme_account', DEP), owned('other_cube', STRANGER)];
263+
const scope = { packageId: OWN, dependencies: [DEP] };
264+
const write = (object: string, packageScope?: typeof scope) =>
265+
runRuntimeAuthoringRules({
266+
type: 'dataset',
267+
item: cleanDataset({ object, dimensions: [], measures: [] }),
268+
context: { objects },
269+
...(packageScope !== undefined ? { packageScope } : {}),
270+
});
271+
const unknownOf = (r: ReturnType<typeof write>) => r.errors.filter((f) => f.rule === 'object-reference-unknown');
272+
const TODAYS_HINT =
273+
'Point it at one of this stack\'s objects, or at a platform object by its full name (the platform ' +
274+
'user object is "sys_user", not "user")';
275+
276+
it('closure in force: an object of an undeclared package is refused with the declaration as the fix', () => {
277+
const [f, ...rest] = unknownOf(write('other_cube', scope));
278+
expect(rest).toEqual([]);
279+
expect(f).toMatchObject({ severity: 'error', rule: 'object-reference-unknown' });
280+
expect(f!.path).toBe('datasets.acme_invoice_metrics.object');
281+
expect(f!.hint).toBe(
282+
'Declare a dependency on the package that defines "other_cube" in the `dependencies` of ' +
283+
`"${OWN}"'s manifest, or point it at an object inside that closure`,
284+
);
285+
});
286+
287+
it('closure in force: a name that exists nowhere gets the same fix — the closure is all the door sees', () => {
288+
const [f] = unknownOf(write('ghost_cube', scope));
289+
expect(f).toMatchObject({ severity: 'error' });
290+
expect(f!.hint).toContain('`dependencies`');
291+
expect(f!.hint).toContain(OWN);
292+
});
293+
294+
it('closure in force: the own package and a declared dependency stay inside it — nothing to fix', () => {
295+
expect(unknownOf(write('acme_invoice', scope))).toEqual([]);
296+
expect(unknownOf(write('acme_account', scope))).toEqual([]);
297+
});
298+
299+
it('no closure: a write that names no package keeps today s hint, and the stranger s object resolves', () => {
300+
// The whole collection is the snapshot, so another package's object is no
301+
// miss at all; a name that exists nowhere gets the hint the CLI prints.
302+
expect(unknownOf(write('other_cube'))).toEqual([]);
303+
const [f] = unknownOf(write('ghost_cube'));
304+
expect(f).toMatchObject({ severity: 'error', rule: 'object-reference-unknown' });
305+
expect(f!.hint).toBe(TODAYS_HINT);
306+
expect(f!.hint).not.toContain('dependencies');
307+
// An empty package id narrows nothing, so it states no closure either.
308+
const [g] = unknownOf(write('ghost_cube', { packageId: '', dependencies: [] }));
309+
expect(g!.hint).toBe(TODAYS_HINT);
310+
// The CLI reading (`os validate` / `os build` / `os lint`): no closure is
311+
// ever stated there, and the line is the one the door prints without one.
312+
const [cli, ...more] = validateObjectReferences({
313+
objects,
314+
datasets: [cleanDataset({ object: 'ghost_cube', dimensions: [], measures: [] })],
315+
});
316+
expect(more).toEqual([]);
317+
expect({ rule: cli!.rule, severity: cli!.severity, message: cli!.message, hint: cli!.hint }).toEqual({
318+
rule: f!.rule,
319+
severity: f!.severity,
320+
message: f!.message,
321+
hint: TODAYS_HINT,
322+
});
323+
});
324+
325+
it('the verdict is the same both ways — only the fix line moves', () => {
326+
const scoped = unknownOf(write('ghost_cube', scope));
327+
const whole = unknownOf(write('ghost_cube'));
328+
const verdictOf = (fs: typeof scoped) => fs.map(({ severity, rule, where, path, message }) =>
329+
({ severity, rule, where, path, message }));
330+
expect(verdictOf(scoped)).toEqual(verdictOf(whole));
331+
expect(scoped.map((f) => f.hint)).not.toEqual(whole.map((f) => f.hint));
332+
});
333+
});
334+
245335
describe('the door s fences hold on a dataset write (#4463 D4)', () => {
246336
it('does not charge this write for a STORED dataset s pre-existing defect', () => {
247337
// The differential's whole purpose. A tenant's stored dataset may already

‎packages/lint/src/runtime-gate.package-closure.test.ts‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,3 +582,64 @@ describe('the flow door under a package closure (#24669)', () => {
582582
expect(g).toMatchObject({ severity: 'error', rule: UNKNOWN });
583583
});
584584
});
585+
586+
// ─────────────────────────────────────────────────────────────────────────────
587+
// #27827 — the DATASET door under a package closure, and the one prescription
588+
// the three doors share: a base object outside the closure is refused with the
589+
// fix the flow and view doors print (ADR-0141 D1), from one builder.
590+
// ─────────────────────────────────────────────────────────────────────────────
591+
592+
describe('the dataset door under a package closure, and the shared prescription (#27827)', () => {
593+
const objects = packagedTenant(3);
594+
const datasetOn = (object: string): AnyRec => ({
595+
name: `probe_${object}`,
596+
label: 'Probe',
597+
object,
598+
dimensions: [],
599+
measures: [],
600+
});
601+
const gate = (type: string, item: AnyRec, scope?: RuntimePackageScope) =>
602+
runRuntimeAuthoringRules({
603+
type,
604+
item,
605+
context: { objects },
606+
...(scope !== undefined ? { packageScope: scope } : {}),
607+
});
608+
/** The one refusal each door gives for `other_0_0` under the closure. */
609+
const refusalOf = (type: string, item: AnyRec, rule: string): AnyRec => {
610+
const fs = gate(type, item, SCOPE).errors.filter((f) => f.rule === rule);
611+
expect(fs, `${type} door`).toHaveLength(1);
612+
return fs[0]!;
613+
};
614+
615+
it('REFUSES a base object of a package the written one never declared — the fix names the declaration', () => {
616+
const f = refusalOf('dataset', datasetOn('other_0_0'), 'object-reference-unknown');
617+
expect(f.severity).toBe('error');
618+
expect(f.hint).toContain(OWN);
619+
expect(f.hint).toContain('`dependencies`');
620+
expect(f.hint).toContain('"other_0_0"');
621+
// Admitted: the written package, its declared dependency, a platform object.
622+
for (const inside of ['crm_account', 'core_user', 'platform_ledger', 'sys_audit_log']) {
623+
expect(gate('dataset', datasetOn(inside), SCOPE).errors, inside).toEqual([]);
624+
}
625+
});
626+
627+
it('no closure stated: the stranger s object resolves, and a ghost keeps the stack-relative fix', () => {
628+
expect(gate('dataset', datasetOn('other_0_0')).errors).toEqual([]);
629+
const [f] = gate('dataset', datasetOn('ghost_object')).errors;
630+
expect(f).toMatchObject({ severity: 'error', rule: 'object-reference-unknown' });
631+
expect(f!.hint.startsWith('Point it at one of this stack\'s objects')).toBe(true);
632+
});
633+
634+
it('the three doors print ONE prescription — the same declaration clause, each door s own alternative', () => {
635+
const flow = refusalOf('flow', flowOn('on_other', 'other_0_0'), 'flow-trigger-unknown-object');
636+
const view = refusalOf('view', listViewOn('other_0_0', ['name']), 'list-view-object-outside-closure');
637+
const dataset = refusalOf('dataset', datasetOn('other_0_0'), 'object-reference-unknown');
638+
const clause =
639+
`Declare a dependency on the package that defines "other_0_0" in the \`dependencies\` of "${OWN}"'s ` +
640+
'manifest, or ';
641+
expect(flow.hint).toBe(`${clause}set config.objectName to an object inside that closure`);
642+
expect(view.hint).toBe(`${clause}bind the view to an object inside that closure`);
643+
expect(dataset.hint).toBe(`${clause}point it at an object inside that closure`);
644+
});
645+
});

‎packages/lint/src/validate-flow-trigger-readiness.ts‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ import {
177177
apiTriggerSecretRefOf,
178178
resolveFlowTriggerKind,
179179
} from '@objectstack/spec/automation';
180-
import { recordsOf, suggestName } from './object-graph.js';
180+
import { closureDependencyRemedy, recordsOf, suggestName } from './object-graph.js';
181181

182182
export type FlowTriggerReadinessSeverity = 'error' | 'warning';
183183

@@ -496,9 +496,8 @@ export function validateFlowTriggerReadiness(
496496
`objects, the objects of the packages its manifest declares in \`dependencies\`, and platform ` +
497497
`objects), so the package may not bind a flow to it.` +
498498
suggestName(objectName, objectNames),
499-
hint:
500-
`Declare a dependency on the package that defines '${objectName}' in the manifest \`dependencies\` ` +
501-
`of package '${pkg}', or set ${at.key} to an object inside that closure`,
499+
// [#27827] The one closure prescription the view and dataset doors print too.
500+
hint: closureDependencyRemedy(objectName, pkg, `set ${at.key} to an object inside that closure`),
502501
};
503502
};
504503

‎packages/lint/src/validate-list-view-field-refs.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@ import { ListViewSchema } from '@objectstack/spec/ui';
398398
import { walkFilterFieldKeys } from './filter-walk.js';
399399
import { retiredKeyRemedy } from './validate-retired-permission-residue.js';
400400
import {
401+
closureDependencyRemedy,
401402
describeFieldPathVerdict,
402403
indexObjectGraph,
403404
isUnjudgeable,
@@ -990,9 +991,8 @@ export function validateListViewFieldRefs(
990991
`the packages its manifest declares in \`dependencies\`, and platform objects), so none of the view's ` +
991992
`field references are checked.` +
992993
suggestName(object, graph.keys()),
993-
hint:
994-
`Declare a dependency on the package that defines "${object}" in the \`dependencies\` of "${pkg}"'s ` +
995-
`manifest, or bind the view to an object inside that closure`,
994+
// [#27827] The one closure prescription the flow and dataset doors print too.
995+
hint: closureDependencyRemedy(object, pkg, 'bind the view to an object inside that closure'),
996996
});
997997
};
998998

0 commit comments

Comments
 (0)