You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 7e1089e
Browse filesBrowse the repository at this point in the historyBrowse files
fix(service-storage): sys_file's file-reference columns are lifecycle-owned (#28454)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} -->
_Requested by **Tesla** · [project
thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6Jao4hCgN6Y7K2BDrS18EpHqTB)_Fixes#27767Fixes#27778
Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — no skill or docs page teaches writing these columns;
they are the storage service's own bookkeeping
Ruling-ref: 6545931776 (letter A)
## 维护者速读
- 改了什么:`sys_file`
上记录"文件归哪条记录、哪个字段"的五列(`ref_object`/`ref_id`/`ref_field`/`released_ref_object`/`released_ref_id`)改由文件引用生命周期独占:对象上设为
`readonly`,并把 #27765 的建档侧守卫扩到这五列;另按 #27778 在对象头部写明 `key`/`status`
的存在由存储写入方保证,并收掉一个无人引用的导出。
- 为什么改:按裁决 A(6545931776),这五列与 #27765 的五个存储列同属生命周期所有,只能由系统上下文维护。
- 风险与回滚:非系统写入这五列时值被丢弃、其余字段照常落库、不报错(与 #27765
同形);系统上下文的认领、释放、复制不受影响。回滚即还原本 PR。
-
先看:`packages/services/service-storage/src/sys-file-storage-owned-columns.ts`(守卫列表)、`src/objects/system-file.object.ts`(五列
`readonly`
与新常量)、`src/sys-file-storage-owned-columns.integration.test.ts`(新钉)。
- 两分钟验证:`pnpm --filter @objectstack/service-storage exec vitest run
src/sys-file-storage-owned-columns.integration.test.ts`,预期 24/24 通过。
- 需要你做的:无,席位落地。
## What changes
- `system-file.object.ts`: a new `SYS_FILE_REFERENCE_COLUMNS` list
beside `SYS_FILE_STORAGE_OWNED_COLUMNS`, and `readonly: true` on the
five file-reference fields. The engine's static readonly strip now takes
a non-system caller's value for them off an update, by id and by
predicate, and lands the rest of the write.
- `sys-file-storage-owned-columns.ts`: the #27765 create-side guard now
drops the storage-owned columns and the file-reference columns from a
non-system create (one list, `LIFECYCLE_OWNED_COLUMNS`, built from the
two). The row is still written, still starts `pending`, and the write is
not refused; one `warn` names the dropped columns. No new refusal code,
no spec key, no governed path, no wiring change.
- **Choice of form:** a second constant beside the first, not one
widened list. The two lists have different owners (the storage backend's
columns vs. the file-reference lifecycle's) and each has its own pin
that declares exactly its five; the guard reads both. One guard, one
hook, one log line.
- #27778 item 1: `system-file.object.ts` now states that `key` and
`status` stay declared `required`, that the engine checks a `readonly`
field for shape only on insert, and that their presence is the storage
writers' invariant (presign/chunked start `pending` with a `key`,
complete commits, the field copy writes a fresh `key` and `committed`).
The declaration stays as it is (triage 6511387772).
- #27778 item 2: the helper the guard calls is no longer exported
(nothing imported it); it is renamed `dropLifecycleOwnedColumns` since
it now covers both lists.
- `.changeset/27767-sys-file-reference-columns.md` (patch,
`@objectstack/service-storage`).
## Tests
New pins in `sys-file-storage-owned-columns.integration.test.ts` (the
#27765 rig: real ObjectQL over sqlite, `StorageServicePlugin` on the
local adapter, data writes as a signed-in caller with no system
context):
1. declares exactly the five file-reference columns, each `readonly`;
2. an update by id cannot change any of the five (one case per column),
and the rest of the write lands;
3. a predicate update cannot change any of them;
4. a create cannot set any of them, and the row is written;
5. a batch create cannot set them on any row;
6. the lifecycle's claim, copy and release still write them;
7. a system create and a system update keep the values they write;
8. #27778: every storage writer sets `key` and `status` (presign,
complete, field copy).
<details><summary>Pins on origin/main 5517005 (premise): 9 red / 15
green</summary>
Measured with only the list exported (no `readonly`, no guard change):
pins 1–5 red (9 cases), pins 6–8 and every pre-existing case green. Pins
1–5 green after the change. The mechanism of the reading is kept off
GitHub under the security dispatch rule.
</details>
<details><summary>Ablations (one per pin, via
scripts/ablation-replace.mjs; each restored to blob == HEAD, git diff
HEAD empty)</summary>
- A1 `readonly` removed from `ref_id` ⇒ 3 red: pin 1, pin 2 (`ref_id`),
pin 3.
- A2 guard list without the reference columns ⇒ 2 red: pins 4 and 5.
- A3 the lifecycle's claim update written without the system context ⇒ 8
red, including pin 6 (and pin 8, pin 2 ×5, the #27765 claim case, which
all start from a claim).
- A4 the guard's system early return deleted ⇒ 17 red, including pin 7.
(A first attempt with an empty `--replacement` was refused by the tool
before running; re-run with `--delete`.)
- A5 the field copy's `key` written null ⇒ 1 red: pin 8.
</details>
## Acceptance notes
- `content/docs/permissions/system-context.mdx` row 47g describes the
guard's system early return in terms of the five storage-owned columns.
It stays true (a system insert still keeps them) but does not name the
reference columns; left untouched, outside this card's file surface.
- `storage-service-plugin.ts`'s wiring comment names the guard by the
storage-owned columns only; no wiring change, left as it is.
- The pending #27399 changeset says every other `sys_file` column
("`name`, `acl`, `scope`, ownership") is written through the data door
as before. If "ownership" is read as the reference columns, this PR's
changeset, in the same release, supersedes it. It is left unedited:
`check-empty-changeset` treats a rewrite of another PR's pending note as
a release decision for a person, so the seat decides whether that one
word changes.
## Verification
Head `7fc7033d` (`2d5586fc` plus the revert of a changeset edit; no
source change between them).
- Targeted pins: `pnpm --filter @objectstack/service-storage exec vitest
run --maxWorkers=2
src/sys-file-storage-owned-columns.integration.test.ts` ⇒ 24/24.
- Package: `pnpm --filter @objectstack/service-storage exec vitest run
--maxWorkers=2` ⇒ Test Files 71/71, Tests 1326/1326; `pnpm --filter
@objectstack/service-storage typecheck` ⇒ exit 0 (dts build of the
closure).
- Lint: `pnpm exec eslint --no-inline-config` on the three touched
source files ⇒ exit 0.
- Dogfood (owed by the narrowing arm), on `7fc7033d`: `pnpm --filter
@objectstack/dogfood test` under the verify lock ⇒ VERDICT command-exit
0, Test Files 361 passed / 3 skipped (364), Tests 2917 passed / 28
skipped (2945).
<details><summary>Derived gates: 62 derived, 62 run, 0 NOT-MEASURED
(dispatch-gates --ran reconciled, exit 0)</summary>
Every `dispatch-gates --commands` line run with stdin closed, exit codes
recorded before any pipe. 58 exit 0 on the first pass. Four were re-run
after their prerequisite and then exit 0: `check-plugin-teardown-shape
--self-test` (exit 3 until the pinned control commit was fetched),
`check:dual-build-cjs-loads` and `check:i18n` (exit 3 until a full dts
build), and `check-empty-changeset` (exit 1 on an edit of the pending
#27399 note, which was reverted; see Acceptance notes). The derived list
is identical for both heads.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01L9qibwQbgGE61XZrK6xRjM)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(service-storage): `sys_file`'s file-reference columns (`ref_object`, `ref_id`, `ref_field`, `released_ref_object`, `released_ref_id`) are owned by the file-reference lifecycle and can no longer be set or changed by a write that does not come from it
6
+
7
+
Clause-②: no (narrowing)
8
+
Metadata-protocol: none
9
+
10
+
<!-- adr-0087: not-required (no-migration-prescription) A data-door write to five lifecycle-owned sys_file columns is no longer applied: no key of any metadata schema is removed, renamed or re-shaped, no export, type or error code is removed or renamed, and no write is refused (an update keeps the stored values, a create starts with none of them). There is nothing for `objectstack migrate meta` to rewrite and no tombstone. -->
11
+
12
+
The five columns that record which record and field hold a file, and which record last let it go, take the same form as the five storage-owned columns:
13
+
14
+
-**Update.** They are now `readonly` on `sys_file`. A non-system update, by id or by predicate, keeps the values the lifecycle wrote and lands the rest of the payload; the write answers as before and the engine logs the dropped column at `warn`, as it does for every read-only column. A caller that passes `strictReadonlyWrites` is refused with `ERR_READONLY_FIELD_REJECTED`, as for every read-only column.
15
+
-**Create.** The create-side guard of the storage-owned columns now drops these five too: a non-system create of a `sys_file` row keeps the caller's other columns, drops any value it gives them, starts `pending` as before, and logs one `warn` naming the dropped columns. The create is not refused.
16
+
-**Unchanged.** The file-reference lifecycle writes them under the system context, as before: a file field's claim, release and copy, and the operator backfill of file references.
17
+
18
+
A file is attached to a record by writing its id into the record's file field, never by writing `sys_file` directly.
19
+
20
+
`system-file.object.ts` now also states that the presence of `key` and `status` on a `sys_file` row is the storage writers' invariant, not the engine's (the engine checks a `readonly` field for shape only on insert), and the guard's unused helper export is no longer exported.
0 commit comments