Repository navigation
fix(lint)!: under a package closure, judge a dashboard or report filter key on another package's dataset against that dataset's object (#30005) - #30231
Conversation
… object under a package closure Claude-Session: https://claude.ai/code/session_01Utp6CZYtWqXwEewuPdKQAJ Co-authored-by: Claude <noreply@anthropic.com>
…ngeset Claude-Session: https://claude.ai/code/session_01Utp6CZYtWqXwEewuPdKQAJ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check5 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a881a71bdb11306aee78cdd08259eae106a188d2 && git checkout a881a71bdb11306aee78cdd08259eae106a188d2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5fa9375ec8279a7ee9e779049c0e93661c1786a3 ae7fa514e5fccd5ed58307428b2783bbb3ce63c6 && git checkout -B drift-repro 5fa9375ec8279a7ee9e779049c0e93661c1786a3 && git merge --no-ff ae7fa514e5fccd5ed58307428b2783bbb3ce63c6
node scripts/docs-audit/affected-docs.mjs --json 5fa9375ec8279a7ee9e779049c0e93661c1786a3 |
|
domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC Contract reviewServed-tier: Face: narrowing. The PR's two code paths decide no face on their own ( ① Derived judgments
② Semver levelChangeset ③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: PASS |
…the changeset, and pin the door's 422 Claude-Session: https://claude.ai/code/session_01Utp6CZYtWqXwEewuPdKQAJ Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC Contract reviewServed-tier: Delta review of the record on ① Derived judgments
② Semver levelUnchanged and still consistent: ③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: PASS |
|
domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC 席位核对ACCEPT · head
|
|
domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC 维护者速读(席位终稿)
席位意见:同意落地。只多拒收闭包外 dataset 上对象确实没有的字段,误伤面有 pin 守住。 席位核对ACCEPT(重申,补全席位记录) · head
|
Requested by Bill · project thread
Fixes #30005
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: updated here — content/docs/deployment/validating-metadata.mdx (the fourth-door paragraph) now names the fifth closure limb for dashboard and report writes; skills/** teaches neither the rules nor the closure.
维护者速读
objects时,dashboard 与 report 的写入额外保留「它所绑定的 dataset 的基础对象」(第五肢)。于是运行时包的看板用别的包的 dataset(如showcase_task_metrics)时,filter: {no_such_field: 'x'}在 widget 路径被 422 拒收;report 的runtimeFilter同理;正确的键照常发布。datasets不收窄、objects收窄,dataset 找得到而它的对象被筛掉,两条筛选键规则走「对象不在快照里」的跳过分支,拼错的键发布 200,读者只看到空图块或报错面板。narrowObjectsToPackageClosure签名与结果不变。回滚即撤 PR。packages/lint/src/runtime-gate.ts的datasetBoundObjectNames与narrowObjectsToWriteClosure的 Limb 5 一行;buildRuntimeWriteSnapshotSet里调用处;测试文件末尾#30005一节。pnpm --filter @objectstack/lint exec vitest run src/runtime-gate.package-closure.test.ts,预期 48 条全过(其中 lint publish gate: widget-filter-field-unknown and report-filter-field-unknown are silent when the dataset's object is outside the written package's closure #30005 一节 9 条)。What changes
The gap.
buildRuntimeWriteSnapshotSetnarrowsobjectsto the written package's closure (ruling #9612) and carriesdatasetswhole. A runtime package's dashboard bound toshowcase_task_metrics, which the showcase package ships overshowcase_task, therefore resolved its dataset but not the dataset's object, andjudgeDatasetFilterKeystook skip 1 ("object this stack does not define") for every key. Bothwidget-filter-field-unknownandreport-filter-field-unknownwere silent, sofilter: { no_such_field: 'x' }published.The fix: route 1 of the card, bring the referenced dataset's object into the snapshot. The closure gains a fifth limb, stated by the snapshot builder: an object survives narrowing when it is the base object of a dataset the written item binds.
datasetBoundObjectNames(type, item, datasets)reads the bound names at the positions the two rules read (a dashboard'swidgets[].dataset; a report'sdatasetand each joined block'sdataset) and resolves them against the carrieddatasets. It answers the empty set for every other write type. The limb applies to the baseline, candidate and stored passes alike, as the existing four do.Why route 1 and not route 2 (narrow
datasetsand give a cross-closure reference its own verdict).$operator) is not refused.Why only dashboard and report writes. Limb 5 re-admits an object the package did not declare. A write that names an object directly is judged against the closure on purpose: a flow trigger outside it is refused (
flow-trigger-unknown-object), a list view bound outside it is refused (list-view-object-outside-closure), a dataset's base object outside it gets the closure fix line onobject-reference-unknown. A list view'schart.datasetre-admitting the object the view lists would wave that verdict through. A dashboard or report reaches an object only through a dataset, so the type gate is the boundary. Pinned in both directions.Public surface.
narrowObjectsToPackageClosurekeeps its signature and its four-limb answer: the implementation moved to an internalnarrowObjectsToWriteClosure, and the public function states an empty limb 5 (pinned).datasetBoundObjectNamesis exported from the module for the test only, and is not on any package entry. That is why the declaration isClause-②: no (narrowing): nothing is widened.Tests
New top-level
describeinpackages/lint/src/runtime-gate.package-closure.test.ts(the closure's existing harness), 9 cases:showcase_task_metricswithfilter: { no_such_field: 'x' }, refused with exactlywidget-filter-field-unknownatdashboards[0].widgets[0].filter.no_such_field, severityerror.runtimeFilter, refused withreport-filter-field-unknownatreports[0].runtimeFilter.no_such_field.{ status: 'done' },{ estimate_hours: { $gte: 1 } }and the injectedcreated_atpublish clean on the widget and on the report.widget-dataset-unknown.showcase_taskand a list view ofshowcase_taskwith achart.datasetonshowcase_task_metrics, both under the same closure and context, are still refused.narrowObjectsToPackageClosurestill dropsshowcase_task.datasetBoundObjectNamesreads the two bound positions and answers empty for a view or with no datasets.Ablation (once, through `scripts/ablation-replace.mjs`)
Mutation: limb 5 disabled,
datasetBoundObjects.has(name)) return true;→datasetBoundObjects.has(name) && !'ABLATED_30005') return true;. The test imports./runtime-gate.jsfrom source, so no build sits between the mutation and the run.The five that stay green under the mutation are the controls, the dangling-dataset case, the DARK case and the two unit pins: they assert what the fix must not change.
Gates, local subset, on head `b37c73da8`
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 57 families; each exit code was recorded to a file before any pipe, then reconciled with--ran:pnpm check:dual-build-cjs-loads: exit 3, it needs the whole workspace built. NOT MEASURED, left to CI.check-plugin-teardown-shape.mjs --self-testandpnpm check:lean-entry-closurefirst exited 3 (shallow clone;objectqlnot built). After fetching the control commit and building the dogfood closure, both exited 0; those are the codes recorded.pnpm --filter @objectstack/lint testexit 0 (Test Files 193 passed, Tests 8003 passed),pnpm --filter @objectstack/lint typecheckexit 0,pnpm lintexit 0.Dogfood battery, once, on head `b37c73da8` (the title carries `!` and the declaration a narrowing arm)
Closure built with
turbo run build --filter='@objectstack/dogfood^...', thenpnpm --filter @objectstack/dogfood testunderos-verify-lock.sh:The one failure is not this change's:
test/update-by-id-refusal-order.dogfood.test.ts› "control: an organization administrator, who reads the platform grant object, keeps the administration gate's sentence for a row and for no row" expects403 PERMISSION_DENIEDfor an update of a missingsys_user_permission_setid and receives404 RECORD_NOT_FOUND. It fails the same way alone (1 failed | 18 passed), and fails the same way withpackages/lint/src/runtime-gate.tsrestored toorigin/mainand@objectstack/lintrebuilt (the builtdistcarries 0 occurrences ofdatasetBoundObjectNames). The diff touches nothing on the data update path. Reported to the seat.Acceptance notes
errorto422 INVALID_METADATA; the cross-closure case is pinned inpackages/metadata-protocol/src/protocol.dashboard-dataset-publish-gate.test.ts(the#30005block: a wrong key is refused 422INVALID_METADATAatdashboards[0].widgets[0].filter.no_such_fieldwith no active row; thestatuskey saves).Verification
Expect 48 passed; the
#30005block is the lastdescribein the file.