diff --git a/.changeset/24796-list-view-binding-explanations.md b/.changeset/24796-list-view-binding-explanations.md new file mode 100644 index 00000000000..646f49a5b21 --- /dev/null +++ b/.changeset/24796-list-view-binding-explanations.md @@ -0,0 +1,12 @@ +--- +'@objectstack/lint': patch +--- + +docs(lint): `os explain` covers `list-view-object-unknown` and `list-view-object-outside-closure`, and the `packageScope` docblock says what a whole-collection miss now means + +Clause-②: no (no verdict, severity, accept set or served body moves: explanation text and comments only) + +- **`os explain list-view-object-outside-closure`** explains the package closure a write into a package is judged against (the package's own objects, its declared `dependencies`' objects, platform objects and provenance-less tenant objects), why a list view bound outside it is refused, and the remedy: declare the dependency, or bind the view inside the closure. +- **`os explain list-view-object-unknown`** explains why a write that names no package can say an object exists nowhere (it is judged against the whole collection the deployment serves), why an empty collection is not judged, and why `os validate` / `os build` / `os lint` still skip the binding. +- **`os explain list-view-field-unknown`** no longer says an unresolvable binding always skips the list view: it does on the CLI, and the runtime save door refuses it under one of the two ids above. +- **`AuthoringRuleContext.packageScope`** (in the published `.d.ts`) no longer says an object missing from a whole snapshot "may come from another package": that is true only on the CLI. At the runtime door with no closure in force the snapshot is the whole served collection, and such a binding is refused `list-view-object-unknown`. diff --git a/packages/lint/src/authoring-rules.ts b/packages/lint/src/authoring-rules.ts index bfa10ee7c59..7e28c489059 100644 --- a/packages/lint/src/authoring-rules.ts +++ b/packages/lint/src/authoring-rules.ts @@ -382,8 +382,15 @@ export interface AuthoringRuleContext { * force; ABSENT on the three CLI commands and on a write that names no * package, whose snapshots carry the whole collection. It is what lets a * rule tell "outside what this package declared it may reference" from - * "not in this stack": an object missing from a narrowed snapshot is the - * first, one missing from a whole one may come from another package. Read + * "exists nowhere": an object missing from a narrowed snapshot is the + * first. What an object missing from a whole snapshot means depends on who + * built it. On the CLI the stack is one app, so the object may come from a + * dependency package the stack does not carry, and "exists nowhere" cannot + * be decided. At the runtime door with no closure in force the snapshot is + * the whole collection the host serves, so the object exists nowhere, and + * [#24668] a list view bound to it is refused `list-view-object-unknown` + * (the reference-integrity suite reads a set `runtimeWriteType` beside an + * absent `packageScope` as that whole collection). Read * by two rules: `validateListViewFieldRefs` (through the reference-integrity * suite), which refuses a list view bound outside the closure instead of * skipping it, and [#24669] `validateFlowTriggerReadiness`, which refuses a diff --git a/packages/lint/src/rule-explanations.ts b/packages/lint/src/rule-explanations.ts index c0c671b20eb..92eb4407f31 100644 --- a/packages/lint/src/rule-explanations.ts +++ b/packages/lint/src/rule-explanations.ts @@ -3142,7 +3142,9 @@ const LIST_VIEW_FIELD_UNKNOWN_EXPLANATION: RuleExplanation = { 'view bound to a provider other than `object`, and the positions other rules own: `sort` ' + '(`sort-field-unknown`), `searchableFields` (`searchable-field-unknown`), the `chart` ' + 'bindings, action names, `conditionalFormatting` predicates and `addRecord.formView`. An ' + - 'unresolvable `data.object` skips the whole list view, so one typo yields one finding.', + 'unresolvable binding skips the whole list view on `os validate`, `os build` and `os lint`, so one ' + + 'typo yields one finding; the runtime save door refuses it once instead, as `list-view-object-unknown` ' + + 'or, for a write into a package, `list-view-object-outside-closure`.', ], }; @@ -3176,6 +3178,64 @@ const LIST_VIEW_FIELD_DOTTED_EXPLANATION: RuleExplanation = { ], }; +/** Which keys a list view's binding is read from, and what stays unjudged (both binding ids write it out). */ +const LIST_VIEW_BINDING_SCOPE = + 'One finding per list view, at the key the binding was read from: the view\'s `object`, or a list ' + + 'view\'s own `data.object` where it sets one. The view\'s field references stay unjudged while the ' + + 'binding does not resolve, so a wrong name is reported once per list view rather than once per ' + + 'position. Not judged: a draft save, and a stored view until its next save — the door judges ' + + 'writes only.'; + +const LIST_VIEW_OBJECT_OUTSIDE_CLOSURE_EXPLANATION: RuleExplanation = { + rule: 'list-view-object-outside-closure', + covers: 'what a package closure holds, and why the view refuses', + paragraphs: [ + 'A view written into a package at the runtime save door is judged against that package\'s ' + + 'closure, never the whole deployment: the package\'s own objects, the objects of the packages its ' + + 'manifest declares in `dependencies`, platform objects, and tenant-authored objects that carry no ' + + 'package. The declared `dependencies` bound what the package MAY reference, so a list view bound ' + + 'to an object outside that closure is not resolvable by declaration — whether the object belongs ' + + 'to a package the manifest never declared or exists nowhere at all, because the closure is all ' + + 'the door can see.', + 'An error, so the publish is refused (`422`): it is the verdict the dataset door gives a base ' + + 'object outside the same closure (`object-reference-unknown`). Judging the view\'s field ' + + 'references against an object the package may not reference would judge the package against the ' + + 'wrong set; declaring the dependency puts the object, and the field judgement with it, inside the ' + + 'closure.', + LIST_VIEW_BINDING_SCOPE, + 'To fix it, declare a dependency on the package that defines the object in the `dependencies` of ' + + 'the written package\'s manifest, or bind the view to an object inside the closure; when the name ' + + 'is a typo, the verdict names the nearest object in reach. A write that names no package is ' + + 'judged against the whole collection instead (`list-view-object-unknown`), and `os validate`, ' + + '`os build` and `os lint` judge neither: a stack there is one app, and an unresolved binding may ' + + 'be a dependency package\'s object the stack does not carry.', + ], +}; + +const LIST_VIEW_OBJECT_UNKNOWN_EXPLANATION: RuleExplanation = { + rule: 'list-view-object-unknown', + covers: 'why the save door can say an object exists nowhere', + paragraphs: [ + 'A view written at the runtime save door with no package named (an overlay write) is judged ' + + 'against the whole collection the deployment serves: every installed package\'s objects, enabled ' + + 'or not, the platform\'s, and the objects saved at run time — the same merge the object metadata ' + + 'listing answers from. Nothing narrows it, so a list view bound to an object missing from it is ' + + 'bound to an object that exists nowhere: there is no other package for it to come from.', + 'An error, so the publish is refused (`422`): left in, the view would be stored with none of its ' + + 'field references checked and fail only when it renders. The nearest object name rides the ' + + '`fix:` line, never the verdict, so a stored view\'s verdict does not change when an unrelated ' + + 'write creates an object with a similar name.', + LIST_VIEW_BINDING_SCOPE, + 'To fix it, bind the view to an object that exists, or save the object before you publish the ' + + 'view. Not judged: a write into a package, which is judged against that package\'s closure ' + + 'instead (`list-view-object-outside-closure`); an empty collection, which means the host supplied ' + + 'none, because no deployment serves zero objects; and `os validate`, `os build` and `os lint`, ' + + 'where a stack is one app and an unresolved binding may be a dependency package\'s object the ' + + 'stack does not carry, so the list view is skipped and the binding left to the object-reference ' + + 'checks.', + ], +}; + // ── Translation bundles (`validate-translation-references.ts`, `validate-translatable-sections.ts`) ── /** How a bundle key is read at all (the two reference ids write it out). */ @@ -4785,6 +4845,8 @@ export const RULE_EXPLANATIONS: Readonly> = Obje [FILTER_EMPTY_NODE_EXPLANATION.rule]: FILTER_EMPTY_NODE_EXPLANATION, [LIST_VIEW_FIELD_UNKNOWN_EXPLANATION.rule]: LIST_VIEW_FIELD_UNKNOWN_EXPLANATION, [LIST_VIEW_FIELD_DOTTED_EXPLANATION.rule]: LIST_VIEW_FIELD_DOTTED_EXPLANATION, + [LIST_VIEW_OBJECT_OUTSIDE_CLOSURE_EXPLANATION.rule]: LIST_VIEW_OBJECT_OUTSIDE_CLOSURE_EXPLANATION, + [LIST_VIEW_OBJECT_UNKNOWN_EXPLANATION.rule]: LIST_VIEW_OBJECT_UNKNOWN_EXPLANATION, [TRANSLATION_TARGET_UNKNOWN_EXPLANATION.rule]: TRANSLATION_TARGET_UNKNOWN_EXPLANATION, [TRANSLATION_OPTION_KEY_UNKNOWN_EXPLANATION.rule]: TRANSLATION_OPTION_KEY_UNKNOWN_EXPLANATION, [TRANSLATION_SECTION_NAME_MISSING_EXPLANATION.rule]: TRANSLATION_SECTION_NAME_MISSING_EXPLANATION, diff --git a/packages/lint/src/runtime-gate.ts b/packages/lint/src/runtime-gate.ts index 3226f321906..b005353ae42 100644 --- a/packages/lint/src/runtime-gate.ts +++ b/packages/lint/src/runtime-gate.ts @@ -1117,7 +1117,10 @@ export function runRuntimeAuthoringRules(args: { // [#24563] The closure the snapshots' `objects` were narrowed to, stated // exactly when the builder above narrowed them (the same predicate), so a // rule can tell an object outside the package's declared reach from one - // a whole-collection snapshot merely does not carry. + // that exists nowhere. [#24668] Absent here, the snapshots carry the whole + // collection the host serves, so an object missing from them exists + // nowhere in this deployment; only the CLI, whose stack is one app, still + // cannot decide that. ...(packageScopeInForce(args.packageScope) ? { packageScope: args.packageScope } : {}), // [#20611] Spelled against the CANDIDATE, the one snapshot that holds the // written item. The baseline pass shares the set and cannot match it: the diff --git a/packages/lint/src/validate-list-view-field-refs.test.ts b/packages/lint/src/validate-list-view-field-refs.test.ts index 3740b379f50..5fcf480f873 100644 --- a/packages/lint/src/validate-list-view-field-refs.test.ts +++ b/packages/lint/src/validate-list-view-field-refs.test.ts @@ -16,6 +16,8 @@ import { validateListViewFieldRefs as validateListViewFieldRefsUnrecorded, LIST_VIEW_FIELD_UNKNOWN, LIST_VIEW_FIELD_DOTTED, + LIST_VIEW_OBJECT_OUTSIDE_CLOSURE, + LIST_VIEW_OBJECT_UNKNOWN, listViewWalkedPositions, type ListViewFieldRefFinding, } from './validate-list-view-field-refs.js'; @@ -1089,6 +1091,21 @@ describe('[#22161] one-line verdicts — list-view-field-unknown, list-view-fiel '`gantt.quickFilters[].field`', '`gantt.tooltipFields[]`', ], + // The two binding verdicts carry their reasoning only here: the closure's + // limbs and which door judges which snapshot. + [LIST_VIEW_OBJECT_OUTSIDE_CLOSURE]: [ + 'declares in `dependencies`', + 'platform objects', + '`object-reference-unknown`', + '`list-view-object-unknown`', + 'a draft save', + ], + [LIST_VIEW_OBJECT_UNKNOWN]: [ + 'the whole collection the deployment serves', + 'no deployment serves zero objects', + '`list-view-object-outside-closure`', + 'a draft save', + ], }; for (const [rule, list] of Object.entries(facts)) { const entry = explainRule(rule);