Skip to content

Commit b4a9d04

Browse files
docs(skills): a user field's declared deleteBehavior is taught in the data skill's field-types and relationships rules (#29781)
domain:skills#1 · huangyiirene · session_01J8VVAnMUWA9rufXzFaEh4J <!-- claude-projects-attribution: {"github_login":"huangyiirene"} --> _Requested by **Irene** · [project thread](https://claude.ai/code/project/chan_01TJXtQ1tVD26ecLDZxFS67x?thread=cmsg_01TJXtQ1tVD26ecLDZxFS67xRY2TwWjfFKsjhqoNR7z9DE)_ Fixes #29400 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:objectstack-data 技能的两份规则文件现在教「`user` 字段上作者写明的 `deleteBehavior` 在删除该用户时真的生效」:field-types 的 `user` 行列出 `deleteBehavior`(无默认;写了就按 lookup 的方式 cascade / set_null / restrict,没写则保留被删用户的 id);relationships 的 Delete Behaviors 节点名删除联动读 `lookup`、`tree`,以及只在声明了值时才读的 `user` 字段。加一条 eval 用例练它。 - 为什么改:PR #29368 已把该行为合入引擎(card #29253),其 `Skills-docs: follow-up` 行欠下本卡;技能文本此前只说 user 字段「存储同 lookup」,没说删除用户时会发生什么,AI 写元数据时会把 `deleteBehavior` 当摆设或漏写。 - 风险与回滚:纯技能文本与 eval 夹具,无代码、无契约改动。两个 token 天花板都就地付账(删掉在本包别处已有完整版本的 Summary 示例块与 When-to-Use 列表),上限行未动,无 re-wrap。回滚即 revert。 - 先看:`skills/objectstack-data/rules/relationships.md` 新段(Delete Behaviors 节末)与被删的 When to Use 节;`skills/objectstack-data/rules/field-types.md` 的 `user` 行与被删的 Summary (Roll-up) 块;`skills/objectstack-data/evals/objects-fields-relationships.json` 的用例 6。 - 两分钟验证:`node scripts/check-skills-token-ratchet.mjs`,预期 field-types.md 3183/3207、relationships.md 3844/3873,全绿。 - 需要你做的:Tier H 受管面(`skills/**`),需要 `GOVERNED_APPROVERS` 的批准;PR 保持 draft。 ## What changes Facts are taken from current `main`, not the card text alone: `content/docs/data-modeling/fields.mdx` as corrected by PR #29712, `statesDeleteSemantics` / `cascadeRelationBehavior` in `packages/objectql/src/engine.ts`, and the pins in `packages/objectql/src/engine-cascade-delete-user-field.test.ts`. 1. **`skills/objectstack-data/rules/field-types.md`, the `user` row.** The Key Config cell adds `deleteBehavior` (no default — declared, deleting the user acts as on `lookup`: `cascade` deletes the holders, `set_null` clears the field or drops that member, `restrict` refuses with `409 DELETE_RESTRICTED`; undeclared, the field keeps the deleted user's id). Everything else in the row is byte-identical. 2. **`skills/objectstack-data/rules/relationships.md`, Delete Behaviors.** One new paragraph after the `master_detail` code block: the walk also reads `lookup` and `tree` (`set_null` by default — the field is cleared; a `multiple` field loses only that member — or an authored `cascade` / `restrict`), and a `user` field only when it declares a value: deleting the user then cascades, clears or refuses exactly as a `lookup` does; undeclared, the field keeps the deleted user's id. `restrict`, and `set_null` on a `required` field (a `multiple` one only when the removal would empty it), refuse the delete with `409 DELETE_RESTRICTED` — the multi-valued narrowing is the engine's (`cascadeDeleteRelations` escalates unconditionally only for a single-valued field and otherwise refuses only the rows the removal would empty; pinned in `engine-cascade-delete.test.ts`). The existing `master_detail` text above it is byte-identical. 3. **`skills/objectstack-data/evals/objects-fields-relationships.json`.** Case 6: a required `owner` user field that must block deleting a held user (`restrict`) and a multi-valued `watchers` field whose deleted member should simply drop out (`set_null`). Must-contain pins the two `deleteBehavior` values, `type: 'user'`, `multiple: true` and `required: true`; must-not-contain refuses `reference: 'user'` and `deleteBehavior: 'cascade'`. The eval README's index line names the `user` case ("`deleteBehavior`, on a `user` field too"). Eval fixtures are measured, not ratcheted. ### Token ratchet, paid in place (no ceiling row moved, no re-wrap) | file | tokens before / after / ceiling | lines before / after | |:--|:--|:--| | `rules/field-types.md` | 3207 / 3183 / 3207 (headroom 0 → 24) | 264 / 250 | | `rules/relationships.md` | 3869 / 3844 / 3873 (headroom 4 → 29) | 420 / 414 | | `evals/objects-fields-relationships.json` (not ratcheted) | 1436 / 1749 | 101 / 120 | | `evals/README.md` (not ratcheted) | 142 / 148 | 12 / 12 | | package `skills/objectstack-data` (17 files) | 44130 / 44399 | 3971 / 3970 | The two payments, each a deletion of text restated elsewhere, with its home: - `field-types.md` → the **Summary (Roll-up)** block under Common Field Configurations (14 lines, 361 bytes). Home: `rules/relationships.md` → Roll-up Summaries carries the same `summaryOperations` block with the same keys and comments plus the `filter` / `relationshipField` teaching; the Calculated table row in field-types.md keeps the key shape (`{ object, field?, function, relationshipField?, filter? }`). The deleted block's own last comment pointed at `rules/relationships.md`. - `relationships.md` → the **When to Use lookup vs master_detail** section (14 lines) and its link in the table of contents (613 bytes together). Home: the Relationship Types table directly above it (Lifecycle, Required, Use Case columns), the Incorrect vs Correct section (lookup vs master_detail on invoice line items), and the Patterns examples. Its one line not restated there — `set_null` is the `lookup` default — moves into the new Delete Behaviors paragraph. Its `task.assigned_to → user` example taught a `lookup` to a `user` object, which this package's own eval 2 forbids (`reference: 'user'` is must-not-contain; a `user` object does not exist), so that line was also stale. The package grows by about 270 tokens: the eval fixture and its index +319 (not loaded into a customer session), the two loaded rule files −24 and −25. Their ceiling rows could be lowered to 3183 and 3844 in a follow-up; this card's file surface does not include `scripts/check-skills-token-ratchet.mjs`. ## Why PR #29368 (card #29253) made a `user` field's declared `deleteBehavior` take effect when the user is deleted, and its `Skills-docs: follow-up` line owed this card. Before this change the data skill said only that a `user` field is "stored identically to `lookup`", and its Delete Behaviors section spoke of `master_detail` alone, so an AI authoring metadata from the skill had no way to know that `deleteBehavior` on a `user` field is now enforced, that it has no default, or that a `required` field's `set_null` refuses the delete. ## Tests - `node scripts/check-skills-token-ratchet.mjs`: exit 0 — `57 authored bundle file(s) within their ceilings`; `rules/field-types.md is 3183 tokens (ceiling 3207; headroom 24)`, `rules/relationships.md is 3844 tokens (ceiling 3873; headroom 29)`. - The eval fixture re-serializes byte-identically for cases 1–5 (checked before appending case 6), and every `must_contain` string of case 6 appears in its `expected_output`. - No code changed, so no package test, typecheck, ablation or dogfood battery is owed (`Clause-②: no`, no `!` in the title). ## Acceptance notes - The edit is made on the branch the platform fixes for this thread (`claude/project-thread-5yjezo`) in this container's own checkout rather than in a sibling worktree: the branch can be checked out in one place only and the container is this card's alone. Noted as a deviation from the worktree-first spelling, not from its purpose. - `master_detail` text in both files is left exactly as it was; the card asked for the `user` teaching only. - The `lookup` row of field-types.md still lists `deleteBehavior` without its default; relationships.md now states the default once. Not expanded further (small feature, small text). - Round 2 (commit 5039eb2) answers the seat's contract review of dceca37 (comment 6595584700): M1, the required + `set_null` sentence now carries the multi-valued qualifier; S1, the eval README index line reads as a sentence. Nothing else moved. `content/docs/data-modeling/fields.mdx` carries the same unqualified sentence for a `user` field and is the docs lane's follow-up, not this PR's. ## Verification Head `5039eb23` (two commits: dceca37 the change, 5039eb2 the review's M1 + S1). No code changed, so no package test or typecheck is owed; the derived gate set is the verification. <details><summary>Local gates on 5039eb2: 32 derived, 31 run with exit 0, 1 NOT MEASURED (`dispatch-gates --ran`); the same 32 on dceca37 read the same</summary> - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 32 commands for the four changed files, the same list on both heads. All 32 ran on each head in a tree with `pnpm install --frozen-lockfile` done and `@objectstack/formula`, `@objectstack/lint`, `@objectstack/spec` and `@objectstack/objectql` built, each with a 900 s timeout, each exit code captured before any pipe. - 31 exited 0 on 5039eb2, among them `check-skills-token-ratchet.mjs` (and its `--self-test`), `check-comment-mask-corpus.mjs`, `check:doc-authoring`, `check:pm-skill-id-lint`, `check:skill-compatibility`, `check:skill-frame-sync`, `check:skill-identifier-liveness`, `check:role-word`, `check:nul-bytes`, `check:pm-governed-merges`, `check:dts-closure` and `check:lean-entry-closure` (on dceca37 the last exited 3 before the `@objectstack/objectql` build and 0 after it). - `pnpm check:dual-build-cjs-loads` exited 3: `PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/` (91 packages; it needs a full `pnpm build`). Not a finding: this diff touches no `packages/**` file, and the gate's own text says nothing was measured. CI's `Lint & Repo Gates` run on the PR is its reading. - `--ran` printed: `32 derived famil(ies) accounted for — 31 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)`. - `node scripts/check-commit-card-trailers.mjs --range origin/main..HEAD`: exit 0 on both heads (two commits, no card relation, model-free trailer pair). - CI on dceca37: every check-run `success` or skipped by path filter or opt-in, including the required Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Governed Surface Queue Guard, Test UI and Live E2E UI. CI on 5039eb2: in progress at the time of writing. - Writes: `git push` ×3 (the bare branch pointer, dceca37, 5039eb2); one relay `pr_create` carrying `skip-changeset`; no GitHub MCP write. </details> ## 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; `pnpm lint` is CI's repo-wide run. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. Nothing publishes from a released package: `skip-changeset` rides the create; both declaration lines are above. - [x] 6. `packages/spec` not touched. - [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 维护者速读 block is the repo's own Chinese reading aid). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01J8VVAkZCePk1V8qh465qcT --- _Generated by [Claude Code](https://claude.ai/code/session_01J8VVAkZCePk1V8qh465qcT)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6f35b74 commit b4a9d04

4 files changed

Lines changed: 30 additions & 31 deletions

File tree

‎skills/objectstack-data/evals/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ JSON fixtures in the objectui shape (`skill_name`, `evals[]`: `prompt`,
55

66
- `objects-fields-relationships.json` — naming, field types (`secret` vs
77
`password`), relationships (junction vs multi-value lookup,
8-
`deleteBehavior`), validation (`state_machine`, unique **index**),
8+
`deleteBehavior`, on a `user` field too), validation (`state_machine`, unique **index**),
99
conditional field rules.
1010
- `hooks-security-seeds-datasources.json` — search mirrors via hooks,
1111
permission-set scopes, external datasources, seeds (`externalId`,

‎skills/objectstack-data/evals/objects-fields-relationships.json‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,25 @@
9696
"name: 'crm_account'"
9797
]
9898
}
99+
},
100+
{
101+
"id": 6,
102+
"prompt": "On `ticket`, `owner` is the agent who holds it and must always be set; deleting that agent's user account must be blocked while they still hold tickets. `watchers` is several users, and a deleted user should just drop out of the list.",
103+
"expected_output": "Both are `type: 'user'` fields, each with an authored `deleteBehavior` — a `user` field joins the referential delete walk only when it declares one. `owner`: `required: true` with `deleteBehavior: 'restrict'` (deleting a held user is refused with `409 DELETE_RESTRICTED`; `set_null` on a required field would refuse too). `watchers`: `multiple: true` with `deleteBehavior: 'set_null'` (only the deleted member is removed; an emptied set is `[]`). Neither is a `lookup` to a `user` object, which does not exist, and a `user` field with no `deleteBehavior` keeps the deleted user's id.",
104+
"files": [],
105+
"assertions": {
106+
"must_contain": [
107+
"type: 'user'",
108+
"deleteBehavior: 'restrict'",
109+
"deleteBehavior: 'set_null'",
110+
"multiple: true",
111+
"required: true"
112+
],
113+
"must_not_contain": [
114+
"reference: 'user'",
115+
"deleteBehavior: 'cascade'"
116+
]
117+
}
99118
}
100119
]
101120
}

