Skip to content

feat(lint): reference-integrity validation for object and action names (#3583) - #3657

Merged
os-zhuang merged 4 commits into
mainfrom
claude/app-metadata-validation-assessment-xskbcg
Jul 27, 2026
Merged

feat(lint): reference-integrity validation for object and action names (#3583)#3657
os-zhuang merged 4 commits into
mainfrom
claude/app-metadata-validation-assessment-xskbcg

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

实现 #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 validateos 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_actionsactionNames、导航 action 项。解析不到 → error,与既有 DASHBOARD_ACTION_TARGET_UNDEFINED 同档;运行时没有任何内置动作名注册表,所以「解析不到」就是死引用,没有中间地带。

顺带修掉的洞(都是画引用面时发现的)

  • defineStack 完全跳过 app.areas[].navigation —— areas 结构的 app 一条导航引用都没校验过;并且只递归 group 的 children,漏掉 object 导航项的 children。
  • i18n 覆盖率只处理 options 的 record-map 形态,而规范形态是 {value,label}[] 数组 —— 规范形态的 select 字段选项翻译覆盖检查从未生效过。
  • hook conditionobject 为数组时丢失全部字段感知;现改为逐目标校验并对目标无关的诊断去重。
  • widget 完全没有 dataset 时,在 raw-config 路径(lint/doctor)会静默旁路掉全部绑定与图表检查;dataset 是 schema 必填,现按已解析路径的同一标准报出。

验证

  • 误报地板:对 examples/app-crmapp-showcase(全家桶)、app-todo 三个真实应用跑新规则,零 finding。这是 ADR-0072 D1 的信任契约 —— 一条死报警,作者就不再信任 linter。
  • 测试:lint 390 / spec 6669 / cli 639 全通过;三个包 build(含 DTS 类型检查)与 eslint 均通过。
  • 每条规则都带以 HotCRM 真实实例为原型的 fixture(reference: 'user'bulkActions: ['mass_update','mass_delete','assign_owner']sys_approval_process 导航)。

未覆盖(方案 §5,需先决策,不在本 PR)

  • agent.knowledge.indexes 在 stack 里没有定义位(KnowledgeSourceSchema 未接入 stack.zod.ts)—— 给一个无定义位的命名空间写规则会把缺口制度化,应先做 spec 决策 D1。
  • hook body 写入是不透明 JS 源码,静态不可分析(D4)。
  • 页面字段引用、报表/列表 chart 轴、翻译孤儿键 —— 方案里的 Phase 2/3,各自独立 PR。

关联:#3583 · #3640(方案)· ADR-0072 / ADR-0078 / ADR-0049


Generated by Claude Code

#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
@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 27, 2026 1:39pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:system tests tooling size/xl labels Jul 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/cli, @objectstack/lint, @objectstack/spec.

110 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via packages/cli, @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/cli, packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint, @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Comment thread packages/lint/src/validate-object-references.ts Fixed
claude added 3 commits July 27, 2026 13:25
- 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
os-zhuang marked this pull request as ready for review July 27, 2026 13:57
@os-zhuang
os-zhuang merged commit f163028 into main Jul 27, 2026
19 checks passed
@os-zhuang
os-zhuang deleted the claude/app-metadata-validation-assessment-xskbcg branch 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:system size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants