You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit cbe20ab
Browse filesBrowse the repository at this point in the historyBrowse files
docs(skills): guard nullable validation operands, teach hook body, list mailbox / package-registry, drop os generate agent (#31127)
<!-- claude-projects-attribution: {"github_login":"huangyiirene"} -->
_Requested by **Irene** · [project
thread](https://claude.ai/code/project/chan_01TJXtQ1tVD26ecLDZxFS67x?thread=cmsg_01TJXtQ1tVD26ecLDZxFS67xAX4MMULyw2ed8nbbZASMh9)_Fixes#31113
Clause-②: no
Metadata-protocol: none
`dispatch-gates --tier` reads a MANDATORY model tier for `skills/**`
(clause ① of the model-tiering ruling); it is waived by the standing
order of the maintainer in this project (2026-11-03), and this card ran
on the project's default model. The dispatching seat runs the contract
review.
## 维护者速读
- 改了什么:`objectstack-data` 的 `rules/validation.md` 六处比较/算术谓词全部加 `!= null`
守卫,并写明未守卫的可空字段在发布时被拒;两个钩子示例(`validation.md`、`examples-objects.md`)改写为沙箱
`body`,`SKILL.md` 索引只把 `handler` 称为已弃用;`objectstack-platform` 的
`requires:` 表补 `mailbox`、`package-registry`;`operations.md` 的 `os
generate` 行去掉已退役的 `agent`。
- 为什么改:这四处把发布门会拒绝、或已退役/已弃用的写法教给 AI(#31113):裸可空比较被 `validate-null-guards`
拒绝;`handler` 已弃用;两个 token 在 `PLATFORM_CAPABILITY_TOKENS` 里却不在表里;`agent`
在 `RETIRED_GENERATORS` 里。
- 风险与回滚:只改技能文字与 eval;五个文件全部在 token 上限内,无上限上调(增长全部用同文件删除支付,见下文);发
`@objectstack/skills` patch;回滚即 revert。
- 先看:`skills/objectstack-data/rules/validation.md` 的 External / Custom
Validation 一节(席位裁定 A:`body` 无外呼 HTTP,外部 API 校验走 Connector recipe)。
- 两分钟验证:`node scripts/check-skills-token-ratchet.mjs`,预期退出 0,`57
authored bundle file(s) within their ceilings`。
- 需要你做的:`skills/**` 属 Tier H(人合),需要 `os-zhuang` 或 `hotlong` 的一次
approval,之后席位落地。
## What changes
### 1. Nullable operands in `rules/validation.md` are guarded
Code site: `packages/lint/src/validate-null-guards.ts` (L29–L43: an
ordering or arithmetic operator on a declared field with no `required:
true` / `defaultValue` is rejected unless a `!= null` / `== null` /
`isBlank()` test dominates it), wired into `validateStackExpressions`
for `validations[].condition` / `.when` / `.then` / `.otherwise`
(`packages/lint/src/validate-expressions.ts` L44), which `os build`, `os
validate`, `os lint` and the runtime publish gate run.
`skills/objectstack-formula/SKILL.md` L112–L122 teaches the same rule.
The card named three sites; the file held six. The new sentence under §
Expression Syntax states the rule, so the other three would have
contradicted it on the same page; all six are guarded:
| Line | Before | After |
|:--|:--|:--|
| L60 script | `record.due_date < today()` | `record.due_date != null &&
record.due_date < today()` |
| L72 pattern | `record.amount < 0` | `record.amount != null &&
record.amount < 0` |
| L78 pattern | `record.end_date < record.start_date` |
`record.start_date != null && record.end_date != null && record.end_date
< record.start_date` |
| L159 cross_field | `record.end_date <= record.start_date` | same
guards, `<=` |
| L166 cross_field | `record.discount_amount > record.subtotal * 0.5` |
`record.discount_amount != null && record.subtotal != null && …` |
| L226 conditional `when` | `record.order_total > 10000` |
`record.order_total != null && record.order_total > 10000` |
Measured, not assumed: `findUnguardedNullableOperands` (the gate's
decision procedure, run through `tsx` with each field nullable) returns
1 or 2 findings for every "before" predicate and 0 for every "after"
predicate.
§ Expression Syntax gains two lines (the line before them is
byte-identical): "An ordering or arithmetic operator on a field with no
`required: true` / `defaultValue` is refused at publish unless a `!=
null` test guards it."
`<!-- os:check -->`: that gate
(`packages/spec/scripts/check-skill-examples.ts`) is a `tsc` type-check,
not the null-guard lint. A trial run marked all four candidate blocks:
the § Script Validation block (L52) and the rewritten hook block (L250)
type-check and keep their markers; the § Common Script Patterns block
(`condition: …` lines) and the § Cross-Field block (`validations: [`
with no import) fail with `TS2304: Cannot find name 'P'`, because they
are fragments, so they stay unmarked.
### 2. `requires:` table names `mailbox` and `package-registry`
Code sites: `PLATFORM_CAPABILITY_TOKENS`
(`packages/spec/src/kernel/platform-capabilities.ts` L58
`package-registry`, L64 `mailbox`; provider rows L216, L218) and
`CAPABILITY_PROVIDERS` (`packages/core/src/capability-providers.ts` L174
`package-registry` → `@objectstack/service-package` /
`PackageServicePlugin`, L186 `mailbox` → `@objectstack/service-mailbox`
/ `MailboxServicePlugin`).
```diff
-| `marketplace` | `@objectstack/service-package` |
+| `mailbox` | `@objectstack/service-mailbox` — inbound mail |
+| `marketplace` `package-registry` | `@objectstack/service-package` |
```
`package-registry` shares the `marketplace` row because both keys
resolve to the same package and plugin today (the comment at
`capability-providers.ts` L164–L173). No count in prose (#27522).
### 3. `os generate <kind>` drops the retired `agent`
Code site: `RETIRED_GENERATORS` in
`packages/cli/src/commands/generate.ts` (L974, `agent` at L980: "agents
are platform-internal (ADR-0063 §2)"; its refusal points at `os g
skill`).
```diff
-| `os generate <kind>` | Scaffold an object / view / flow / agent from a template |
+| `os generate <kind>` | Scaffold an object / view / flow / skill from a template |
```
### 4. Hooks are taught as `body`
Code sites: `HookSchema.handler` (`packages/spec/src/data/hook.zod.ts`
L485: "DEPRECATED, prefer `body`") and `HookSchema.body` (L487–L498:
preferred; when both are present the runtime uses `body`). The body's
capabilities are `HookBodyCapability`
(`packages/spec/src/data/hook-body.zod.ts` L48–L54); there is no
`http.fetch` (L38: "outbound calls go through Connector recipes"). The
`ctx.api.object(name).findOne({ where })` shape is the one the body
runner exposes (`packages/runtime/src/sandbox/body-runner.ts` L1119,
gated as a READ by `api.read` in `quickjs-runner.ts` L705).
- `references/examples-objects.md` § Lifecycle Hooks: the example is a
`body` (`language: 'js'`, `source` = the function body). The prose after
it says `body` is the metadata-native form and names the inline
`handler` only as deprecated.
- `rules/validation.md` § External / Custom Validation: the seat ruled
option A. The example is a `body` that reads `blocked_tax_id` through
`ctx.api` with `capabilities: ['api.read']` and throws. The lead now
says "hitting another object or running arbitrary code", and adds that a
`body` has no outbound HTTP, so an external-API check goes through a
Connector recipe (home: `references/data-hooks.md` § Capabilities,
L447).
- `SKILL.md` L46 index: "the sandboxed `body` (ctx + capability
contract; the inline `handler` is deprecated)".
## Token readings (`node scripts/check-skills-token-ratchet.mjs`, exit
0)
| File | `main` | This branch | Ceiling |
|:--|--:|--:|--:|
| `skills/objectstack-platform/SKILL.md` | 6011 | 6007 | 6011 |
| `skills/objectstack-data/SKILL.md` | 6071 | 6077 | 6078 (+6 inside
existing headroom, no deletion owed) |
| `skills/objectstack-data/references/examples-objects.md` | 1728 | 1710
| 1728 |
| `skills/objectstack-data/rules/validation.md` | 2746 | 2748 | 2756 |
| `skills/objectstack-platform/references/operations.md` | 3285 | 3285 |
3287 |
No ceiling is raised, so the ruling path was not used. No line was
re-wrapped. Every deletion, with the facts it held and where each
survives:
- **`objectstack-platform/SKILL.md` § The Template:** "The memory driver
and the Hono server are NOT in the file — the CLI auto-registers both at
boot". Facts: no driver and no server in the scaffolded config; the CLI
registers both. Homes, same file: § Wiring a driver — you usually do not
("Under `os dev` / `os serve` / `os start` the CLI resolves the driver
itself … Do not put a driver in your config's `plugins:` array") and §
Runtime Boot Sequence step 4 (`DriverPlugin`, `HonoServerPlugin`
auto-registered). The line before it loses its trailing "The".
- **`objectstack-data/references/examples-objects.md`:** the
`ctx.input.created_at = new Date().toISOString();` line. It wrote a
column the platform stamps on insert itself (objectql's audit hook,
`packages/objectql/src/plugin.ts` L1975), so it taught a redundant
write. The old prose's facts (inline in-process `handler`; preferred
`body` of `language` / `source` / `capabilities`; isolated VM; the shape
AI/Studio hooks and build artifacts carry) all remain in the new prose
or the example.
- **`objectstack-data/rules/validation.md`:**
- "Comprehensive guide for implementing validation rules in
ObjectStack." Home: the H1 and the table of contents.
- The `No async / custom type` bullet's tail ("external checks and
arbitrary validation code belong in a `beforeInsert` / `beforeUpdate`
lifecycle hook (see data-hooks.md)"). It now links § External / Custom
Validation, which holds the hook rule and the `data-hooks.md` link. The
"external checks" part also contradicted ruling A.
- Common Script Patterns `// Conditional required field` (`record.type
== 'enterprise' && isBlank(record.account_manager)`). Homes: the same
block's "Require field when another field has value" pattern, and §
Conditional Validation's `enterprise_requires_manager`, the same
predicate.
- "Lower numbers execute **first**." Home: the Priority block's comments
"(run first)" / "(run last)".
- Incorrect vs Correct "Script Logic Inverted" / "Correct — Script
Logic". Homes: "⚠️ CRITICAL: … validation **fails** when it evaluates to
`true`" (§ Expression Syntax) and `// ❌ Fails when this is TRUE` in §
Script Validation; the correct amount check is the "Prevent negative
values" pattern. The pair also held two unguarded `amount` comparisons.
The "Validation Fires Too Often" pair stays, so § Incorrect vs Correct
and its contents link remain.
## Eval cases
- `objectstack-data/evals/objects-fields-relationships.json` case 3
(corrected): `must_contain` gains `!= null`; its `expected_output`
already asked for explicit guards. (Site 1.)
- `objectstack-data/evals/hooks-security-seeds-datasources.json` case 10
(new): block an `account` whose `tax_id` is in `blocked_tax_id`, and ask
whether the hook can call a tax-authority API. Must contain
`beforeInsert`, `beforeUpdate`, `language: 'js'`, `'api.read'`,
`ctx.api.object(`; must not contain `handler:`, `HookContext`,
`'http.fetch'`. (Site 4.)
- `objectstack-platform/evals/config-plugins-ops.json` case 18 (new):
inbound mail into mailboxes. Must contain `requires`, `'mailbox'`,
`@objectstack/service-mailbox`; must not contain
`MailboxServicePlugin(`. (Site 2.)
- `objectstack-platform/evals/config-plugins-ops.json` case 19 (new):
scaffold an object, a flow and an AI agent with the CLI. Must contain
`os generate object`, `os generate flow`, `os generate skill`; must not
contain `defineAgent(`. (Site 3.)
## Deviations (all three accepted by the seat before writing)
- **Generator roster.** The card's "live roster" (`object fields view
list action flow dashboard app skill picklist`) is wrong: `fields` and
`list` are keys inside templates. `GENERATORS` in `generate.ts` keys
`object view action flow dashboard app skill picklist permission
dataset`. The fix is unchanged (drop `agent`); `skill` takes its place,
since that is what the refusal points at.
- **`os:check`.** Only the two blocks the gate accepts are marked, as §
1 says. The card's acceptance asked for all three validation examples.
- **Site 4 in `validation.md`** follows the seat's ruling A (a `body`
cannot call an external API), not a literal port of the `handler`
example.
## Other sites found (not changed: outside this card's file surface)
- `skills/objectstack-data/references/data-hooks.md` L219: hook
``condition: P`record.annual_revenue > 1000000` `` is an unguarded
nullable operand. A hook `condition` is a covered surface
(`validate-null-guards.ts` wiring table).
- `skills/objectstack-data/references/data-hooks.md`: teaches the inline
`handler` form first: 3 sites above § Sandboxed Hook Bodies (L309: L97,
L161, L170) and 4 below (L739, L817, L845, L890), plus the comparison
table at L319, so the reference itself still teaches the deprecated door
first.
- `skills/objectstack-formula/SKILL.md` L268: ``P`record.amount > 10000
&& …` `` is an unguarded comparison in an `os:check` block. It has no
object, so it is not refused by itself, but it is the shape that gets
copied.
## Verification
<details><summary>Local gates on 12dd0d0: 47 derived, 47 exit 0, 0 NOT
MEASURED</summary>
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 47 commands for the 9 changed paths;
each was run with its exit code captured before any pipe.
- `pnpm check:dual-build-cjs-loads` exited 3 (NOT MEASURED: no `dist/`)
on the first run. After `pnpm build` (exit 0) it exited 0. The other 46
exited 0 on the first run, including `check:skill-identifier-liveness`,
`check:skill-top-level-keys`, `check:skill-compatibility`,
`check:skill-frame-sync`, `check:skill-docs`, `check:skill-refs`,
`check:doc-authoring`, `check:corpus-claim-drift` and the token ratchet.
</details>
- `pnpm check:skill-identifier-liveness`, `pnpm
check:skill-top-level-keys`, `pnpm check:skill-compatibility`, `pnpm
--filter @objectstack/spec check:skill-refs`, `node
scripts/check-skills-token-ratchet.mjs`: exit 0 each.
- `pnpm --filter @objectstack/spec check:skill-examples` (after building
`@objectstack/spec`, `@objectstack/client` and
`@objectstack/client-react`): exit 0, `285 prose examples type-check`.
- `node scripts/check-commit-card-trailers.mjs --range
origin/main..HEAD`: exit 0 before the push.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(No schema touched.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset
(`'@objectstack/skills': patch`), and both declaration lines are filled
in here and in the changeset.
- [x] 6. `packages/spec` not touched; nothing to regenerate.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English. (The 维护者速读 section is
Chinese, per the lane template.)
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01J8VVAkfHzwVhtRa85nYA4e
<!-- signed: domain:skills#3 · huangyiirene ·
session_01J8VVAj7CvjkGXvgXzd6RZ7 -->
Co-authored-by: Claude <noreply@anthropic.com>
docs(skills): validation examples guard nullable fields, hooks teach `body`, the `requires:` table names `mailbox` and `package-registry`, `os generate` drops `agent`
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
`objectstack-data``rules/validation.md` now guards every ordering or arithmetic operand in its examples with `!= null` (the due-date, date-range, amount, discount and order-total predicates) and states the rule once: an unguarded `<` or `+` on a field with no `required: true` / `defaultValue` is refused at publish. Its external-validation hook and the `examples-objects.md` lifecycle hook are written as a sandboxed `body`, and the `SKILL.md` index names the inline `handler` only as the deprecated form; the validation section says a `body` has no outbound HTTP, so an external-API check goes through a Connector recipe. `objectstack-platform` lists `mailbox` (`@objectstack/service-mailbox`) and `package-registry` (`@objectstack/service-package`) in the `requires:` table, and `references/operations.md` no longer offers the retired `os generate agent`. The platform behaviour already shipped; this release only teaches it.
-**[Data Lifecycle & Retention](./rules/lifecycle.md)** — `lifecycle` classes (record/audit/telemetry/transient/event), retention/TTL/rotation/archive policies; ❗ append-only objects must declare one (distinct from lifecycle *hooks* below); plus the **save notice** a hook hands the saver (`ctx.notice`)
46
-
-**[Lifecycle Hooks](./references/data-hooks.md)** — the lifecycle events, `handler` vs sandboxed `body` (ctx + capability contract), registration, canonical patterns
46
+
-**[Lifecycle Hooks](./references/data-hooks.md)** — the lifecycle events, the sandboxed `body` (ctx + capability contract; the inline `handler` is deprecated), registration, canonical patterns
47
47
-**[Datasources & Federation](./rules/datasources.md)** — `defineDatasource`, external/federated objects (`remoteName`/`columnMap`), auto-connect gating, credentials; ❌ no `field.columnName` on external objects
Copy file name to clipboardExpand all lines: skills/objectstack-data/evals/hooks-security-seeds-datasources.json
+10Lines changed: 10 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -90,6 +90,16 @@
90
90
"must_contain": ["INVALID_FIELD", "is_not_null"],
91
91
"must_not_contain": ["starts_with"]
92
92
}
93
+
},
94
+
{
95
+
"id": 10,
96
+
"prompt": "An `account` whose `tax_id` appears in our `blocked_tax_id` object must not save, on create or edit. Our tax authority also offers a lookup API; can the same hook call it?",
97
+
"expected_output": "A `beforeInsert` / `beforeUpdate` hook on `account` with a sandboxed `body: { language: 'js', source, capabilities: ['api.read'] }`: the source returns when the write carries no `tax_id`, reads `ctx.api.object('blocked_tax_id').findOne({ where: { tax_id: ctx.input.tax_id } })` and throws when it finds a row. Not a validation rule (there is no `custom` / `async` type) and not the deprecated inline `handler`. The tax-authority API is not called from the hook: a `body` has no outbound HTTP, so that check goes through a Connector recipe.",
Copy file name to clipboardExpand all lines: skills/objectstack-platform/evals/config-plugins-ops.json
+32Lines changed: 32 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -301,6 +301,38 @@
301
301
"os push"
302
302
]
303
303
}
304
+
},
305
+
{
306
+
"id": 18,
307
+
"prompt": "Our `os serve` app must receive customers' inbound email into mailboxes and link each message to its records. What turns that on?",
308
+
"expected_output": "`requires: ['mailbox']` on the stack root: the token mounts `@objectstack/service-mailbox` (inbound mail). No `MailboxServicePlugin` instance in `plugins:` and no hand-built mail objects; `defineStack` rejects an unknown token at authoring time.",
309
+
"files": [],
310
+
"assertions": {
311
+
"must_contain": [
312
+
"requires",
313
+
"'mailbox'",
314
+
"@objectstack/service-mailbox"
315
+
],
316
+
"must_not_contain": [
317
+
"MailboxServicePlugin("
318
+
]
319
+
}
320
+
},
321
+
{
322
+
"id": 19,
323
+
"prompt": "Scaffold, with the CLI, an object, a flow and an AI agent for our support app.",
324
+
"expected_output": "`os generate object <name>` and `os generate flow <name>` scaffold the first two. There is no agent generator: `os generate agent` is retired (agents are platform-internal, ADR-0063 §2), so the AI extension is a skill, `os generate skill <name>`. No hand-written `defineAgent` file.",
|`os serve`| Serve a compiled stack in production mode |
22
22
|`os start`| Quick-start a server: auto-compiles `objectstack.config.ts` when no artifact is present, and falls back to an empty kernel with the Console + marketplace when there is no config at all. It does **not** validate env or apply migrations — run `os validate` / `os migrate apply` yourself |
23
-
|`os generate <kind>`| Scaffold an object / view / flow / agent from a template |
23
+
|`os generate <kind>`| Scaffold an object / view / flow / skill from a template |
0 commit comments