Skip to content

Commit 56bd596

Browse files
docs(skills): objectstack-data rules/lifecycle.md teaches the save notice (#28813)
<!-- claude-projects-attribution: {"github_login":"marchtian"} --> _Requested by **田騏** · [project thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSiHXKqxUqDPJKZyNHP7BoqTb)_ Fixes #28253 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:`objectstack-data` 技能的 `rules/lifecycle.md` 新增「Save Notice」一节:保存钩子用 `ctx.notice('…')` 告诉保存者这次保存触发了什么(inline `handler` 与沙箱 `body` 同一拼法、不需要能力声明),记录触发的 flow 用 `save_notice` 节点设同一条消息;info 级、一次保存一条、后设者胜、与校验 `warnings` 和 `record:alert` 横幅的区别。另附 changeset(`@objectstack/skills` patch)。 - **为什么改**:运行时早已支持(hook 面与 flow 面都已合入主干),但技能里没教,AI 作者写保存钩子时仍去拼 `record:alert` 横幅来「告诉用户」;这是 #28000 的 Skills-docs 欠账。 - **风险与回滚**:纯文档,不改 schema、不改运行时;整文件 token 不超原天花板(1590 → 1587)。回滚即 revert 本 PR。 - **先看**:`skills/objectstack-data/rules/lifecycle.md` 末尾新节(主要 hunk),以及被删去的「Decision Tree」一节(用它付 token 账)。 - **两分钟验证**:`node scripts/check-skills-token-ratchet.mjs`,预期看到 `rules/lifecycle.md is 1587 tokens (ceiling 1590; headroom 3)` 且整体 `✓`。 - **需要你做的**:`skills/**` 为受管面(Tier H),需要授权账号的 APPROVED 评审后由席位落地。 ## What changes `skills/objectstack-data/rules/lifecycle.md` gains **Save Notice — what the save set in motion**, teaching what the live code does: - A create/update hook calls `ctx.notice('Sent to your manager for approval')` — inline `handler` and sandboxed `body` alike, no capability (`HookContext.notice`, bound by the engine on every insert/update dispatch; the body runner marshals it as a host function). - The create/update answer carries it as `notice` and the console shows it on the save toast. Info severity: an outcome, never a problem — distinct from validation `warnings`, and from a `record:alert` banner (page metadata shown by the record's state on every visit); the notice is for what *this* save started. - One message per write, the last call wins, never stored; insert/update dispatches only; a write nobody waits on (batch, seed, import, a nested write) sets nothing; a non-string refuses the write; a refused write answers its error, no notice. - A flow a record create/update trigger starts sets the same channel with a `save_notice` node (`config.message`, `{{ }}` renders); refused in any other flow, and after an `approval` / `wait`. The intro's "not to be confused with lifecycle hooks" sentence now says the page also carries the save notice. **Paid inside the file's token ceiling** (no ceiling moves), by deleting restating prose in the same file: - the **Decision Tree** section — items 1–5 restated the Lifecycle Classes table (class, contract, typical use) and the four Syntax examples; item 6 restated the MIXED syntax example's comment and the fifth Validation rule (`onlyWhen` + rotation/archive); - the telemetry-growth anecdote in *Why This Exists* (an incident narrative; the rule sentence that follows it stays); - "Nothing is deleted." in the first Safety Semantics bullet, which restated *Why This Exists* ("rows are never deleted"). `.changeset/28253-skill-save-notice.md` — `@objectstack/skills: patch` (the skill ships in that package's `dist`). **Skills readings** (`skills/**` diff, two readings each): | Reading | before | after | |:--|:--|:--| | `rules/lifecycle.md` lines | 156 | 161 | | `rules/lifecycle.md` bytes / tokens (`ceil(bytes/4)`) | 6360 / 1590 (ceiling 1590, headroom 0) | 6347 / 1587 (ceiling 1590, headroom 3) | | package: sum of all 11 `skills/*/SKILL.md` lines | 4561 | 4561 (no SKILL.md touched) | | bundle total (whole shipped tree, tokens) | 222242 | 222239 | Not touched: `skills/objectstack-data/SKILL.md` (PR #28795 is open on it). Its rule index entry for `rules/lifecycle.md` (line 45) still describes the page as retention/TTL/rotation/archive only and does not name the save notice; if the index must name the new topic, that is a follow-up on that file once #28795 lands. ## Tests Docs-only diff in a published skill package: no test file is owed. The gate family that measures the page is the token ratchet, run on the final head: - `node scripts/check-skills-token-ratchet.mjs` → exit 0 — `rules/lifecycle.md is 1587 tokens (ceiling 1590; headroom 3)`; `57 authored bundle file(s) within their ceilings`. ## Acceptance notes - Every sentence in the new section is taken from the live code, not from the card's summary: `packages/objectql/src/hook-notice.ts` (binding, the non-string refusal, the no-listener no-op), `packages/spec/src/data/hook.zod.ts` (`HookContext.notice`, handler and body alike, no capability), `packages/runtime/src/sandbox/body-runner.ts` and `quickjs-runner.ts` (the sandbox marshal), `packages/spec/src/contracts/data-engine.ts` (last call wins, refused write answers its error, nothing stored), `packages/spec/src/api/protocol.zod.ts` (the answer's `notice`, info severity, distinct from `warnings`), `ui/packages/react/src/record-save-feedback.ts` (the save toast), `packages/spec/src/automation/save-notice-placement.ts` and `packages/services/service-automation/src/builtin/save-notice-node.ts` (the flow node and its refusals). - Noted, not filed (no defect class): `skills/objectstack-data/references/data-hooks.md` — the sandbox `ctx.*` table and the `HookContext` interface listing carry no `ctx.notice` row; that file sits at 2 tokens of headroom, so a row there must be paid for by deletion in that file. Carrier: none. - No label was sent with the create: the dispatch named none, and `skip-changeset` does not apply (a changeset is included). ## Verification Gate derivation on the final head (`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, no paths, change set derived from git: 2 files, +34 / −18): 43 families derived; every command recorded with its exit code captured before any pipe, then reconciled with `--ran`. - First sweep at head `63afc09f2d` (install only, no `dist`): 38 families exit 0; 5 exit 3 (`PREREQUISITE NOT MET`, no built `dist`): `check:doc-formula-expressions` (lint), `check:dts-closure`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:sourcemap-no-sources-content`. - `pnpm build --concurrency=2` under `scripts/pm/os-verify-lock.sh` (`VERDICT command-exit 0`, 116/117 tasks replayed from the shared turbo cache), then the five re-run on the same head: all exit 0. - `node scripts/pm/dispatch-gates.mjs --ran` over the merged record: `43 derived famil(ies) accounted for — 43 run, 0 NOT-MEASURED, 0 UNRUN` (exit 0). - Among them, every skills family the derivation names: `check:skills-token-ratchet` (+ `--self-test`), `check:skill-frame-sync`, `check:skill-identifier-liveness`, `check:skill-compatibility`, `check:doc-authoring` (the no-tracker-id rule over `skills/**`), `check:pm-skill-id-lint`, `check:role-word`, `check:corpus-claim-drift`, `check:published-files`, the changeset gates (`check-empty-changeset`, `check-changeset-no-major`, `check-adr-0087-registration`) — all exit 0. - Local-only families not derived and not run: `check:dev-prereqs`, `check:skill-frame-freshness` (the diff touches no frame file and no `packages/spec` source). - CI families (`Lint & Repo Gates`, the type-check lanes and the rest) are the PR's check-runs; nothing there is re-run locally. Dogfood battery: n/a — no `!` in the title and no `(narrowing)` arm. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7107bde commit 56bd596

2 files changed

Lines changed: 35 additions & 19 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): objectstack-data rules/lifecycle.md teaches the save notice
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** A hook can tell the person who made a create or update what the save set in motion (`ctx.notice(message)`, answered as the create/update `notice`), and a flow a record create/update trigger starts can set the same message with a `save_notice` node, but no published skill taught it, so an AI author writing a save hook from `objectstack-data` kept reaching for a `record:alert` banner.
11+
- **Now.** `objectstack-data/rules/lifecycle.md` gains *Save Notice — what the save set in motion*: the `ctx.notice` spelling for inline `handler` and sandboxed `body` alike (no capability), info severity and the distinction from validation `warnings` and from a `record:alert` banner, one message per write with the last call winning, the dispatches that carry it, the non-string refusal, and the flow face (`save_notice`, refused outside a create/update record trigger and after an `approval` / `wait`).
12+
- The file stays under its token ceiling: the new text is paid for by deleting the Decision Tree (which restated the class table and the syntax examples), the telemetry-growth anecdote and a Safety Semantics restatement. No ceiling moves; no schema, key or behaviour changes.

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

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
# Data Lifecycle & Retention
22

