Split out of #3911 per Prime Directive #10 — out of scope for the fix in #3926, but it is the reason that bug felt silent to the reporter.
The gap
SeedLoaderService has two distinct failure outcomes, and only one of them is counted:
| Outcome |
errors[] |
errored / totalErrored |
Row written? |
| Record dropped (unresolvable ref, no pass 2; write threw; unresolvable CEL) |
✅ |
✅ |
no |
Field dropped (invalid reference value → delete record[ref.field]) |
✅ |
❌ not incremented |
yes, minus the association |
The field-drop paths (packages/metadata-protocol/src/seed-loader.ts) push a ReferenceResolutionError and log a warn, but never errored++. result.success does flip to false (it reads allErrors.length), so the load is not silent — but every count understates the damage.
Why it matters downstream
packages/runtime/src/app-plugin.ts feeds the CLI boot banner from the summary:
recordSeedOutcome(ctx, { source, inserted, updated, skipped, rejected: totalErrored });
A seed that writes 10 rows, 3 of which lost a lookup, reports rejected: 0. The banner (the one signal that survives os dev's boot-quiet window and the default warn log level — the whole point of #3415/#3430) reads clean. And the warn that does fire says:
[Seeder] Seed loading completed with 0 dropped record(s) and 1 error(s) for <app>
"0 dropped record(s)" is literally true and actively misleading: nothing was dropped — a field was.
Repro
Any invalid reference value on a lookup field, e.g. the wrapper form:
defineSeed(Contact, { externalId: 'email', records: [
{ email: 'a@example.com', account: { externalId: 'Acme' } }, // object, not a natural key
]});
→ row lands without account; summary.totalErrored === 0; banner shows rejected: 0.
Before #3926 a natural-key array on a multiple: true lookup hit the same path — which is exactly why #3911 was reported as "记录落库但整个关联字段缺失 … 仅在日志出现一条 warn".
Suggested direction
Not just errored++ on the field-drop paths — that would overstate the other direction (the row was written, so counting it as a rejected record makes inserted + updated + skipped + rejected stop reconciling against total). Probably a distinct counter, e.g. referencesDropped on SeedLoadResult / SeedLoaderSummary, surfaced by recordSeedOutcome as its own banner column so "wrote the row, lost the link" is a first-class, countable outcome rather than an inference from success: false.
Worth deciding deliberately since it touches the SeedLoaderResult contract in packages/spec/src/data/seed-loader.zod.ts.
Split out of #3911 per Prime Directive #10 — out of scope for the fix in #3926, but it is the reason that bug felt silent to the reporter.
The gap
SeedLoaderServicehas two distinct failure outcomes, and only one of them is counted:errors[]errored/totalErroreddelete record[ref.field])The field-drop paths (
packages/metadata-protocol/src/seed-loader.ts) push aReferenceResolutionErrorand log a warn, but nevererrored++.result.successdoes flip tofalse(it readsallErrors.length), so the load is not silent — but every count understates the damage.Why it matters downstream
packages/runtime/src/app-plugin.tsfeeds the CLI boot banner from the summary:A seed that writes 10 rows, 3 of which lost a lookup, reports
rejected: 0. The banner (the one signal that survivesos dev's boot-quiet window and the defaultwarnlog level — the whole point of #3415/#3430) reads clean. And the warn that does fire says:"0 dropped record(s)" is literally true and actively misleading: nothing was dropped — a field was.
Repro
Any invalid reference value on a lookup field, e.g. the wrapper form:
→ row lands without
account;summary.totalErrored === 0; banner showsrejected: 0.Before #3926 a natural-key array on a
multiple: truelookup hit the same path — which is exactly why #3911 was reported as "记录落库但整个关联字段缺失 … 仅在日志出现一条 warn".Suggested direction
Not just
errored++on the field-drop paths — that would overstate the other direction (the row was written, so counting it as a rejected record makesinserted + updated + skipped + rejectedstop reconciling againsttotal). Probably a distinct counter, e.g.referencesDroppedonSeedLoadResult/SeedLoaderSummary, surfaced byrecordSeedOutcomeas its own banner column so "wrote the row, lost the link" is a first-class, countable outcome rather than an inference fromsuccess: false.Worth deciding deliberately since it touches the
SeedLoaderResultcontract inpackages/spec/src/data/seed-loader.zod.ts.