Skip to content

Commit f506b79

Browse files
docs(adr): ADR-0148 — the recycle bin is a separate store, restore under the original ids, retention in days (#3146 slice 1) (#30263)
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K <!-- claude-projects-attribution: {"github_login":"os-support-ai"} --> _Requested by **Yibo Wang** · [project thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBQ7Z9pv7AYo2NQ7mH38M7VE)_ Fixes #30234 Part of #3146 · Ruling-ref: 6629858351 (maintainer, 2026-11-04, 「同意,企业版功能在 cloud 中立卡后续开发」) Clause-②: no (docs only, an ADR; the protocol addition rides #30235) Metadata-protocol: none Skills-docs: none — docs only and `Metadata-protocol: none`; the two passages of `skills/objectstack-api/SKILL.md` that teach "the trash surface is retired" and "there is no recycle bin" stay true until the engine slice ships, so their rewrite is owed by #30235 / #30237, not by this record Prior rulings read: recycle bin,softdelete,trash,retention → 3 hits; ADR-0006 D1, ADR-0045 Decision §6, ADR-0131 D1; thread: none; repo: objectstack-ai/objectstack ## 维护者速读 - **改了什么**:新增 `docs/adr/0148-recycle-bin-separate-store-restore-under-original-ids-retention-in-days.md`,把你 2026-11-04 在 #3146 上的裁决(回收站独立存储、每次删除一条记录、按原 id 恢复、保留期按天经宿主钩子、企业层放 cloud)记成正式 ADR;ADR-0049 的 `**Status**:` 行末尾追加一条日期注记,说明旧键保持移除;`docs/adr/README.md` 由 `scripts/adr-index.mjs` 重新生成。无代码、无 schema、无生成产物。 - **为什么改**:#3146 拆出的三张子卡(#30235 spec、#30237 engine、#30239 UI)都要引用一份 ADR;ADR-0049 当年因无读者删掉了 `enable.trash` / `softDelete` / `allowRestore` / `allowPurge`,新面必须先把未来的读者逐项点名,否则又是「声明而不执行」。 - **风险与回滚**:仅文档,回滚即 revert 本 PR。记录里每项机制都写成「已裁决、待落地」并点名承载卡,不声称任何已上线行为;ADR-0049 的决策正文一字未动。 - **先看**:① ADR-0148 的 D1(为什么选独立存储而不是 `IsDeleted` 列)、D3(天数与宿主钩子,框架只知天数不知套餐)、D5(新面 → 读者 → 承载卡的表,以及旧键保持墓碑:按 ADR-0087 的 kit 规则,`allowRestore` / `allowPurge` 的 kit 在 18 仍保留、19 退出,`enable.trash` kit 的转换已在 18 切版时移除,两者都不在加载路径上重放)、D7(记录自己定下、裁决未定的几项选择,逐项标明);② ADR-0049 第 3 行状态行末尾的注记;③ 「Where "days, never plans" is written down」一节——仓内没有 ADR 写下这条原则,它写在 `resolveFeatures` 接缝的注释里并引用 cloud ADR-0012。 - **两分钟验证**:在本分支跑 `pnpm check:adr-index && pnpm check:adr-links && pnpm check:adr-symbol-anchors`,预期三条全绿,索引显示 149 条记录、ADR-0148 状态为 Accepted。 - **需要你做的**:`docs/adr/**` 是 Tier H,需要你 Approve(或亲手合并);席位不会排队、不会开 auto-merge。 ## What changes - **New `docs/adr/0148-recycle-bin-separate-store-restore-under-original-ids-retention-in-days.md`** (427 lines). It records the ruling in the ruling's own terms; where it fixes a name the ruling offered as an example or settles something the ruling left open, it says so at that point and lists each such choice once in D7. Status `Accepted — 2026-11-05`, with the sentence "This record declares; it implements nothing": nothing in it ships on `main`, and every mechanism is worded as decided and planned, with its tracking card. - **`docs/adr/0049-no-unenforced-security-properties.md`**: one dated amendment note appended to the end of its `**Status**:` line (the line still starts `**Status**:`; the keyword `Accepted` is unmoved). The note points at ADR-0148 and says the removed keys stay removed — tombstones stand; under ADR-0087's kit rule the `allowRestore` / `allowPurge` kit (`entries/18`, retired after 17.2.0) is carried at 18 and ages out at 19, the `enable.trash` kit's conversion was removed when 18 was cut and only its tombstone remains, neither replays at load (both `retiredFromLoadPath`); `softDelete` never had one. The decision text is untouched. - **`docs/adr/README.md`**: regenerated by `node scripts/adr-index.mjs` (the index is generated, not hand-kept): 148 → 149 records, 143 → 144 numbers, one new row. ### Where each ruling item is recorded | Ruling 6629858351 | ADR-0148 | |---|---| | 1. A separate recycle-bin store, not a `deleted_at` column; one entry per delete operation; the cascade set snapshotted; the delete stays physical; every read path untouched; why not Salesforce's `IsDeleted` | D1 — Shape | | Restore under the original ids; `created_by` / `created_at` kept; its own restored event, never a create; the three refusals (unique taken, parent purged, package uninstalled); lookup re-link only when unchanged since; flow enrollments not restored; rollups recompute; a cascaded child not restored alone; a person restores what they deleted, an admin sees all | D2 — Restore | | 2. Open source owns the mechanism; retention in days, default 30, `OS_RECYCLE_BIN_RETENTION_DAYS`; a host resolver hook on the `resolveFeatures` pattern; days, never plans; TTL purge on the LifecycleService following the `sys_file` tombstone; permanent delete never enters the bin | D3 — Retention | | 3. Cloud sets per-plan retention (Free 7 / Team+ 30) through the hook; the enterprise layer (per-object retention, legal hold, longer retention, audit reports, bulk restore, erasure proof) is a cloud card | D3 and Provenance | | Proposed v1 scope (in / refusals / lookups / forward-only / later); risks (restore fidelity, delete latency, bin storage shown separately, privacy: FLS on listing, secret fields out or encrypted, purge on organization or environment deletion) | D4 — v1 scope, forward-only, later items, and the privacy rules | | "It needs an ADR, or an ADR-0049 amendment, naming the readers that will exist … new keys, not the tombstoned ones" | D5 — the readers table (surface → reader → card: #30235 / #30237 / #30239 / the cloud card) and "The old keys stay retired"; plus the ADR-0049 status note | | What the record fixes that the ruling offered as an example or left open: the names `sys_recycle_bin` and `OS_RECYCLE_BIN_RETENTION_DAYS`; the hook's input, output and validation; the purge deadline stamped on the entry at delete time; the entry stored by the default datasource; secret fields out of the snapshot in v1; the `restored` event's name and the grant keys added to #30235's scope; no object-level authoring key (by rule, not by a flag); the privacy rules as standing rules that bind v1 and everything after it | D7 — each marked as the record's choice where it is made | | What the record leaves to the cards (the remaining spellings, the bulk cap, the Console's wording, the cloud card's shape) | D6 | ### Measurements this record is built on - **Next free ADR number.** `main` (bb86ff9) ends at ADR-0147. Every one of the 34 open PRs was read for a `docs/adr/01NN-*.md` it adds — #29866 (the one open `docs(adr)` PR) amends ADR-0015 and adds no record; the 17 other non-mailbox PRs, the 15 mailbox PRs and the Version Packages PR add none (each head's `docs/adr/` tree listed). 0148 is free. - **ADR-0049's removals**, read from the tombstones rather than the card: `enable.trash` and `softDelete` removed in the 16.x line (#2377; `packages/spec/src/data/object.zod.ts`, which still says "there is no soft-delete/recycle-bin runtime … Deletes are hard deletes"); `allowRestore` / `allowPurge` removed with #12497 (the permission-schema tombstone says `@objectstack/spec` 17, the conversion entry `permission-allow-restore-purge-removed.ts` says protocol 18; the ADR cites the card number and not a version). The card's "removed in 16.0" is therefore exact for two of the four keys; the record reads the code. - **"The framework knows days, never plans (ADR-0012)."** No ADR under `docs/adr/` states it (grep for `resolveFeatures`, "plan-agnostic", "plan name": zero hits). This repository's ADR-0012 is the messaging platform. The principle is written in the docblock of `packages/cloud-connection/src/runtime-config-plugin.ts` ("Feature seam (open-core boundary — cloud ADR-0012)": the open package owns the mechanism; which billing plan unlocks a key is a distribution concern never enumerated in the framework; hosts inject through `resolveFeatures`, which takes an opaque token — cloud passes the plan string — and returns an open-ended map merged verbatim) and in the package's 9.5.0 changelog entry ("plan-agnostic"). The nearest ADR here is ADR-0132 D2. So the ruling's "ADR-0012" is cloud ADR-0012; the record says so in its Provenance and in a section of its own, and cites it as Prime Directive #13 spells a cloud record. - **Code the ruling names**, cited by path: `packages/objectql/src/engine-delete-cascade.ts` (`collectCascadeDeleteSet`, `refuseRestrictsInsideOwnCascade`), `packages/services/service-storage/src/attachment-lifecycle.ts` and `packages/services/service-storage/src/objects/system-file.object.ts` (the `sys_file` tombstone and its 30-day reap), `packages/objectql/src/lifecycle/lifecycle-service.ts` (`LifecycleService`), `packages/objectql/src/engine-secret-internal-fields.ts` (`releaseSecretHandles`, #27581), `packages/plugins/plugin-security/src/permission-evaluator.ts` (`DESTRUCTIVE_OPERATIONS`, `OPERATION_TO_PERMISSION`). No line anchors. ## Why The maintainer unparked #3146 on 2026-11-04 with the recycle-bin shape, and triage split it into four children chained by `Blocked-by:`; this card is the first, and the p1 spec card #30235 cannot start without a record to cite. ADR-0049 removed the earlier soft-delete keys because nothing read them; a new surface under the same policy has to name its readers before it is declared, which is what D5 does. ## Verification - `node scripts/pm/check-prior-rulings.mjs --card 30234 --terms "recycle bin,softDelete,trash,retention"` — the `Prior rulings read:` line above. The three ADR hits are on other senses of the terms (ADR-0006 D1 keeps the word `project`; ADR-0045 §6 is the unpublished-build trash-can discard, which ADR-0148 names as a different surface; ADR-0131 D1 is NULL organization ids). None rules on the recycle bin. - The derived gate list (`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, change set derived from the merge base bb86ff9 of `origin/main`): 21 commands, run serially at head 0d59704 (the second review-fix commit; the same 21 ran at b28128a and 9b45718 with the same codes); the readings are in the details block below and reconciled with `--ran` (21 derived, 21 run, 0 NOT-MEASURED, 0 UNRUN). - `docs/adr/**` is Tier H (Prime Directive #14): this PR stays a draft; the seat reviews and ACCEPTs, and the maintainer lands it by hand. No automerge is armed and none will be. <details> <summary>Derived gates at 0d59704 — 21 commands, every exit code 0 (recorded before any pipe; reconciled: 21 derived, 21 run, 0 NOT-MEASURED, 0 UNRUN)</summary> ```text node scripts/check-adr-links.mjs --self-test :: exit 0 node scripts/check-adr-links.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0 pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0 pnpm check:adr-anchors :: exit 0 pnpm check:adr-index :: exit 0 pnpm check:adr-symbol-anchors :: exit 0 pnpm check:ci-filter-parity :: exit 0 pnpm check:closed-ceremony-text :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:doc-authoring :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:pm-governed-merges :: exit 0 pnpm check:pm-prior-rulings :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:watch-hint-literal :: exit 0 ``` Verdict lines of the three ADR gates, quoted from their own output: ```text check-adr-links: 991 relative link destination(s) under docs/adr/ resolve adr-index --report: OK — docs/adr/README.md is current, every record parsed, self-test 46 assertions green. check-adr-symbol-anchors: 3133 anchors across 150 records resolve — 785 symbol (758 declaration, 27 literal), 2301 file-level, 47 cross-repo, 6 exempt, 34 continuation. ``` `check:doc-formula-expressions` ran after its prerequisite build (`turbo run build --filter=@objectstack/formula --filter=@objectstack/lint` under `os-verify-lock.sh`, VERDICT command-exit 0; 97 s held at b28128a, 5 s at 9b45718 where turbo's cache restored the two dists, which 0d59704 reused unchanged). `pnpm lint` (the repo-wide eslint) is CI's run and was not run locally. </details> ## Acceptance notes - **Observation, not carded:** #3146's body cites `docs/audits/2026-06-objectschema-property-liveness.md`, which is not on `main` (the June 2026 sheets present are `appschema`, `security-identity`, `validationschema` and `record-search`). ADR-0148 cites the audit by its card (#1878) and the `docs/audits/` directory instead of the missing path. A dead citation in an issue body is not one of the three carded classes. - **Observation, not carded:** the card and the ruling say all four keys were "removed in 16.0 under ADR-0049"; the spec's own tombstones date `allowRestore` / `allowPurge` to #12497 (spec 17 / protocol 18). The record reads the tombstones. No metadata author is misled: both spellings are refused with the same prescription. - **Owed elsewhere (Skills-docs line):** `skills/objectstack-api/SKILL.md` teaches "the trash surface is retired" and "there is no recycle bin". Both stay true until #30237 ships; the rewrite rides #30235 / #30237 under their own `Skills-docs:` lines. - **Deviation, reported:** the session's harness asks for a model-named `Co-Authored-By` trailer; the commit carries the repository's model-free pair (`Co-authored-by: Claude` + `Claude-Session:`), which the pre-push hook requires. - **Deviation, reported:** the harness's post-create step (assign the requester, request their review) is skipped on purpose: `.claude/agents/os-dev.md` rule 2 keeps the PR's people with the seat at hand-over, and `pr_create` went out with no assignees and no reviewers. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (No schema moved.) - [x] 2. Every name I added uses the casing for its kind. (`sys_recycle_bin`, `OS_RECYCLE_BIN_RETENTION_DAYS` — named in prose only.) - [x] 3. No formatter ran; `pnpm lint` is CI's run (docs only; no `.ts` moved). - [x] 4. One issue, one branch off an up-to-date `main` (the harness-pinned card-thread branch, at `main`'s bb86ff9), one pull request. - [x] 5. A change that publishes carries a changeset — this one publishes nothing (`docs/adr/**` only), label `skip-changeset`; both declaration lines are above. - [x] 6. `packages/spec` touched: no. - [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 maintainer's rulings are quoted verbatim and untranslated, as AGENTS.md requires). - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01WGufzfC3wLXmeCXwxa963a --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent a93ef3e commit f506b79

3 files changed

Lines changed: 432 additions & 4 deletions

File tree

‎docs/adr/0049-no-unenforced-security-properties.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ADR-0049: Spec must not declare security properties the runtime does not enforce (enforce-or-remove gate)
22

3-
**Status**: Accepted (2026-06-15) — implemented: fail-closed `DESTRUCTIVE_OPERATIONS` (`packages/plugins/plugin-security/src/permission-evaluator.ts#DESTRUCTIVE_OPERATIONS`), lifecycle bits RBAC-gated, `apiEnabled` enforced (`runtime/src/api-exposure.ts`), `PolicySchema` removed, EXPERIMENTAL tag convention live. Two gate-valid disposition deviations: agent access-control shipped experimental-tagged (not enforced), `flow.runAs` kept + enforced (not removed). `action.disabled` CEL enforcement to confirm in objectui. **Amended 2026-09-04** (#14402) — recording that an author-written validation rule's `message` became translatable in 17.3.0 (#14381) under the object-scoped bundle key `objects.<object_name>._validations.<rule_name>.message`. That is this gate being **enforced, not reversed**: the 17.0.0 retirement of the top-level `validationMessages` group stands, and the two are not the same route. See **"Amendment (2026-09-04): validation-rule messages are translatable again — enforced, not reversed"** at the end.
3+
**Status**: Accepted (2026-06-15) — implemented: fail-closed `DESTRUCTIVE_OPERATIONS` (`packages/plugins/plugin-security/src/permission-evaluator.ts#DESTRUCTIVE_OPERATIONS`), lifecycle bits RBAC-gated, `apiEnabled` enforced (`runtime/src/api-exposure.ts`), `PolicySchema` removed, EXPERIMENTAL tag convention live. Two gate-valid disposition deviations: agent access-control shipped experimental-tagged (not enforced), `flow.runAs` kept + enforced (not removed). `action.disabled` CEL enforcement to confirm in objectui. **Amended 2026-09-04** (#14402) — recording that an author-written validation rule's `message` became translatable in 17.3.0 (#14381) under the object-scoped bundle key `objects.<object_name>._validations.<rule_name>.message`. That is this gate being **enforced, not reversed**: the 17.0.0 retirement of the top-level `validationMessages` group stands, and the two are not the same route. See **"Amendment (2026-09-04): validation-rule messages are translatable again — enforced, not reversed"** at the end. **Amended 2026-11-05** ([ADR-0148](./0148-recycle-bin-separate-store-restore-under-original-ids-retention-in-days.md), #30234, ruling record 6629858351 on #3146) — the recycle bin returns as a **new** surface whose readers that record names (a separate `sys_recycle_bin` store, restore under the original ids, retention in days through a host hook); the keys this gate removed — `enable.trash` and `softDelete` (16.x, #2377), `allowRestore` and `allowPurge` (#12497) — **stay removed**: their tombstones stand; under ADR-0087's kit rule the `allowRestore` / `allowPurge` kit (`packages/spec/src/conversions/entries/18/permission-allow-restore-purge-removed.ts`, retired after 17.2.0) is carried at 18 and ages out at 19, while the `enable.trash` kit's conversion (`object-enable-trash-mru-removed`, 16.x) was removed when 18 was cut and only its tombstone remains; neither conversion replays at load (both `retiredFromLoadPath`, `packages/spec/src/conversions/retired-after.census.json`); `softDelete` never had one; and none returns under its old spelling. The gate is enforced, not reversed; this decision text is unchanged.
44
**Deciders**: ObjectStack Protocol Architects
55
**Builds on**: [ADR-0005](./0005-metadata-customization-overlay.md) (artifact vs runtime overlay), [ADR-0010](./0010-metadata-protection-model.md) (package provenance), [ADR-0027](./0027-metadata-authoring-lifecycle.md) (authoring lifecycle)
66
**Consumers**: `@objectstack/spec` (security/identity schemas), `@objectstack/plugin-security` (`PermissionEvaluator`, `SecurityPlugin`), spec authors, the metadata-property liveness audit follow-ups (#1878 P0 cluster).

0 commit comments

Comments
 (0)