fix(lint): resolve a dashboard widget's own filter keys and options.sortBy at validate/build - #14276

Merged
baozhoutao merged 4 commits into
mainfrom
claude/issue-14148-widget-filter-sortby
Sep 1, 2026
Merged

fix(lint): resolve a dashboard widget's own filter keys and options.sortBy at validate/build#14276
baozhoutao merged 4 commits into
mainfrom
claude/issue-14148-widget-filter-sortby

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes#14148

A dashboard widget could filter by a column that does not exist, and order by a name it never selected, and objectstack validate exited 0 with "Validation passed"; build — the publish gate — wrote the dashboard into dist/objectstack.json. The widget then rendered empty, and per the card that is the expensive part: the board it was measured on leads with a "not moving" tile, and "an empty tile is indistinguishable from a healthy team: a missing number reads as zero, and zero is the answer the manager is hoping for." The failure is silent in the direction the reader wants to believe.

Premise re-check — measured on this branch's base, not assumed

Base is 345fc33ad (PR #14267, the #14105 family opener). Verified before writing code:

premiseverdictevidence
the seam landed and is exported from the package indexHOLDSpackages/lint/src/index.ts exports walkFilterFieldKeys (filter-walk.ts) and indexObjectGraph / resolveFieldPath (object-graph.ts); the export-site comment names this card as an intended consumer
gap A — widget filter KEYS unresolvedHOLDSvalidate-widget-bindings.ts at base reads w.dataset, w.dimensions, w.values, w.chartConfig, w.filterBindings — and never w.filter
gap B — options.sortBy uncheckedHOLDSvalidate-sortable-fields.ts explicitly defers it: "Dashboard widget sort config. Verified present but out of this predicate's domain … Judging those needs the dataset's measure index, which is validateChartBindings' family, not this one." Nothing in that family judged it

The card's six-row "what IS caught" table is a positive control, and it still holds at base: widget-dataset-unknown, widget-dimension-unknown, widget-measure-unknown, filter-token-unknown and dashboard-filter-field-unknown all live and gating. So the two zeros were readings, not an unread file. premise_still_valid: true on all three.

What lands

Three gating rule ids, all at the site that already emits widget-dataset-unknown / dashboard-filter-field-unknown:

  • widget-filter-field-unknown (limb A) — a key of the widget's own filter resolves to no column on the bound dataset's object graph. Reported path-precise at dashboards[i].widgets[j].filter.KEY.
  • widget-filter-field-not-included (limb A, second clause) — the key resolves, but its relationship prefix is not declared in the dataset's include.
  • widget-sortby-unselected (limb B) — options.sortBy names neither a dimensions[] nor a values[] entry of the widget, reported at dashboards[i].widgets[j].options.sortBy.

All three name dashboard, widget, key and object, and print the object's field list (limb A) or what the widget selects (limb B).

The open sub-question, answered explicitly: a dotted path through a declared include is RESOLVED

The card required this be stated rather than passed through silently, since a third silent pass-through would reproduce the card. Decision: resolve it, under the same two clauses the dataset-level sibling applies one level down — existence first, then ADR-0021 joinability.

The reasoning is mechanical rather than aesthetic. A widget's filter is ANDed into the dataset query as runtimeFilter (DashboardWidgetSchema.filter; dataset-executor.tscombineFilters(compiled.filter, selection.runtimeFilter)), and that compiled query carries only the joins include declared (dataset-compiler.ts derives every join alias from include). So a dotted key is exactly as decidable here as at a dataset dimension.

And the runtime is not a backstop for the second clause: dataset-compiler.ts's assertDeclared is called at two sites only — assertDeclared(d.field, 'dimension', …) and assertDeclared(m.field, 'measure', …) — never over runtimeFilter. Nothing between the author and the empty tile asked this question.

Note this deliberately differs from the neighbouring dashboard-filter-field-unknown, which still skips a dotted field (if (field.includes('.')) continue;). That skip was correct when nothing in this package could walk hops; it is now a real gap, and migrating it is filed rather than smuggled in here (see Follow-ups).

Reuse, not a third implementation

Triage was explicit: "generalise the one working implementation, not three parallel rules." No second hop-walker and no second filter-key reader were written. Both limbs are built on walkFilterFieldKeys (which already handles all three authored filter shapes — Mongo condition objects, { field, operator, value } rules, and [field, op, value] triples) and indexObjectGraph / resolveFieldPath.

Two helpers that were local to validate-dataset-references.ts moved into object-graph.ts and are now exported: joinablePrefixes (how ADR-0021 expands an include into joinable prefixes) and describeFieldPathVerdict (how one verdict reads in prose, renamed from existenceMessage now that it is shared). Copying either would have been the second implementation the seam exists to prevent, one release after it was written to prevent it. The sibling's behaviour is unchanged — its 526-line test file passes untouched.

The three skips are inherited unchanged, so an object this stack does not define, an ADR-0015 external object with no readable field map, and a registry-injected system column are never reported. Injected columns resolve per object via the graph rather than through the flat SYSTEM_FIELDS union the neighbouring rule uses, which is strictly better: a reference to owner_id on an ownership: 'none' object stays a real finding.

Verification

Ratchet and gate readings below were taken on the final commit, 5216886e2.

Acceptance criterion pinned end-to-end, not inferred. The card's binding criterion is that both limbs fail validate AND build — a validate-only fix was not acceptable. Rather than assert this from the registry entry's commands: ALL, six tests drive runAuthoringRules('validate' | 'build', …) + splitBySeverity over the repro stacks and assert the rule id appears in errors, plus a clean shape passing on both. Nothing else in the file would notice if that entry's commands were narrowed later.

Ablation — the new tests genuinely fail without the implementation. Both limb bodies were disabled at their guards (implementation committed first; mutation confirmed on disk by injected-marker count 2, original-anchor count 0, and a blob hash differing from the HEAD blob). Result: 13 failed / 75 passed, including all four validate/build acceptance pins. Restored via git checkout HEAD -- ABSOLUTE_PATH under an EXIT INT TERM trap and proven byte-identical: git diff HEAD empty, git hash-object back to 4c3379d74c386d50f13e6d7bf7c07133816f745f, zero markers remaining. No rebuild leg is skipped or needed here, and that is a property rather than an omission: the test reaches the rule through a relative source import (./validate-widget-bindings.js), resolved by vitest to src/, never through the package exports/dist.

Note the "clean shape" tests stay green under ablation, which is correct — an ablated rule reports nothing — so the 75 passes are not evidence of anything.

Runs:

  • pnpm --filter @objectstack/lint test90 files, 2558 tests, all pass
  • pnpm --filter @objectstack/lint typecheck — clean (tsc --noEmit)
  • pnpm lint (repo-scale eslint . --no-inline-config) — exit 0, no narrowing
  • Gate families derived from the real change set via scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands — 33 families (29 by path + 6 by kind, 2 overlapping) — 31 green
  • Also run as dispatched: check-ratchet-remedy-authority (green, 183 scripts swept) and check:declared-population-live (green, 156/200 families reach the tree)
  • check:type-check-coverage green; check:type-check-debt --re-measure green after building the closure — "27 ledger entries re-measured, 1217 raw tsc errors total, none above its recorded number"
  • check:dual-build-cjs-loads green after the same build (it had reported exit 3 PREREQUISITE NOT MET beforehand)
  • check:nul-bytes green; diff additionally self-scanned for raw control bytes — clean

One family NOT MEASURED locally, by its own instruction:scripts/check-test-completeness.mjs requires a saved turbo run test log that CI tees and this invocation has none, so it exits 3 and its own text says "running the family locally, record this gate as NOT MEASURED … It is not a red, and there is nothing here to fix." Recorded as NOT MEASURED rather than as a pass.

A note on the ratchet the type gates could not see

packages/lint/tsconfig.json excludes **/*.test.ts, so pnpm --filter @objectstack/lint typecheck says nothing about the test file this PR adds. That is measured, not assumed — tsc --listFiles returns the three source files and not the test. The tests here were therefore type-checked separately under a temporary config that includes them (removed again): zero errors attributable to this diff, against 22 pre-existing errors in seven untouched test files. That separate run is what caught a genuine defect in this PR's own test helper — a non-generic idsOf that erased the finding type — which vitest cannot see and which would otherwise have shipped. The structural half is accounted for: check:type-check-coverage records this package under TEST_DEBT, and the ratchet re-measure is green.

Scope

  • test/dashboard.test.ts in objectstack-ai/duly is the downstream stopgap, written to be deleted when this lands. Deleting it is a follow-up in that repo, not this PR's scope — this PR does not touch duly.
  • Both limbs ride the existing validateWidgetBindings registry entry, whose surfaces is per-RULE. As with the six error ids already on it, the three new ones therefore also reach the runtime publish gate for dashboard writes. That is safe in the direction that matters: RuntimeStackContext carries objects, so the graph resolves there; were it ever not carried, resolveFieldPath returns unknowable and the rules go silent rather than inventing findings.

Follow-ups


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 11 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/lint/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx(via validateWidgetBindings (symbol, a top-level function))
  • content/docs/releases/v17.mdx(via validateWidgetBindings (symbol, a top-level function))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/lint/src/index.ts) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 6eb8e3cc5022c4b1d0007962220444f4f947036dpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 4acf3c1f3ab8017c7066fdb36c8410a51cd8873c — the merge of head 5216886e2b62f59830dd01a8b98ea486a5de9c6d into base 6eb8e3cc5022c4b1d0007962220444f4f947036d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4acf3c1f3ab8017c7066fdb36c8410a51cd8873c && git checkout 4acf3c1f3ab8017c7066fdb36c8410a51cd8873c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6eb8e3cc5022c4b1d0007962220444f4f947036d 5216886e2b62f59830dd01a8b98ea486a5de9c6d && git checkout -B drift-repro 6eb8e3cc5022c4b1d0007962220444f4f947036d && git merge --no-ff 5216886e2b62f59830dd01a8b98ea486a5de9c6d
node scripts/docs-audit/affected-docs.mjs --json 6eb8e3cc5022c4b1d0007962220444f4f947036d

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 6eb8e3cc5022c4b1d0007962220444f4f947036d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 1, 2026 20:08
@baozhoutao
baozhoutao added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit fa1eca3Sep 1, 2026
35 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-14148-widget-filter-sortby branch September 1, 2026 20:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A dashboard widget's OWN filter keys and options.sortBy are not resolved at author time — validate/build exit 0, widget renders empty

2 participants

@baozhoutao@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(lint): resolve a dashboard widget's own filter keys and options.sortBy at validate/build - #14276

Merged
baozhoutao merged 4 commits into
mainfrom
claude/issue-14148-widget-filter-sortby
Sep 1, 2026
Merged

fix(lint): resolve a dashboard widget's own filter keys and options.sortBy at validate/build#14276
baozhoutao merged 4 commits into
mainfrom
claude/issue-14148-widget-filter-sortby

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes#14148

A dashboard widget could filter by a column that does not exist, and order by a name it never selected, and objectstack validate exited 0 with "Validation passed"; build — the publish gate — wrote the dashboard into dist/objectstack.json. The widget then rendered empty, and per the card that is the expensive part: the board it was measured on leads with a "not moving" tile, and "an empty tile is indistinguishable from a healthy team: a missing number reads as zero, and zero is the answer the manager is hoping for." The failure is silent in the direction the reader wants to believe.

Premise re-check — measured on this branch's base, not assumed

Base is 345fc33ad (PR #14267, the #14105 family opener). Verified before writing code:

premiseverdictevidence
the seam landed and is exported from the package indexHOLDSpackages/lint/src/index.ts exports walkFilterFieldKeys (filter-walk.ts) and indexObjectGraph / resolveFieldPath (object-graph.ts); the export-site comment names this card as an intended consumer
gap A — widget filter KEYS unresolvedHOLDSvalidate-widget-bindings.ts at base reads w.dataset, w.dimensions, w.values, w.chartConfig, w.filterBindings — and never w.filter
gap B — options.sortBy uncheckedHOLDSvalidate-sortable-fields.ts explicitly defers it: "Dashboard widget sort config. Verified present but out of this predicate's domain … Judging those needs the dataset's measure index, which is validateChartBindings' family, not this one." Nothing in that family judged it

The card's six-row "what IS caught" table is a positive control, and it still holds at base: widget-dataset-unknown, widget-dimension-unknown, widget-measure-unknown, filter-token-unknown and dashboard-filter-field-unknown all live and gating. So the two zeros were readings, not an unread file. premise_still_valid: true on all three.

What lands

Three gating rule ids, all at the site that already emits widget-dataset-unknown / dashboard-filter-field-unknown:

  • widget-filter-field-unknown (limb A) — a key of the widget's own filter resolves to no column on the bound dataset's object graph. Reported path-precise at dashboards[i].widgets[j].filter.KEY.
  • widget-filter-field-not-included (limb A, second clause) — the key resolves, but its relationship prefix is not declared in the dataset's include.
  • widget-sortby-unselected (limb B) — options.sortBy names neither a dimensions[] nor a values[] entry of the widget, reported at dashboards[i].widgets[j].options.sortBy.

All three name dashboard, widget, key and object, and print the object's field list (limb A) or what the widget selects (limb B).

The open sub-question, answered explicitly: a dotted path through a declared include is RESOLVED

The card required this be stated rather than passed through silently, since a third silent pass-through would reproduce the card. Decision: resolve it, under the same two clauses the dataset-level sibling applies one level down — existence first, then ADR-0021 joinability.

The reasoning is mechanical rather than aesthetic. A widget's filter is ANDed into the dataset query as runtimeFilter (DashboardWidgetSchema.filter; dataset-executor.tscombineFilters(compiled.filter, selection.runtimeFilter)), and that compiled query carries only the joins include declared (dataset-compiler.ts derives every join alias from include). So a dotted key is exactly as decidable here as at a dataset dimension.

And the runtime is not a backstop for the second clause: dataset-compiler.ts's assertDeclared is called at two sites only — assertDeclared(d.field, 'dimension', …) and assertDeclared(m.field, 'measure', …) — never over runtimeFilter. Nothing between the author and the empty tile asked this question.

Note this deliberately differs from the neighbouring dashboard-filter-field-unknown, which still skips a dotted field (if (field.includes('.')) continue;). That skip was correct when nothing in this package could walk hops; it is now a real gap, and migrating it is filed rather than smuggled in here (see Follow-ups).

Reuse, not a third implementation

Triage was explicit: "generalise the one working implementation, not three parallel rules." No second hop-walker and no second filter-key reader were written. Both limbs are built on walkFilterFieldKeys (which already handles all three authored filter shapes — Mongo condition objects, { field, operator, value } rules, and [field, op, value] triples) and indexObjectGraph / resolveFieldPath.

Two helpers that were local to validate-dataset-references.ts moved into object-graph.ts and are now exported: joinablePrefixes (how ADR-0021 expands an include into joinable prefixes) and describeFieldPathVerdict (how one verdict reads in prose, renamed from existenceMessage now that it is shared). Copying either would have been the second implementation the seam exists to prevent, one release after it was written to prevent it. The sibling's behaviour is unchanged — its 526-line test file passes untouched.

The three skips are inherited unchanged, so an object this stack does not define, an ADR-0015 external object with no readable field map, and a registry-injected system column are never reported. Injected columns resolve per object via the graph rather than through the flat SYSTEM_FIELDS union the neighbouring rule uses, which is strictly better: a reference to owner_id on an ownership: 'none' object stays a real finding.

Verification

Ratchet and gate readings below were taken on the final commit, 5216886e2.

Acceptance criterion pinned end-to-end, not inferred. The card's binding criterion is that both limbs fail validate AND build — a validate-only fix was not acceptable. Rather than assert this from the registry entry's commands: ALL, six tests drive runAuthoringRules('validate' | 'build', …) + splitBySeverity over the repro stacks and assert the rule id appears in errors, plus a clean shape passing on both. Nothing else in the file would notice if that entry's commands were narrowed later.

Ablation — the new tests genuinely fail without the implementation. Both limb bodies were disabled at their guards (implementation committed first; mutation confirmed on disk by injected-marker count 2, original-anchor count 0, and a blob hash differing from the HEAD blob). Result: 13 failed / 75 passed, including all four validate/build acceptance pins. Restored via git checkout HEAD -- ABSOLUTE_PATH under an EXIT INT TERM trap and proven byte-identical: git diff HEAD empty, git hash-object back to 4c3379d74c386d50f13e6d7bf7c07133816f745f, zero markers remaining. No rebuild leg is skipped or needed here, and that is a property rather than an omission: the test reaches the rule through a relative source import (./validate-widget-bindings.js), resolved by vitest to src/, never through the package exports/dist.

Note the "clean shape" tests stay green under ablation, which is correct — an ablated rule reports nothing — so the 75 passes are not evidence of anything.

Runs:

  • pnpm --filter @objectstack/lint test90 files, 2558 tests, all pass
  • pnpm --filter @objectstack/lint typecheck — clean (tsc --noEmit)
  • pnpm lint (repo-scale eslint . --no-inline-config) — exit 0, no narrowing
  • Gate families derived from the real change set via scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands — 33 families (29 by path + 6 by kind, 2 overlapping) — 31 green
  • Also run as dispatched: check-ratchet-remedy-authority (green, 183 scripts swept) and check:declared-population-live (green, 156/200 families reach the tree)
  • check:type-check-coverage green; check:type-check-debt --re-measure green after building the closure — "27 ledger entries re-measured, 1217 raw tsc errors total, none above its recorded number"
  • check:dual-build-cjs-loads green after the same build (it had reported exit 3 PREREQUISITE NOT MET beforehand)
  • check:nul-bytes green; diff additionally self-scanned for raw control bytes — clean

One family NOT MEASURED locally, by its own instruction:scripts/check-test-completeness.mjs requires a saved turbo run test log that CI tees and this invocation has none, so it exits 3 and its own text says "running the family locally, record this gate as NOT MEASURED … It is not a red, and there is nothing here to fix." Recorded as NOT MEASURED rather than as a pass.

A note on the ratchet the type gates could not see

packages/lint/tsconfig.json excludes **/*.test.ts, so pnpm --filter @objectstack/lint typecheck says nothing about the test file this PR adds. That is measured, not assumed — tsc --listFiles returns the three source files and not the test. The tests here were therefore type-checked separately under a temporary config that includes them (removed again): zero errors attributable to this diff, against 22 pre-existing errors in seven untouched test files. That separate run is what caught a genuine defect in this PR's own test helper — a non-generic idsOf that erased the finding type — which vitest cannot see and which would otherwise have shipped. The structural half is accounted for: check:type-check-coverage records this package under TEST_DEBT, and the ratchet re-measure is green.

Scope

  • test/dashboard.test.ts in objectstack-ai/duly is the downstream stopgap, written to be deleted when this lands. Deleting it is a follow-up in that repo, not this PR's scope — this PR does not touch duly.
  • Both limbs ride the existing validateWidgetBindings registry entry, whose surfaces is per-RULE. As with the six error ids already on it, the three new ones therefore also reach the runtime publish gate for dashboard writes. That is safe in the direction that matters: RuntimeStackContext carries objects, so the graph resolves there; were it ever not carried, resolveFieldPath returns unknowable and the rules go silent rather than inventing findings.

Follow-ups


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 11 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/lint/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx(via validateWidgetBindings (symbol, a top-level function))
  • content/docs/releases/v17.mdx(via validateWidgetBindings (symbol, a top-level function))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/lint/src/index.ts) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 6eb8e3cc5022c4b1d0007962220444f4f947036dpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 4acf3c1f3ab8017c7066fdb36c8410a51cd8873c — the merge of head 5216886e2b62f59830dd01a8b98ea486a5de9c6d into base 6eb8e3cc5022c4b1d0007962220444f4f947036d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4acf3c1f3ab8017c7066fdb36c8410a51cd8873c && git checkout 4acf3c1f3ab8017c7066fdb36c8410a51cd8873c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6eb8e3cc5022c4b1d0007962220444f4f947036d 5216886e2b62f59830dd01a8b98ea486a5de9c6d && git checkout -B drift-repro 6eb8e3cc5022c4b1d0007962220444f4f947036d && git merge --no-ff 5216886e2b62f59830dd01a8b98ea486a5de9c6d
