Repository navigation
Commit 82e6da1
docs(spec): guard the nullable amount in the docs examples and the validation condition example (#31358)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1X72ZKaHCwtzbWjtWFMnY9ec)_
Fixes #31334
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:七个文档页和 `ValidationRule.condition` 的 spec 示例里,对可空 `amount` 的比较补上
`record.amount != null && …` 守卫;`common-patterns` 的报销单把 `amount` 改为
`required: true`;用 `gen:docs` 重新生成 `references/data/validation.mdx`(只变 4
行)。
- 为什么改:发布期空值守卫规则会拒绝对可空字段做未守卫的大小比较或算术;照抄这些示例,要么发布被拒,要么 `requiredWhen`
静默失效(什么都不强制)。
- 风险与回滚:纯文档与 spec 描述文字,不改 schema 形状、不移动任何接受集;`@objectstack/spec`
patch;回滚即 revert。
- 先看:`content/docs/getting-started/common-patterns.mdx`(唯一选了 `required:
true` 而非守卫的一处);`packages/spec/src/data/validation.zod.ts`
L180;`content/docs/data-modeling/formulas.mdx` L394 起的说明文字。
- 两分钟验证:`pnpm --filter @objectstack/spec run check:docs`,预期 exit
0(生成页与源一致);下方表格是每处表达式经真实规则 `validateStackExpressions` 的前后读数。
- 需要你做的:无,席位落地。
## What changes
The publish-time null-guard rule
(`packages/lint/src/validate-null-guards.ts`, wired by
`validateStackExpressions`, which `os build`, `os validate`, `os lint`
and the runtime publish gate all run) refuses an ordering or arithmetic
operator on a declared-nullable field unless a `!= null` / `== null` /
`isBlank()` test guards it. Every site in the card compared
`record.amount` unguarded in a gated position. Each is fixed with the
shape the formula skill teaches (`skills/objectstack-formula/SKILL.md`
L112-L122, L404).
| Site | Surface | Fix | Why this fix |
|:--|:--|:--|:--|
| `content/docs/getting-started/common-patterns.mdx` L312, flow edges
`e2`/`e3` | `record_change` flow on `expense_report` | `amount` declared
`required: true` | The example declares its object, and the flow
branches on `amount` (`< 100` auto-approves, `>= 100` asks). Guarding
the start condition instead would let a submitted report with no amount
launch no flow at all, so it would skip approval silently. An expense
report always has an amount, so `required: true` is the honest fix. The
edges stay as written. |
| `content/docs/automation/approvals.mdx` L145, start `condition` |
`record_change` flow start on `opportunity` | guard: `record.amount !=
null && record.amount >= 50000 && …` | The object is not declared. Per
the ruling, a flow is guarded at its start condition, which covers every
edge and branch (`validate-expressions.ts` L2967-L2976). |
| `content/docs/automation/hook-bodies.mdx` L74 | hook `condition` |
guard | The object is not declared. |
| `content/docs/data-modeling/formulas.mdx` L119 | field `requiredWhen`
| guard | The `Invoice` example declares fields, but not `amount` (nor
`subtotal`, `tax_rate`, `invoice_no`, `customer`); it is a fragment for
`amount`, so it is guarded rather than given a `required` declaration it
does not have. |
| `content/docs/data-modeling/formulas.mdx` L356 | field `requiredWhen`
| guard | The object is not declared. |
| `content/docs/data-modeling/formulas.mdx` L394 | validation rule
`condition` | guard, plus one sentence under it saying why an optional
field needs the `!= null` test | The object is not declared. |
| `content/docs/data-modeling/validation.mdx` L329 | validation rule
`condition` (a `validations:` fragment) | guard | The object is not
declared. |
| `content/docs/automation/flows.mdx` L2473 | flow start-node
`condition` (syntax table) | guard | The object is not declared. |
| `packages/spec/src/data/validation.zod.ts` L180 `.describe()` |
validation `condition` | guard: ``P`record.amount != null &&
record.amount < 0` `` | The object is not declared.
`content/docs/references/data/validation.mdx` L170/L291/L505/L571
regenerated with `pnpm --filter @objectstack/spec gen:schema &&
gen:docs`, never hand-edited. |
| `packages/spec/src/data/validation.zod.ts` L257 and L532 (doc
comments) | `cross_field` and `conditional` rule examples | guard |
Bounded in-place fix, see below. |
**Bounded in-place fix (same file, outside the card's table).** The two
doc-comment examples in `validation.zod.ts` (the `cross_field`
`discount_limit` at L257, and the `conditional` `enterprise_high_value`
`then` at L532) have the same defect, and the rule refuses both (table
below). All four conditions hold: the same defect class, the same
mechanical guard shape, no other claim on the file (PR #31322 touches
`hook.zod.ts`, not this file), and the same gate families. The doc
comments ship in `@objectstack/spec`'s `.d.ts`, where an author or agent
reads them. They do not reach the generated reference page.
Not changed (out of scope by ruling): `validation.mdx` L51 (`amount` is
`required: true` at L31), the `ui/` SchemaForm test constant, release
notes and CHANGELOGs.
## Tests
Each changed expression was run, in its declared context, through the
real gating rule: `validateStackExpressions` from the built
`@objectstack/lint` `dist/` (built on this branch's base `dc7f5c4b4`).
The fixture object declares `amount` as `{ type: 'currency' }`, which is
nullable (no `required`, no default), as on the CRM example's
`opportunity`. Counts are error-severity null-guard findings; every row
had 0 other errors.
| Site | Before | After |
|:--|:--|:--|
| common-patterns flow `e2`/`e3` | REFUSED (2: edge `e2`, edge `e3`) |
accepted (`amount` `required: true`) |
| approvals.mdx start condition | REFUSED (1: start condition) |
accepted |
| hook-bodies.mdx hook condition | REFUSED (1) | accepted |
| formulas.mdx L119 / L356 `requiredWhen` | REFUSED (1) | accepted |
| formulas.mdx L394 / validation.mdx L329 script rule | REFUSED (1) |
accepted |
| flows.mdx L2473 start condition | REFUSED (1) | accepted |
| validation.zod.ts L180 condition | REFUSED (1) | accepted |
| validation.zod.ts L257 `cross_field` | REFUSED (2: `discount`,
`amount`) | accepted |
| validation.zod.ts L532 `conditional` `then` | REFUSED (1) | accepted |
<details><summary>Probe output</summary>
```text
REFUSED | null-guard errors 2 | other errors 0 | common-patterns.mdx flow e2/e3 (amount declared nullable) | flow 'expense_approval' · edge 'e2' (check_amount→auto_approve) condition; flow 'expense_approval' · edge 'e3' (check_amount→request_approval) condition
accepted | null-guard errors 0 | other errors 0 | common-patterns.mdx flow e2/e3 (amount required: true)
REFUSED | null-guard errors 1 | other errors 0 | approvals.mdx L145 start condition BEFORE | flow 'opportunity_approval' · node 'start' (start) condition
accepted | null-guard errors 0 | other errors 0 | approvals.mdx L145 start condition AFTER
REFUSED | null-guard errors 1 | other errors 0 | hook-bodies.mdx L74 hook condition BEFORE | hook 'h' (opportunity) condition
accepted | null-guard errors 0 | other errors 0 | hook-bodies.mdx L74 hook condition AFTER
REFUSED | null-guard errors 1 | other errors 0 | formulas.mdx L119/L356 requiredWhen BEFORE | object 'opportunity' · field 'po_number' requiredWhen
accepted | null-guard errors 0 | other errors 0 | formulas.mdx L119/L356 requiredWhen AFTER
REFUSED | null-guard errors 1 | other errors 0 | formulas.mdx L394 / validation.mdx L329 script rule BEFORE | object 'opportunity' · validation rule 'amount_positive'
accepted | null-guard errors 0 | other errors 0 | formulas.mdx L394 / validation.mdx L329 script rule AFTER
REFUSED | null-guard errors 1 | other errors 0 | flows.mdx L2473 start condition BEFORE | flow 'opportunity_approval' · node 'start' (start) condition
accepted | null-guard errors 0 | other errors 0 | flows.mdx L2473 start condition AFTER
REFUSED | null-guard errors 1 | other errors 0 | validation.zod.ts L180 condition BEFORE | object 'opportunity' · validation rule 'r'
accepted | null-guard errors 0 | other errors 0 | validation.zod.ts L180 condition AFTER
REFUSED | null-guard errors 2 | other errors 0 | validation.zod.ts L257 cross_field (jsdoc) BEFORE | object 'opportunity' · validation rule 'discount_limit'; object 'opportunity' · validation rule 'discount_limit'
REFUSED | null-guard errors 1 | other errors 0 | validation.zod.ts L532 conditional then (jsdoc) BEFORE | object 'opportunity' · validation rule 'enterprise_high_value' then → 'amount_approval'
accepted | null-guard errors 0 | other errors 0 | validation.zod.ts L257 cross_field (jsdoc) AFTER
accepted | null-guard errors 0 | other errors 0 | validation.zod.ts L532 conditional then (jsdoc) AFTER
```
</details>
No test file changes: this is docs and description text. No test pins
the old describe string (`git grep` over `packages/**`).
## Acceptance notes
- `packages/spec/src/automation/flow.zod.ts` L1070-L1071, the
`FlowSchema` `@example`, has the same `record.amount < 500` / `>= 500`
edges. As written its start node carries no `config.objectName`, so the
rule has no object to resolve and does not refuse it (probe: 0 findings
as written, 2 once the start is bound to an `order` with a nullable
`amount`). Outside this card's surface and not refused at a public door
today, so it is noted, not filed.
- `skills/objectstack-data/rules/validation.md` L164 (`discount_limit`)
is already guarded.
## Verification
On head `c4bbb0ad` (merge base `dc7f5c4b4`), after building the
`@objectstack/lint` closure, then `@objectstack/spec` and the
`@objectstack/client-react` closure, under
`NODE_OPTIONS=--max-old-space-size=8192`: **98 of the 99 commands from
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` exit 0.** `pnpm check:dual-build-cjs-loads`
exits 3 (PREREQUISITE NOT MET: it needs every workspace package's
`dist/`), so it is NOT MEASURED locally and CI reads it. `dispatch-gates
--ran`: 99 derived, 98 run, 1 NOT-MEASURED, 0 UNRUN (exit 0). Changed
lines: 35 + the changeset, 9 files.
Also run: `packages/spec` `src/data/validation.test.ts` +
`validation-boundary-description.test.ts` (2 files, 90 / 90 passed, lock
VERDICT command-exit 0); eslint on `validation.zod.ts` (exit 0); `node
scripts/check-commit-card-trailers.mjs --range origin/main..HEAD` (exit
0); `pnpm check:skill-frame-freshness` (exit 0). `pnpm
check:dev-prereqs` exits 1 on local state only (65 of 107 packages have
no `dist/` in this worktree, since only the needed closures were built);
its self-test passes, and it is not a finding about this diff.
The NOT MEASURED families `dispatch-gates` names outside the derived
list (CI-only steps, type-check lanes, the dogfood and build jobs) run
in CI.
<details><summary>Each command with its exit code (head
c4bbb0a)</summary>
```text
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-adr-links.mjs :: exit 0
node scripts/check-adr-links.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dev-prereqs.mjs --self-test :: exit 0
node scripts/check-doc-route-spelling.mjs --advisory :: exit 0
node scripts/check-doc-route-spelling.mjs --self-test :: exit 0
node scripts/check-docs-section-name.mjs :: exit 0
node scripts/check-docs-section-name.mjs --self-test :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/check-section-landing-index.mjs :: exit 0
node scripts/check-section-landing-index.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/ci/snapshot-next.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0
pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0
pnpm --filter @objectstack/spec run check:api-surface :: exit 0
pnpm --filter @objectstack/spec run check:authorable-surface :: exit 0
pnpm --filter @objectstack/spec run check:browser-reachable-entries :: exit 0
pnpm --filter @objectstack/spec run check:docs :: exit 0
pnpm --filter @objectstack/spec run check:dual-source-exports :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm --filter @objectstack/spec run check:empty-state :: exit 0
pnpm --filter @objectstack/spec run check:entry-nameability :: exit 0
pnpm --filter @objectstack/spec run check:export-origins :: exit 0
pnpm --filter @objectstack/spec run check:exported-any :: exit 0
pnpm --filter @objectstack/spec run check:generated :: exit 0
pnpm --filter @objectstack/spec run check:liveness :: exit 0
pnpm --filter @objectstack/spec run check:llms-txt :: exit 0
pnpm --filter @objectstack/spec run check:skill-examples :: exit 0
pnpm --filter @objectstack/spec run check:skill-refs :: exit 0
pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0
pnpm --filter @objectstack/spec run check:variant-docs :: exit 0
pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0
pnpm check:adr-symbol-anchors :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:corpus-claim-drift :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:doc-anchors :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:doc-frontmatter :: exit 0
pnpm check:docs-audit-scope :: exit 0
pnpm check:docs-redirects :: exit 0
pnpm check:docs-single-h1 :: exit 0
pnpm check:docs-spec-enumerations :: exit 0
pnpm check:docs-transcript-drift :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 3
pnpm check:gitlink-declared :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:merge-driver :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:pm-prior-rulings :: exit 0
pnpm check:published-files :: exit 0
pnpm check:published-readme-links :: exit 0
pnpm check:react-page-adapter-contract :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:role-word :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:spec-docblock-symbol-anchors :: exit 0
pnpm check:spec-parsed-alias :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:vendor-version-stamps :: exit 0
pnpm check:vouch-for-reference-callers :: exit 0
pnpm check:watch-hint-literal :: exit 0
```
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5kcxM8mwtPqSQpzbVT)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent f4b4c8a commit 82e6da1
9 files changed
Lines changed: 31 additions & 16 deletions
File tree
- .changeset
- content/docs
- automation
- data-modeling
- getting-started
- references/data
- packages/spec/src/data
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
142 | 142 | | |
143 | 143 | | |
144 | 144 | | |
145 | | - | |
| 145 | + | |
146 | 146 | | |
147 | 147 | | |
148 | 148 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2470 | 2470 | | |
2471 | 2471 | | |
2472 | 2472 | | |
2473 | | - | |
| 2473 | + | |
2474 | 2474 | | |
2475 | 2475 | | |
2476 | 2476 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
71 | 71 | | |
72 | 72 | | |
73 | 73 | | |
74 | | - | |
| 74 | + | |
75 | 75 | | |
76 | 76 | | |
77 | 77 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
116 | 116 | | |
117 | 117 | | |
118 | 118 | | |
119 | | - | |
| 119 | + | |
120 | 120 | | |
121 | 121 | | |
122 | 122 | | |
| |||
353 | 353 | | |
354 | 354 | | |
355 | 355 | | |
356 | | - | |
| 356 | + | |
357 | 357 | | |
358 | 358 | | |
359 | 359 | | |
| |||
391 | 391 | | |
392 | 392 | | |
393 | 393 | | |
394 | | - | |
| 394 | + | |
395 | 395 | | |
396 | 396 | | |
397 | 397 | | |
398 | 398 | | |
399 | 399 | | |
400 | 400 | | |
401 | | - | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
402 | 404 | | |
403 | 405 | | |
404 | 406 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
326 | 326 | | |
327 | 327 | | |
328 | 328 | | |
329 | | - | |
| 329 | + | |
330 | 330 | | |
331 | 331 | | |
332 | 332 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
309 | 309 | | |
310 | 310 | | |
311 | 311 | | |
312 | | - | |
| 312 | + | |
| 313 | + | |
313 | 314 | | |
314 | 315 | | |
315 | 316 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
167 | 167 | | |
168 | 168 | | |
169 | 169 | | |
170 | | - | |
| 170 | + | |
171 | 171 | | |
172 | 172 | | |
173 | 173 | | |
| |||
288 | 288 | | |
289 | 289 | | |
290 | 290 | | |
291 | | - | |
| 291 | + | |
292 | 292 | | |
293 | 293 | | |
294 | 294 | | |
| |||
502 | 502 | | |
503 | 503 | | |
504 | 504 | | |
505 | | - | |
| 505 | + | |
506 | 506 | | |
507 | 507 | | |
508 | 508 | | |
| |||
568 | 568 | | |
569 | 569 | | |
570 | 570 | | |
571 | | - | |
| 571 | + | |
572 | 572 | | |
573 | 573 | | |
574 | 574 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
177 | 177 | | |
178 | 178 | | |
179 | 179 | | |
180 | | - | |
| 180 | + | |
181 | 181 | | |
182 | 182 | | |
183 | 183 | | |
| |||
254 | 254 | | |
255 | 255 | | |
256 | 256 | | |
257 | | - | |
| 257 | + | |
258 | 258 | | |
259 | 259 | | |
260 | 260 | | |
| |||
529 | 529 | | |
530 | 530 | | |
531 | 531 | | |
532 | | - | |
| 532 | + | |
533 | 533 | | |
534 | 534 | | |
535 | 535 | | |
| |||
0 commit comments