Found while implementing #11386 (adopting the referenceVia pointer pair on four system objects). Filed unassigned, not fixed there — fixing it means touching the seed loader, which is outside that card.
What was measured
SeedLoaderService defers an unresolvable reference to pass 2 by deleting the column from the row and recording a DeferredUpdate keyed on recordExternalId (packages/metadata-protocol/src/seed-loader.ts, the config.multiPass branch). Pass 2 then looks the row back up by its externalId to write the resolved value.
A dataset that declares no externalId — mode: 'insert', the honest authoring for an object with no natural key — has no such key. Pass 2 resolves the target and then has nowhere to write it, and takes the recordExternalId === '' branch: "Deferred reference DROPPED … has an empty externalId, so no internal id", success: false.
Measured directly, with a positive control that isolates the cause (both now pinned in packages/metadata-protocol/src/seed-loader-pointer-pair.test.ts):
- ledger dataset seeded BEFORE its target,
mode: 'insert' (no externalId) → success: false, the pointer is dropped with the empty-externalId error; - the same seeds, same order, with
externalId declared on the ledger dataset → heals in pass 2, pointer resolves.
So the cause is keylessness of the dataset, not the pointer pair and not the deferral machinery.
Why it matters more after #11386
This is pre-existing and applies to any reference kind, but #11386 widened the blast radius: the four newly adopted objects (sys_audit_log, sys_approval_request, sys_record_share, sys_share_link) are exactly the engine-owned rows that have no natural key of their own, so their seed datasets are the keyless case by default. sys_activity — the object #11339 adopted — is not affected in the same way in practice, and a reader who carried its order-independence over to these four would be wrong.
The failure is LOUD, which is the ruled family direction, so nothing is silently wrong. The cost is that the "pointer pairs contribute no static ordering edge, pass 2 heals it" property that #11339's design leans on does not hold for keyless datasets.
Options (not decided here)
- Leave as is, and document the constraint where pointer pairs are declared (cheapest; the error message already names
externalId, though it does not say "or order the target dataset first"). - Make pass 2 able to write back by the internal id captured at insert time, rather than re-resolving through
externalId — removes the constraint for every reference kind, and is the only option that makes the deferral property honest. - Have the loader refuse or warn at load time when a dataset both declares a pointer pair (or any reference) and has no
externalId, so the author learns the constraint before the ordering bites.
Second, NOT measured
On sys_approval_request, sys_record_share and sys_share_link the id half is also required: true. Since pass 1 defers by deleting the column, a real engine enforcing required on seed writes would reject that insert too — a second, independent road to the same loud failure. The engine double in the seed-loader suite does not validate, so this was deliberately left unasserted rather than claimed either way. Worth confirming against a real driver when this is triaged.
Found while implementing #11386 (adopting the
referenceViapointer pair on four system objects). Filed unassigned, not fixed there — fixing it means touching the seed loader, which is outside that card.What was measured
SeedLoaderServicedefers an unresolvable reference to pass 2 by deleting the column from the row and recording aDeferredUpdatekeyed onrecordExternalId(packages/metadata-protocol/src/seed-loader.ts, theconfig.multiPassbranch). Pass 2 then looks the row back up by its externalId to write the resolved value.A dataset that declares no
externalId—mode: 'insert', the honest authoring for an object with no natural key — has no such key. Pass 2 resolves the target and then has nowhere to write it, and takes therecordExternalId === ''branch: "Deferred reference DROPPED … has an empty externalId, so no internal id",success: false.Measured directly, with a positive control that isolates the cause (both now pinned in
packages/metadata-protocol/src/seed-loader-pointer-pair.test.ts):mode: 'insert'(no externalId) →success: false, the pointer is dropped with the empty-externalId error;externalIddeclared on the ledger dataset → heals in pass 2, pointer resolves.So the cause is keylessness of the dataset, not the pointer pair and not the deferral machinery.
Why it matters more after #11386
This is pre-existing and applies to any reference kind, but #11386 widened the blast radius: the four newly adopted objects (
sys_audit_log,sys_approval_request,sys_record_share,sys_share_link) are exactly the engine-owned rows that have no natural key of their own, so their seed datasets are the keyless case by default.sys_activity— the object #11339 adopted — is not affected in the same way in practice, and a reader who carried its order-independence over to these four would be wrong.The failure is LOUD, which is the ruled family direction, so nothing is silently wrong. The cost is that the "pointer pairs contribute no static ordering edge, pass 2 heals it" property that #11339's design leans on does not hold for keyless datasets.
Options (not decided here)
externalId, though it does not say "or order the target dataset first").externalId— removes the constraint for every reference kind, and is the only option that makes the deferral property honest.externalId, so the author learns the constraint before the ordering bites.Second, NOT measured
On
sys_approval_request,sys_record_shareandsys_share_linkthe id half is alsorequired: true. Since pass 1 defers by deleting the column, a real engine enforcingrequiredon seed writes would reject that insert too — a second, independent road to the same loud failure. The engine double in the seed-loader suite does not validate, so this was deliberately left unasserted rather than claimed either way. Worth confirming against a real driver when this is triaged.