Repository navigation
Commit d9d42d4
feat(spec): declare the recycle-bin surface: the sys_recycle_bin entry contract, the retention hook, the restore refusals, the restored event and two grant keys (ADR-0148) (#30577)
<!-- claude-projects-attribution: {"github_login":"os-sales"} -->
_Requested by **Sales** · [project
thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4Uu8iKZzw1r2XCrNituG3deMH)_
Fixes #30235
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: follow-up #30237 — `skills/objectstack-api/SKILL.md`
teaches that the trash surface is retired and that there is no recycle
bin (the `apiMethods` section and the delete note); both stay true after
this PR, which declares and reads nothing, so the rewrite rides the
engine slice that ships the reader.
## 维护者速读
- 改了什么:在 `@objectstack/spec` 里声明回收站的契约:`sys_recycle_bin`
条目的字段表与值形状、宿主保留期钩子(只认天数)、三个恢复拒绝码、`data.record.restored`
事件名、两个对象权限键;运行时一行未动。
- 为什么改:ADR-0148 D5 把这些拼写交给本卡,引擎(#30237)和控制台(#30239)要在它上面建;没有它,删除仍然不可恢复。
- 风险与回滚:纯新增,可选键无默认值,已有元数据和已解析的权限条目不变;回滚就是 revert 本 PR。
-
先看:`packages/spec/src/data/recycle-bin.zod.ts`(钩子签名与拒绝文案)、`packages/spec/src/security/permission.zod.ts`(两个新键)、`packages/spec/src/api/error-code-ledger.zod.ts`(三个拒绝码先于生产者登记)。
- 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run --project local
src/data/recycle-bin.zod.test.ts`,预期 1 个文件、16 个用例全过。
- 需要你做的:无,席位落地。
## What changes
The spec half of the recycle bin
([ADR-0148](docs/adr/0148-recycle-bin-separate-store-restore-under-original-ids-retention-in-days.md)
D5, Rollout step 2; ruling 6629858351 on #3146, D7's eight choices
ratified 6659364907 on #30234). Every name below is **declared here and
read elsewhere**: nothing in this PR writes or reads the bin, and every
delete is still a physical delete.
- **`packages/spec/src/data/recycle-bin.zod.ts`** (new, exported from
`@objectstack/spec/data`):
- `RECYCLE_BIN_OBJECT` = `'sys_recycle_bin'` and
`RECYCLE_BIN_ENTRY_FIELDS`, the entry's one field list (`id`,
`object_name`, `record_id`, `record_count`, `snapshot`, `deleted_by`,
`deleted_at`, `retention_days`, `purge_after`), all read-only;
`RecycleBinEntrySchema` is its value shape, key for key. The object
file, its plugin registration and its `PLATFORM_OBJECTS_BY_PACKAGE` row
are the engine slice's (seat decision 2): that registry means "a table
exists".
- `RecycleBinSnapshotSchema` (with `RecycleBinSnapshotRowSchema`,
`RecycleBinClearedReferenceSchema`): the cascade set, master first,
secret-channel refs left out (D4), plus the `set_null` references the
delete cleared (D2's re-link rule).
- The retention hook (D3): `RecycleBinRetentionResolver = (token: string
| undefined) => number | undefined`, the same token type
`resolveFeatures` takes in
`packages/cloud-connection/src/runtime-config-plugin.ts`;
`RECYCLE_BIN_RETENTION_DAYS_ENV` = `'OS_RECYCLE_BIN_RETENTION_DAYS'`,
`RECYCLE_BIN_DEFAULT_RETENTION_DAYS` = 30;
`RecycleBinRetentionDaysSchema` / `RecycleBinRetentionDaysEnvSchema`
refuse zero and every non-whole-number with
`recycleBinRetentionRefusal(source, value)`, which names the source and
the fix and no plan.
- `RECYCLE_BIN_RESTORE_REFUSAL_CODES` (three codes) and
`RECYCLE_BIN_RESTORE_REFUSAL_STATUS` = 409.
- `RECORD_RESTORED_EVENT` = `'data.record.restored'`, a spec constant
only (seat decision 1): `DataEventType` / `RealtimeEventType` list
emitted events, so it joins them in the change that emits it.
- `RECYCLE_BIN_GRANT_KEYS`.
- **Error-code ledger**
(`packages/spec/src/api/error-code-ledger.zod.ts`):
`RESTORE_UNIQUE_VALUE_TAKEN`, `RESTORE_PARENT_PURGED`,
`RESTORE_PACKAGE_UNINSTALLED` under `@objectstack/objectql`, ahead of
their producer (seat decision 4).
- **Object permissions**
(`packages/spec/src/security/permission.zod.ts`):
`allowRestoreOwnDeleted` and `manageRecycleBin`, optional with no
default, so no parsed entry grows a key. Both enter
`liveness/permission.json` as `planned`, naming their readers (#30237
evaluator mapping and engine doors, #30239 Console) (seat decision 3).
The tombstoned `allowRestore` / `allowPurge` stay refused.
- **Regenerated artifacts**: api-surface, export-origins,
declaration-map, authorable-surface, json-schema manifest,
`content/docs/references/**`, the strictness-ledger counts, liveness
state counts, `llms.txt`, and the type-alias pin (+6 isomorphic pins,
831 → 837).
- **Changeset**: `@objectstack/spec` minor.
⛔ Not here: the engine's delete path, restore, purge, empty, the TTL
reap, the permission mapping, the REST doors (#30237); the Console
(#30239); any tombstoned key (`enable.trash`, `softDelete`,
`allowRestore`, `allowPurge`, the retired `apiMethods` values).
## Why
A deleted record is gone for good today. ADR-0148 rules a separate bin
store and leaves the spellings to this card (D6); the engine and Console
slices build on them, and ADR-0049's enforce-or-remove gate is passed by
naming each reader in its liveness row.
## Tests
All readings at head `25af14cf`, after `origin/main` `c23eafa8` was
merged in and the build state refreshed.
- `packages/spec/src/data/recycle-bin.zod.test.ts` (new, 16 cases): the
three names; the hook takes the `resolveFeatures` token and answers days
or nothing; `RecycleBinRetentionDaysSchema` accepts 1/7/30/365 and
refuses `0, -1, 1.5, NaN, Infinity, null, '30', true` with the refusal
text; the env schema accepts `'1' '30' '365'` and refuses `'0' '030'
'-7' '7.5' ' 7' 'thirty' ''`; the refusal names the source and the
remedy and no plan; the snapshot round-trips and refuses zero rows; the
field contract (types, required, read-only, `deleted_by` → `sys_user`)
builds a valid `ObjectSchema` under `sys_recycle_bin`;
`RecycleBinEntrySchema` matches the field keys one for one; the refusal
codes are registered and answer 409; `data.record.restored` stays out of
`DataEventType` / `RealtimeEventType`; the two grant keys parse, are
never defaulted, and the retired `allowRestore` / `allowPurge` stay
refused.
- `packages/spec/src/api/error-code-ledger.test.ts`: the three codes
parse as `ErrorCode`, sit under `@objectstack/objectql`, and need no
standard-synonym waiver.
- `packages/spec/src/type-alias-convention.pin.test.ts`: +6 isomorphic
pins (831 → 837).
<details><summary>Commands and results</summary>
- `pnpm --filter @objectstack/spec build` · VERDICT command-exit 0
- `pnpm --filter @objectstack/spec check:generated` · exit 0 · all 14
generated artifacts up to date
- `pnpm --filter @objectstack/spec test` (vitest `--project local`) ·
VERDICT command-exit 0 · Test Files 816 passed (816) · Tests 25660
passed, 1 todo
- `pnpm --filter @objectstack/spec typecheck` (tsc, scripts typecheck,
test typecheck) · VERDICT command-exit 0
- `turbo run build --filter=!@objectstack/docs` · 119 successful, 119
total
- Downstream consumers (`...@objectstack/spec` direction), the three the
diff's widened types reach (object permissions, the error-code union):
`pnpm --filter @objectstack/plugin-security typecheck` ·
`@objectstack/core typecheck` · `@objectstack/objectql typecheck` · each
VERDICT command-exit 0
- `pnpm --filter @objectstack/plugin-security test` · Test Files 244
passed (244) · Tests 5379 passed, 45 skipped
- `@objectstack/core` `vitest run src/security` · Test Files 63 passed
(63) · Tests 1221 passed
- `@objectstack/lint` `vitest run src/validate-security-posture.test.ts
src/validate-retired-permission-residue.test.ts` · Test Files 2 passed ·
Tests 209 passed
</details>
No ablation: the diff changes no tested runtime behaviour (declarations
only). No dogfood battery: no `!` in the title and no `(narrowing)` arm.
## Acceptance notes
- **Resumed work, re-verified.** The previous card thread pushed five
commits to `claude/project-thread-jpl1gr` (last `884764a2`). Each hunk
was re-read against ADR-0148 and the seat's four decisions; all are
kept. One correction: the changeset's `Clause-②:` line carried a
free-text arm, and now reads `yes (widening)`, the claim's form
(`25af14cf`). The net diff touches no file outside `packages/spec`,
`content/docs/references/**`,
`docs/audits/2026-07-unknown-key-strictness-ledger*` and `.changeset/`;
the object file the first commit wrote under `platform-objects` was
moved out by the predecessor itself (seat decision 2).
- **Why the grants are object-permission keys.** ADR-0148 D5 says
`OPERATION_TO_PERMISSION` in
`packages/plugins/plugin-security/src/permission-evaluator.ts` must map
`restore` / `purge` to the new grants; that map targets
object-permission keys, so both keys live on the per-object entry.
`manageRecycleBin` is bin administration on that object; emptying purges
every entry the holder manages.
- **What the readers must add (the engine and Console cards, not this
PR).** `EffectiveObjectPermission` (the `/auth/me/permissions` shape)
gains both optional keys because it derives from the same base; nothing
fills them yet. `GRANT_BITS` in
`packages/core/src/security/effective-object-permissions.ts`,
`ObjectPermissionVerbTarget` / `objectPermissionGrants` in
`packages/spec/src/security/permission.zod.ts`, and `CAPS` in
`ui/packages/app-shell/src/views/metadata-admin/previews/PermissionPreview.tsx`
list neither key today. Carrier: #30237 (merge, fold, evaluator map,
flipping both liveness rows to `live`) and #30239 (the Console columns).
- **Strictness.** The four object sites in `recycle-bin.zod.ts` are
`z.object` and triaged `wire` in the strictness ledger: persisted
runtime state the engine writes and reads, never authored; the
snapshot's `values` carry whatever fields the deleted rows had.
- **No ADR anchor file.** ADR-0148's id sits in the code at each
declaration; no `scripts/adr-anchors/` entry was added, because the
claim's file surface does not name one. The seat may ask for it.
- **Timestamps.** `deleted_at` / `purge_after` accept ISO 8601 with an
offset (`datetime({ offset: true })`), a little wider than the
repository's common `datetime()` form, so an engine writing a local
offset is not refused.
## Verification
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at head `25af14cf` (29 paths vs merge base
`c23eafa8`, +852 / -18) derived 106 commands. All 106 ran serially on
that head after the full build, each exit code captured before any pipe:
**106 × exit 0**. `--ran` reconciliation: 106 derived, 106 run, 0
NOT-MEASURED, 0 UNRUN (a derived zero). `pnpm lint` and the rest of the
CI farm are CI's run; the PR's check-runs are the record for them.
<details><summary>The 106 derived commands, all exit 0</summary>
- `check-adr-0087-registration --base origin/main` (no declared-breaking
changeset; 1 non-breaking seen) and `--self-test`;
`check-changeset-no-major --base origin/main` (no `major` bump; protocol
lockstep 18 / 18.3.0; re-run offline with this body as the `--event`
payload: LEVEL AXIS ✓, `yes (widening)` + `added`, no moved package
graded `patch`); `check-empty-changeset`; `check-issue-citations` (13
citations resolve on the board); `check-adr-links`;
`check-closing-keyword-parity`; `check-comment-mask-adoption`,
`check-comment-mask-corpus`; `check-doc-route-spelling --advisory`;
`check-docs-section-name`; `check-section-landing-index`;
`check-plugin-teardown-shape` (fixture `621a4876` fetched for the
self-test); `check-dev-prereqs`, `check-dts-emitted`,
`assert-turbo-remote-cache`, `plan-test-core-shards`,
`prune-turbo-cache`, `release-rehearsal-clone`,
`release-pending-publish` self-tests; `docs-audit/check-affected-docs`,
`check-drift-comment`.
- `@objectstack/spec`: `check:api-surface`, `check:authorable-surface`,
`check:browser-reachable-entries`, `check:docs`,
`check:dual-source-exports`, `check:duration-unit-keys`,
`check:empty-state`, `check:entry-nameability` (626 probes, 0 new
unnameable mentions), `check:error-code-provenance`,
`check:export-origins`, `check:exported-any`, `check:generated`,
`check:liveness`, `check:llms-txt`, `check:skill-examples`,
`check:skill-refs`, `check:strictness-ledger`, `check:variant-docs`,
`check:yaml-examples`.
- `@objectstack/lint`: `check:doc-formula-expressions`,
`check:doc-security-posture`.
- Root: `check:changeset-gate-self-tests`, `ci-filter-parity`,
`closed-ceremony-text`, `corpus-claim-drift`,
`cross-package-test-inputs`, `dispatcher-error-vocabulary`,
`doc-anchors`, `doc-authoring`, `doc-frontmatter`, `docs-audit-scope`,
`docs-redirects`, `docs-single-h1`, `docs-spec-enumerations`,
`docs-transcript-drift`, `driver-memory-census`, `dts-closure`,
`dual-build-cjs-loads`, `engine-double-contract`, `error-code-casing`,
`error-status-conformance`, `gitlink-declared`, `issue-citations`,
`keyed-text-bounds`, `lean-entry-closure`, `logger-receiver-detach`,
`merge-driver`, `nul-bytes`, `objectql-double-limit`, `org-identifier`,
`page-declaration-shape`, `platform-checklist`,
`platform-object-tenancy-census`, `pm-prior-rulings`, `published-files`,
`published-readme-links`, `query-options-erasure`,
`react-page-adapter-contract`, `refd-timer-probe`,
`registry-log-declared`, `rest-log-spy-declared`, `role-word`,
`skill-identifier-liveness`, `slot-lookup`,
`sourcemap-no-sources-content`, `spec-docblock-symbol-anchors`,
`spec-parsed-alias`, `system-context-census`, `test-source-alias`,
`tier-file-adoption`, `type-check-coverage`, `type-check-debt`,
`undeclared-dep-imports`, `vendor-version-stamps`, `watch-hint-literal`,
`where-matcher`.
</details>
Branch: `claude/project-thread-uwrsn3`, the card thread's harness-pinned
branch (a measured platform exception to `claude/issue-30235-…`),
resumed from `claude/project-thread-jpl1gr@884764a` by merge, never
rebase; every picked-up hunk was re-read and every check re-run on this
head.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed.
- [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.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01L6bhgWAJDEGE4g7p29M3zq)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01L6bhgWAJDEGE4g7p29M3zq
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent bdbaf80 commit d9d42d4
29 files changed
Lines changed: 852 additions & 18 deletions
File tree
- .changeset
- content/docs/references
- api
- data
- security
- docs/audits
- 2026-07-unknown-key-strictness-ledger.counts
- packages/spec
- api-surface
- authorable-surface
- declaration-map
- export-origins
- json-schema.manifest
- liveness
- state-counts
- src
- api
- data
- security
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
110 | 110 | | |
111 | 111 | | |
112 | 112 | | |
| 113 | + | |
| 114 | + | |
113 | 115 | | |
114 | 116 | | |
115 | 117 | | |
| |||
171 | 173 | | |
172 | 174 | | |
173 | 175 | | |
| 176 | + | |
| 177 | + | |
174 | 178 | | |
175 | 179 | | |
176 | 180 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
417 | 417 | | |
418 | 418 | | |
419 | 419 | | |
420 | | - | |
| 420 | + | |
421 | 421 | | |
422 | 422 | | |
423 | 423 | | |
| |||
705 | 705 | | |
706 | 706 | | |
707 | 707 | | |
| 708 | + | |
| 709 | + | |
| 710 | + | |
708 | 711 | | |
709 | 712 | | |
710 | 713 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
31 | | - | |
| 31 | + | |
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
| |||
323 | 323 | | |
324 | 324 | | |
325 | 325 | | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
326 | 329 | | |
327 | 330 | | |
328 | 331 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
490 | 490 | | |
491 | 491 | | |
492 | 492 | | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
493 | 496 | | |
494 | 497 | | |
495 | 498 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1197 | 1197 | | |
1198 | 1198 | | |
1199 | 1199 | | |
| 1200 | + | |
| 1201 | + | |
1200 | 1202 | | |
1201 | 1203 | | |
1202 | 1204 | | |
| |||
1658 | 1660 | | |
1659 | 1661 | | |
1660 | 1662 | | |
| 1663 | + | |
| 1664 | + | |
1661 | 1665 | | |
1662 | 1666 | | |
1663 | 1667 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
| 39 | + | |
39 | 40 | | |
40 | 41 | | |
41 | 42 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
| 39 | + | |
39 | 40 | | |
40 | 41 | | |
41 | 42 | | |
0 commit comments