feat(lint): reference-integrity validation for object and action names (#3583) - #3657
Merged
os-zhuang merged 4 commits intoJul 27, 2026
Merged
Conversation
#3583) The HotCRM audit found ~20 shipped instances of one bug class — metadata naming something that does not exist — all passing `objectstack validate` and `objectstack lint` cleanly and failing silently at runtime. This closes the object-name and action-name half of that class. Root cause of the false negatives: every cross-reference check answered "might this come from another package?" with a PREFIX GUESS (`startsWith('sys_')`), which cannot distinguish `sys_user` (real) from `sys_approval_process` (fictional — removed by ADR-0019, registered by nothing). So spec now ships a curated PLATFORM_PROVIDED_OBJECT_NAMES registry, kept honest by a conformance test that scans each package's *.object.ts declarations, following the PLATFORM_CAPABILITY_NAMES precedent (lint keeps its one-way lint -> spec dependency). New rules, wired into BOTH `os validate` and `os lint` in the same change so the paths cannot drift: - validate-object-references: action-param reference/objectOverride, dashboard globalFilters optionsFrom.object, nav requiresObject gates. Unresolved + unprefixed => error (the pure typo class); unresolved + platform-prefixed but unregistered => warning (a third-party package may still provide it). - validate-action-name-refs: bulkActions/rowActions, page quick-actions actionNames, nav action items. Unresolved => error, matching the existing dashboard-action-target posture; the runtime ships no built-in action names, so a miss is dead, full stop. Fixes found while mapping the surface: - defineStack skipped app.areas[].navigation entirely, so an areas-based app got NO nav cross-reference checking; it also only recursed into `group` children, missing children on `object` nav items. - i18n coverage only walked the record-map shape of field `options`, but the canonical shape is a {value,label}[] ARRAY — option-label coverage never fired for canonically-shaped select fields. - Hook conditions lost all field-awareness when `object` was an array of targets; now checked per target with de-duplicated diagnostics. - A widget binding no `dataset` silently bypassed every binding and chart check on the raw-config paths; `dataset` is schema-required, so it is now reported. Verified zero false positives against all three example apps (app-crm, app-showcase, app-todo). Suites: lint 390, spec 6669, cli 639 passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GBks5G7AkwrvgF2kL5rfkT
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 3 package(s): 110 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
- Raw NUL byte in the hook-dedup key (validate-expressions.ts) tripped
`check:nul-bytes`. A raw NUL makes grep/ripgrep treat the whole file
as binary and silently return zero matches, so the file drops out of
code search and every grep-based lint. Written as the \u0000 escape
per the repo convention — byte-identical at runtime.
- CodeQL (high): the `{…}` interpolation test used /\{[^}]+\}/, which
backtracks quadratically on a long run of `{` with no closing brace.
Replaced with a linear two-index scan; same semantics, including the
"at least one character between the braces" rule.
- spec public API gained 6 exports (the platform-object registry), so
the api-surface snapshot needed regenerating. 0 breaking, 6 added.
Also wire the two rules into `os compile` alongside `validate`/`lint`.
Every sibling reference rule (widget bindings, dashboard action refs,
filter tokens) already runs there, so leaving these out would recreate
the exact validate/lint/compile wiring drift the assessment flagged.
Document the new checks in docs/deployment/validating-metadata.mdx —
a "Dangling object and action names" section with the resolvability
severity table, plus the entry-points row.
Suites: lint 390, spec 6669, cli 636 passing; eslint, nul-bytes and
api-surface checks clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBks5G7AkwrvgF2kL5rfkT
…alidation-assessment-xskbcg # Conflicts: # packages/spec/api-surface.json
Merging main brought in the ADR-0104 deployment-migration gate, whose `sys_migration` object the platform-object registry did not list — and the conformance ratchet failed exactly as intended, which is the point of having it rather than a prefix heuristic. Also regenerates the api-surface snapshot for the union of main's new exports and this branch's. Re-verified on the merged base: lint 390, spec 6681, cli 637 passing; eslint, nul-bytes, api-surface clean; and the three example apps (app-crm, app-showcase, app-todo) still produce zero findings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GBks5G7AkwrvgF2kL5rfkT
os-zhuang
marked this pull request as ready for review
July 27, 2026 13:57
os-zhuang
added a commit
that referenced
this pull request
Jul 27, 2026
…ngs (#3583) (#3684) Phase 2 of the #3583 assessment: the two remaining reference classes the HotCRM audit found, both wired into validate/lint/compile together. validate-page-field-bindings — `PageComponent.properties` is an untyped bag, so a highlights strip, KPI card, or details section can name a field the bound object lacks and the component silently skips it. Object binding follows dataSource.object -> properties.object -> page.object, so a multi-object page is judged per element instead of against one page-wide guess; `record:related_list` resolves columns/sort/filter against the RELATED object and its add-picker against the picker's own. Advisory, matching FORM_FIELD_UNKNOWN. The descriptor table is hand-written on purpose: a Zod schema cannot say which `z.string()` prop is a field name, and `PageComponent.type` accepts arbitrary strings, so unknown types are skipped silently. It also covers shapes the props schemas do not describe but real pages author anyway (record:details `sections[].fields` and `hideFields`, the picker's `labelField`) — linting the schema shape alone would find nothing on the actual corpus. validate-chart-bindings — extends ADR-0021 axis checking past dashboards to report charts (report.chart + blocks[].chart), list-view charts (three container paths), and dataset-bound page chart components. An axis naming a raw field instead of a declared measure is an error (post-cutover rows are keyed by measure NAME, so the series returns empty); a declared but unselected measure is a warning. Reports needed their own handling: ReportChartSchema narrows xAxis/yAxis to bare STRINGS, which the dashboard rule's Array.isArray guard skips silently. The react <ObjectChart> block is object-bound rather than dataset-bound and nothing in the repo defines what its aggregate names the result column, so it is left out rather than guessed at (ADR-0078: verify, then enforce). Fixes a defect in the rule shipped by #3657: its page walk read a top-level `page.components` array that PageSchema does not have — components live under regions[].components[] and slots, and sub-trees nest inside the untyped properties bag (children, items[].children, body, footer), not a `children` key on the component (PageComponentSchema is strict). The quick-actions check was visiting nothing on a parsed stack, and its tests encoded the invented shape so they passed. Traversal is now one shared, tested module; on the showcase app it reaches 194 components where the old shape found 46. Source-authored pages (html/react/jsx) are skipped — their regions hold a derived cache the source wins over. Verified zero false positives on app-crm, app-showcase and app-todo with the fuller traversal, after confirming the walk actually visits their 194 components, 1 report chart and 2 list charts (a silently-empty walk would pass vacuously). Suites: lint 435, cli 637 passing; eslint, nul-bytes and api-surface clean. Claude-Session: https://claude.ai/code/session_01GBks5G7AkwrvgF2kL5rfkT Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Jul 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
实现 #3583 的对象名 / 动作名引用完整性校验(方案文档见已合并的 #3640)。
根因:前缀猜测
现存所有交叉引用检查,回答「这个名字会不会来自别的包?」用的都是前缀猜测(
startsWith('sys_')直接跳过)。这个猜测分不清sys_user(真实)和sys_approval_process(虚构 —— ADR-0019 废除,任何包都不注册),所以每一个虚构的平台前缀引用都畅通发布。因此先在 spec 落一份策展注册表
PLATFORM_PROVIDED_OBJECT_NAMES(73 个本仓库对象 + 5 个 cloud 对象),沿用PLATFORM_CAPABILITY_NAMES先例,lint→spec 单向依赖不变。注册表由一个 conformance 测试扫描各包*.object.ts声明来保证不漂移 —— 一份过期的注册表比它取代的启发式更危险,因为使用方现在会信任它。新增规则(同一 PR 内同时接入
os validate与os lint,避免再次接线漂移)validate-object-references—— action param 的reference/objectOverride、仪表盘globalFilters[].optionsFrom.object、导航requiresObject能力门。严重级按可解析性分档:未前缀且解析不到 → error(纯 typo 类,object: 'user'而平台对象是sys_user);平台前缀但注册表里没有 → warning(第三方包仍可能提供,advisory 是诚实的上限)。validate-action-name-refs—— 按名字绑定动作的各个面:列表视图bulkActions/rowActions、页面record:quick_actions的actionNames、导航 action 项。解析不到 → error,与既有DASHBOARD_ACTION_TARGET_UNDEFINED同档;运行时没有任何内置动作名注册表,所以「解析不到」就是死引用,没有中间地带。顺带修掉的洞(都是画引用面时发现的)
defineStack完全跳过app.areas[].navigation—— areas 结构的 app 一条导航引用都没校验过;并且只递归group的 children,漏掉object导航项的 children。options的 record-map 形态,而规范形态是{value,label}[]数组 —— 规范形态的 select 字段选项翻译覆盖检查从未生效过。condition在object为数组时丢失全部字段感知;现改为逐目标校验并对目标无关的诊断去重。dataset时,在 raw-config 路径(lint/doctor)会静默旁路掉全部绑定与图表检查;dataset是 schema 必填,现按已解析路径的同一标准报出。验证
examples/app-crm、app-showcase(全家桶)、app-todo三个真实应用跑新规则,零 finding。这是 ADR-0072 D1 的信任契约 —— 一条死报警,作者就不再信任 linter。reference: 'user'、bulkActions: ['mass_update','mass_delete','assign_owner']、sys_approval_process导航)。未覆盖(方案 §5,需先决策,不在本 PR)
agent.knowledge.indexes在 stack 里没有定义位(KnowledgeSourceSchema未接入stack.zod.ts)—— 给一个无定义位的命名空间写规则会把缺口制度化,应先做 spec 决策 D1。关联:#3583 · #3640(方案)· ADR-0072 / ADR-0078 / ADR-0049
Generated by Claude Code