33
Guide for declaring how long an object's data lives and how its space is
4-
reclaimed (ADR-0057 — system data lifecycle & retention). Not to be
5-
confused with **lifecycle hooks** (`beforeInsert` / `afterUpdate` …) —
6-
those run business logic on data operations; *this* page is about
7-
retention, rotation, and archival of the rows themselves.
4+
reclaimed (ADR-0057 — system data lifecycle & retention). Distinct from
5+
**lifecycle hooks** (`beforeInsert` / `afterUpdate` …), which run logic on
6+
writes: this page covers the rows' retention, rotation and archival
7+
— and, at its end, the save notice a hook hands the saver.
88

99
## Why This Exists
1010

1111
Every object without a `lifecycle` block is **immortal**: rows are never
1212
deleted, and on SQLite the file never shrinks. That's correct for business
13-
records — and a guaranteed disk leak for anything append-only. A scheduled
14-
flow writing telemetry every 20 seconds once grew a dev database from 2 MB
15-
to 260 MB with zero business data in it.
13+
records — and a guaranteed disk leak for anything append-only.
1614

1715
**Rule: any append-only, high-write-rate object (event log, run history,
1816
delivery outbox, ephemeral tokens) MUST declare a `lifecycle` block.**
@@ -119,8 +117,7 @@ lifecycle: {
119117

120118
## Safety Semantics
121119

122-
- **No `lifecycle` block = today's behavior.** Nothing is deleted. `record`
123-
is the implicit default.
120+
- **No `lifecycle` block = today's behavior**: `record` is the implicit default.
124121
- **Archive-then-delete is atomic-ish and safe by default**: an object
125122
declaring `archive` is never hot-deleted before the copy to the archive
126123
datasource succeeded. If no datasource is registered under the `archive.to`
@@ -144,13 +141,20 @@ lifecycle: {
144141
| `growth_alert_rows` | settings | Per-sweep growth spike alert |
145142
| `OS_LIFECYCLE_DISABLED=1` | env | Disables sweeping entirely |
146143

147-
## Decision Tree
148-
149-
1. Users create/edit it and it represents business state? → `record` (omit the block).
150-
2. Is it a compliance/audit trail? → `audit` + `retention`, add `archive` if cold storage exists.
151-
3. Written by machines at high frequency, value decays in days/weeks? → `telemetry` + `retention` (add rotation `storage` for the hottest tables).
152-
4. Meaningless after a deadline (tokens, receipts, read-state)? → `transient` + `ttl` on the natural expiry field.
153-
5. Bus/fan-out messages? → `event` + a short `ttl` (hours).
154-
6. Table mixes prunable history with live state (e.g. terminal vs paused runs)?
155-
→ add `retention.onlyWhen: { status: { $in: [...] } }` so rows outside the
156-
filter are retained regardless of age. Not combinable with rotation/archive.
144+
## Save Notice
145+
146+
A create/update hook calls `ctx.notice('Sent to your manager for approval')`
147+
— a sandboxed `body` or the deprecated inline `handler`, no capability. The
148+
answer carries it as `notice`; the console shows it on the save toast. **Info
149+
severity**: an outcome, never a problem — distinct from validation
150+
`warnings`. A `record:alert` banner is page metadata shown whenever the
151+
record's state says so; the notice is for what *this* save started.
152+
153+
- One message per write, **the last call wins**; never stored.
154+
- Insert/update dispatches only. A write nobody waits on (batch, seed,
155+
import, a nested write) sets nothing.
156+
- Not a non-empty string ⇒ the write refuses; a refused write answers its
157+
error, no notice.
158+
- A flow a record create/update trigger starts sets it with a `save_notice`
159+
node (`config.message`, `{{ }}` renders); refused in any other flow, and
160+
after an `approval` / `wait`.

0 commit comments

Comments
 (0)