Repository navigation
Commit 210ef60
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XTVkGrf2CjQTF79hBERgqiJ)_
domain:server#5 · os-project-manager · session_01FEkQ5ktJb5rQ5n2GUXSQMR
Fixes #31425
Clause-②: yes (widening) — `POST
/api/v1/marketplace/install-local/:id/reseed-sample-data` and `GET
…/:id/sample-data` now serve the base of an unmanaged copy this door
recorded, which they answered `404` before; nothing accepted today is
refused; one contract-review-tier review owed before the queue
Metadata-protocol: none
Skills-docs: one move —
`content/docs/deployment/marketplace-handbook.mdx` (step 6, Sample data)
states that the doors act on a copy by its base id and what uninstalling
a copy does to its rows; no `skills/**` page teaches these doors.
## 维护者速读
- 改了什么:以"无托管副本"方式安装并带示例数据的应用,现在可以用副本的 base id
调"重新灌示例数据"(reseed)和"示例数据计数"(count)两个入口。两扇门和"清空示例数据"(purge)读同一份种子收据,用的是同一个读取函数。第一次灌数据失败(`seed-receipt-unread`
/ `seed-error`)后,也可以用 reseed 从存储的收据重试,不再只有再装一次换来的 409。
- 为什么改:副本没有安装台账条目,所以以前 reseed 和 count 对副本一律回 404,第一次灌数据失败后也没有办法补救。
- 风险与回滚:只放宽。按副本记录判定,只有本入口自己复制出来的 base 才能被这两扇门访问,其它 base 照旧 404(包括 Studio
建的、带种子项的 base)。收据读不出来时回 503,不会当成"没有示例数据"。卸载副本的行为不变:默认删表,`keepData`
时保留所有行。没有新增台账条目,也没有第二个收据读取者。回滚即还原本 PR。
- 先看:`packages/cloud-connection/src/marketplace-install-local-plugin.ts`
中的 `handleReseed` 和 `handleSampleDataCount`
的副本分支、`unmanagedCopySeedDatasets`(原 purge
的读取函数改名)、`seedUnmanagedCopy`、`unreadableCopyReceipt`。
- 两分钟验证:`pnpm --filter @objectstack/cloud-connection exec vitest run
src/marketplace-install-local-install-modes.test.ts`,预期 63 个用例全绿,其中
#31425 新增 7 个。
- 需要你做的:无。两个前提问题已写入报告的 `open_questions`,席位已同意本 PR 的处理方式。
## What changes
`packages/cloud-connection/src/marketplace-install-local-plugin.ts`:
1. **One receipt reader, three doors.** The purge's read of a copy's
seed receipt is renamed `unmanagedCopySeedDatasets`, and the reseed and
the count now ask it as well. Its checks are unchanged: an installed
writable base, this door's record of the copy, the record tied to the
base row in the package store, and the receipt's `seed` items read
through the protocol. There is no new ledger entry and no second reader.
2. **Reseed of a copy** (no ledger entry):
- `404` for any base this door did not record;
- `400 RESEED_SKIPPED` for a copy made without the sample data;
- `503 SERVICE_UNAVAILABLE` for a receipt that could not be read;
- otherwise a run of the datasets through `seedUnmanagedCopy`, the
install's own copy seed run (the walled-organization rule,
`runInlineSeed`, rows owned by the caller), answered by `reseedAnswer`.
That is the ledger reseed's shaping, factored out: the same four counts,
the intact-baseline success, `RESEED_NO_ROWS`. A copy has no ledger
flags to write.
3. **Retry.** A copy whose first load at install failed
(`seed-receipt-unread`, `seed-error`) is still recorded with `seeded:
true`, so the reseed loads it from the stored receipt.
4. **Count of a copy.** `404` as above; `count: 0` for a copy made
without the sample data; the purge's `503` for an unread receipt;
otherwise the purge's match over the receipt's datasets.
5. **`unreadableCopyReceipt`** is the purge's existing 503 answer,
shared by the three doors: the door's record file to repair, or "retry
once the stores answer". The store's text goes to the log only.
6. **Doc comments.** These now say what the doors do for a copy, what
uninstalling a copy does, and what remains about the store's `null`:
- the module header (`seedSampleData`);
- `handleReseed` and `handleSampleDataCount`;
- `unmanagedCopyInstance`;
- `marketplace-install-local-unmanaged-copies.ts`.
`content/docs/deployment/marketplace-handbook.mdx`, step 6 (Sample
data): one paragraph.
## The four halves of the card
### 1. Uninstalling a copy — keep the rows, no code change
The card offers "purges its sample rows (as a managed uninstall's
cleanup does)" or "keeps them and says so in the answer". Measured
first:
- **A managed install-local uninstall keeps every row.** Its docblock
says "Its tables and rows are untouched", and the handbook says "Records
stored in the app's objects stay in the database". So the card's
parenthetical does not hold: there is no managed cleanup that purges
sample rows.
- **A copy is uninstalled through `DELETE /api/v1/packages/:baseId`**
(`deletePackage`). This door has no uninstall for a copy: its `DELETE
…/install-local/:id` answers `404` without a ledger entry. Measured on a
real boot (showcase composition, the CRM example as the copy with its
sample data, a throwaway probe that is not committed):
```
install 200 {"mode":"inline","inserted":28,…} after install {"crm_account":4,"crm_contact":3,"crm_opportunity":12,"crm_lead":5,"crm_activity":5}
— default —
uninstall 200 …"deletedCount":30… purge after uninstall 404
reinstall without sample data 200 after reinstall {"crm_account":0,"crm_contact":0,"crm_opportunity":0,"crm_lead":0,"crm_activity":0}
— ?keepData=true —
uninstall 200 …"deletedCount":30… purge after uninstall 404
reinstall without sample data 200 after reinstall {"crm_account":4,"crm_contact":3,"crm_opportunity":12,"crm_lead":5,"crm_activity":5}
crm_account names ["Acme Corp","Globex Ltd","Initech","User Authored Co"]
purge of the reinstall 400 NOTHING_TO_PURGE
```
So by default the copy's tables go with every row in them, the sample
rows included. The rows outlive the uninstall only under `keepData:
true`, which is the caller asking to keep every row.
Four axes:
- **Real need.** The measured default already leaves no sample row
behind. The only outliving case is one the caller chose.
- **Long term.** One uninstall semantics across both doors, with no
hidden data delete: a managed uninstall keeps rows, and a copy follows
the verb's declared `keepData`.
- **AI-safety.** A purge hidden inside an uninstall that was asked to
keep data is exactly the silent behaviour the axis warns against.
- **Startup focus.** No new surface.
The seat agreed to keep the rows, with no field added to
`deletePackage`'s answer in this PR. The handbook now says so and says
to purge first.
### 2. Reseed and the count reach a recorded copy through the same
receipt — done
See *What changes*. There is no new ledger entry and no second receipt
reader.
### 3. A failed first load can be retried — done
Both failure reasons are pinned: the receipt unreadable at install, and
the seed run throwing. In each case the reseed then loads 3 rows.
### 4. Seat 3's scope addition: the store's `null` at install — measured
and stated
The package store's `get` catches an ordinary driver error, logs it and
answers `null` (`service-package`, `get`). Right after the import, the
door therefore cannot tell a passing error from "no row". The #30808 pin
measures this on the current tree: with an ordinary fault injected at
install, the answer carries `copyRecord: { written: false, … }`, no
record is written, and the purge answers 404. The reseed and the count
now go through the same reader, so they answer 404 for that copy too.
Re-recording later, as the scope addition asks, needs the base row and
the receipt to prove the base is a copy. They do not: the import stores
no marker on the base manifest (#30808's own docblock), and a
Studio-authored base with published seed items reads the same. A
re-record from them would be a re-record on an id alone, which is what
the scope addition rules out.
The seat agreed to leave the binding out of this PR. The follow-up I
named is for the import to answer the base row's `version` and
`created_at` itself, so the door never re-reads the store at install.
That is a metadata-protocol card, which the seat files.
## Tests
`packages/cloud-connection/src/marketplace-install-local-install-modes.test.ts`,
inside #30808's copy harness (the real `SeedLoaderService` over an
in-memory engine; the import, receipt and package store as doubles).
There are seven new cases, and no new test file:
- the reseed of a purged copy puts the 3 rows back, owned by the
reseeder, and the lookup resolves; a second reseed is `200` with
`inserted: 0, skipped: 3`; nothing is ledgered;
- the count after the install is 3 with a customer row beside it, and 0
after the purge;
- a first load with `seed-receipt-unread` is retried by the reseed and
lands 3 rows;
- a first load with `seed-error` (the metadata read throws once) is
retried the same way;
- reseed and count of a Studio base with published seed items, and of an
id that is no base: `404`, nothing written;
- a copy made without the sample data: reseed `400 RESEED_SKIPPED` ("No
sample data was copied into …"), count 0;
- an unreadable receipt: reseed and count `503 SERVICE_UNAVAILABLE`, the
store's text not quoted, nothing written.
The harness gains `reseed` and `count` beside `purge`, and the metadata
read is a spy so one load can throw.
<details><summary>Red on main (the new cases run against the unfixed
plugin at base 2ae3e30, before the fix was written)</summary>
```
× the reseed of a recorded copy puts the purged sample rows back from the receipt, owned by the reseeder; nothing is ledgered
× the count of a recorded copy is the purge's match: every sample row after the install, none after the purge
× a first load whose receipt could not be read (`seed-receipt-unread`) is retried by the reseed, from the stored receipt
× a first load that threw (`seed-error`) is retried by the reseed, from the stored receipt
× a copy made without the sample data: the reseed says no sample data was copied (400), and the count is 0
× an unreadable receipt is never read as "no sample data": the reseed and the count answer 503 and write nothing
AssertionError: {"success":false,"error":{"code":"RESOURCE_NOT_FOUND","message":"No marketplace install for com.example.crm-copy."}}: expected 404 to be 200
Tests 6 failed | 52 passed (58)
```
The counts are of the suite at 2ae3e30, before #31475 added five cases
to it. The seventh case (any other base answers 404) passes on main by
construction. It is the negative pin, and ablation 3 shows it red.
</details>
<details><summary>Ablations (fix committed at 7bb1588, each
restored)</summary>
All three ran through `node scripts/ablation-replace.mjs` on
`marketplace-install-local-plugin.ts`, against the copy suite. The suite
imports the plugin from `src/`, so no build applies.
1. The count reads an unread receipt as `count: 0`:
```
ok mutation landed: anchor 1 -> 0, blob cf4f8a8 -> 5d41b24e356e
× an unreadable receipt is never read as "no sample data": the reseed and the count answer 503 and write nothing
Tests 1 failed | 57 passed (58)
ok restored: blob == HEAD (cf4f8a8) and `git diff HEAD` is empty
```
2. The reseed reads an unread receipt as a skipped run (400):
```
ok mutation landed: anchor 1 -> 0, blob cf4f8a8 -> 84c801a8af03
× an unreadable receipt is never read as "no sample data": the reseed and the count answer 503 and write nothing
Tests 1 failed | 57 passed (58)
ok restored: blob == HEAD (cf4f8a8) and `git diff HEAD` is empty
```
3. The shared reader reaches a base with no record of this door's (the
receipt read on the id alone):
```
ok mutation landed: anchor 1 -> 0, blob cf4f8a8 -> 7f2d6d85a54d
× a Studio-authored writable base with published seed items is refused at purge: 404, nothing deleted
× a copy made before the record existed is not reached: no record, 404, nothing deleted
× a record that is not kept is said in the install answer (`copyRecord`), and the copy is then out of the purge's reach
× uninstalling the copy removes its record through the protocol's uninstall cleanups; the purge then answers 404
× reseed and count reach no other base: a Studio base with published seed items, and an id that is no base, answer 404 and write nothing
Tests 5 failed | 53 passed (58)
ok restored: blob == HEAD (cf4f8a8) and `git diff HEAD` is empty
```
</details>
## Verification
Final head `6cf7abe7`: the fix 7bb1588, one merge of `origin/main`
(ff76c20), and a second merge (6cf7abe). The second merge was needed
because #31475 (#30964) landed on the same two files and conflicted.
Both sides were kept: the audit helper beside `unreadableCopyReceipt`,
and the harness's `grantsOf` / `auditEvents` beside `reseed` / `count` /
`metadataGetObject`. Every exit code was captured before any pipe.
- **Full build**: `pnpm turbo run build --concurrency=2` under the
verify lock, VERDICT command-exit 0 (120 of 120), on 6cf7abe.
- **Derived gate union**, `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` on 6cf7abe: 99 families, the same
list as on ff76c20, and every one exit 0 on its first run. `--ran`
reports "99 derived famil(ies) accounted for — 99 run, 0 NOT-MEASURED (a
DERIVED zero — all 99 recorded an exit code and none of them is 3)".
`check:dts-closure`, `check:dual-build-cjs-loads` and
`check:type-check-debt` ran after the build. The union was also run
whole on ff76c20, 99 of 99 exit 0.
- **Facade gate**, `pnpm check:facade-delegation-only`, run by hand:
exit 0.
- **Package**: `pnpm --filter @objectstack/cloud-connection test` on
6cf7abe: Test Files 75 passed, Tests 1024 passed (exit 0). `typecheck`
(src and `tsconfig.test.json`): exit 0.
- **Lint**: `eslint --no-inline-config` on the three touched `.ts`
files: exit 0.
- **Full dogfood battery**, `pnpm --filter @objectstack/dogfood test` on
ff76c20 (before the second merge) under the verify lock: VERDICT
command-exit 0, Test Files 439 passed / 3 skipped (442), Tests 3629
passed / 41 skipped (3670). It was not re-run after the conflict merge,
because this PR has no narrowing arm.
- CI-only families (the Test Core shards, the type-check lanes, Build
Docs) are read from this PR's check-runs.
## Acceptance notes
- **Clause-② from the diff.** It is `yes (widening)` only: two doors
serve a copy they refused (`404`), and nothing they accepted is refused
now. The claim's possible narrowing arm was for the store-null half,
which changes no answer here.
- **Locale of a copy's reseed.** At install, the seed locale falls back
to the source package's `i18n.defaultLocale`. The reseed has no source
body: the copy keeps none. So it hands over the copy base's own manifest
from the registry, and the organization's and the caller's locale still
come first.
- **Governed surface.** None. The diff is one source file, the record
module's docblock, one test file, one docs page and one changeset.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01FEkQ5ktJb5rQ5n2GUXSQMR
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5ktJb5rQ5n2GUXSQMR)_
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 47bc621 commit 210ef60
5 files changed
Lines changed: 346 additions & 51 deletions
File tree
- .changeset
- content/docs/deployment
- packages/cloud-connection/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
226 | 226 | | |
227 | 227 | | |
228 | 228 | | |
229 | | - | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
230 | 236 | | |
231 | 237 | | |
232 | 238 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
780 | 780 | | |
781 | 781 | | |
782 | 782 | | |
| 783 | + | |
| 784 | + | |
783 | 785 | | |
784 | 786 | | |
785 | 787 | | |
| |||
812 | 814 | | |
813 | 815 | | |
814 | 816 | | |
815 | | - | |
| 817 | + | |
816 | 818 | | |
817 | 819 | | |
818 | 820 | | |
| |||
841 | 843 | | |
842 | 844 | | |
843 | 845 | | |
| 846 | + | |
| 847 | + | |
844 | 848 | | |
845 | 849 | | |
846 | 850 | | |
847 | 851 | | |
848 | 852 | | |
849 | 853 | | |
850 | | - | |
| 854 | + | |
851 | 855 | | |
852 | 856 | | |
853 | 857 | | |
| |||
1156 | 1160 | | |
1157 | 1161 | | |
1158 | 1162 | | |
| 1163 | + | |
| 1164 | + | |
| 1165 | + | |
| 1166 | + | |
| 1167 | + | |
| 1168 | + | |
| 1169 | + | |
| 1170 | + | |
| 1171 | + | |
| 1172 | + | |
| 1173 | + | |
| 1174 | + | |
| 1175 | + | |
| 1176 | + | |
| 1177 | + | |
| 1178 | + | |
| 1179 | + | |
| 1180 | + | |
| 1181 | + | |
| 1182 | + | |
| 1183 | + | |
| 1184 | + | |
| 1185 | + | |
| 1186 | + | |
| 1187 | + | |
| 1188 | + | |
| 1189 | + | |
| 1190 | + | |
| 1191 | + | |
| 1192 | + | |
| 1193 | + | |
| 1194 | + | |
| 1195 | + | |
| 1196 | + | |
| 1197 | + | |
| 1198 | + | |
| 1199 | + | |
| 1200 | + | |
| 1201 | + | |
| 1202 | + | |
| 1203 | + | |
| 1204 | + | |
| 1205 | + | |
| 1206 | + | |
| 1207 | + | |
| 1208 | + | |
| 1209 | + | |
| 1210 | + | |
| 1211 | + | |
| 1212 | + | |
| 1213 | + | |
| 1214 | + | |
| 1215 | + | |
| 1216 | + | |
| 1217 | + | |
| 1218 | + | |
| 1219 | + | |
| 1220 | + | |
| 1221 | + | |
| 1222 | + | |
| 1223 | + | |
| 1224 | + | |
| 1225 | + | |
| 1226 | + | |
| 1227 | + | |
| 1228 | + | |
| 1229 | + | |
| 1230 | + | |
| 1231 | + | |
| 1232 | + | |
| 1233 | + | |
| 1234 | + | |
| 1235 | + | |
| 1236 | + | |
| 1237 | + | |
| 1238 | + | |
| 1239 | + | |
| 1240 | + | |
| 1241 | + | |
| 1242 | + | |
| 1243 | + | |
| 1244 | + | |
| 1245 | + | |
| 1246 | + | |
| 1247 | + | |
| 1248 | + | |
| 1249 | + | |
| 1250 | + | |
| 1251 | + | |
| 1252 | + | |
| 1253 | + | |
| 1254 | + | |
| 1255 | + | |
| 1256 | + | |
| 1257 | + | |
| 1258 | + | |
| 1259 | + | |
| 1260 | + | |
| 1261 | + | |
| 1262 | + | |
| 1263 | + | |
| 1264 | + | |
| 1265 | + | |
| 1266 | + | |
| 1267 | + | |
| 1268 | + | |
| 1269 | + | |
| 1270 | + | |
| 1271 | + | |
| 1272 | + | |
| 1273 | + | |
| 1274 | + | |
| 1275 | + | |
| 1276 | + | |
| 1277 | + | |
| 1278 | + | |
| 1279 | + | |
| 1280 | + | |
| 1281 | + | |
| 1282 | + | |
1159 | 1283 | | |
1160 | 1284 | | |
1161 | 1285 | | |
| |||
0 commit comments