node scripts/docs-audit/affected-docs.mjs --json 6eb8e3cc5022c4b1d0007962220444f4f947036d

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 6eb8e3cc5022c4b1d0007962220444f4f947036d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 1, 2026 20:08
@baozhoutao
baozhoutao added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit fa1eca3Sep 1, 2026
35 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-14148-widget-filter-sortby branch September 1, 2026 20:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A dashboard widget's OWN filter keys and options.sortBy are not resolved at author time — validate/build exit 0, widget renders empty

2 participants

@baozhoutao@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(lint): resolve a dashboard widget's own filter keys and options.sortBy at validate/build - #14276

Merged
baozhoutao merged 4 commits into
mainfrom
claude/issue-14148-widget-filter-sortby
Sep 1, 2026
Merged

fix(lint): resolve a dashboard widget's own filter keys and options.sortBy at validate/build#14276
baozhoutao merged 4 commits into
mainfrom
claude/issue-14148-widget-filter-sortby

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes#14148

A dashboard widget could filter by a column that does not exist, and order by a name it never selected, and objectstack validate exited 0 with "Validation passed"; build — the publish gate — wrote the dashboard into dist/objectstack.json. The widget then rendered empty, and per the card that is the expensive part: the board it was measured on leads with a "not moving" tile, and "an empty tile is indistinguishable from a healthy team: a missing number reads as zero, and zero is the answer the manager is hoping for." The failure is silent in the direction the reader wants to believe.

