Repository navigation
Commit f506b79
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
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| |||
0 commit comments