‎skills/objectstack-data/rules/field-types.md‎

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ options: [
9393
| `lookup` | Reference another object (independent) | `reference`, `lookupFilters`, `multiple`, `deleteBehavior`, `lookupAutofill` |
9494
| `master_detail` | Parent–child with lifecycle control | `reference`, `deleteBehavior` (`cascade`/`restrict` — `set_null` is refused), `lookupAutofill` |
9595
| `tree` | Hierarchical self-reference | `reference` — optional; when written, this object's own name (any other object is refused at parse) |
96-
| `user` | Person picker — a lookup specialized to `sys_user` (assignee, watchers). Stored identically to `lookup` | `multiple` (collaborators), `defaultValue: 'current_user'` |
96+
| `user` | Person picker — a lookup specialized to `sys_user` (assignee, watchers). Stored identically to `lookup` | `multiple` (collaborators), `defaultValue: 'current_user'`, `deleteBehavior` (no default — declared, deleting the user acts as on `lookup`: `cascade` deletes the holders, `set_null` clears the field or drops that member, `restrict` refuses with `409 DELETE_RESTRICTED`; undeclared, the field keeps the deleted user's id) |
9797

9898
**`lookupAutofill: [{ target, source, overwrite? }]`** (single-value `lookup` /
9999
`master_detail`): a pick copies the picked record's `source` into this object's
@@ -203,20 +203,6 @@ const total = {
203203
};
204204
```
205205

206-
### Summary (Roll-up)
207-
208-
```typescript
209-
{
210-
type: 'summary',
211-
summaryOperations: {
212-
object: 'invoice_line_item', // child object to aggregate
213-
field: 'amount', // child field
214-
function: 'sum', // 'count' | 'sum' | 'min' | 'max' | 'avg'
215-
// relationshipField / filter — optional (see rules/relationships.md)
216-
},
217-
}
218-
```
219-
220206
### Autonumber
221207

222208
```typescript

‎skills/objectstack-data/rules/relationships.md‎

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Relationship Patterns
22

3-
- [Relationship Types](#relationship-types) · [When to Use lookup vs master_detail](#when-to-use-lookup-vs-master_detail)
3+
- [Relationship Types](#relationship-types)
44
- [Patterns](#patterns) — [lookup](#one-to-many-lookup) · [master_detail](#one-to-many-master_detail) · [Junction Object](#many-to-many-junction-object) · [tree](#hierarchical-tree-self-reference)
55
- [Delete Behaviors](#delete-behaviors) · [Roll-up Summaries](#roll-up-summaries)
66
- [Inline Editing (Master-Detail Entry)](#inline-editing-master-detail-entry) · [Detail-page related lists](#detail-page-related-lists-the-read-side-mirror)
@@ -16,20 +16,6 @@ Guide for modeling relationships between objects using `lookup`, `master_detail`
1616
| `master_detail` | Coupled (cascade delete) | Forced only under `controlled_by_parent`; else lint-warned | Inherits parent; owner opt-in `ownerMayEdit` (below) | Supported via `summary` | "Owned by" |
1717
| `tree` | Self-reference | Optional — and so is `reference`, which when written must name this object itself; any other object is refused at parse, not lint-warned | N/A | Not available | Hierarchical |
1818

19-
## When to Use lookup vs master_detail
20-
21-
### Use `lookup` When:
22-
- Child record can exist independently
23-
- Parent deletion should only clear the FK (`deleteBehavior: 'set_null'`, the default)
24-
- Relationship is optional
25-
- **Example:** `task.assigned_to → user` (task can exist without assignment)
26-
27-
### Use `master_detail` When:
28-
- Child record is meaningless without parent
29-
- Parent deletion should cascade to children
30-
- Relationship is mandatory
31-
- **Example:** `invoice_line_item.invoice_id → invoice` (line items belong to invoice)
32-
3319
## Patterns
3420

3521
### One-to-Many: lookup
@@ -186,6 +172,14 @@ an authored `set_null` is refused at publish (it would orphan the detail row).
186172
}
187173
```
188174

175+
The walk also reads `lookup` and `tree` (`set_null` by default — the field is
176+
cleared; a `multiple` field loses only that member — or an authored `cascade` /
177+
`restrict`), and a `user` field only when it declares a value: deleting the user
178+
then cascades, clears or refuses exactly as a `lookup` does; undeclared, the
179+
field keeps the deleted user's id. `restrict`, and `set_null` on a `required`
180+
field (a `multiple` one only when the removal would empty it), refuse the delete
181+
with `409 DELETE_RESTRICTED`.
182+
189183
## Roll-up Summaries
190184

191185
A parent `summary` field aggregates a child collection. The engine recomputes it

0 commit comments

Comments
 (0)