Premise re-check — measured on this branch's base, not assumed

Base is 345fc33ad (PR #14267, the #14105 family opener). Verified before writing code:

premiseverdictevidence
the seam landed and is exported from the package indexHOLDSpackages/lint/src/index.ts exports walkFilterFieldKeys (filter-walk.ts) and indexObjectGraph / resolveFieldPath (object-graph.ts); the export-site comment names this card as an intended consumer
gap A — widget filter KEYS unresolvedHOLDSvalidate-widget-bindings.ts at base reads w.dataset, w.dimensions, w.values, w.chartConfig, w.filterBindings — and never w.filter
gap B — options.sortBy uncheckedHOLDSvalidate-sortable-fields.ts explicitly defers it: "Dashboard widget sort config. Verified present but out of this predicate's domain … Judging those needs the dataset's measure index, which is validateChartBindings' family, not this one." Nothing in that family judged it

The card's six-row "what IS caught" table is a positive control, and it still holds at base: widget-dataset-unknown, widget-dimension-unknown, widget-measure-unknown, filter-token-unknown and dashboard-filter-field-unknown all live and gating. So the two zeros were readings, not an unread file. premise_still_valid: true on all three.

What lands

Three gating rule ids, all at the site that already emits widget-dataset-unknown / dashboard-filter-field-unknown:

  • widget-filter-field-unknown (limb A) — a key of the widget's own filter resolves to no column on the bound dataset's object graph. Reported path-precise at dashboards[i].widgets[j].filter.KEY.
  • widget-filter-field-not-included (limb A, second clause) — the key resolves, but its relationship prefix is not declared in the dataset's include.
  • widget-sortby-unselected (limb B) — options.sortBy names neither a dimensions[] nor a values[] entry of the widget, reported at dashboards[i].widgets[j].options.sortBy.

All three name dashboard, widget, key and object, and print the object's field list (limb A) or what the widget selects (limb B).

The open sub-question, answered explicitly: a dotted path through a declared include is RESOLVED

The card required this be stated rather than passed through silently, since a third silent pass-through would reproduce the card. Decision: resolve it, under the same two clauses the dataset-level sibling applies one level down — existence first, then ADR-0021 joinability.

The reasoning is mechanical rather than aesthetic. A widget's filter is ANDed into the dataset query as runtimeFilter (DashboardWidgetSchema.filter; dataset-executor.tscombineFilters(compiled.filter, selection.runtimeFilter)), and that compiled query carries only the joins include declared (dataset-compiler.ts derives every join alias from include). So a dotted key is exactly as decidable here as at a dataset dimension.

And the runtime is not a backstop for the second clause: dataset-compiler.ts's assertDeclared is called at two sites only — assertDeclared(d.field, 'dimension', …) and assertDeclared(m.field, 'measure', …) — never over runtimeFilter. Nothing between the author and the empty tile asked this question.

Note this deliberately differs from the neighbouring dashboard-filter-field-unknown, which still skips a dotted field (if (field.includes('.')) continue;). That skip was correct when nothing in this package could walk hops; it is now a real gap, and migrating it is filed rather than smuggled in here (see Follow-ups).

Reuse, not a third implementation

Triage was explicit: "generalise the one working implementation, not three parallel rules." No second hop-walker and no second filter-key reader were written. Both limbs are built on walkFilterFieldKeys (which already handles all three authored filter shapes — Mongo condition objects, { field, operator, value } rules, and [field, op, value] triples) and indexObjectGraph / resolveFieldPath.

Two helpers that were local to validate-dataset-references.ts moved into object-graph.ts and are now exported: joinablePrefixes (how ADR-0021 expands an include into joinable prefixes) and describeFieldPathVerdict (how one verdict reads in prose, renamed from existenceMessage now that it is shared). Copying either would have been the second implementation the seam exists to prevent, one release after it was written to prevent it. The sibling's behaviour is unchanged — its 526-line test file passes untouched.

The three skips are inherited unchanged, so an object this stack does not define, an ADR-0015 external object with no readable field map, and a registry-injected system column are never reported. Injected columns resolve per object via the graph rather than through the flat SYSTEM_FIELDS union the neighbouring rule uses, which is strictly better: a reference to owner_id on an ownership: 'none' object stays a real finding.

Verification

Ratchet and gate readings below were taken on the final commit, 5216886e2.

Acceptance criterion pinned end-to-end, not inferred. The card's binding criterion is that both limbs fail validate AND build — a validate-only fix was not acceptable. Rather than assert this from the registry entry's commands: ALL, six tests drive runAuthoringRules('validate' | 'build', …) + splitBySeverity over the repro stacks and assert the rule id appears in errors, plus a clean shape passing on both. Nothing else in the file would notice if that entry's commands were narrowed later.

Ablation — the new tests genuinely fail without the implementation. Both limb bodies were disabled at their guards (implementation committed first; mutation confirmed on disk by injected-marker count 2, original-anchor count 0, and a blob hash differing from the HEAD blob). Result: 13 failed / 75 passed, including all four validate/build acceptance pins. Restored via git checkout HEAD -- ABSOLUTE_PATH under an EXIT INT TERM trap and proven byte-identical: git diff HEAD empty, git hash-object back to 4c3379d74c386d50f13e6d7bf7c07133816f745f, zero markers remaining. No rebuild leg is skipped or needed here, and that is a property rather than an omission: the test reaches the rule through a relative source import (./validate-widget-bindings.js), resolved by vitest to src/, never through the package exports/dist.

Note the "clean shape" tests stay green under ablation, which is correct — an ablated rule reports nothing — so the 75 passes are not evidence of anything.

Runs:

  • pnpm --filter @objectstack/lint test90 files, 2558 tests, all pass
  • pnpm --filter @objectstack/lint typecheck — clean (tsc --noEmit)
  • pnpm lint (repo-scale eslint . --no-inline-config) — exit 0, no narrowing
  • Gate families derived from the real change set via scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands — 33 families (29 by path + 6 by kind, 2 overlapping) — 31 green
  • Also run as dispatched: check-ratchet-remedy-authority (green, 183 scripts swept) and check:declared-population-live (green, 156/200 families reach the tree)
  • check:type-check-coverage green; check:type-check-debt --re-measure green after building the closure — "27 ledger entries re-measured, 1217 raw tsc errors total, none above its recorded number"
  • check:dual-build-cjs-loads green after the same build (it had reported exit 3 PREREQUISITE NOT MET beforehand)
  • check:nul-bytes green; diff additionally self-scanned for raw control bytes — clean

One family NOT MEASURED locally, by its own instruction:scripts/check-test-completeness.mjs requires a saved turbo run test log that CI tees and this invocation has none, so it exits 3 and its own text says "running the family locally, record this gate as NOT MEASURED … It is not a red, and there is nothing here to fix." Recorded as NOT MEASURED rather than as a pass.

A note on the ratchet the type gates could not see

packages/lint/tsconfig.json excludes **/*.test.ts, so pnpm --filter @objectstack/lint typecheck says nothing about the test file this PR adds. That is measured, not assumed — tsc --listFiles returns the three source files and not the test. The tests here were therefore type-checked separately under a temporary config that includes them (removed again): zero errors attributable to this diff, against 22 pre-existing errors in seven untouched test files. That separate run is what caught a genuine defect in this PR's own test helper — a non-generic idsOf that erased the finding type — which vitest cannot see and which would otherwise have shipped. The structural half is accounted for: check:type-check-coverage records this package under TEST_DEBT, and the ratchet re-measure is green.

Scope

  • test/dashboard.test.ts in objectstack-ai/duly is the downstream stopgap, written to be deleted when this lands. Deleting it is a follow-up in that repo, not this PR's scope — this PR does not touch duly.
  • Both limbs ride the existing validateWidgetBindings registry entry, whose surfaces is per-RULE. As with the six error ids already on it, the three new ones therefore also reach the runtime publish gate for dashboard writes. That is safe in the direction that matters: RuntimeStackContext carries objects, so the graph resolves there; were it ever not carried, resolveFieldPath returns unknowable and the rules go silent rather than inventing findings.

Follow-ups


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 11 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/lint/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx(via validateWidgetBindings (symbol, a top-level function))
  • content/docs/releases/v17.mdx(via validateWidgetBindings (symbol, a top-level function))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/lint/src/index.ts) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 6eb8e3cc5022c4b1d0007962220444f4f947036dpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 4acf3c1f3ab8017c7066fdb36c8410a51cd8873c — the merge of head 5216886e2b62f59830dd01a8b98ea486a5de9c6d into base 6eb8e3cc5022c4b1d0007962220444f4f947036d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4acf3c1f3ab8017c7066fdb36c8410a51cd8873c && git checkout 4acf3c1f3ab8017c7066fdb36c8410a51cd8873c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6eb8e3cc5022c4b1d0007962220444f4f947036d 5216886e2b62f59830dd01a8b98ea486a5de9c6d && git checkout -B drift-repro 6eb8e3cc5022c4b1d0007962220444f4f947036d && git merge --no-ff 5216886e2b62f59830dd01a8b98ea486a5de9c6d
node scripts/docs-audit/affected-docs.mjs --json 6eb8e3cc5022c4b1d0007962220444f4f947036d

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 6eb8e3cc5022c4b1d0007962220444f4f947036d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 1, 2026 20:08
@baozhoutao
baozhoutao added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit fa1eca3Sep 1, 2026
35 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-14148-widget-filter-sortby branch September 1, 2026 20:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A dashboard widget's OWN filter keys and options.sortBy are not resolved at author time — validate/build exit 0, widget renders empty

2 participants

@baozhoutao@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(lint): resolve a dashboard widget's own filter keys and options.sortBy at validate/build - #14276

Merged
baozhoutao merged 4 commits into
mainfrom
claude/issue-14148-widget-filter-sortby
Sep 1, 2026
Merged

fix(lint): resolve a dashboard widget's own filter keys and options.sortBy at validate/build#14276
baozhoutao merged 4 commits into
mainfrom
claude/issue-14148-widget-filter-sortby

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes#14148

A dashboard widget could filter by a column that does not exist, and order by a name it never selected, and objectstack validate exited 0 with "Validation passed"; build — the publish gate — wrote the dashboard into dist/objectstack.json. The widget then rendered empty, and per the card that is the expensive part: the board it was measured on leads with a "not moving" tile, and "an empty tile is indistinguishable from a healthy team: a missing number reads as zero, and zero is the answer the manager is hoping for." The failure is silent in the direction the reader wants to believe.

Premise re-check — measured on this branch's base, not assumed

Base is 345fc33ad (PR #14267, the #14105 family opener). Verified before writing code:

premiseverdictevidence
the seam landed and is exported from the package indexHOLDSpackages/lint/src/index.ts exports walkFilterFieldKeys (filter-walk.ts) and indexObjectGraph / resolveFieldPath (object-graph.ts); the export-site comment names this card as an intended consumer
gap A — widget filter KEYS unresolvedHOLDSvalidate-widget-bindings.ts at base reads w.dataset, w.dimensions, w.values, w.chartConfig, w.filterBindings — and never w.filter
gap B — options.sortBy uncheckedHOLDSvalidate-sortable-fields.ts explicitly defers it: "Dashboard widget sort config. Verified present but out of this predicate's domain … Judging those needs the dataset's measure index, which is validateChartBindings' family, not this one." Nothing in that family judged it

The card's six-row "what IS caught" table is a positive control, and it still holds at base: widget-dataset-unknown, widget-dimension-unknown, widget-measure-unknown, filter-token-unknown and dashboard-filter-field-unknown all live and gating. So the two zeros were readings, not an unread file. premise_still_valid: true on all three.

What lands

Three gating rule ids, all at the site that already emits widget-dataset-unknown / dashboard-filter-field-unknown:

  • widget-filter-field-unknown (limb A) — a key of the widget's own filter resolves to no column on the bound dataset's object graph. Reported path-precise at dashboards[i].widgets[j].filter.KEY.
  • widget-filter-field-not-included (limb A, second clause) — the key resolves, but its relationship prefix is not declared in the dataset's include.
  • widget-sortby-unselected (limb B) — options.sortBy names neither a dimensions[] nor a values[] entry of the widget, reported at dashboards[i].widgets[j].options.sortBy.

All three name dashboard, widget, key and object, and print the object's field list (limb A) or what the widget selects (limb B).

The open sub-question, answered explicitly: a dotted path through a declared include is RESOLVED

The card required this be stated rather than passed through silently, since a third silent pass-through would reproduce the card. Decision: resolve it, under the same two clauses the dataset-level sibling applies one level down — existence first, then ADR-0021 joinability.

The reasoning is mechanical rather than aesthetic. A widget's filter is ANDed into the dataset query as runtimeFilter (DashboardWidgetSchema.filter; dataset-executor.tscombineFilters(compiled.filter, selection.runtimeFilter)), and that compiled query carries only the joins include declared (dataset-compiler.ts derives every join alias from include). So a dotted key is exactly as decidable here as at a dataset dimension.

And the runtime is not a backstop for the second clause: dataset-compiler.ts's assertDeclared is called at two sites only — assertDeclared(d.field, 'dimension', …) and assertDeclared(m.field, 'measure', …) — never over runtimeFilter. Nothing between the author and the empty tile asked this question.

Note this deliberately differs from the neighbouring dashboard-filter-field-unknown, which still skips a dotted field (if (field.includes('.')) continue;). That skip was correct when nothing in this package could walk hops; it is now a real gap, and migrating it is filed rather than smuggled in here (see Follow-ups).

Reuse, not a third implementation

Triage was explicit: "generalise the one working implementation, not three parallel rules." No second hop-walker and no second filter-key reader were written. Both limbs are built on walkFilterFieldKeys (which already handles all three authored filter shapes — Mongo condition objects, { field, operator, value } rules, and [field, op, value] triples) and indexObjectGraph / resolveFieldPath.

Two helpers that were local to validate-dataset-references.ts moved into object-graph.ts and are now exported: joinablePrefixes (how ADR-0021 expands an include into joinable prefixes) and describeFieldPathVerdict (how one verdict reads in prose, renamed from existenceMessage now that it is shared). Copying either would have been the second implementation the seam exists to prevent, one release after it was written to prevent it. The sibling's behaviour is unchanged — its 526-line test file passes untouched.

The three skips are inherited unchanged, so an object this stack does not define, an ADR-0015 external object with no readable field map, and a registry-injected system column are never reported. Injected columns resolve per object via the graph rather than through the flat SYSTEM_FIELDS union the neighbouring rule uses, which is strictly better: a reference to owner_id on an ownership: 'none' object stays a real finding.

Verification

Ratchet and gate readings below were taken on the final commit, 5216886e2.

Acceptance criterion pinned end-to-end, not inferred. The card's binding criterion is that both limbs fail validate AND build — a validate-only fix was not acceptable. Rather than assert this from the registry entry's commands: ALL, six tests drive runAuthoringRules('validate' | 'build', …) + splitBySeverity over the repro stacks and assert the rule id appears in errors, plus a clean shape passing on both. Nothing else in the file would notice if that entry's commands were narrowed later.

Ablation — the new tests genuinely fail without the implementation. Both limb bodies were disabled at their guards (implementation committed first; mutation confirmed on disk by injected-marker count 2, original-anchor count 0, and a blob hash differing from the HEAD blob). Result: 13 failed / 75 passed, including all four validate/build acceptance pins. Restored via git checkout HEAD -- ABSOLUTE_PATH under an EXIT INT TERM trap and proven byte-identical: git diff HEAD empty, git hash-object back to 4c3379d74c386d50f13e6d7bf7c07133816f745f, zero markers remaining. No rebuild leg is skipped or needed here, and that is a property rather than an omission: the test reaches the rule through a relative source import (./validate-widget-bindings.js), resolved by vitest to src/, never through the package exports/dist.

Note the "clean shape" tests stay green under ablation, which is correct — an ablated rule reports nothing — so the 75 passes are not evidence of anything.

Runs:

  • pnpm --filter @objectstack/lint test90 files, 2558 tests, all pass
  • pnpm --filter @objectstack/lint typecheck — clean (tsc --noEmit)
  • pnpm lint (repo-scale eslint . --no-inline-config) — exit 0, no narrowing
  • Gate families derived from the real change set via scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands — 33 families (29 by path + 6 by kind, 2 overlapping) — 31 green
  • Also run as dispatched: check-ratchet-remedy-authority (green, 183 scripts swept) and check:declared-population-live (green, 156/200 families reach the tree)
  • check:type-check-coverage green; check:type-check-debt --re-measure green after building the closure — "27 ledger entries re-measured, 1217 raw tsc errors total, none above its recorded number"
  • check:dual-build-cjs-loads green after the same build (it had reported exit 3 PREREQUISITE NOT MET beforehand)
  • check:nul-bytes green; diff additionally self-scanned for raw control bytes — clean

One family NOT MEASURED locally, by its own instruction:scripts/check-test-completeness.mjs requires a saved turbo run test log that CI tees and this invocation has none, so it exits 3 and its own text says "running the family locally, record this gate as NOT MEASURED … It is not a red, and there is nothing here to fix." Recorded as NOT MEASURED rather than as a pass.

A note on the ratchet the type gates could not see

packages/lint/tsconfig.json excludes **/*.test.ts, so pnpm --filter @objectstack/lint typecheck says nothing about the test file this PR adds. That is measured, not assumed — tsc --listFiles returns the three source files and not the test. The tests here were therefore type-checked separately under a temporary config that includes them (removed again): zero errors attributable to this diff, against 22 pre-existing errors in seven untouched test files. That separate run is what caught a genuine defect in this PR's own test helper — a non-generic idsOf that erased the finding type — which vitest cannot see and which would otherwise have shipped. The structural half is accounted for: check:type-check-coverage records this package under TEST_DEBT, and the ratchet re-measure is green.

Scope

  • test/dashboard.test.ts in objectstack-ai/duly is the downstream stopgap, written to be deleted when this lands. Deleting it is a follow-up in that repo, not this PR's scope — this PR does not touch duly.
  • Both limbs ride the existing validateWidgetBindings registry entry, whose surfaces is per-RULE. As with the six error ids already on it, the three new ones therefore also reach the runtime publish gate for dashboard writes. That is safe in the direction that matters: RuntimeStackContext carries objects, so the graph resolves there; were it ever not carried, resolveFieldPath returns unknowable and the rules go silent rather than inventing findings.

Follow-ups


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 11 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/lint/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx(via validateWidgetBindings (symbol, a top-level function))
  • content/docs/releases/v17.mdx(via validateWidgetBindings (symbol, a top-level function))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/lint/src/index.ts) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 6eb8e3cc5022c4b1d0007962220444f4f947036dpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 4acf3c1f3ab8017c7066fdb36c8410a51cd8873c — the merge of head 5216886e2b62f59830dd01a8b98ea486a5de9c6d into base 6eb8e3cc5022c4b1d0007962220444f4f947036d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4acf3c1f3ab8017c7066fdb36c8410a51cd8873c && git checkout 4acf3c1f3ab8017c7066fdb36c8410a51cd8873c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6eb8e3cc5022c4b1d0007962220444f4f947036d 5216886e2b62f59830dd01a8b98ea486a5de9c6d && git checkout -B drift-repro 6eb8e3cc5022c4b1d0007962220444f4f947036d && git merge --no-ff 5216886e2b62f59830dd01a8b98ea486a5de9c6d
