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 308f826
Browse filesBrowse the repository at this point in the historyBrowse files
fix(metadata-protocol): a failed seed in a package publish counts the rows earlier seeds wrote (#27802)
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
<!-- claude-projects-attribution: {"github_login":"os-sales"} -->
_Requested by **Sales** · [project
thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuPMZ8BHWtPf6nBTp51qSM64)_Fixes#27393
Clause-②: no
Metadata-protocol: none
Skills-docs: none — nothing an author writes changes; only the values of
an existing receipt become truthful
## 维护者速读
- 改了什么:包发布(publish-drafts)里某个 seed 失败时,`seedApplied` 回执的
`inserted`/`updated` 现在等于失败前实际写入的行数,`error` 点名是哪个对象的 seed 让加载停下;回执形状不变。
- 为什么改:以前失败分支硬编码 `inserted: 0, updated: 0`,但前面的 seed
已经写进表里,回执说"什么都没写"是错的数据。
- 风险与回滚:只改失败分支的取值与文案;发布哪些 seed、失败后后续 seed 不再应用、不回滚,这三点都不变。回滚即 revert 本
PR。
- 先看:`packages/metadata-protocol/src/protocol-publish-batch.ts`(catch
分支)、`packages/metadata-protocol/src/seed-loader.ts`(`load()` 的 catch 与
`notePartialWrites`)、`packages/objectql/src/publish-package-drafts-seed-receipt-counts.test.ts`。
- 两分钟验证:`pnpm --filter @objectstack/metadata-protocol build && pnpm
--filter @objectstack/objectql exec vitest run
src/publish-package-drafts-seed-receipt-counts.test.ts`,预期 5 passed。
- 需要你做的:无,席位落地。
## What changes
Semantics chosen and stated: **truthful aggregate counts**, inside the
existing `seedApplied` shape (`success/inserted/updated/error/errors`).
When a seed in the batch fails:
- `inserted` / `updated` are the rows the loader actually wrote before
it stopped — the finished datasets plus any rows the failing dataset
itself wrote before it threw;
- `success` stays `false`;
- `error` names the seed that stopped the load by the object its dataset
populates: `seed apply stopped at the seed for object 'OBJECT': ...`,
followed by the same sentence as before (a declared refusal quoted,
anything else withheld as `seed apply failed`).
Not changed: which seeds are published, that the loader stops at the
failing dataset (later seeds are not applied), and that nothing is
rolled back. All-or-none and per-seed counts were not chosen: the first
is a larger behaviour change, the second needs a spec edit.
Mechanism:
- `seed-loader.ts`: `load()` now catches a throw and records on the
thrown error (a module-private `WeakMap`, read through the new
`seedLoadPartialWrites(err)`) what the load had written: the sum over
finished dataset results plus the in-flight dataset's counters, and that
dataset's object. The error itself is rethrown unchanged, so its
declared status and message reach the caller as before. `loadDataset`
keeps its insert/update counters in one object the load can read.
`seedLoadPartialWrites` is not re-exported from the package index: the
published surface does not move.
- `protocol-publish-batch.ts`: the `applySeedBodies` catch reads that
record instead of hard-coding zeros. No record means the throw came
before the loader wrote anything (the request parse, or the loader
import), so 0 is the truthful count there.
## Tests
-
`packages/objectql/src/publish-package-drafts-seed-receipt-counts.test.ts`
(new) — the real `publishPackageDrafts` door, real ObjectQL engine, real
`SeedLoaderService`, in-memory driver (objectql carries no sqlite
driver; harness copied from
`publish-package-drafts-response-conformance.test.ts`). Every count is
checked against `engine.find` on the seeded object:
1. seed 1 writes 2 rows, seed 2 names a missing object → `success:
false`, `inserted: 2`, `error` names `seedrepro_nope`; outcome
`published`, `publishedCount: 2` (unchanged).
2. seed 1 UPDATES 2 existing rows, seed 2 fails → `updated: 2`,
`inserted: 0`.
3. control: every seed succeeds → `{ success: true, inserted: 3,
updated: 0 }`, as before.
4. the first seed fails, a good one follows → engine holds 0 rows,
receipt 0/0.
5. the only seed fails → 0/0.
- `packages/metadata-protocol/src/seed-loader-seed-files.test.ts` (3
cases added, reusing that file's pinned engine double) — a throw INSIDE
a dataset after one of its rows landed (the file service failing while
the row's seed file settles): `{ inserted: 3, updated: 0, failedObject:
'pw_doc' }` against the store; a throw before any dataset: `{ inserted:
0, updated: 0 }`; a foreign error carries no reading.
Red first: on the unfixed tree, cases 1 and 2 failed with `expected +0
to be 2`; 3 passed.
## Acceptance notes
- The runtime route `POST /packages/:id/publish-drafts` keeps a fallback
seed apply (`packages/runtime/src/domains/packages.ts`,
`applyPublishedSeeds`, with its own `../seed-loader.js` copy) for
protocols that do not self-apply seeds. Its catch reports `seedApplied`
without `inserted`/`updated` at all. It runs only when the protocol
returned no `seedApplied`; `metadata-protocol`'s `publishPackageDrafts`
always returns one when the batch has a seed, so this door is not
reached by the stock protocol. Not touched here (outside this card's
file surface).
- The failing seed is named by its object, not its metadata name: the
seed bodies handed to `applySeedBodies` carry no name, and threading the
names through would edit `protocol.ts` beyond this card's surface.
## Verification
<details><summary>Ablation — hard-coded zeros restored
(metadata-protocol dist rebuilt, door test)</summary>
```
ablation-replace: ok mutation landed: anchor 1 -> 0, blob 838c1b9 -> abbf2653dc95
✓ dist/: marker present in 2 built files -- the ablation is live in the artifact the suite consumes.
× seed 1 writes 2 rows, seed 2 names a missing object: inserted 2, success false, error names seed 2
× seed 1 UPDATES 2 existing rows, seed 2 fails: updated 2, inserted 0
AssertionError: expected +0 to be 2 // Object.is equality
Tests 2 failed | 3 passed (5)
ablation-replace: ok restored: blob == HEAD (838c1b9) and `git diff HEAD` is empty
-- restore leg: rebuilt; ablation-dist-preflight --absent: marker absent from all 24 built files; tree clean against HEAD
Tests 5 passed (5)
```
</details>
<details><summary>Ablation — in-flight dataset count dropped
(seed-loader, src-resolved)</summary>
```
ablation-replace: ok mutation landed: anchor 1 -> 0, blob 6557a0c -> a234cd9c4dfb
× counts the finished datasets AND the rows the throwing dataset wrote, naming its object
- "inserted": 3,
+ "inserted": 2,
Tests 1 failed | 2 passed (3)
ablation-replace: ok restored: blob == HEAD (6557a0c) and `git diff HEAD` is empty
```
(taken before the cases moved into `seed-loader-seed-files.test.ts`;
same assertions)
</details>
<details><summary>Package tests, typecheck, lint, gates (head
1c64cb6)</summary>
- `@objectstack/metadata-protocol` full suite (after the origin/main
merge, at 86b1fe7): Test Files 281 passed | 4 skipped; Tests 21805
passed | 26 skipped. At 1c64cb6: `seed-loader-seed-files.test.ts` 22
passed; objectql `publish-package-drafts-seed-receipt-counts` +
`protocol-publish-package-drafts` +
`publish-package-drafts-response-conformance` 38 passed.
- `typecheck`: metadata-protocol exit 0, objectql exit 0.
- eslint `--no-inline-config` on `packages/metadata-protocol` and
`packages/objectql`: exit 0.
- `dispatch-gates --commands`: 59 derived families, all run; `--ran`
reconciliation: 58 run, 1 NOT MEASURED — `check:dual-build-cjs-loads`
exit 3, PREREQUISITE NOT MET (it needs every package's dist/; left to
CI).
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(metadata-protocol): when a seed in a package publish fails, the `seedApplied` receipt counts the rows the earlier seeds wrote and names the seed that stopped the load
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
`POST /api/v1/packages/:id/publish-drafts` applies every published seed body in one load. The load writes seed by seed and stops at the first one that throws (for example, a seed naming an object that does not exist); the rows the seeds ahead of it wrote stay written. The receipt used to answer `inserted: 0, updated: 0` all the same, so an author or Studio reading it was told nothing had been written while the table held those rows.
11
+
12
+
Now the receipt keeps its shape and its counters describe what was written:
13
+
14
+
-`inserted` / `updated` are the rows the load wrote before it stopped, including any rows the failing seed itself wrote before it threw.
15
+
-`success` is `false`, as before.
16
+
-`error` names the seed that stopped the load by the object its dataset populates: `seed apply stopped at the seed for object 'OBJECT': ...`, followed by the same sentence as before (a refusal the error declared is quoted, anything else is withheld as `seed apply failed`).
17
+
18
+
Unchanged: which seeds are published, that the seeds after the failing one are not applied, and that nothing is rolled back. A throw before the loader writes anything (a malformed seed body, for example) still answers `inserted: 0, updated: 0` and the same `error` as before.
0 commit comments