From 133e8fec8d2b35b144be3afd281fd69a4aa662cd Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 31 Jul 2026 13:01:30 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix(lint,cli):=20os=20lint=20/=20os=20compi?= =?UTF-8?q?le=20=E4=B8=8D=E5=86=8D=E6=94=BE=E8=A1=8C=E4=B8=80=E4=B8=AA=20o?= =?UTF-8?q?s=20validate=20=E4=BC=9A=E6=8B=92=E7=BB=9D=E7=9A=84=20react=20?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=20(#4340=20=E5=90=8E=E7=BB=AD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `validateReactPageProps` 只手工接在 `os validate` 上,`os lint` 和 `os compile` 从来没跑过它。在 showcase 的 react 页面植入一处 gating 违规 (`` —— 谓词命中不了任何行, SqlDriver 吞掉 "no such column" 后返回 [],列表回空且与「本来就没数据」无法区分) 实测: os lint os compile os validate 修复前 exit 0 放行 exit 0 放行 exit 1 拒绝 修复后 exit 1 拒绝 exit 1 拒绝 exit 1 拒绝 #4340 之后这条规则已经是整个 react 页面表面唯一的字段解析闸门: 的 columns/fields/sort/grouping/userFilters、 的 fields/initialValues/sections/subforms、record:* 一族(与元数据表面共用同一张 COMPONENT_FIELD_SPECS)、 的 aggregate/axes,以及 searchableFields。 所以漏接不是少几条警告 —— 而是这些绑定在 build 路径上完全没人看,包括其中会 gate 的那些。 现接入 REFERENCE_INTEGRITY_RULES,`os validate` 那处手工接线随之删除,三个命令的 答案由构造保证一致。这正是 suite 设立要终结的漂移(#3583 §5 D5),也是 validateReadonlyFlowWrites 在 #4394 里刚走过的同一条路:那次是「一张 map、 两个检查、两套命令集合」,这次是「一次 JSX parse、七个 rule id、一套命令集合」。 按 charter,react-prop-missing-required / react-prop-typo 是 shape 而非解析问题, 本不属于 suite。它们随行的理由与 validateActionBodyWrites 完全相同(其注释已写明): 落在同一次 parse 的同一份 source 上,拆成第二个 member 就要把每个 react 页面 parse 两遍,而改回手工接线正是 suite 要终结的那种漂移。同时更正了那条注释里 "The ONE member here that emits two rule ids" 的说法 —— 现在不止一个。 规则行为零变化:id、严重级、文案都不动;输入也不变(`os validate` 原本就传 result.data,suite 拿到的是同一个)。#4402 的接线守卫会在下次有人想再手工接一条 规则时直接报错——本次删除 validate.ts 的直接 import 也正是被它逼出来的。 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012rK2McPogrTJBKdUsThrGs --- .../react-page-props-joins-the-suite.md | 35 ++++++++++++++++ packages/cli/src/commands/validate.ts | 41 ++++--------------- .../src/reference-integrity-suite.test.ts | 30 ++++++++++++-- .../lint/src/reference-integrity-suite.ts | 27 +++++++++++- 4 files changed, 95 insertions(+), 38 deletions(-) create mode 100644 .changeset/react-page-props-joins-the-suite.md diff --git a/.changeset/react-page-props-joins-the-suite.md b/.changeset/react-page-props-joins-the-suite.md new file mode 100644 index 0000000000..adda0f9f64 --- /dev/null +++ b/.changeset/react-page-props-joins-the-suite.md @@ -0,0 +1,35 @@ +--- +"@objectstack/lint": patch +"@objectstack/cli": patch +--- + +fix(lint,cli): `os lint` / `os compile` 不再放行一个 `os validate` 会拒绝的 react 页面 + +`validateReactPageProps` 只手工接在 `os validate` 上,另外两个命令从来没跑过它。 +在 showcase 的 react 页面上植入一处 gating 违规(`` +—— 谓词命中不了任何行,列表回空,和「本来就没数据」无法区分)实测: + +``` + os lint os compile os validate + 修复前 exit 0 放行 exit 0 放行 exit 1 拒绝 + 修复后 exit 1 拒绝 exit 1 拒绝 exit 1 拒绝 +``` + +这条规则在 #4340 之后已经是**整个 react 页面表面唯一**的字段解析闸门: +`` 的 columns/fields/sort/grouping/userFilters、`` 的 +fields/initialValues/sections/subforms、`record:*` 一族(与元数据表面共用同一张 +`COMPONENT_FIELD_SPECS`)、`` 的 aggregate/axes、以及 `searchableFields`。 +漏接不是少几条警告 —— 而是这些绑定在 build 路径上**完全没人看**,包括其中会 gate 的那些。 + +现接入 `REFERENCE_INTEGRITY_RULES`,`os validate` 里那处手工接线随之删除,三个命令的 +答案由构造保证一致。这正是 suite 设立要终结的漂移(#3583 §5 D5),也是 +`validateReadonlyFlowWrites` 在 #4394 里刚走过的同一条路 —— 那次的教训是 +「一张 map、两个检查、两套命令集合」,这次是「一次 JSX parse、七个 rule id、 +一套命令集合」。 + +规则行为零变化:id、严重级、文案都不动;喂进去的输入也不变(`os validate` 原本就 +传 `result.data`,suite 拿到的是同一个)。`#4402` 的接线守卫会在下一次有人想再手工 +接一条规则时直接报错。 + +`validateReactPageProps` 沿用 `validateHookBodyWrites` / `validateActionBodyWrites` +的惰性约定:只有真的存在 `kind:'react'` 页面时才加载 TypeScript 编译器。 diff --git a/packages/cli/src/commands/validate.ts b/packages/cli/src/commands/validate.ts index ae5d462beb..1c0d0007cf 100644 --- a/packages/cli/src/commands/validate.ts +++ b/packages/cli/src/commands/validate.ts @@ -23,7 +23,7 @@ import { validateDashboardActionRefs } from '@objectstack/lint'; import { validateFilterTokens } from '@objectstack/lint'; import { validateReferenceIntegrity } from '@objectstack/lint'; import { validateResponsiveStyles } from '@objectstack/lint'; -import { validateJsxPages, validateReactPages, validateReactPageProps, validatePageSourceStyling } from '@objectstack/lint'; +import { validateJsxPages, validateReactPages, validatePageSourceStyling } from '@objectstack/lint'; import { validateCapabilityReferences } from '@objectstack/lint'; import { validateVisibilityPredicates } from '@objectstack/lint'; import { validateSecurityPosture, validateOrgAxisRedLines } from '@objectstack/lint'; @@ -454,38 +454,13 @@ export default class Validate extends Command { this.exit(1); } - // 3d. React-source pages — prop usage against the component contract - // (ADR-0081 Phase 2): missing required bindings (error) + likely - // prop typos (warning), parsed from the real JSX. - if (!flags.json) printStep('Checking React-source page props (ADR-0081)...'); - const reactPropFindings = validateReactPageProps(result.data as Record); - const reactPropErrors = reactPropFindings.filter((f) => f.severity === 'error'); - const reactPropWarnings = reactPropFindings.filter((f) => f.severity === 'warning'); - if (!flags.json) { - for (const w of reactPropWarnings.slice(0, 50)) { - console.log(chalk.yellow(` \u26a0 ${w.where}: ${w.message}`)); - console.log(chalk.dim(` ${w.hint}`)); - } - } - if (reactPropErrors.length > 0) { - if (flags.json) { - await emitJson({ - valid: false, - errors: reactPropErrors, - warnings: [...widgetWarnings, ...styleWarnings, ...jsxWarnings, ...reactPropWarnings], - duration: timer.elapsed(), - }); - this.exit(1); - } - console.log(''); - printError(`React-source page prop check failed (${reactPropErrors.length} issue${reactPropErrors.length > 1 ? 's' : ''})`); - for (const f of reactPropErrors.slice(0, 50)) { - console.log(` \u2022 ${f.where}: ${f.message}`); - console.log(chalk.dim(` ${f.hint}`)); - console.log(chalk.dim(` rule: ${f.rule} at ${f.path}`)); - } - this.exit(1); - } + // 3d. React-source page PROPS are checked by `REFERENCE_INTEGRITY_RULES` + // (step 3a above), not from here (#4340 follow-up). They ran from + // this call site ALONE, so `os lint` and `os compile` accepted a + // react page whose every field binding was stale — including the + // gating ones. That is `validateReadonlyFlowWrites`' divergence + // (#4394) one surface over. The input is unchanged: the suite is + // handed the same `result.data` this block passed. // 3e. Source-tier page styling (ADR-0065): Tailwind className in a // kind:'html'/'react' page source silently no-ops (the build never diff --git a/packages/lint/src/reference-integrity-suite.test.ts b/packages/lint/src/reference-integrity-suite.test.ts index 3072c75959..7ae1b17586 100644 --- a/packages/lint/src/reference-integrity-suite.test.ts +++ b/packages/lint/src/reference-integrity-suite.test.ts @@ -30,6 +30,7 @@ describe('reference-integrity suite — membership', () => { 'validateActionBodyWrites', 'validateFlowNodeWrites', 'validateReadonlyFlowWrites', + 'validateReactPageProps', ]); }); @@ -107,6 +108,17 @@ describe('reference-integrity suite — every member actually runs', () => { }, ], }, + // validateReactPageProps: the same component family one surface over, + // authored as JSX. `` with no `objectName` binds nothing, so + // this GATES — and it gated on `os validate` alone until this member + // joined the suite. A separate page from `lead_detail` on purpose: the + // metadata walk above must stay the only source of `page-field-unknown` + // here, or this member could go silent behind it. + { + name: 'lead_console', + kind: 'react', + source: 'function Page(){ return ; }', + }, ], datasets: [ { @@ -221,11 +233,21 @@ describe('reference-integrity suite — every member actually runs', () => { expect(rules).toContain('agent-authoring-withdrawn'); expect(rules).toContain('hook-body-write-unknown-field'); expect(rules).toContain('action-body-write-unknown-field'); - // The one member that emits a second rule id — see the suite's comment on - // why it rides along instead of becoming its own entry. + // Members that emit a rule id beyond their resolution one — see the suite's + // comments on why these ride along instead of becoming their own entries. expect(rules).toContain('action-record-write-discarded'); expect(rules).toContain('flow-node-write-unknown-field'); expect(rules).toContain('flow-update-readonly-field'); + expect(rules).toContain('react-prop-missing-required'); + }); + + it('carries a gating react-page-prop finding through the suite (#4340)', () => { + const findings = validateReferenceIntegrity(stack); + const react = findings.find((f) => f.rule === 'react-prop-missing-required'); + // Must reach the CLI as an ERROR: `os lint` and `os compile` saw NOTHING + // from this rule until it joined the suite, so a react page binding nothing + // sailed through the build the way a readonly flow write did (#4394). + expect(react?.severity).toBe('error'); }); it('carries a gating flow-template finding through the suite (#3810)', () => { @@ -247,9 +269,9 @@ describe('reference-integrity suite — every member actually runs', () => { expect(typeof f.message).toBe('string'); expect(typeof f.hint).toBe('string'); } - // Object references run first, readonly flow writes last. + // Object references run first, react page props last. expect(findings[0].rule).toBe('object-reference-unknown'); - expect(findings[findings.length - 1].rule).toBe('flow-update-readonly-field'); + expect(findings[findings.length - 1].rule).toBe('react-prop-missing-required'); }); it('returns nothing for an empty stack', () => { diff --git a/packages/lint/src/reference-integrity-suite.ts b/packages/lint/src/reference-integrity-suite.ts index 49cb040d93..e31fa0daa1 100644 --- a/packages/lint/src/reference-integrity-suite.ts +++ b/packages/lint/src/reference-integrity-suite.ts @@ -69,6 +69,7 @@ import { validateHookBodyWrites } from './validate-hook-body-writes.js'; import { validateActionBodyWrites } from './validate-action-body-writes.js'; import { validateFlowNodeWrites } from './validate-flow-node-writes.js'; import { validateReadonlyFlowWrites } from './validate-readonly-flow-writes.js'; +import { validateReactPageProps } from './validate-react-page-props.js'; export type ReferenceIntegritySeverity = 'error' | 'warning'; @@ -126,7 +127,8 @@ export const REFERENCE_INTEGRITY_RULES: readonly ReferenceIntegrityRule[] = [ // carries over — an action's `ctx.input` is its params bag, not a record // (see that module's ledger). Lazy on the same terms. // - // The ONE member here that emits two rule ids. Besides resolving `ctx.api` + // The first member here to emit more than one rule id (`validateReactPageProps` + // below is the other, and carries the most). Besides resolving `ctx.api` // writes against declared fields (`action-body-write-unknown-field`), it // reports a `ctx.record` write that can reach nothing // (`action-record-write-discarded`, #4345) — not a resolution question, so @@ -154,6 +156,29 @@ export const REFERENCE_INTEGRITY_RULES: readonly ReferenceIntegrityRule[] = [ // build the other command would have stopped. Joining the suite is the whole // fix; the two hand-wired call sites are deleted with it (#4345 follow-up). { name: 'validateReadonlyFlowWrites', run: validateReadonlyFlowWrites }, + // The `kind:'react'` page surface. Every prop a react block binds BY FIELD + // NAME is resolved against the object it names (#4340) — ``, + // ``, the `record:*` family through the SAME + // `COMPONENT_FIELD_SPECS` table `validatePageFieldBindings` walks one surface + // over, plus ``'s aggregate/axes (#3701/#3729) and + // `searchableFields` (#4329). Squarely the charter's question, on the surface + // where it had no answer at all. + // + // It was hand-wired into `os validate` ALONE, so `os lint` and `os compile` + // accepted a react page whose every field binding was stale — including the + // gating ones (a missing required binding, a filter position naming no field: + // the predicate can never match and the list comes back empty). That is + // `validateReadonlyFlowWrites`' divergence again, one surface over, and it is + // the reason this entry exists rather than a fourth hand-wiring. + // + // Like `validateActionBodyWrites` above, it emits ids that are not resolution + // questions — `react-prop-missing-required` and `react-prop-typo` are shape, + // and by the charter belong outside. They ride along for the same reason: they + // fall out of the SAME TypeScript parse of the SAME page source, and splitting + // them into a second member would parse every react page twice to say two + // things about one walk. Lazy on the same terms as the hook/action body rules + // — only a page that is actually `kind:'react'` loads the compiler. + { name: 'validateReactPageProps', run: validateReactPageProps }, ]; /** From b7973c8110a4c0a884973f9843bddff9cb35bcbe Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 31 Jul 2026 13:12:21 +0000 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20validating-metadata=20=E5=8F=8D?= =?UTF-8?q?=E6=98=A0=20react=20=E9=A1=B5=E9=9D=A2=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E7=BB=91=E5=AE=9A=E7=8E=B0=E5=9C=A8=E4=B8=89=E4=B8=AA=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E9=83=BD=E8=B7=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本 PR 把 react 页面的字段绑定检查从「只在 os validate」变成「suite 成员,三个命令 都跑」,这份文档有三处随之失真 —— 而 docs-drift 的包级扇出恰好没点到它(它按 @objectstack/cli / @objectstack/lint 列了 20 份,漏了语义上唯一真正耦合的这份)。 - "one gate, two entry points" 表补一行 React page block field bindings(两列都 ✓)。 - 表下那句把 "JSX/React page sources" 整体算作 os validate 独有 —— 现在只有 「源码能否 parse」这个 SHAPE 闸门是独有的,里面的字段绑定属于 reference-integrity, suite 跑到哪它就跑到哪(含 os lint)。改写为分开陈述,并记下这里曾经的漂移与 #4384 的接线守卫。 - 示例 os validate 输出里的 `→ Checking React-source page props (ADR-0081)...` 随手工接线一并删除,已按真实 CLI 输出核对:示例里的每一行 step 现在都能在 showcase 上逐字复现(comm 比对为空)。 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012rK2McPogrTJBKdUsThrGs --- .../docs/deployment/validating-metadata.mdx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/content/docs/deployment/validating-metadata.mdx b/content/docs/deployment/validating-metadata.mdx index 9acce34f0f..5cadf05f09 100644 --- a/content/docs/deployment/validating-metadata.mdx +++ b/content/docs/deployment/validating-metadata.mdx @@ -330,6 +330,7 @@ another package defines. | Dashboard action/route references (ADR-0049) | ✓ | ✓ | | Object & action name references (#3583) | ✓ | ✓ | | Page-component field bindings (#3583) | ✓ | ✓ | +| React page block field bindings — §10 (#4340) | ✓ | ✓ | | Chart bindings outside dashboards (#3583) | ✓ | ✓ | | Navigation vs. granted access (ADR-0090 D6) | ✓ | ✓ | | Security posture (ADR-0090 — e.g. every custom object declares `sharingModel`) | ✓ | ✓ | @@ -346,9 +347,20 @@ will not fail `os build` on schema/predicate/binding grounds — a test in the C asserts that every gate `os build` runs is also run by `os validate`, so the two cannot drift apart again (#3782). Both entry points also check SDUI styling (ADR-0065), and `os validate` additionally runs a set of -view- and page-shape checks — list-view navigation modes (ADR-0053), view -container shape, and JSX/React page sources (ADR-0080/0081) — that catch UI -metadata which would otherwise be silently dropped. +view- and page-SHAPE checks — list-view navigation modes (ADR-0053), view +container shape, and whether a JSX/React page source parses at all +(ADR-0080/0081) — that catch UI metadata which would otherwise be silently +dropped. + +The field bindings INSIDE a react page source are a different matter: they are +reference-integrity, so they run wherever the suite runs. `os lint` gets them +too — it shares the same `REFERENCE_INTEGRITY_RULES` list, which is why the +table's reference rows are the ones a cheap pre-flight can rely on. That was not +always true: the react-page prop gate was hand-wired into `os validate` alone +until #4340's follow-up, so `os lint` and `os build` accepted a page whose every +field binding was stale — the same divergence #4394 closed for readonly flow +writes. A CLI test now asserts no command reaches for a suite member directly +(#4384). A clean run walks each gate and reports timing: @@ -367,7 +379,6 @@ A clean run walks each gate and reports timing: → Checking SDUI styling (ADR-0065)... → Checking JSX-source pages (ADR-0080)... → Checking React-source pages (ADR-0081)... - → Checking React-source page props (ADR-0081)... → Checking source-page styling (ADR-0065)... → Checking capability references (ADR-0066)... → Checking flow trigger wiring...