node scripts/docs-audit/affected-docs.mjs --json 6eb8e3cc5022c4b1d0007962220444f4f947036d

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 6eb8e3cc5022c4b1d0007962220444f4f947036d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 1, 2026 20:08
@baozhoutao
baozhoutao added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit fa1eca3Sep 1, 2026
35 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-14148-widget-filter-sortby branch September 1, 2026 20:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A dashboard widget's OWN filter keys and options.sortBy are not resolved at author time — validate/build exit 0, widget renders empty

2 participants

@baozhoutao@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(lint): resolve a dashboard widget's own filter keys and options.sortBy at validate/build - #14276

Merged
baozhoutao merged 4 commits into
mainfrom
claude/issue-14148-widget-filter-sortby
Sep 1, 2026
Merged

fix(lint): resolve a dashboard widget's own filter keys and options.sortBy at validate/build#14276
baozhoutao merged 4 commits into
mainfrom
claude/issue-14148-widget-filter-sortby

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes#14148

A dashboard widget could filter by a column that does not exist, and order by a name it never selected, and objectstack validate exited 0 with "Validation passed"; build — the publish gate — wrote the dashboard into dist/objectstack.json. The widget then rendered empty, and per the card that is the expensive part: the board it was measured on leads with a "not moving" tile, and "an empty tile is indistinguishable from a healthy team: a missing number reads as zero, and zero is the answer the manager is hoping for." The failure is silent in the direction the reader wants to believe.

Premise re-check — measured on this branch's base, not assumed

Base is 345fc33ad (PR #14267, the #14105 family opener). Verified before writing code:

premiseverdictevidence
the seam landed and is exported from the package indexHOLDSpackages/lint/src/index.ts exports walkFilterFieldKeys (filter-walk.ts) and indexObjectGraph / resolveFieldPath (object-graph.ts); the export-site comment names this card as an intended consumer
gap A — widget filter KEYS unresolvedHOLDSvalidate-widget-bindings.ts at base reads w.dataset, w.dimensions, w.values, w.chartConfig, w.filterBindings — and never w.filter
gap B — options.sortBy uncheckedHOLDSvalidate-sortable-fields.ts explicitly defers it: "Dashboard widget sort config. Verified present but out of this predicate's domain … Judging those needs the dataset's measure index, which is validateChartBindings' family, not this one." Nothing in that family judged it

The card's six-row "what IS caught" table is a positive control, and it still holds at base: widget-dataset-unknown, widget-dimension-unknown, widget-measure-unknown, filter-token-unknown and dashboard-filter-field-unknown all live and gating. So the two zeros were readings, not an unread file. premise_still_valid: true on all three.

What lands

Three gating rule ids, all at the site that already emits widget-dataset-unknown / dashboard-filter-field-unknown:

  • widget-filter-field-unknown (limb A) — a key of the widget's own filter resolves to no column on the bound dataset's object graph. Reported path-precise at dashboards[i].widgets[j].filter.KEY.
  • widget-filter-field-not-included (limb A, second clause) — the key resolves, but its relationship prefix is not declared in the dataset's include.
  • widget-sortby-unselected (limb B) — options.sortBy names neither a dimensions[] nor a values[] entry of the widget, reported at dashboards[i].widgets[j].options.sortBy.

All three name dashboard, widget, key and object, and print the object's field list (limb A) or what the widget selects (limb B).

The open sub-question, answered explicitly: a dotted path through a declared include is RESOLVED

The card required this be stated rather than passed through silently, since a third silent pass-through would reproduce the card. Decision: resolve it, under the same two clauses the dataset-level sibling applies one level down — existence first, then ADR-0021 joinability.

The reasoning is mechanical rather than aesthetic. A widget's filter is ANDed into the dataset query as runtimeFilter (DashboardWidgetSchema.filter; dataset-executor.tscombineFilters(compiled.filter, selection.runtimeFilter)), and that compiled query carries only the joins include declared (dataset-compiler.ts derives every join alias from include). So a dotted key is exactly as decidable here as at a dataset dimension.

And the runtime is not a backstop for the second clause: dataset-compiler.ts's assertDeclared is called at two sites only — assertDeclared(d.field, 'dimension', …) and assertDeclared(m.field, 'measure', …) — never over runtimeFilter. Nothing between the author and the empty tile asked this question.

Note this deliberately differs from the neighbouring dashboard-filter-field-unknown, which still skips a dotted field (if (field.includes('.')) continue;). That skip was correct when nothing in this package could walk hops; it is now a real gap, and migrating it is filed rather than smuggled in here (see Follow-ups).

Reuse, not a third implementation

Triage was explicit: "generalise the one working implementation, not three parallel rules." No second hop-walker and no second filter-key reader were written. Both limbs are built on walkFilterFieldKeys (which already handles all three authored filter shapes — Mongo condition objects, { field, operator, value } rules, and [field, op, value] triples) and indexObjectGraph / resolveFieldPath.

Two helpers that were local to validate-dataset-references.ts moved into object-graph.ts and are now exported: joinablePrefixes (how ADR-0021 expands an include into joinable prefixes) and describeFieldPathVerdict (how one verdict reads in prose, renamed from existenceMessage now that it is shared). Copying either would have been the second implementation the seam exists to prevent, one release after it was written to prevent it. The sibling's behaviour is unchanged — its 526-line test file passes untouched.

The three skips are inherited unchanged, so an object this stack does not define, an ADR-0015 external object with no readable field map, and a registry-injected system column are never reported. Injected columns resolve per object via the graph rather than through the flat SYSTEM_FIELDS union the neighbouring rule uses, which is strictly better: a reference to owner_id on an ownership: 'none' object stays a real finding.

Verification

Ratchet and gate readings below were taken on the final commit, 5216886e2.

Acceptance criterion pinned end-to-end, not inferred. The card's binding criterion is that both limbs fail validate AND build — a validate-only fix was not acceptable. Rather than assert this from the registry entry's commands: ALL, six tests drive runAuthoringRules('validate' | 'build', …) + splitBySeverity over the repro stacks and assert the rule id appears in errors, plus a clean shape passing on both. Nothing else in the file would notice if that entry's commands were narrowed later.

Ablation — the new tests genuinely fail without the implementation. Both limb bodies were disabled at their guards (implementation committed first; mutation confirmed on disk by injected-marker count 2, original-anchor count 0, and a blob hash differing from the HEAD blob). Result: 13 failed / 75 passed, including all four validate/build acceptance pins. Restored via git checkout HEAD -- ABSOLUTE_PATH under an EXIT INT TERM trap and proven byte-identical: git diff HEAD empty, git hash-object back to 4c3379d74c386d50f13e6d7bf7c07133816f745f, zero markers remaining. No rebuild leg is skipped or needed here, and that is a property rather than an omission: the test reaches the rule through a relative source import (./validate-widget-bindings.js), resolved by vitest to src/, never through the package exports/dist.

Note the "clean shape" tests stay green under ablation, which is correct — an ablated rule reports nothing — so the 75 passes are not evidence of anything.

Runs:

  • pnpm --filter @objectstack/lint test90 files, 2558 tests, all pass
  • pnpm --filter @objectstack/lint typecheck — clean (tsc --noEmit)
  • pnpm lint (repo-scale eslint . --no-inline-config) — exit 0, no narrowing
  • Gate families derived from the real change set via scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands — 33 families (29 by path + 6 by kind, 2 overlapping) — 31 green
  • Also run as dispatched: check-ratchet-remedy-authority (green, 183 scripts swept) and check:declared-population-live (green, 156/200 families reach the tree)
  • check:type-check-coverage green; check:type-check-debt --re-measure green after building the closure — "27 ledger entries re-measured, 1217 raw tsc errors total, none above its recorded number"
  • check:dual-build-cjs-loads green after the same build (it had reported exit 3 PREREQUISITE NOT MET beforehand)
  • check:nul-bytes green; diff additionally self-scanned for raw control bytes — clean

One family NOT MEASURED locally, by its own instruction:scripts/check-test-completeness.mjs requires a saved turbo run test log that CI tees and this invocation has none, so it exits 3 and its own text says "running the family locally, record this gate as NOT MEASURED … It is not a red, and there is nothing here to fix." Recorded as NOT MEASURED rather than as a pass.

A note on the ratchet the type gates could not see

packages/lint/tsconfig.json excludes **/*.test.ts, so pnpm --filter @objectstack/lint typecheck says nothing about the test file this PR adds. That is measured, not assumed — tsc --listFiles returns the three source files and not the test. The tests here were therefore type-checked separately under a temporary config that includes them (removed again): zero errors attributable to this diff, against 22 pre-existing errors in seven untouched test files. That separate run is what caught a genuine defect in this PR's own test helper — a non-generic idsOf that erased the finding type — which vitest cannot see and which would otherwise have shipped. The structural half is accounted for: check:type-check-coverage records this package under TEST_DEBT, and the ratchet re-measure is green.

Scope

  • test/dashboard.test.ts in objectstack-ai/duly is the downstream stopgap, written to be deleted when this lands. Deleting it is a follow-up in that repo, not this PR's scope — this PR does not touch duly.
  • Both limbs ride the existing validateWidgetBindings registry entry, whose surfaces is per-RULE. As with the six error ids already on it, the three new ones therefore also reach the runtime publish gate for dashboard writes. That is safe in the direction that matters: RuntimeStackContext carries objects, so the graph resolves there; were it ever not carried, resolveFieldPath returns unknowable and the rules go silent rather than inventing findings.

Follow-ups


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 11 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/lint/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx(via validateWidgetBindings (symbol, a top-level function))
  • content/docs/releases/v17.mdx(via validateWidgetBindings (symbol, a top-level function))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/lint/src/index.ts) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 6eb8e3cc5022c4b1d0007962220444f4f947036dpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 4acf3c1f3ab8017c7066fdb36c8410a51cd8873c — the merge of head 5216886e2b62f59830dd01a8b98ea486a5de9c6d into base 6eb8e3cc5022c4b1d0007962220444f4f947036d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4acf3c1f3ab8017c7066fdb36c8410a51cd8873c && git checkout 4acf3c1f3ab8017c7066fdb36c8410a51cd8873c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6eb8e3cc5022c4b1d0007962220444f4f947036d 5216886e2b62f59830dd01a8b98ea486a5de9c6d && git checkout -B drift-repro 6eb8e3cc5022c4b1d0007962220444f4f947036d && git merge --no-ff 5216886e2b62f59830dd01a8b98ea486a5de9c6d
node scripts/docs-audit/affected-docs.mjs --json 6eb8e3cc5022c4b1d0007962220444f4f947036d

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 6eb8e3cc5022c4b1d0007962220444f4f947036d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 1, 2026 20:08
@baozhoutao
baozhoutao added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit fa1eca3Sep 1, 2026
35 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-14148-widget-filter-sortby branch September 1, 2026 20:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A dashboard widget's OWN filter keys and options.sortBy are not resolved at author time — validate/build exit 0, widget renders empty

2 participants

@baozhoutao@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(lint): resolve a dashboard widget's own filter keys and options.sortBy at validate/build - #14276

Merged
baozhoutao merged 4 commits into
mainfrom
claude/issue-14148-widget-filter-sortby
Sep 1, 2026
Merged

fix(lint): resolve a dashboard widget's own filter keys and options.sortBy at validate/build#14276
baozhoutao merged 4 commits into
mainfrom
claude/issue-14148-widget-filter-sortby

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes#14148

A dashboard widget could filter by a column that does not exist, and order by a name it never selected, and objectstack validate exited 0 with "Validation passed"; build — the publish gate — wrote the dashboard into dist/objectstack.json. The widget then rendered empty, and per the card that is the expensive part: the board it was measured on leads with a "not moving" tile, and "an empty tile is indistinguishable from a healthy team: a missing number reads as zero, and zero is the answer the manager is hoping for." The failure is silent in the direction the reader wants to believe.

Premise re-check — measured on this branch's base, not assumed

Base is 345fc33ad (PR #14267, the #14105 family opener). Verified before writing code:

premiseverdictevidence
the seam landed and is exported from the package indexHOLDSpackages/lint/src/index.ts exports walkFilterFieldKeys (filter-walk.ts) and indexObjectGraph / resolveFieldPath (object-graph.ts); the export-site comment names this card as an intended consumer
gap A — widget filter KEYS unresolvedHOLDSvalidate-widget-bindings.ts at base reads w.dataset, w.dimensions, w.values, w.chartConfig, w.filterBindings — and never w.filter
gap B — options.sortBy uncheckedHOLDSvalidate-sortable-fields.ts explicitly defers it: "Dashboard widget sort config. Verified present but out of this predicate's domain … Judging those needs the dataset's measure index, which is validateChartBindings' family, not this one." Nothing in that family judged it

The card's six-row "what IS caught" table is a positive control, and it still holds at base: widget-dataset-unknown, widget-dimension-unknown, widget-measure-unknown, filter-token-unknown and dashboard-filter-field-unknown all live and gating. So the two zeros were readings, not an unread file. premise_still_valid: true on all three.

What lands

Three gating rule ids, all at the site that already emits widget-dataset-unknown / dashboard-filter-field-unknown:

  • widget-filter-field-unknown (limb A) — a key of the widget's own filter resolves to no column on the bound dataset's object graph. Reported path-precise at dashboards[i].widgets[j].filter.KEY.
  • widget-filter-field-not-included (limb A, second clause) — the key resolves, but its relationship prefix is not declared in the dataset's include.
  • widget-sortby-unselected (limb B) — options.sortBy names neither a dimensions[] nor a values[] entry of the widget, reported at dashboards[i].widgets[j].options.sortBy.

All three name dashboard, widget, key and object, and print the object's field list (limb A) or what the widget selects (limb B).

The open sub-question, answered explicitly: a dotted path through a declared include is RESOLVED

The card required this be stated rather than passed through silently, since a third silent pass-through would reproduce the card. Decision: resolve it, under the same two clauses the dataset-level sibling applies one level down — existence first, then ADR-0021 joinability.

The reasoning is mechanical rather than aesthetic. A widget's filter is ANDed into the dataset query as runtimeFilter (DashboardWidgetSchema.filter; dataset-executor.tscombineFilters(compiled.filter, selection.runtimeFilter)), and that compiled query carries only the joins include declared (dataset-compiler.ts derives every join alias from include). So a dotted key is exactly as decidable here as at a dataset dimension.

And the runtime is not a backstop for the second clause: dataset-compiler.ts's assertDeclared is called at two sites only — assertDeclared(d.field, 'dimension', …) and assertDeclared(m.field, 'measure', …) — never over runtimeFilter. Nothing between the author and the empty tile asked this question.

Note this deliberately differs from the neighbouring dashboard-filter-field-unknown, which still skips a dotted field (if (field.includes('.')) continue;). That skip was correct when nothing in this package could walk hops; it is now a real gap, and migrating it is filed rather than smuggled in here (see Follow-ups).

Reuse, not a third implementation

Triage was explicit: "generalise the one working implementation, not three parallel rules." No second hop-walker and no second filter-key reader were written. Both limbs are built on walkFilterFieldKeys (which already handles all three authored filter shapes — Mongo condition objects, { field, operator, value } rules, and [field, op, value] triples) and indexObjectGraph / resolveFieldPath.

Two helpers that were local to validate-dataset-references.ts moved into object-graph.ts and are now exported: joinablePrefixes (how ADR-0021 expands an include into joinable prefixes) and describeFieldPathVerdict (how one verdict reads in prose, renamed from existenceMessage now that it is shared). Copying either would have been the second implementation the seam exists to prevent, one release after it was written to prevent it. The sibling's behaviour is unchanged — its 526-line test file passes untouched.

The three skips are inherited unchanged, so an object this stack does not define, an ADR-0015 external object with no readable field map, and a registry-injected system column are never reported. Injected columns resolve per object via the graph rather than through the flat SYSTEM_FIELDS union the neighbouring rule uses, which is strictly better: a reference to owner_id on an ownership: 'none' object stays a real finding.

Verification

Ratchet and gate readings below were taken on the final commit, 5216886e2.

Acceptance criterion pinned end-to-end, not inferred. The card's binding criterion is that both limbs fail validate AND build — a validate-only fix was not acceptable. Rather than assert this from the registry entry's commands: ALL, six tests drive runAuthoringRules('validate' | 'build', …) + splitBySeverity over the repro stacks and assert the rule id appears in errors, plus a clean shape passing on both. Nothing else in the file would notice if that entry's commands were narrowed later.

Ablation — the new tests genuinely fail without the implementation. Both limb bodies were disabled at their guards (implementation committed first; mutation confirmed on disk by injected-marker count 2, original-anchor count 0, and a blob hash differing from the HEAD blob). Result: 13 failed / 75 passed, including all four validate/build acceptance pins. Restored via git checkout HEAD -- ABSOLUTE_PATH under an EXIT INT TERM trap and proven byte-identical: git diff HEAD empty, git hash-object back to 4c3379d74c386d50f13e6d7bf7c07133816f745f, zero markers remaining. No rebuild leg is skipped or needed here, and that is a property rather than an omission: the test reaches the rule through a relative source import (./validate-widget-bindings.js), resolved by vitest to src/, never through the package exports/dist.

Note the "clean shape" tests stay green under ablation, which is correct — an ablated rule reports nothing — so the 75 passes are not evidence of anything.

Runs:

  • pnpm --filter @objectstack/lint test90 files, 2558 tests, all pass
  • pnpm --filter @objectstack/lint typecheck — clean (tsc --noEmit)
  • pnpm lint (repo-scale eslint . --no-inline-config) — exit 0, no narrowing
  • Gate families derived from the real change set via scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands — 33 families (29 by path + 6 by kind, 2 overlapping) — 31 green
  • Also run as dispatched: check-ratchet-remedy-authority (green, 183 scripts swept) and check:declared-population-live (green, 156/200 families reach the tree)
  • check:type-check-coverage green; check:type-check-debt --re-measure green after building the closure — "27 ledger entries re-measured, 1217 raw tsc errors total, none above its recorded number"
  • check:dual-build-cjs-loads green after the same build (it had reported exit 3 PREREQUISITE NOT MET beforehand)
  • check:nul-bytes green; diff additionally self-scanned for raw control bytes — clean

One family NOT MEASURED locally, by its own instruction:scripts/check-test-completeness.mjs requires a saved turbo run test log that CI tees and this invocation has none, so it exits 3 and its own text says "running the family locally, record this gate as NOT MEASURED … It is not a red, and there is nothing here to fix." Recorded as NOT MEASURED rather than as a pass.

A note on the ratchet the type gates could not see

packages/lint/tsconfig.json excludes **/*.test.ts, so pnpm --filter @objectstack/lint typecheck says nothing about the test file this PR adds. That is measured, not assumed — tsc --listFiles returns the three source files and not the test. The tests here were therefore type-checked separately under a temporary config that includes them (removed again): zero errors attributable to this diff, against 22 pre-existing errors in seven untouched test files. That separate run is what caught a genuine defect in this PR's own test helper — a non-generic idsOf that erased the finding type — which vitest cannot see and which would otherwise have shipped. The structural half is accounted for: check:type-check-coverage records this package under TEST_DEBT, and the ratchet re-measure is green.

Scope

  • test/dashboard.test.ts in objectstack-ai/duly is the downstream stopgap, written to be deleted when this lands. Deleting it is a follow-up in that repo, not this PR's scope — this PR does not touch duly.
  • Both limbs ride the existing validateWidgetBindings registry entry, whose surfaces is per-RULE. As with the six error ids already on it, the three new ones therefore also reach the runtime publish gate for dashboard writes. That is safe in the direction that matters: RuntimeStackContext carries objects, so the graph resolves there; were it ever not carried, resolveFieldPath returns unknowable and the rules go silent rather than inventing findings.

Follow-ups


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 11 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/lint/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx(via validateWidgetBindings (symbol, a top-level function))
  • content/docs/releases/v17.mdx(via validateWidgetBindings (symbol, a top-level function))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/lint/src/index.ts) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 6eb8e3cc5022c4b1d0007962220444f4f947036dpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 4acf3c1f3ab8017c7066fdb36c8410a51cd8873c — the merge of head 5216886e2b62f59830dd01a8b98ea486a5de9c6d into base 6eb8e3cc5022c4b1d0007962220444f4f947036d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4acf3c1f3ab8017c7066fdb36c8410a51cd8873c && git checkout 4acf3c1f3ab8017c7066fdb36c8410a51cd8873c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6eb8e3cc5022c4b1d0007962220444f4f947036d 5216886e2b62f59830dd01a8b98ea486a5de9c6d && git checkout -B drift-repro 6eb8e3cc5022c4b1d0007962220444f4f947036d && git merge --no-ff 5216886e2b62f59830dd01a8b98ea486a5de9c6d
