Skip to content

Commit 88cc145

Browse files
docs(lint): os explain for the two list-view binding verdicts, and the whole-collection docblocks say what is now true (#24858)
Closes #24796 Clause-②: no Comments and explanation text only. No verdict, severity or accept set moves. ## What changed - **`packages/lint/src/authoring-rules.ts`**: the `AuthoringRuleContext.packageScope` docblock (it ships in the published `.d.ts`). It used to say an object missing from a whole snapshot "may come from another package". It now says that what such a miss means depends on who built the snapshot. On the CLI the stack is one app, so "exists nowhere" cannot be decided. At the runtime door with no closure in force the snapshot is the whole served collection, so the object exists nowhere, and a list view bound to it is refused `list-view-object-unknown`. - **`packages/lint/src/runtime-gate.ts`**: the comment at the `packageScope` spread in `runRuntimeAuthoringRules` gets the same fix. It used to say "from one a whole-collection snapshot merely does not carry". It now says that without a closure the snapshots carry the whole collection, and only the CLI still cannot decide. - **`packages/lint/src/rule-explanations.ts`**: - New `os explain list-view-object-outside-closure` entry. It covers the package closure's limbs (as `narrowObjectsToPackageClosure` keeps them), why the binding is not resolvable by declaration, the matching dataset-door verdict, why field positions stay unjudged, and the remedy (declare the dependency, or bind inside the closure). - New `os explain list-view-object-unknown` entry. It covers the whole collection a write with no package is judged against, why that lets the door say "exists nowhere", why the nearest name rides the `fix:` line rather than the verdict, why an empty collection is not judged, and why the CLI still skips the binding. - The two entries share one paragraph (`LIST_VIEW_BINDING_SCOPE`): the binding keys, one finding per list view, and that drafts and stored rows are not judged. This is the pattern `LIST_VIEW_HEAD_ONLY` already uses. - Bounded in-place fix, same staleness class and same file: the last paragraph of the existing `list-view-field-unknown` entry said "An unresolvable `data.object` skips the whole list view". That is now true only on `os validate` / `os build` / `os lint`. The paragraph now names the two runtime ids that refuse the binding instead. The pinned phrase "one typo yields one finding" is kept. - **`packages/lint/src/validate-list-view-field-refs.test.ts`**: the existing pin "`os explain` carries what the verdicts no longer say" now also covers the two new ids. Each must have an entry, and each entry must name its closure/collection facts and its sibling id. - **`.changeset/24796-list-view-binding-explanations.md`**: `@objectstack/lint` patch. Each text was checked against the implementation: `validate-list-view-field-refs.ts` (the two report functions and their section docblocks), `reference-integrity-suite.ts` (the `wholeCollection` / `packageScope` dispatch), and `runtime-gate.ts` (`narrowObjectsToPackageClosure`, `packageScopeInForce`). Neither entry describes the flow door's `flow-trigger-unknown-object` verdict, which open PR #24839 is changing. ## Parity / pin tests `rule-explanations.test.ts` checks only that each key is an exported rule id. It does not require every rule id to have an entry. Both ids are already exported from the root barrel (`LIST_VIEW_OBJECT_OUTSIDE_CLOSURE`, `LIST_VIEW_OBJECT_UNKNOWN`), and that test passes unchanged. The parity pattern this family uses is the per-rule facts pin in `validate-list-view-field-refs.test.ts`, and that is what this PR extends. No generated artifact pins the explanation set. ## Verification (head `f6a4f21b73`) - `pnpm --filter '@objectstack/lint^...' build`: VERDICT command-exit 0. - `vitest run` on `rule-explanations.test.ts`, `validate-list-view-field-refs.test.ts`, `runtime-gate.whole-collection.test.ts` and `runtime-gate.package-closure.test.ts`: 4 files, 185 tests passed. - `@objectstack/lint` `pnpm test`: 150 files passed; 6974 tests passed, 5 skipped. - `@objectstack/lint` `pnpm typecheck`: `tsc --noEmit` clean, and `check:test-typecheck` OK. - `@objectstack/cli` `test/explain-rule-id.test.ts` (unit tier, after building lint and the cli dependency closure): 14 tests passed. That suite holds rule ids disjoint from `os explain` schema names. - `dispatch-gates --commands`: 66 derived commands (65, plus `check-changeset-fixed`, whose roster sits under `.changeset/`) all exit 0. `check:dual-build-cjs-loads`, `check:lean-entry-closure` and `check:type-check-debt` first exited 3 (PREREQUISITE NOT MET: no workspace build, and a lock queue timeout). That is not a measurement. They went green when re-run after `check:type-check-debt --re-measure` built the workspace: re-measure OK, 26 raw errors, none above the record. `check:doc-authoring` (no tracker number in author-facing strings) and `check:nul-bytes` are among the greens. - Ablation of the new pin: with the fix committed, `scripts/ablation-replace.mjs --delete` removed the `list-view-object-unknown` registry row (anchor 1 to 0, blob `0cd9479e66` to `6ea77f0c56`). The facts test went red with the assertion naming the missing `os explain list-view-object-unknown` entry (expected undefined to be defined). The file was then restored with `git checkout HEAD --`: the blob equals HEAD (`0cd9479e66`) and `git diff HEAD` is empty. The test reads `src` through a relative import, so no rebuild leg applies. ## Acceptance notes - **Textual overlap with open PR #24839** (the #24669 flow-trigger closure). It edits the same `packageScope` docblock in `authoring-rules.ts` on the lines next to this one ("Read by `validateListViewFieldRefs` only" becomes "Read by two rules"). It also edits other parts of `rule-explanations.ts`. Whichever lands second gets a mechanical conflict in that docblock. The two sentences do not contradict each other, so the merge only needs both kept. --- _Generated by [Claude Code](https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3fbef41 commit 88cc145

5 files changed

Lines changed: 105 additions & 4 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/lint': patch
3+
---
4+
5+
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
6+
7+
Clause-②: no (no verdict, severity, accept set or served body moves: explanation text and comments only)
8+
9+
- **`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.
10+
- **`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.
11+
- **`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.
12+
- **`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`.

‎packages/lint/src/authoring-rules.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -382,8 +382,15 @@ export interface AuthoringRuleContext {
382382
* force; ABSENT on the three CLI commands and on a write that names no
383383
* package, whose snapshots carry the whole collection. It is what lets a
384384
* rule tell "outside what this package declared it may reference" from
385-
* "not in this stack": an object missing from a narrowed snapshot is the
386-
* first, one missing from a whole one may come from another package. Read
385+
* "exists nowhere": an object missing from a narrowed snapshot is the
386+
* first. What an object missing from a whole snapshot means depends on who
387+
* built it. On the CLI the stack is one app, so the object may come from a
388+
* dependency package the stack does not carry, and "exists nowhere" cannot
389+
* be decided. At the runtime door with no closure in force the snapshot is
390+
* the whole collection the host serves, so the object exists nowhere, and
391+
* [#24668] a list view bound to it is refused `list-view-object-unknown`
392+
* (the reference-integrity suite reads a set `runtimeWriteType` beside an
393+
* absent `packageScope` as that whole collection). Read
387394
* by two rules: `validateListViewFieldRefs` (through the reference-integrity
388395
* suite), which refuses a list view bound outside the closure instead of
389396
* skipping it, and [#24669] `validateFlowTriggerReadiness`, which refuses a

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

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3143,7 +3143,9 @@ const LIST_VIEW_FIELD_UNKNOWN_EXPLANATION: RuleExplanation = {
31433143
'view bound to a provider other than `object`, and the positions other rules own: `sort` ' +
31443144
'(`sort-field-unknown`), `searchableFields` (`searchable-field-unknown`), the `chart` ' +
31453145
'bindings, action names, `conditionalFormatting` predicates and `addRecord.formView`. An ' +
3146-
'unresolvable `data.object` skips the whole list view, so one typo yields one finding.',
3146+
'unresolvable binding skips the whole list view on `os validate`, `os build` and `os lint`, so one ' +
3147+
'typo yields one finding; the runtime save door refuses it once instead, as `list-view-object-unknown` ' +
3148+
'or, for a write into a package, `list-view-object-outside-closure`.',
31473149
],
31483150
};
31493151

@@ -3177,6 +3179,64 @@ const LIST_VIEW_FIELD_DOTTED_EXPLANATION: RuleExplanation = {
31773179
],
31783180
};
31793181

3182+
/** Which keys a list view's binding is read from, and what stays unjudged (both binding ids write it out). */
3183+
const LIST_VIEW_BINDING_SCOPE =
3184+
'One finding per list view, at the key the binding was read from: the view\'s `object`, or a list ' +
3185+
'view\'s own `data.object` where it sets one. The view\'s field references stay unjudged while the ' +
3186+
'binding does not resolve, so a wrong name is reported once per list view rather than once per ' +
3187+
'position. Not judged: a draft save, and a stored view until its next save — the door judges ' +
3188+
'writes only.';
3189+
3190+
const LIST_VIEW_OBJECT_OUTSIDE_CLOSURE_EXPLANATION: RuleExplanation = {
3191+
rule: 'list-view-object-outside-closure',
3192+
covers: 'what a package closure holds, and why the view refuses',
3193+
paragraphs: [
3194+
'A view written into a package at the runtime save door is judged against that package\'s ' +
3195+
'closure, never the whole deployment: the package\'s own objects, the objects of the packages its ' +
3196+
'manifest declares in `dependencies`, platform objects, and tenant-authored objects that carry no ' +
3197+
'package. The declared `dependencies` bound what the package MAY reference, so a list view bound ' +
3198+
'to an object outside that closure is not resolvable by declaration — whether the object belongs ' +
3199+
'to a package the manifest never declared or exists nowhere at all, because the closure is all ' +
3200+
'the door can see.',
3201+
'An error, so the publish is refused (`422`): it is the verdict the dataset door gives a base ' +
3202+
'object outside the same closure (`object-reference-unknown`). Judging the view\'s field ' +
3203+
'references against an object the package may not reference would judge the package against the ' +
3204+
'wrong set; declaring the dependency puts the object, and the field judgement with it, inside the ' +
3205+
'closure.',
3206+
LIST_VIEW_BINDING_SCOPE,
3207+
'To fix it, declare a dependency on the package that defines the object in the `dependencies` of ' +
3208+
'the written package\'s manifest, or bind the view to an object inside the closure; when the name ' +
3209+
'is a typo, the verdict names the nearest object in reach. A write that names no package is ' +
3210+
'judged against the whole collection instead (`list-view-object-unknown`), and `os validate`, ' +
3211+
'`os build` and `os lint` judge neither: a stack there is one app, and an unresolved binding may ' +
3212+
'be a dependency package\'s object the stack does not carry.',
3213+
],
3214+
};
3215+
3216+
const LIST_VIEW_OBJECT_UNKNOWN_EXPLANATION: RuleExplanation = {
3217+
rule: 'list-view-object-unknown',
3218+
covers: 'why the save door can say an object exists nowhere',
3219+
paragraphs: [
3220+
'A view written at the runtime save door with no package named (an overlay write) is judged ' +
3221+
'against the whole collection the deployment serves: every installed package\'s objects, enabled ' +
3222+
'or not, the platform\'s, and the objects saved at run time — the same merge the object metadata ' +
3223+
'listing answers from. Nothing narrows it, so a list view bound to an object missing from it is ' +
3224+
'bound to an object that exists nowhere: there is no other package for it to come from.',
3225+
'An error, so the publish is refused (`422`): left in, the view would be stored with none of its ' +
3226+
'field references checked and fail only when it renders. The nearest object name rides the ' +
3227+
'`fix:` line, never the verdict, so a stored view\'s verdict does not change when an unrelated ' +
3228+
'write creates an object with a similar name.',
3229+
LIST_VIEW_BINDING_SCOPE,
3230+
'To fix it, bind the view to an object that exists, or save the object before you publish the ' +
3231+
'view. Not judged: a write into a package, which is judged against that package\'s closure ' +
3232+
'instead (`list-view-object-outside-closure`); an empty collection, which means the host supplied ' +
3233+
'none, because no deployment serves zero objects; and `os validate`, `os build` and `os lint`, ' +
3234+
'where a stack is one app and an unresolved binding may be a dependency package\'s object the ' +
3235+
'stack does not carry, so the list view is skipped and the binding left to the object-reference ' +
3236+
'checks.',
3237+
],
3238+
};
3239+
31803240
// ── Translation bundles (`validate-translation-references.ts`, `validate-translatable-sections.ts`) ──
31813241

31823242
/** How a bundle key is read at all (the two reference ids write it out). */
@@ -4786,6 +4846,8 @@ export const RULE_EXPLANATIONS: Readonly<Record<string, RuleExplanation>> = Obje
47864846
[FILTER_EMPTY_NODE_EXPLANATION.rule]: FILTER_EMPTY_NODE_EXPLANATION,
47874847
[LIST_VIEW_FIELD_UNKNOWN_EXPLANATION.rule]: LIST_VIEW_FIELD_UNKNOWN_EXPLANATION,
47884848
[LIST_VIEW_FIELD_DOTTED_EXPLANATION.rule]: LIST_VIEW_FIELD_DOTTED_EXPLANATION,
4849+
[LIST_VIEW_OBJECT_OUTSIDE_CLOSURE_EXPLANATION.rule]: LIST_VIEW_OBJECT_OUTSIDE_CLOSURE_EXPLANATION,
4850+
[LIST_VIEW_OBJECT_UNKNOWN_EXPLANATION.rule]: LIST_VIEW_OBJECT_UNKNOWN_EXPLANATION,
47894851
[TRANSLATION_TARGET_UNKNOWN_EXPLANATION.rule]: TRANSLATION_TARGET_UNKNOWN_EXPLANATION,
47904852
[TRANSLATION_OPTION_KEY_UNKNOWN_EXPLANATION.rule]: TRANSLATION_OPTION_KEY_UNKNOWN_EXPLANATION,
47914853
[TRANSLATION_SECTION_NAME_MISSING_EXPLANATION.rule]: TRANSLATION_SECTION_NAME_MISSING_EXPLANATION,

‎packages/lint/src/runtime-gate.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1117,7 +1117,10 @@ export function runRuntimeAuthoringRules(args: {
11171117
// [#24563] The closure the snapshots' `objects` were narrowed to, stated
11181118
// exactly when the builder above narrowed them (the same predicate), so a
11191119
// rule can tell an object outside the package's declared reach from one
1120-
// a whole-collection snapshot merely does not carry.
1120+
// that exists nowhere. [#24668] Absent here, the snapshots carry the whole
1121+
// collection the host serves, so an object missing from them exists
1122+
// nowhere in this deployment; only the CLI, whose stack is one app, still
1123+
// cannot decide that.
11211124
...(packageScopeInForce(args.packageScope) ? { packageScope: args.packageScope } : {}),
11221125
// [#20611] Spelled against the CANDIDATE, the one snapshot that holds the
11231126
// written item. The baseline pass shares the set and cannot match it: the

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import {
1616
validateListViewFieldRefs as validateListViewFieldRefsUnrecorded,
1717
LIST_VIEW_FIELD_UNKNOWN,
1818
LIST_VIEW_FIELD_DOTTED,
19+
LIST_VIEW_OBJECT_OUTSIDE_CLOSURE,
20+
LIST_VIEW_OBJECT_UNKNOWN,
1921
listViewWalkedPositions,
2022
type ListViewFieldRefFinding,
2123
} from './validate-list-view-field-refs.js';
@@ -1089,6 +1091,21 @@ describe('[#22161] one-line verdicts — list-view-field-unknown, list-view-fiel
10891091
'`gantt.quickFilters[].field`',
10901092
'`gantt.tooltipFields[]`',
10911093
],
1094+
// The two binding verdicts carry their reasoning only here: the closure's
1095+
// limbs and which door judges which snapshot.
1096+
[LIST_VIEW_OBJECT_OUTSIDE_CLOSURE]: [
1097+
'declares in `dependencies`',
1098+
'platform objects',
1099+
'`object-reference-unknown`',
1100+
'`list-view-object-unknown`',
1101+
'a draft save',
1102+
],
1103+
[LIST_VIEW_OBJECT_UNKNOWN]: [
1104+
'the whole collection the deployment serves',
1105+
'no deployment serves zero objects',
1106+
'`list-view-object-outside-closure`',
1107+
'a draft save',
1108+
],
10921109
};
10931110
for (const [rule, list] of Object.entries(facts)) {
10941111
const entry = explainRule(rule);

0 commit comments

Comments
 (0)