Skip to content

docs(adr): ADR-0148 — the recycle bin is a separate store, restore under the original ids, retention in days (#3146 slice 1) - #30263

Open
objectstack-fleet[bot] wants to merge 3 commits into
mainfrom
claude/project-thread-cxrsk5
Open

objectstack-fleet[bot] wants to merge 3 commits into
mainfrom
claude/project-thread-cxrsk5

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Requested by Yibo Wang · project thread

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

维护者速读

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 — docs(adr): under a walled tenancy posture every external object states its organization scope (ADR-0015 §10) #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 ([11.0][A2] Remove dead author-facing metadata properties (ADR-0049 enforce-or-remove) #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 spec: retire the allowRestore / allowPurge permission props (ruled 2026-08-26; M2 anchor stays open, keys return with M2) #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 [WIP] Add Chinese version of the documentation #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, objectql: deleting a record does not release the sys_secret row its secret field names #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 feat: Comprehensive CRM example demonstrating all ObjectStack protocol features #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.
Derived gates at 0d59704 — 21 commands, every exit code 0 (recorded before any pipe; reconciled: 21 derived, 21 run, 0 NOT-MEASURED, 0 UNRUN)
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:

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.

Acceptance notes

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (No schema moved.)
  • 2. Every name I added uses the casing for its kind. (sys_recycle_bin, OS_RECYCLE_BIN_RETENTION_DAYS — named in prose only.)
  • 3. No formatter ran; pnpm lint is CI's run (docs only; no .ts moved).
  • 4. One issue, one branch off an up-to-date main (the harness-pinned card-thread branch, at main's bb86ff9), one pull request.
  • 5. A change that publishes carries a changeset — this one publishes nothing (docs/adr/** only), label skip-changeset; both declaration lines are above.
  • 6. packages/spec touched: no.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the maintainer's rulings are quoted verbatim and untranslated, as AGENTS.md requires).
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzfC3wLXmeCXwxa963a

…ore under the original ids, retention in days

The maintainer ruled the recycle-bin shape on #3146 (ruling record 6629858351): one
sys_recycle_bin entry per delete operation with the cascade set snapshotted, the delete
staying physical and every read path untouched; restore under the original ids with
three named refusals; retention as days (default 30, OS_RECYCLE_BIN_RETENTION_DAYS)
through a host resolver hook on the resolveFeatures pattern, so the framework knows
days and never plans. The record names, for every new surface, the reader that will
enforce it and the card that lands it, and says the keys ADR-0049 removed stay removed.

ADR-0049 gains one dated note on its Status line; its decision text is unchanged.
docs/adr/README.md is regenerated by scripts/adr-index.mjs.

Claude-Session: https://claude.ai/code/session_01WGufzfC3wLXmeCXwxa963a
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Nov 5, 2026
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation labels Nov 5, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Contract-review-claim: session_01WGufzfgvGexKn9vLHsy89K · head b28128a · 2026-11-05T04:10Z

Full review of the first head at CONTRACT_REVIEW_TIER (an ADR, Tier H: the recycle bin ruled on #3146).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b28128a568b637abbab954f48f6305f3361ddee5
Local-runs: none
Scope: docs/adr/0148-recycle-bin-separate-store-restore-under-original-ids-retention-in-days.md (all 368 lines), the diff of docs/adr/0049-no-unenforced-security-properties.md and docs/adr/README.md against merge base bb86ff9; ADR-0049 and ADR-0012 at main in full; AGENTS.md (Prime Directives 9, 13, 14); card #30234 and its 3 comments; parent #3146 body and all 4 comments (ruling 6629858351, triage 6631469646, split 6648418282); children #30235, #30237, #30239; PR #30263 body, 1 comment, check-runs; at main: scripts/adr-index.mjs, packages/cloud-connection/src/runtime-config-plugin.ts (docblock 205-223), packages/cloud-connection/CHANGELOG.md 9.5.0, packages/objectql/src/engine-delete-cascade.ts, engine-secret-internal-fields.ts, lifecycle/lifecycle-service.ts, engine-middleware-operation-vocabulary.test.ts, packages/services/service-storage/src/attachment-lifecycle.ts, packages/plugins/plugin-security/src/permission-evaluator.ts, packages/spec/src/data/object.zod.ts, field.zod.ts, api-derivation.ts, packages/spec/src/security/permission.zod.ts, packages/spec/src/conversions/ (entries tree and retired-after.census.json); issues #27581 and #12497.

① Derived judgments

  • Fidelity: every ruled item of 6629858351 is present and matched to a section (D1 shape and the verbatim read-path list; D2 restore under original ids, created_by/created_at kept, own restored event, the three refusals, lookup re-link only when unchanged, enrollments not restored, rollups recompute, cascaded child not alone; D3 days default 30 via OS_RECYCLE_BIN_RETENTION_DAYS and a resolver hook, OSS mechanism list, cloud Free 7 / Team+ 30 and the enterprise items; D4 v1 list, forward-only with release notes, later items, risks and the four privacy rules). Nothing dropped or softened.
  • Shipped vs planned: L6 "This record declares; it implements nothing"; every D5 row carries its card; nothing is claimed shipped. Cited symbols are real at main and say what is claimed: collectCascadeDeleteSet / refuseRestrictsInsideOwnCascade (engine-delete-cascade.ts:6-8), releaseSecretHandles + objectql: deleting a record does not release the sys_secret row its secret field names #27581 (engine-secret-internal-fields.ts:21-24), LifecycleService (lifecycle-service.ts:15-21), the sys_file tombstone and 30d reap (attachment-lifecycle.ts:22-27), DESTRUCTIVE_OPERATIONS = transfer/restore/purge (permission-evaluator.ts:93) and the spec: retire the allowRestore / allowPurge permission props (ruled 2026-08-26; M2 anchor stays open, keys return with M2) #12497 ruling date 2026-08-26 (:64-65), CAPABILITIES_RETIRED_KEY_GUIDANCE (object.zod.ts:167), the permission tombstones and "M2 lifecycle initiative" text (permission.zod.ts:638-647), api-derivation restore/purge (:147-148), the vocabulary pin (engine-middleware-operation-vocabulary.test.ts:3-4), PD#9 OS_{DOMAIN}_{NAME} (AGENTS.md:216).
  • Readers: D5 names a reader and a card per surface (L261-270); the four old keys stay tombstoned (L272-277); spec: declare the recycle-bin surface — the sys_recycle_bin object, the retention hook and the restore refusals (#3146 slice 2) #30235/engine: snapshot deletes into the recycle bin, restore under original ids, purge, and the TTL reap (#3146 slice 3) #30237/ui: the admin recycle-bin page (list, restore, empty) and the 已删除 · 撤销 toast (#3146 slice 4) #30239 exist with the scopes 6648418282 filed.
  • "Days, never plans": true as the dev reports. docs/adr/0012-notification-platform.md is messaging, superseded by ADR-0030; git grep of docs/adr for resolveFeatures / plan-agnostic / "plan name" is 0 hits; runtime-config-plugin.ts:205 heads the seam "open-core boundary — cloud ADR-0012"; CHANGELOG 9.5.0 says "plan-agnostic". The record states this in Provenance (L62-64) and its own section (L296-317) and cites cloud ADR-0012 as PD#13 requires. No invented citation.
  • ADR-0049 amendment: appended at the end of the Status line, Accepted still at column 0, only line 3 changes, decision text untouched. README diff: 148→149 records, 143→144 numbers, Accepted 129→130, Total, one new row; nothing else moved.
  • Body matches files: both declaration lines, skip-changeset label, Fixes #30234, Part of #3146 · Ruling-ref: 6629858351, Prior rulings read:, the ruling-to-section table, 维护者速读; "368 lines" and the index counts are exact. Checks: all completed runs green; Test Core (1/8) and Type Check · workspace still in_progress at read time.

must-fix M1 — the ADR-0049 note states a fact main does not carry. 0049 L3 (new text) and ADR-0148 L39, L272-274 say the four keys' "tombstones and ADR-0087 conversions stand". At main only allowRestore/allowPurge have a live conversion (packages/spec/src/conversions/entries/18/permission-allow-restore-purge-removed.ts). object-enable-trash-mru-removed is listed as retired-from-load-path in retired-after.census.json with no entry file, and the enable.trash tombstone itself says the tool no longer carries it (object.zod.ts:175); softDelete never had a conversion entry (top-level unknown-key guidance only, object.zod.ts:1629). The amended line in a prior ADR must say "tombstones stand; the permission conversion stands; the 16.x trash conversion has aged out as ADR-0087 provides".

must-fix M2 — a mechanism decided in D1/D5 and undecided in D6. D1 L146-147 says an entry records "the retention window it was given"; D5 row 1 L263 says "the resolved window"; D6 L288-291 says whether the window is stamped at delete or resolved by the sweep is #30237's choice. The ruling decided neither. #30235 (field list) and #30237 would each read a different answer; pick one place and one wording.

should-fix S1 — sys_recycle_bin (L75, L145, L164-165) and OS_RECYCLE_BIN_RETENTION_DAYS (L78, L201) were "for example" in the ruling (items 1 and 2) and in #30235's body; L21 says the record "adds no mechanism of its own". Fixing them is what #30235 needs; say the ruling offered them as examples and this record fixes them.
should-fix S2 — D3 L203-206 states the hook's input (opaque token, plan string), output ("days, or nothing to keep the default") and validation ("a positive whole number") as decided, then hands the contract to #30235. The ruling decided only days-through-a-resolver-hook on the resolveFeatures pattern; mark these as the record's choices or leave them to #30235 (0 days is undefined either way).
should-fix S3 — D1 L148-149 "empty purges every entry the caller may see" grants a non-admin an empty/purge right; the ruling gives a person only "restores what they deleted" and admin "sees all". Invented permission rule.
should-fix S4 — Context L92-93 cites "the onDelete vocabulary of packages/spec/src/data/field.zod.ts"; the property at main is deleteBehavior (field.zod.ts:1868, z.enum(['set_null','cascade','restrict'])); onDelete has 0 hits in that file.
should-fix S5 — the Status value spans L3-13; scripts/adr-index.mjs reads one line (STATUS_LINE, :86), so the README cell ends "ruling of 2026-11-04 on" and the Tier H / "implements nothing" clauses fall outside the parsed status. ADR-0049 and ADR-0147 keep theirs on one line.
should-fix S6 — Consumers L41-42, D5 L266-269 and Rollout L358-359 assign #30235 the restored event name, the grant names and "the operation vocabulary"; #30235's body lists only the object, any object-level key, the hook contract and the refusal codes, and the vocabulary pin is an objectql test, not spec. Say the record widens #30235, and move the vocabulary row to #30237.
should-fix S7 — D4 L245-247 resolves the ruling's "keep secret fields out of the snapshot or encrypt them" to "in v1 they stay out … come back empty". It follows from the later item but is the record's choice; say so. Same for D1 L162-163 "stored by the default datasource", which nothing ruled.

② Semver level

None. Docs only under docs/adr/; no package publishes; skip-changeset is correct; Clause-②: no and Metadata-protocol: none are accurate (the protocol addition is #30235's).

③ Boundary flags

  • Tier H (docs/adr/**, PD#14): lands only by the maintainer's hand; the PR is a draft with no auto-merge. The contract-review claim 6649445847 is on the PR.
  • Open/closed boundary: cited as cloud ADR-0012, never a bare number (PD#13); the commercial half is left in cloud and not numbered here (L54-58).
  • Prior ADR touched: ADR-0049 Status line only, decision text unchanged; M1 is the one inaccuracy in that line.

④ Design judgment

The record is faithful to 6629858351 item by item, honest about what ships (nothing) and about the ADR-0012 citation, and gives #30235 enough to build from (object name, no object-level key, days-only hook, three refusals with codes left to the ADR-0112 ledger, and D6 naming who decides the rest). The two must-fixes are drafting errors of the kind the card warns will be paid by every child: a false "conversions stand" claim written into ADR-0049's own Status line, and a retention-window field decided in D1/D5 and left open in D6. Both are one-line corrections; the should-fixes are provenance honesty (which spellings the record fixed vs the ruling fixed) and one wrong property name.

Implemented-by: claude/project-thread-cxrsk5
Reviewed-by: session_01WGufzfgvGexKn9vLHsy89K

VERDICT: FAIL

…deadline at delete, and reads the ADR-0087 conversions as main carries them

Contract review 6649543275 on #30263 found two must-fixes and seven should-fixes,
all text. This commit answers them in one change:

- The claim that the old keys' ADR-0087 conversions "stand" was false: only the
  allowRestore / allowPurge conversion (entries/18) is still carried; the 16.x
  enable.trash conversion has aged out of the load path (retired in the census, no
  entry file) and softDelete never had one. ADR-0148's Builds on, Leaves unchanged,
  Context and D5 now say exactly that, and ADR-0049's Status note says the same.
- The retention window is now decided once: the resolver is asked at delete and its
  answer is stamped on the entry as the purge deadline (D1, D3, D5, Rollout), with
  the reasons, and D6 no longer leaves it open.
- sys_recycle_bin and OS_RECYCLE_BIN_RETENTION_DAYS were the ruling's examples; the
  record now says it fixes them. The hook's input, output and validation, the
  stamped deadline, the default datasource and "secret fields out in v1" are marked
  as the record's choices, listed once in a new D7. The "adds no mechanism of its
  own" sentence is gone.
- "Empty" is an admin's verb (the ruling gives a person restore of their own
  deletes and an admin everything); the non-admin reading is removed.
- The reference-field vocabulary is deleteBehavior, not onDelete.
- The Status value is one line again; docs/adr/README.md is regenerated.
- #30235's body lists the object, the hook contract and the refusal codes; the
  record now says it adds the restored event's name and the grant keys to that
  card, and moves the operation-vocabulary pin (an objectql test) to #30237.

Claude-Session: https://claude.ai/code/session_01WGufzfC3wLXmeCXwxa963a
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Contract-review-claim: session_01WGufzfgvGexKn9vLHsy89K · head 9b45718 · 2026-11-05T04:34Z

Delta review of fix round 1 (b28128a5..9b457180) at CONTRACT_REVIEW_TIER, against the FAIL record 6649543275.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9b4571803ea413e102b1f4d270305813e269f082
Local-runs: none
Scope: the delta git diff b28128a5 9b457180 (3 files); docs/adr/0148-recycle-bin-separate-store-restore-under-original-ids-retention-in-days.md at head in full (409 lines) and docs/adr/0049-no-unenforced-security-properties.md line 3 at head; the README row for ADR-0148; the FAIL record 6649543275, ruling 6629858351, triage split 6648418282, bodies of #30235 and #30237; PR #30263 body (live, still the b28 text), its 3 comments, 0 reviews, 3 files, check-runs on the head; /mnt/project-files/issue-30234/pr-body.md; at origin/main: packages/spec/src/conversions/retired-after.census.json (:1-30, :100, :161, :224, :292, :301, :370, :379), entries/ tree (one file: entries/18/permission-allow-restore-purge-removed.ts :1-60), packages/spec/src/data/object.zod.ts (:167-176 trash tombstone, :203-242 refused-not-stripped, :1004-1009 ttl, :1629-1633 softDelete), field.zod.ts:1868, permission-evaluator.ts:70-105, engine-delete-cascade.ts:7-8, engine-secret-internal-fields.ts:21, attachment-lifecycle.ts:19-27 and :107-116, packages/objectql/src/lifecycle/lifecycle-service.ts:20-27 and :215-262, docs/adr/0087 (Status, :558-568, :1390-1425), docs/adr/0100:54-71, scripts/adr-index.mjs:86 and :200, AGENTS.md (Prime Directives 9, 12, 13, 14).

① Derived judgments

  • M1 closed, truthfully against main. ADR-0049 L3 and ADR-0148 L21-23, L32-33, L114-117, L288-295 now say: tombstones stand; the allowRestore/allowPurge conversion stands; object-enable-trash-mru-removed has aged out (census, no entry file); softDelete never had one. Verified: the entries tree holds exactly one of the two files (entries/18/permission-allow-restore-purge-removed.ts, id at :48), the census lists object-enable-trash-mru-removed at :100-:370 with no entry file anywhere, softDelete exists only as top-level guidance (object.zod.ts:1629), and the trash tombstone's "apply it by hand, or run the tool on a release that still carries the conversion" (object.zod.ts:175) is what L116-117 and L294 paraphrase. "Refused" is right: enable is a strictObject that rejects trash with guidance, not a strip (object.zod.ts:240-241).
  • M2 closed and consistent: "stamped at delete" is stated in D1 (L147-150, L168), D3 (L215-220, L223), D5 rows 1 and 3 (L279, L281), D6 (L306-307), D7 (L322-324) and Rollout 3 (L401); no section still says "resolved by the sweep" or "the retention window it was given". D3 L215 labels it the record's choice ("The ruling decided days-through-a-hook and no more; this record decides") and D1 L149-150 points there. The reason covers the plan-change case both ways: a later shorter answer never shortens a window already shown, a later longer answer applies only to later deletes (L217-220). No conflict with the ruling, whose "sys_file tombstone pattern" names the one sweeper, not when the window is fixed.
  • S1-S7 closed: S1 at L146-147, L205-207, D7 L317-319 (ruling items 1-2 do say 「for example」); S2 at L209-213, D7 L320-321 (zero refused as a configuration defect); S3 at L151-152, L195-196, L331-332 (empty is the admin's, per the ruling's page "list, restore, empty"); S4 at L88-89 (deleteBehavior, enum order matches field.zod.ts:1868); S5 the Status value is one line (L3) and the README row now ends "…" at the generator's 75-char cut (adr-index.mjs:200), Accepted first, same shape as the ADR-0147 row; S6 at L35-37, L282-285, L309-310, L398-403 (vocabulary pin moved to engine: snapshot deletes into the recycle bin, restore under original ids, purge, and the TTL reap (#3146 slice 3) #30237; the event name and grant keys declared as the record's widening of spec: declare the recycle-bin surface — the sys_recycle_bin object, the retention hook and the restore refusals (#3146 slice 2) #30235, whose body lists only the object, an object-level key, the hook contract and the codes); S7 at L166-167, L259-263, D7 L325-327.
  • D7 lists the six choices and nothing the ruling decided; L331-332 keeps the permission rule at the ruling's. Nothing else in Decision is presented as the ruling's that it did not decide, with the two S10 exceptions below.
  • Nothing new in the delta is wrong in kind: every added path exists at main; no shipped claim (L3 "implements nothing" kept); all nine in-file anchors resolve to the headings at L46-395 (D7's at L313); the README row is the regenerator's output; Accepted is unmoved on both Status lines. Checks on the head: 30 completed (success or skipped, none failed), 8 in_progress at read time (Type Check x4, Lint x3, Test Core 1/8); Governed Surface Queue Guard and PR-body guards green.
  • Body file matches the head: 409 lines (wc 409), the section map gains the D7 row and reworded D6 row, the ADR-0049 bullet states the M1 facts, gate block says head 9b45718 and 3131 anchors / 150 records; declarations unchanged (Clause-②: no, Metadata-protocol: none, Skills-docs: none). The live PR body is still the b28 text (368 lines, "adds no mechanism of its own") until the seat PATCHes.

should-fix S8 — ADR-0049 L3 and ADR-0148 L114-115, L292 say the trash conversion "aged out of the load path" / "is retired from the load path (listed as such in the census)", against the permission conversion that "stands". retiredFromLoadPath is true of both: the permission entry sets it at entries/18/permission-allow-restore-purge-removed.ts:50-51 (retiredAfter: '17.2.0', migrate-meta-only per :39-46) and the census lists it at :301 and :379. The distinction main carries is ADR-0087's kit (addendum L1390-1397): the trash kit was removed at 18, the permission kit is carried at 18 and ages out at 19. Say "kit", not "load path", in both files, so a reader of ADR-0049 does not infer the permission conversion still replays at load.
should-fix S9 — D4 L259-261's reason for "out": "an encrypted copy of a value would point at a handle that no longer exists" conflates value and handle. Under ADR-0100 L54 the business row carries only the opaque handle and the value lives in sys_secret; what the snapshot would hold is the handle, which dangles once the committed delete releases it, and encrypting the value into the entry would be a second store for a secret outside sys_secret. The choice is right; the sentence is not.
should-fix S10 — D7 omits D5 L300-302 "No object-level authoring key is named by this record … by rule, not by a flag": the ruling said only "business objects (not sys_*)" and #30235's body expects "any object-level key the ADR names", so "none" is the record's choice. D4 L255 "bind v1 and everything after it" also strengthens the ruling's "risks to plan for" without a label.
should-fix S11 — D3 L218-219 "the sweep then reads one stored column, as the sys_file reap reads deleted_at" is loose: that reap reads deleted_at plus a static expireAfter (object.zod.ts:1004-1005; attachment-lifecycle.ts:109), and the LifecycleService already resolves per-tenant windows at sweep time (lifecycle-service.ts:219-222, :256; ADR-0057 §3.2), which is the alternative the record rejects. Name that machinery as the rejected alternative and say the stamped column's reap shape (a ttl on the deadline with no added window, or a reap guard) is #30237's.

② Semver level

None. Docs only under docs/adr/; skip-changeset; Clause-②: no and Metadata-protocol: none stay accurate.

③ Boundary flags

  • Tier H (docs/adr/**, Prime Directive feat: Comprehensive CRM example demonstrating all ObjectStack protocol features #14): draft, head repo is the base repo, no auto-merge, no reviews; lands by the maintainer's hand only. The second claim 6649772660 names this head.
  • Prior ADR touched: ADR-0049 Status line only, decision text unchanged; S8 is the one wording residue in that line. Open/closed boundary still cited as cloud ADR-0012 (L58-60, L344-357).

④ Design judgment

The fix round answers every finding of 6649543275 and does so against main rather than against the review's wording: the ADR-0087 facts are read from the census and the one surviving entry, the deadline is fixed in one place with a plan-change rationale that holds in both directions, and D7 gives #30235 and #30237 a single list of what the record, not the maintainer, chose. The four should-fixes are precision residue (an ADR-0087 term that names the wrong concept, a muddled sentence, one unlisted choice, a loose analogy), none of which a child card would build wrongly on.

Implemented-by: claude/project-thread-cxrsk5
Reviewed-by: session_01WGufzfgvGexKn9vLHsy89K

VERDICT: PASS

…ld hold, and resolve-at-sweep as the rejected alternative

Delta review 6649866889 on #30263 passed with four should-fixes, all text:

- "Aged out of the load path" named the wrong concept: both conversions are
  retiredFromLoadPath. The difference main carries is ADR-0087's kit rule — the
  enable.trash kit's conversion was removed when 18 was cut (its tombstone stays),
  the allowRestore/allowPurge kit is carried at 18 and ages out at 19, and neither
  replays at load. ADR-0049's Status note and ADR-0148's Builds on, Leaves
  unchanged, Context and D5 now say "kit".
- D4's reason for keeping secrets out of the snapshot no longer conflates the value
  with the ref: under ADR-0100 the row carries only the secret ref, so the snapshot
  would hold a ref the committed delete released, and encrypting the value into the
  entry would open a second store outside sys_secret.
- D7 gains the two choices it omitted: no object-level authoring key (by rule, not
  by a flag) and the privacy rules as standing rules that bind v1 and everything
  after it; D5 and D4 mark them where they are made.
- D3 names resolve-at-sweep (the LifecycleService's lifecycle-namespace windows,
  global and per tenant; the sys_file reap's deleted_at plus a static expireAfter)
  as the alternative the record rejects, and leaves the reap shape on the stamped
  column to #30237.

docs/adr/README.md is unchanged by this commit (the Status lines did not move).

Claude-Session: https://claude.ai/code/session_01WGufzfC3wLXmeCXwxa963a
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

维护者速读

  • 改了什么: 新增 ADR-0148,把你 2026-11-04 在 Implement soft delete (recycle bin): enable.trash / softDelete are spec-only with zero runtime readers #3146 上对回收站的裁决记成正式记录:独立存储,每次删除一条记录;按原 id 恢复,遇到三种情况拒绝;保留期按天,经宿主钩子注入;企业层放 cloud。ADR-0049 状态行末尾加一条注记,说明旧键保持移除。ADR 索引重新生成。
  • 为什么改: Implement soft delete (recycle bin): enable.trash / softDelete are spec-only with zero runtime readers #3146 的 spec、engine、UI 三张子卡都要引用这份 ADR。ADR-0049 当年因为没有读者删掉了旧的回收站键,所以新面先逐项点名将来的读者和承载卡。
  • 风险与回滚: 仅文档,回滚即还原本 PR。每项机制都写成「已裁决、待落地」,不声称已上线。
  • 先看: D7 一节。它逐项列出记录自己定下、而裁决没定的八项选择,例如名字 sys_recycle_bin、保留期在删除时盖章写进条目、密钥字段不进快照、没有对象级开关。若有不同意的,在 PR 上说即可。
  • 两分钟验证: pnpm check:adr-index && pnpm check:adr-links && pnpm check:adr-symbol-anchors,三条全绿。
  • 需要你做的: docs/adr/** 是 Tier H,需要你 Approve 或亲手合并。席位不排队,也不开 auto-merge。

席位意见:fable 契约复审三轮。

  • 首轮 FAIL,两条必须修,都是文字:ADR-0049 注记把旧键的转换现状写错了;保留期是否在删除时写进条目,前后矛盾。
  • 第二轮 delta PASS,剩四条措辞建议,dev 又推一次全部采纳,席位亲自核对了这次差异。

席位核对 — ACCEPT · domain:spec#3 · head 0d59704e85602e1c5a648fd349de079ac95b6d97

  • Shape: 3 files on origin/main bb86ff96 (+432 −4), in three commits:
    • the new docs/adr/0148-recycle-bin-separate-store-restore-under-original-ids-retention-in-days.md (427 lines, D1-D7 plus Rollout);
    • one dated note appended to ADR-0049's **Status**: line (Accepted unmoved; decision text unchanged);
    • docs/adr/README.md regenerated by scripts/adr-index.mjs (counts and the one new row).
    • No code, schema, generated artifact or changeset.
  • Premise measured: at origin/main, no ADR names a recycle bin (ADR-0045's trash-can is the unpublished-build discard). ADR-0049 lists allowRestore / allowPurge among the removed lifecycle bits. The review verified at main every code path the record cites (the delete cascade, the sys_file reap, LifecycleService, the objectql: deleting a record does not release the sys_secret row its secret field names #27581 sys_secret release, deleteBehavior) and the conversion kits under ADR-0087.
  • Gates: the dev ran all 21 derived gates at 0d59704e, all exit 0, and --ran reconciles 21/21 with 0 NOT-MEASURED (adr-links 991, symbol-anchors 3133, adr-index OK). Check-runs at this head: 45, 26 success, 19 skipped (path-filtered), 0 red; mergeable_state: clean.
  • Contract review:
    • Full review, 6649543275 on b28128a5: FAIL. M1 was the ADR-0049 note claiming that "conversions stand" for all four keys. M2 was the retention window being decided in D1/D5 but called undecided in D6. There were seven should-fixes.
    • Delta review, 6649866889 on 9b457180: PASS. M1, M2 and S1-S7 were closed and checked against main; four wording should-fixes remained (S8-S11).
    • Seat read of 9b457180..0d59704e (2 files, +44 −26): S8 now says "kit", not "load path"; S9 states ADR-0100 correctly (the row carries only the secret: ref); S10 adds two items to D7; S11 names resolve-at-sweep as the rejected alternative and leaves the reap shape to engine: snapshot deletes into the recycle bin, restore under original ids, purge, and the TTL reap (#3146 slice 3) #30237. Nothing else moved.
  • Declarations: Clause-②: no (docs only, an ADR; the protocol addition rides #30235), Metadata-protocol: none, skip-changeset.
  • Accepted deviations:
    • The record makes eight choices the ruling left open, each listed once in D7 and marked where it is made. The two weightiest: the retention deadline is stamped on the entry at delete time (a later host change never shortens a window a person was shown), and secret fields stay out of the snapshot in v1 and come back empty on restore. The maintainer's merge is the ruling on them.
    • The ruling cites ADR-0012 for "the framework knows days, never plans". ADR-0012 in this repo is the messaging platform, and no ADR here states the principle. The record says so and cites the resolveFeatures seam and cloud ADR-0012, rather than inventing a citation.
  • Carried:
  • Landing: Tier H (check-governed-merges --pr 30263: GOVERNED, docs/adr/** ×3, 436 lines, under the 3000 threshold). Hand-off: pr_ready, which makes the relay request the GOVERNED_APPROVERS, plus needs-user-decision. The maintainer merges by hand. The seat does not queue it or arm auto-merge.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation needs-user-decision size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs(adr): the recycle bin — a separate store, restore under original ids, retention in days (#3146 slice 1)

1 participant