node scripts/docs-audit/affected-docs.mjs --json 6eb8e3cc5022c4b1d0007962220444f4f947036d

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 6eb8e3cc5022c4b1d0007962220444f4f947036d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 1, 2026 20:08
@baozhoutao
baozhoutao added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit fa1eca3Sep 1, 2026
35 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-14148-widget-filter-sortby branch September 1, 2026 20:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A dashboard widget's OWN filter keys and options.sortBy are not resolved at author time — validate/build exit 0, widget renders empty

2 participants

@baozhoutao@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(lint): resolve a dashboard widget's own filter keys and options.sortBy at validate/build - #14276

Merged
baozhoutao merged 4 commits into
mainfrom
claude/issue-14148-widget-filter-sortby
Sep 1, 2026
Merged

fix(lint): resolve a dashboard widget's own filter keys and options.sortBy at validate/build#14276
baozhoutao merged 4 commits into
mainfrom
claude/issue-14148-widget-filter-sortby

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes#14148

A dashboard widget could filter by a column that does not exist, and order by a name it never selected, and objectstack validate exited 0 with "Validation passed"; build — the publish gate — wrote the dashboard into dist/objectstack.json. The widget then rendered empty, and per the card that is the expensive part: the board it was measured on leads with a "not moving" tile, and "an empty tile is indistinguishable from a healthy team: a missing number reads as zero, and zero is the answer the manager is hoping for." The failure is silent in the direction the reader wants to believe.

Premise re-check — measured on this branch's base, not assumed

Base is 345fc33ad (PR #14267, the #14105 family opener). Verified before writing code:

premiseverdictevidence
the seam landed and is exported from the package indexHOLDSpackages/lint/src/index.ts exports walkFilterFieldKeys (filter-walk.ts) and indexObjectGraph / resolveFieldPath (object-graph.ts); the export-site comment names this card as an intended consumer
gap A — widget filter KEYS unresolvedHOLDSvalidate-widget-bindings.ts at base reads w.dataset, w.dimensions, w.values, w.chartConfig, w.filterBindings — and never w.filter
gap B — options.sortBy uncheckedHOLDSvalidate-sortable-fields.ts explicitly defers it: "Dashboard widget sort config. Verified present but out of this predicate's domain … Judging those needs the dataset's measure index, which is validateChartBindings' family, not this one." Nothing in that family judged it

The card's six-row "what IS caught" table is a positive control, and it still holds at base: widget-dataset-unknown, widget-dimension-unknown, widget-measure-unknown, filter-token-unknown and dashboard-filter-field-unknown all live and gating. So the two zeros were readings, not an unread file. premise_still_valid: true on all three.

What lands

Three gating rule ids, all at the site that already emits widget-dataset-unknown / dashboard-filter-field-unknown:

  • widget-filter-field-unknown (limb A) — a key of the widget's own filter resolves to no column on the bound dataset's object graph. Reported path-precise at dashboards[i].widgets[j].filter.KEY.
  • widget-filter-field-not-included (limb A, second clause) — the key resolves, but its relationship prefix is not declared in the dataset's include.
  • widget-sortby-unselected (limb B) — options.sortBy names neither a dimensions[] nor a values[] entry of the widget, reported at dashboards[i].widgets[j].options.sortBy.

All three name dashboard, widget, key and object, and print the object's field list (limb A) or what the widget selects (limb B).

The open sub-question, answered explicitly: a dotted path through a declared include is RESOLVED

The card required this be stated rather than passed through silently, since a third silent pass-through would reproduce the card. Decision: resolve it, under the same two clauses the dataset-level sibling applies one level down — existence first, then ADR-0021 joinability.

The reasoning is mechanical rather than aesthetic. A widget's filter is ANDed into the dataset query as runtimeFilter (DashboardWidgetSchema.filter; dataset-executor.tscombineFilters(compiled.filter, selection.runtimeFilter)), and that compiled query carries only the joins include declared (dataset-compiler.ts derives every join alias from include). So a dotted key is exactly as decidable here as at a dataset dimension.

And the runtime is not a backstop for the second clause: dataset-compiler.ts's assertDeclared is called at two sites only — assertDeclared(d.field, 'dimension', …) and assertDeclared(m.field, 'measure', …) — never over runtimeFilter. Nothing between the author and the empty tile asked this question.

Note this deliberately differs from the neighbouring dashboard-filter-field-unknown, which still skips a dotted field (if (field.includes('.')) continue;). That skip was correct when nothing in this package could walk hops; it is now a real gap, and migrating it is filed rather than smuggled in here (see Follow-ups).

Reuse, not a third implementation

Triage was explicit: "generalise the one working implementation, not three parallel rules." No second hop-walker and no second filter-key reader were written. Both limbs are built on walkFilterFieldKeys (which already handles all three authored filter shapes — Mongo condition objects, { field, operator, value } rules, and [field, op, value] triples) and indexObjectGraph / resolveFieldPath.

Two helpers that were local to validate-dataset-references.ts moved into object-graph.ts and are now exported: joinablePrefixes (how ADR-0021 expands an include into joinable prefixes) and describeFieldPathVerdict (how one verdict reads in prose, renamed from existenceMessage now that it is shared). Copying either would have been the second implementation the seam exists to prevent, one release after it was written to prevent it. The sibling's behaviour is unchanged — its 526-line test file passes untouched.

The three skips are inherited unchanged, so an object this stack does not define, an ADR-0015 external object with no readable field map, and a registry-injected system column are never reported. Injected columns resolve per object via the graph rather than through the flat SYSTEM_FIELDS union the neighbouring rule uses, which is strictly better: a reference to owner_id on an ownership: 'none' object stays a real finding.

Verification

Ratchet and gate readings below were taken on the final commit, 5216886e2.

Acceptance criterion pinned end-to-end, not inferred. The card's binding criterion is that both limbs fail validate AND build — a validate-only fix was not acceptable. Rather than assert this from the registry entry's commands: ALL, six tests drive runAuthoringRules('validate' | 'build', …) + splitBySeverity over the repro stacks and assert the rule id appears in errors, plus a clean shape passing on both. Nothing else in the file would notice if that entry's commands were narrowed later.

Ablation — the new tests genuinely fail without the implementation. Both limb bodies were disabled at their guards (implementation committed first; mutation confirmed on disk by injected-marker count 2, original-anchor count 0, and a blob hash differing from the HEAD blob). Result: 13 failed / 75 passed, including all four validate/build acceptance pins. Restored via git checkout HEAD -- ABSOLUTE_PATH under an EXIT INT TERM trap and proven byte-identical: git diff HEAD empty, git hash-object back to 4c3379d74c386d50f13e6d7bf7c07133816f745f, zero markers remaining. No rebuild leg is skipped or needed here, and that is a property rather than an omission: the test reaches the rule through a relative source import (./validate-widget-bindings.js), resolved by vitest to src/, never through the package exports/dist.

Note the "clean shape" tests stay green under ablation, which is correct — an ablated rule reports nothing — so the 75 passes are not evidence of anything.

Runs:

  • pnpm --filter @objectstack/lint test90 files, 2558 tests, all pass
  • pnpm --filter @objectstack/lint typecheck — clean (tsc --noEmit)
  • pnpm lint (repo-scale eslint . --no-inline-config) — exit 0, no narrowing
  • Gate families derived from the real change set via scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands — 33 families (29 by path + 6 by kind, 2 overlapping) — 31 green
  • Also run as dispatched: check-ratchet-remedy-authority (green, 183 scripts swept) and check:declared-population-live (green, 156/200 families reach the tree)
  • check:type-check-coverage green; check:type-check-debt --re-measure green after building the closure — "27 ledger entries re-measured, 1217 raw tsc errors total, none above its recorded number"
  • check:dual-build-cjs-loads green after the same build (it had reported exit 3 PREREQUISITE NOT MET beforehand)
  • check:nul-bytes green; diff additionally self-scanned for raw control bytes — clean

One family NOT MEASURED locally, by its own instruction:scripts/check-test-completeness.mjs requires a saved turbo run test log that CI tees and this invocation has none, so it exits 3 and its own text says "running the family locally, record this gate as NOT MEASURED … It is not a red, and there is nothing here to fix." Recorded as NOT MEASURED rather than as a pass.

A note on the ratchet the type gates could not see

packages/lint/tsconfig.json excludes **/*.test.ts, so pnpm --filter @objectstack/lint typecheck says nothing about the test file this PR adds. That is measured, not assumed — tsc --listFiles returns the three source files and not the test. The tests here were therefore type-checked separately under a temporary config that includes them (removed again): zero errors attributable to this diff, against 22 pre-existing errors in seven untouched test files. That separate run is what caught a genuine defect in this PR's own test helper — a non-generic idsOf that erased the finding type — which vitest cannot see and which would otherwise have shipped. The structural half is accounted for: check:type-check-coverage records this package under TEST_DEBT, and the ratchet re-measure is green.

Scope

  • test/dashboard.test.ts in objectstack-ai/duly is the downstream stopgap, written to be deleted when this lands. Deleting it is a follow-up in that repo, not this PR's scope — this PR does not touch duly.
  • Both limbs ride the existing validateWidgetBindings registry entry, whose surfaces is per-RULE. As with the six error ids already on it, the three new ones therefore also reach the runtime publish gate for dashboard writes. That is safe in the direction that matters: RuntimeStackContext carries objects, so the graph resolves there; were it ever not carried, resolveFieldPath returns unknowable and the rules go silent rather than inventing findings.

Follow-ups


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 11 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/lint/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx(via validateWidgetBindings (symbol, a top-level function))
  • content/docs/releases/v17.mdx(via validateWidgetBindings (symbol, a top-level function))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/lint/src/index.ts) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 6eb8e3cc5022c4b1d0007962220444f4f947036dpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 4acf3c1f3ab8017c7066fdb36c8410a51cd8873c — the merge of head 5216886e2b62f59830dd01a8b98ea486a5de9c6d into base 6eb8e3cc5022c4b1d0007962220444f4f947036d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4acf3c1f3ab8017c7066fdb36c8410a51cd8873c && git checkout 4acf3c1f3ab8017c7066fdb36c8410a51cd8873c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6eb8e3cc5022c4b1d0007962220444f4f947036d 5216886e2b62f59830dd01a8b98ea486a5de9c6d && git checkout -B drift-repro 6eb8e3cc5022c4b1d0007962220444f4f947036d && git merge --no-ff 5216886e2b62f59830dd01a8b98ea486a5de9c6d
node scripts/docs-audit/affected-docs.mjs --json 6eb8e3cc5022c4b1d0007962220444f4f947036d

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 6eb8e3cc5022c4b1d0007962220444f4f947036d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 1, 2026 20:08
@baozhoutao
baozhoutao added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit fa1eca3Sep 1, 2026
35 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-14148-widget-filter-sortby branch September 1, 2026 20:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A dashboard widget's OWN filter keys and options.sortBy are not resolved at author time — validate/build exit 0, widget renders empty

2 participants

@baozhoutao@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(lint): resolve a dashboard widget's own filter keys and options.sortBy at validate/build - #14276

Merged
baozhoutao merged 4 commits into
mainfrom
claude/issue-14148-widget-filter-sortby
Sep 1, 2026
Merged

fix(lint): resolve a dashboard widget's own filter keys and options.sortBy at validate/build#14276
baozhoutao merged 4 commits into
mainfrom
claude/issue-14148-widget-filter-sortby

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes#14148

A dashboard widget could filter by a column that does not exist, and order by a name it never selected, and objectstack validate exited 0 with "Validation passed"; build — the publish gate — wrote the dashboard into dist/objectstack.json. The widget then rendered empty, and per the card that is the expensive part: the board it was measured on leads with a "not moving" tile, and "an empty tile is indistinguishable from a healthy team: a missing number reads as zero, and zero is the answer the manager is hoping for." The failure is silent in the direction the reader wants to believe.

Premise re-check — measured on this branch's base, not assumed

Base is 345fc33ad (PR #14267, the #14105 family opener). Verified before writing code:

premiseverdictevidence
the seam landed and is exported from the package indexHOLDSpackages/lint/src/index.ts exports walkFilterFieldKeys (filter-walk.ts) and indexObjectGraph / resolveFieldPath (object-graph.ts); the export-site comment names this card as an intended consumer
gap A — widget filter KEYS unresolvedHOLDSvalidate-widget-bindings.ts at base reads w.dataset, w.dimensions, w.values, w.chartConfig, w.filterBindings — and never w.filter
gap B — options.sortBy uncheckedHOLDSvalidate-sortable-fields.ts explicitly defers it: "Dashboard widget sort config. Verified present but out of this predicate's domain … Judging those needs the dataset's measure index, which is validateChartBindings' family, not this one." Nothing in that family judged it

The card's six-row "what IS caught" table is a positive control, and it still holds at base: widget-dataset-unknown, widget-dimension-unknown, widget-measure-unknown, filter-token-unknown and dashboard-filter-field-unknown all live and gating. So the two zeros were readings, not an unread file. premise_still_valid: true on all three.

What lands

Three gating rule ids, all at the site that already emits widget-dataset-unknown / dashboard-filter-field-unknown:

  • widget-filter-field-unknown (limb A) — a key of the widget's own filter resolves to no column on the bound dataset's object graph. Reported path-precise at dashboards[i].widgets[j].filter.KEY.
  • widget-filter-field-not-included (limb A, second clause) — the key resolves, but its relationship prefix is not declared in the dataset's include.
  • widget-sortby-unselected (limb B) — options.sortBy names neither a dimensions[] nor a values[] entry of the widget, reported at dashboards[i].widgets[j].options.sortBy.

All three name dashboard, widget, key and object, and print the object's field list (limb A) or what the widget selects (limb B).

The open sub-question, answered explicitly: a dotted path through a declared include is RESOLVED

The card required this be stated rather than passed through silently, since a third silent pass-through would reproduce the card. Decision: resolve it, under the same two clauses the dataset-level sibling applies one level down — existence first, then ADR-0021 joinability.

The reasoning is mechanical rather than aesthetic. A widget's filter is ANDed into the dataset query as runtimeFilter (DashboardWidgetSchema.filter; dataset-executor.tscombineFilters(compiled.filter, selection.runtimeFilter)), and that compiled query carries only the joins include declared (dataset-compiler.ts derives every join alias from include). So a dotted key is exactly as decidable here as at a dataset dimension.

And the runtime is not a backstop for the second clause: dataset-compiler.ts's assertDeclared is called at two sites only — assertDeclared(d.field, 'dimension', …) and assertDeclared(m.field, 'measure', …) — never over runtimeFilter. Nothing between the author and the empty tile asked this question.

Note this deliberately differs from the neighbouring dashboard-filter-field-unknown, which still skips a dotted field (if (field.includes('.')) continue;). That skip was correct when nothing in this package could walk hops; it is now a real gap, and migrating it is filed rather than smuggled in here (see Follow-ups).

Reuse, not a third implementation

Triage was explicit: "generalise the one working implementation, not three parallel rules." No second hop-walker and no second filter-key reader were written. Both limbs are built on walkFilterFieldKeys (which already handles all three authored filter shapes — Mongo condition objects, { field, operator, value } rules, and [field, op, value] triples) and indexObjectGraph / resolveFieldPath.

Two helpers that were local to validate-dataset-references.ts moved into object-graph.ts and are now exported: joinablePrefixes (how ADR-0021 expands an include into joinable prefixes) and describeFieldPathVerdict (how one verdict reads in prose, renamed from existenceMessage now that it is shared). Copying either would have been the second implementation the seam exists to prevent, one release after it was written to prevent it. The sibling's behaviour is unchanged — its 526-line test file passes untouched.

The three skips are inherited unchanged, so an object this stack does not define, an ADR-0015 external object with no readable field map, and a registry-injected system column are never reported. Injected columns resolve per object via the graph rather than through the flat SYSTEM_FIELDS union the neighbouring rule uses, which is strictly better: a reference to owner_id on an ownership: 'none' object stays a real finding.

Verification

Ratchet and gate readings below were taken on the final commit, 5216886e2.

Acceptance criterion pinned end-to-end, not inferred. The card's binding criterion is that both limbs fail validate AND build — a validate-only fix was not acceptable. Rather than assert this from the registry entry's commands: ALL, six tests drive runAuthoringRules('validate' | 'build', …) + splitBySeverity over the repro stacks and assert the rule id appears in errors, plus a clean shape passing on both. Nothing else in the file would notice if that entry's commands were narrowed later.

Ablation — the new tests genuinely fail without the implementation. Both limb bodies were disabled at their guards (implementation committed first; mutation confirmed on disk by injected-marker count 2, original-anchor count 0, and a blob hash differing from the HEAD blob). Result: 13 failed / 75 passed, including all four validate/build acceptance pins. Restored via git checkout HEAD -- ABSOLUTE_PATH under an EXIT INT TERM trap and proven byte-identical: git diff HEAD empty, git hash-object back to 4c3379d74c386d50f13e6d7bf7c07133816f745f, zero markers remaining. No rebuild leg is skipped or needed here, and that is a property rather than an omission: the test reaches the rule through a relative source import (./validate-widget-bindings.js), resolved by vitest to src/, never through the package exports/dist.

Note the "clean shape" tests stay green under ablation, which is correct — an ablated rule reports nothing — so the 75 passes are not evidence of anything.

Runs:

  • pnpm --filter @objectstack/lint test90 files, 2558 tests, all pass
  • pnpm --filter @objectstack/lint typecheck — clean (tsc --noEmit)
  • pnpm lint (repo-scale eslint . --no-inline-config) — exit 0, no narrowing
  • Gate families derived from the real change set via scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands — 33 families (29 by path + 6 by kind, 2 overlapping) — 31 green
  • Also run as dispatched: check-ratchet-remedy-authority (green, 183 scripts swept) and check:declared-population-live (green, 156/200 families reach the tree)
  • check:type-check-coverage green; check:type-check-debt --re-measure green after building the closure — "27 ledger entries re-measured, 1217 raw tsc errors total, none above its recorded number"
  • check:dual-build-cjs-loads green after the same build (it had reported exit 3 PREREQUISITE NOT MET beforehand)
  • check:nul-bytes green; diff additionally self-scanned for raw control bytes — clean

One family NOT MEASURED locally, by its own instruction:scripts/check-test-completeness.mjs requires a saved turbo run test log that CI tees and this invocation has none, so it exits 3 and its own text says "running the family locally, record this gate as NOT MEASURED … It is not a red, and there is nothing here to fix." Recorded as NOT MEASURED rather than as a pass.

A note on the ratchet the type gates could not see

packages/lint/tsconfig.json excludes **/*.test.ts, so pnpm --filter @objectstack/lint typecheck says nothing about the test file this PR adds. That is measured, not assumed — tsc --listFiles returns the three source files and not the test. The tests here were therefore type-checked separately under a temporary config that includes them (removed again): zero errors attributable to this diff, against 22 pre-existing errors in seven untouched test files. That separate run is what caught a genuine defect in this PR's own test helper — a non-generic idsOf that erased the finding type — which vitest cannot see and which would otherwise have shipped. The structural half is accounted for: check:type-check-coverage records this package under TEST_DEBT, and the ratchet re-measure is green.

Scope

  • test/dashboard.test.ts in objectstack-ai/duly is the downstream stopgap, written to be deleted when this lands. Deleting it is a follow-up in that repo, not this PR's scope — this PR does not touch duly.
  • Both limbs ride the existing validateWidgetBindings registry entry, whose surfaces is per-RULE. As with the six error ids already on it, the three new ones therefore also reach the runtime publish gate for dashboard writes. That is safe in the direction that matters: RuntimeStackContext carries objects, so the graph resolves there; were it ever not carried, resolveFieldPath returns unknowable and the rules go silent rather than inventing findings.

Follow-ups


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 11 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/lint/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx(via validateWidgetBindings (symbol, a top-level function))
  • content/docs/releases/v17.mdx(via validateWidgetBindings (symbol, a top-level function))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/lint/src/index.ts) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 6eb8e3cc5022c4b1d0007962220444f4f947036dpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 4acf3c1f3ab8017c7066fdb36c8410a51cd8873c — the merge of head 5216886e2b62f59830dd01a8b98ea486a5de9c6d into base 6eb8e3cc5022c4b1d0007962220444f4f947036d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4acf3c1f3ab8017c7066fdb36c8410a51cd8873c && git checkout 4acf3c1f3ab8017c7066fdb36c8410a51cd8873c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6eb8e3cc5022c4b1d0007962220444f4f947036d 5216886e2b62f59830dd01a8b98ea486a5de9c6d && git checkout -B drift-repro 6eb8e3cc5022c4b1d0007962220444f4f947036d && git merge --no-ff 5216886e2b62f59830dd01a8b98ea486a5de9c6d
node scripts/docs-audit/affected-docs.mjs --json 6eb8e3cc5022c4b1d0007962220444f4f947036d

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 6eb8e3cc5022c4b1d0007962220444f4f947036d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 1, 2026 20:08
@baozhoutao
baozhoutao added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit fa1eca3Sep 1, 2026
35 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-14148-widget-filter-sortby branch September 1, 2026 20:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A dashboard widget's OWN filter keys and options.sortBy are not resolved at author time — validate/build exit 0, widget renders empty

2 participants

@baozhoutao@claude