@proteinjs/db 1.47.1 → 1.47.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/src/Table.d.ts +12 -5
  3. package/dist/src/Table.d.ts.map +1 -1
  4. package/dist/src/source/SourceRecord.d.ts +3 -1
  5. package/dist/src/source/SourceRecord.d.ts.map +1 -1
  6. package/dist/src/source/SourceRecordLoader.d.ts +12 -0
  7. package/dist/src/source/SourceRecordLoader.d.ts.map +1 -1
  8. package/dist/src/source/SourceRecordLoader.js +50 -25
  9. package/dist/src/source/SourceRecordLoader.js.map +1 -1
  10. package/dist/test/SearchTokenizer.test.js +2 -2
  11. package/dist/test/SourceRecordLoader.test.js +6 -6
  12. package/dist/test/reusable/CrudTests.js +1 -1
  13. package/dist/test/reusable/CrudTests.js.map +1 -1
  14. package/dist/test/reusable/SourceRecordSyncTests.d.ts +5 -4
  15. package/dist/test/reusable/SourceRecordSyncTests.d.ts.map +1 -1
  16. package/dist/test/reusable/SourceRecordSyncTests.js +238 -34
  17. package/dist/test/reusable/SourceRecordSyncTests.js.map +1 -1
  18. package/dist/test/util/tables/sourceRecordSyncTestTables.d.ts +2 -2
  19. package/dist/test/util/tables/sourceRecordSyncTestTables.d.ts.map +1 -1
  20. package/dist/test/util/tables/sourceRecordSyncTestTables.js +1 -1
  21. package/dist/test/util/tables/sourceRecordSyncTestTables.js.map +1 -1
  22. package/package.json +2 -2
  23. package/src/Table.ts +12 -5
  24. package/src/source/SourceRecord.ts +3 -1
  25. package/src/source/SourceRecordLoader.ts +35 -4
  26. package/test/SearchTokenizer.test.ts +2 -2
  27. package/test/SourceRecordLoader.test.ts +6 -6
  28. package/test/reusable/CrudTests.ts +1 -1
  29. package/test/reusable/SourceRecordSyncTests.ts +168 -29
  30. package/test/util/tables/sourceRecordSyncTestTables.ts +2 -2
package/src/Table.ts CHANGED
@@ -452,11 +452,18 @@ export type SourceRecordOptions<T = any> = {
452
452
  */
453
453
  onSourceRemoved?: 'delete' | 'keep' | { update: Partial<T> };
454
454
  /**
455
- * When set, the sync keys records on this column instead of `id` — matching, adoption, and
456
- * the removed reconcile all use it. An existing row matched by natural key is ADOPTED in
457
- * place: it keeps its id (the declared id is used only for fresh inserts — existing ids may
458
- * be referenced from other tables), gets stamped `is_loaded_from_source = true`, and has its
459
- * declared fields reverted to source. Drift comparison excludes `id`, so adoption converges.
455
+ * When set, the sync keys records on this column instead of `id` — matching and the removed
456
+ * reconcile both use it. A row the sync already owns (`is_loaded_from_source = true`) matched
457
+ * by natural key keeps its id (the declared id is used only for fresh inserts — an
458
+ * environment's own ids may be referenced from other tables) and has its declared fields
459
+ * reverted to source. Drift comparison excludes `id`, so the id difference converges.
460
+ *
461
+ * A row the sync does NOT own that holds the key is never taken over: a natural key is an
462
+ * identity the world shares (an email a person registered, say), unlike a declared id, which
463
+ * only the declaration mints. The declaration is REFUSED for that boot — the row left exactly
464
+ * as it is, the boot continuing, one warning naming the declaration and the key, counted
465
+ * `refused` in the load summary — and lands as a fresh insert on the first boot after that row
466
+ * is gone.
460
467
  *
461
468
  * Preconditions, validated at boot by the loader (loud failures):
462
469
  * - the column is declared unique (`ColumnOptions.unique` or a single-column unique index in
@@ -123,7 +123,9 @@ type OptionalProperties<T> = Pick<
123
123
  /**
124
124
  * Use this to load a record from source into the db.
125
125
  *
126
- * On Db.init, the record will be inserted if it doesn't exist, and updated if it does exist to mirror what is in source.
126
+ * On Db.init, the record will be inserted if it doesn't exist, and updated if it does exist to mirror what is in source —
127
+ * except that a row the sync does not own is never taken over through the table's natural key: the declaration is
128
+ * refused while such a row holds the key (see `SourceRecordOptions.naturalKey`).
127
129
  *
128
130
  * If the SourceRecordLoader is deleted from source, the record will be deleted from the db on server startup (per the
129
131
  * table's `onSourceRemoved` policy) — by the next boot of a build that still carries the declaring package at its
@@ -25,6 +25,12 @@ export type SourceRecordTableLoadSummary = {
25
25
  updates: number;
26
26
  unchanged: number;
27
27
  adopted: number;
28
+ /**
29
+ * Declarations whose natural key is held by a row the sync does not own (written at runtime —
30
+ * a person's own row, say): refused for this boot, that row left exactly as it is (see
31
+ * {@link SourceRecordOptions.naturalKey}).
32
+ */
33
+ refused: number;
28
34
  deletes: number;
29
35
  removedUpdates: number;
30
36
  /** Rows stamped by a newer version of their declaring package — left exactly as they are. */
@@ -59,6 +65,12 @@ export class SourceRecordLoader {
59
65
  * tables it no longer declares anything for: dropping the last declaration is a removal.
60
66
  * - Rows with no owner stamp (written before `source_package` existed) are claimed by the
61
67
  * declaration that still matches them; the rest are reported as unowned and left alone.
68
+ * - A declaration never takes over a row the sync does not own through a natural key: a
69
+ * natural key (an email, say) is an identity the world shares, so a runtime row holding it
70
+ * may be anyone's — that declaration is REFUSED for the boot (the row untouched, the boot
71
+ * continuing, one warning naming the declaration, counted `refused`). A declared id, minted
72
+ * by the declaration itself, still claims a runtime row holding it (see
73
+ * {@link SourceRecordOptions.naturalKey}).
62
74
  * - The version stamp records the version that last WROTE the row (its insert or its last
63
75
  * redefinition). A newer version whose declaration is unchanged writes nothing — it neither
64
76
  * re-stamps the version nor bumps `updated` — so a release that touches no declaration
@@ -100,6 +112,7 @@ export class SourceRecordLoader {
100
112
  let updateCount = 0;
101
113
  let unchangedCount = 0;
102
114
  let adoptedCount = 0;
115
+ let refusedCount = 0;
103
116
  let skippedNewer = removed.skippedNewer;
104
117
  for (const { source, qualifiedName, name, record } of records) {
105
118
  let sourceRecord = record;
@@ -135,6 +148,21 @@ export class SourceRecordLoader {
135
148
  }
136
149
 
137
150
  if (existingRecord) {
151
+ if (existingRecord.isLoadedFromSource !== true && keyProperty !== 'id') {
152
+ // A natural key held by a row the sync does not own: never taken over (see load's
153
+ // ownership model). The row stays exactly as it is and the boot goes on.
154
+ refusedCount += 1;
155
+ this.logger.warn({
156
+ message: `(${table.name}) Refused declaration '${name}': a row the sync does not own holds ${keyProperty} '${(sourceRecord as any)[keyProperty]}' — the row is left untouched and the declaration does not load while it stands`,
157
+ obj: {
158
+ declaration: qualifiedName,
159
+ [keyProperty]: (sourceRecord as any)[keyProperty],
160
+ id: existingRecord.id,
161
+ },
162
+ });
163
+ continue;
164
+ }
165
+
138
166
  if (
139
167
  existingRecord.sourcePackage === source &&
140
168
  this.isNewerStamp(existingRecord.sourcePackageVersion, sourceVersion)
@@ -147,14 +175,16 @@ export class SourceRecordLoader {
147
175
  }
148
176
 
149
177
  if (existingRecord.id !== sourceRecord.id) {
150
- // Adopt in place: the existing row keeps its id — other tables may reference it.
151
- // The declared id is only ever used for fresh inserts.
178
+ // A natural-key match on a row the sync owns under an environment's own id (one an
179
+ // earlier build adopted): the row keeps its id — other tables may reference it. The
180
+ // declared id is only ever used for fresh inserts.
152
181
  sourceRecord = { ...sourceRecord, id: existingRecord.id };
153
182
  }
154
183
 
155
184
  if (existingRecord.isLoadedFromSource !== true) {
156
- // A pre-existing (runtime-created) row is being taken over by a declaration —
157
- // deliberate, but loud: a declaration asserts ownership of the row's identity.
185
+ // A runtime-created row holding the declaration's OWN id (natural-key tables never
186
+ // reach here — refused above) is being taken over by that declaration — deliberate,
187
+ // but loud: a declaration asserts ownership of the row's identity.
158
188
  adoptedCount += 1;
159
189
  this.logger.info({
160
190
  message: `(${table.name}) Adopting existing record into source ownership`,
@@ -196,6 +226,7 @@ export class SourceRecordLoader {
196
226
  updates: updateCount,
197
227
  unchanged: unchangedCount,
198
228
  adopted: adoptedCount,
229
+ refused: refusedCount,
199
230
  deletes: removed.deleteCount,
200
231
  removedUpdates: removed.removedUpdateCount,
201
232
  skippedNewer,
@@ -24,8 +24,8 @@ describe('SearchTokenizer', () => {
24
24
  });
25
25
 
26
26
  test('normalization: case folds, punctuation splits words, numbers are word characters', () => {
27
- const tokens = tokenizer.tokensForValue('N3xa-Dev NOTES');
28
- expect(tokens).toContain('w:n3xa');
27
+ const tokens = tokenizer.tokensForValue('Utf8-Dev NOTES');
28
+ expect(tokens).toContain('w:utf8');
29
29
  expect(tokens).toContain('w:dev');
30
30
  expect(tokens).toContain('w:notes');
31
31
  });
@@ -173,7 +173,7 @@ describe('SourceRecordLoader.hasChanges — stamp hygiene on the migration ledge
173
173
  description: 'Marks every existing account onboarded',
174
174
  run: async () => undefined,
175
175
  isLoadedFromSource: true,
176
- sourcePackage: '@n3xa/app-server',
176
+ sourcePackage: '@acme/app-server',
177
177
  sourcePackageVersion: '1.24.0',
178
178
  ...overrides,
179
179
  });
@@ -183,7 +183,7 @@ describe('SourceRecordLoader.hasChanges — stamp hygiene on the migration ledge
183
183
  id: 'bee7a15c-369f-4e77-abf7-ccd5dc3ae60c',
184
184
  description: 'Marks every existing account onboarded',
185
185
  isLoadedFromSource: true,
186
- sourcePackage: '@n3xa/app-server',
186
+ sourcePackage: '@acme/app-server',
187
187
  sourcePackageVersion: '1.23.0',
188
188
  status: 'success',
189
189
  startTime: moment('2026-08-30T10:00:00Z'),
@@ -228,16 +228,16 @@ describe('getSourceRecordLoaders — declaration identity', () => {
228
228
  it("carries the declaration's own name (the loader class) beside its package and qualified name", () => {
229
229
  namedObjectCache()['@proteinjs/db/SourceRecordLoader'] = [
230
230
  {
231
- qualifiedName: '@n3xa/app-server/BackfillOnboardingStateForExistingAccounts',
232
- packageName: '@n3xa/app-server',
231
+ qualifiedName: '@acme/app-server/BackfillOnboardingStateForExistingAccounts',
232
+ packageName: '@acme/app-server',
233
233
  object: { table: {}, record: {} },
234
234
  },
235
235
  ];
236
236
 
237
237
  const [declaration] = getSourceRecordLoaders() as any[];
238
238
  expect(declaration).toMatchObject({
239
- source: '@n3xa/app-server',
240
- qualifiedName: '@n3xa/app-server/BackfillOnboardingStateForExistingAccounts',
239
+ source: '@acme/app-server',
240
+ qualifiedName: '@acme/app-server/BackfillOnboardingStateForExistingAccounts',
241
241
  name: 'BackfillOnboardingStateForExistingAccounts',
242
242
  });
243
243
  });
@@ -34,7 +34,7 @@ export const crudTests = (
34
34
  name: 'Veronican',
35
35
  department: 'Cake Factory',
36
36
  object:
37
- '{"cookie":{"originalMaxAge":5184000000,"expires":"2024-07-08T06:16:07.134Z","httpOnly":true,"path":"/"},"passport":{"user":"brent@n3xa.io"}}',
37
+ '{"cookie":{"originalMaxAge":5184000000,"expires":"2024-07-08T06:16:07.134Z","httpOnly":true,"path":"/"},"passport":{"user":"someone@example.com"}}',
38
38
  },
39
39
  { id: insertedEmployee.id }
40
40
  );
@@ -52,10 +52,11 @@ class RecordingMachineAccountWatcher implements TableWatcher<SyncMachineAccount>
52
52
 
53
53
  /**
54
54
  * Emulator-backed outcome tests for the source-record sync's mixed-table semantics:
55
- * declare-only ownership (human rows structurally untouchable), natural-key adoption
56
- * (existing row keeps its id and runtime fields), `onSourceRemoved` policies (flag-not-delete
57
- * through `Db.update` so watchers fire; default delete unchanged), boot-time natural-key
58
- * validation, and the unique-index duplicate preflight.
55
+ * declare-only ownership (human rows structurally untouchable), natural-key sync (an owned row
56
+ * keeps its id and runtime fields; a row the sync does not own refuses the declaration and is
57
+ * never taken over), `onSourceRemoved` policies (flag-not-delete through `Db.update` so
58
+ * watchers fire; default delete unchanged), boot-time natural-key validation, and the
59
+ * unique-index duplicate preflight.
59
60
  */
60
61
  export const sourceRecordSyncTests = (
61
62
  driver: DbDriver,
@@ -201,39 +202,171 @@ export const sourceRecordSyncTests = (
201
202
  expect(afterIdleBoot.updated.valueOf()).toBe(stampBefore);
202
203
  });
203
204
 
204
- test('natural-key adoption: an existing row is adopted in place — id and runtime fields preserved', async () => {
205
+ test('a natural key held by a row the sync does not own refuses the declaration: the row untouched, the boot continues, the refusal counted and named', async () => {
205
206
  const db = getDbAsSystem();
206
- // The hand-made bridge row: env-random id, runtime-provisioned fields, never flagged.
207
- const handMade = await db.insert(machineTable, {
207
+ // A row written at runtime that happens to hold the declared natural key — a person's own
208
+ // row (a signup under the address before the declaration shipped): their fields, their
209
+ // credential (the runtime note stands in for the password hash), never source-owned.
210
+ const persons = await db.insert(machineTable, {
208
211
  email: 'bridge@test.local',
209
- displayName: 'Hand-made bridge',
212
+ displayName: 'A person',
213
+ runtimeNote: 'their-password-hash',
214
+ });
215
+ const bridge = machineDeclaration({ id: 'declared-id', email: 'bridge@test.local', displayName: 'Ops bridge' });
216
+ const sibling = machineDeclaration({ id: 'sibling-id', email: 'sibling@test.local', displayName: 'Sibling' });
217
+
218
+ const logged: string[] = [];
219
+ const consoleSpies = (['log', 'info', 'warn', 'error'] as const).map((level) =>
220
+ jest.spyOn(console, level).mockImplementation((...parts: unknown[]) => {
221
+ logged.push(parts.map((part) => (typeof part === 'string' ? part : JSON.stringify(part))).join(' '));
222
+ })
223
+ );
224
+ const summary = await boot([bridge, sibling]).finally(() => consoleSpies.forEach((spy) => spy.mockRestore()));
225
+
226
+ // The boot continued: the sibling declaration landed; the refused one is counted, not adopted.
227
+ expect(summary[machineTable.name]).toMatchObject({ inserts: 1, adopted: 0, refused: 1 });
228
+ expect(await db.get(machineTable, { id: 'sibling-id' })).toMatchObject({ isLoadedFromSource: true });
229
+ // The person's row is exactly as it was: no declared field, no ownership stamp, no write.
230
+ const after = await db.get(machineTable, { id: persons.id });
231
+ expect(after).toMatchObject({
232
+ email: 'bridge@test.local',
233
+ displayName: 'A person',
234
+ runtimeNote: 'their-password-hash',
235
+ });
236
+ expect(after.isLoadedFromSource).toBeFalsy();
237
+ expect(after.sourcePackage).toBeFalsy();
238
+ expect(after.status).toBeFalsy();
239
+ expect(after.updated.valueOf()).toBe(persons.updated.valueOf());
240
+ expect(RecordingMachineAccountWatcher.updates.filter((update) => update.id === persons.id)).toHaveLength(0);
241
+ // Nothing was inserted under the declared id, and nothing is registered as a source record.
242
+ expect(await db.get(machineTable, { id: 'declared-id' })).toBeUndefined();
243
+ expect(new SourceRecordRepo().getSourceRecord(machineTable.name, persons.id)).toBeUndefined();
244
+ expect(new SourceRecordRepo().getSourceRecord(machineTable.name, 'declared-id')).toBeUndefined();
245
+ // One plain line names the declaration and the reason.
246
+ const refusals = logged.filter((line) => line.includes('Refused declaration'));
247
+ expect(refusals).toHaveLength(1);
248
+ expect(refusals[0]).toContain('SeededLoader0');
249
+ expect(refusals[0]).toContain('bridge@test.local');
250
+ expect(refusals[0]).toContain('does not own');
251
+
252
+ // Idempotent: the next boot refuses again and still writes nothing to the row.
253
+ const again = await boot([bridge, sibling]);
254
+ expect(again[machineTable.name]).toMatchObject({ inserts: 0, refused: 1 });
255
+ expect((await db.get(machineTable, { id: persons.id })).updated.valueOf()).toBe(persons.updated.valueOf());
256
+
257
+ // Once the row is gone, the declaration lands as a fresh insert under its declared id.
258
+ await db.delete(machineTable, { id: persons.id });
259
+ const landed = await boot([bridge, sibling]);
260
+ expect(landed[machineTable.name]).toMatchObject({ inserts: 1, refused: 0 });
261
+ const fresh = await db.get(machineTable, { email: 'bridge@test.local' });
262
+ expect(fresh).toMatchObject({ id: 'declared-id', displayName: 'Ops bridge', isLoadedFromSource: true });
263
+ expect(fresh.runtimeNote).toBeFalsy();
264
+ });
265
+
266
+ test('a row the sync already owns keeps syncing by its natural key under an environment-random id — id and runtime fields kept, converged boots write nothing', async () => {
267
+ const db = getDbAsSystem();
268
+ // The shape of an account a deployed environment adopted in an earlier build: source-owned,
269
+ // stamped by its package, holding the id the environment gave it (not the declared one),
270
+ // with a provisioned credential and a declared field that has drifted.
271
+ const owned = await db.insert(machineTable, {
272
+ email: 'bridge@test.local',
273
+ displayName: 'Drifted',
210
274
  runtimeNote: 'the-password-hash',
275
+ status: 'active',
276
+ isLoadedFromSource: true,
277
+ sourcePackage: DEFAULT_TEST_SOURCE,
278
+ });
279
+ const bridge = machineDeclaration({
280
+ id: 'declared-owned-id',
281
+ email: 'bridge@test.local',
282
+ displayName: 'Ops bridge',
211
283
  });
212
284
 
213
- await boot([machineDeclaration({ id: 'declared-id', email: 'bridge@test.local', displayName: 'Ops bridge' })]);
285
+ const summary = await boot([bridge]);
214
286
 
287
+ expect(summary[machineTable.name]).toMatchObject({ updates: 1, refused: 0, inserts: 0 });
215
288
  const rows = await machineRows({ email: 'bridge@test.local' });
216
289
  expect(rows).toHaveLength(1);
217
- // Adopted: the existing id survives (scoped rows reference it); declared fields reverted;
218
- // runtime fields (the credential) preserved; the row is now source-owned.
219
290
  expect(rows[0]).toMatchObject({
220
- id: handMade.id,
291
+ id: owned.id,
221
292
  displayName: 'Ops bridge',
222
- status: 'active',
223
293
  runtimeNote: 'the-password-hash',
224
294
  isLoadedFromSource: true,
225
295
  });
226
- expect(await db.get(machineTable, { id: 'declared-id' })).toBeUndefined();
227
-
228
- // The in-process repo registers the record under the ADOPTED id, not the declared one.
229
- expect(new SourceRecordRepo().getSourceRecord(machineTable.name, handMade.id)).toBeDefined();
230
- expect(new SourceRecordRepo().getSourceRecord(machineTable.name, 'declared-id')).toBeUndefined();
296
+ expect(await db.get(machineTable, { id: 'declared-owned-id' })).toBeUndefined();
297
+ // Registered under the row's own id, not the declared one.
298
+ expect(new SourceRecordRepo().getSourceRecord(machineTable.name, owned.id)).toBeDefined();
299
+ expect(new SourceRecordRepo().getSourceRecord(machineTable.name, 'declared-owned-id')).toBeUndefined();
231
300
 
232
- // Adoption converges: the id difference is not perpetual drift.
301
+ // The id difference is not perpetual drift.
233
302
  const stampBefore = rows[0].updated.valueOf();
234
- await boot([machineDeclaration({ id: 'declared-id', email: 'bridge@test.local', displayName: 'Ops bridge' })]);
235
- const afterIdleBoot = await db.get(machineTable, { id: handMade.id });
236
- expect(afterIdleBoot.updated.valueOf()).toBe(stampBefore);
303
+ await boot([bridge]);
304
+ expect((await db.get(machineTable, { id: owned.id })).updated.valueOf()).toBe(stampBefore);
305
+ });
306
+
307
+ test('an owned row that lost its ownership marker outside the sync is refused like any unowned row — never re-stamped, the row and its runtime fields standing; the marker set back is the recovery', async () => {
308
+ const db = getDbAsSystem();
309
+ const bridge = machineDeclaration({
310
+ id: 'declared-lost-id',
311
+ email: 'bridge@test.local',
312
+ displayName: 'Ops bridge',
313
+ });
314
+ // Boot 1: the declaration lands as a fresh insert — owned and stamped; a credential is minted at runtime.
315
+ expect((await boot([bridge]))[machineTable.name]).toMatchObject({ inserts: 1, refused: 0 });
316
+ const owned = await db.get(machineTable, { email: 'bridge@test.local' });
317
+ expect(owned).toMatchObject({
318
+ id: 'declared-lost-id',
319
+ isLoadedFromSource: true,
320
+ sourcePackage: DEFAULT_TEST_SOURCE,
321
+ });
322
+ await db.update(machineTable, { id: owned.id, runtimeNote: 'the-minted-hash' });
323
+
324
+ // The marker is cleared outside the sync (a migration, a hand edit — the service door protects it).
325
+ await db.update(machineTable, { id: owned.id, isLoadedFromSource: false });
326
+
327
+ // Boot 2: the sync no longer owns the row, so the declaration is refused and the row is left as
328
+ // it is — its declared fields, its credential and its status stand; nothing re-stamps it.
329
+ const refusedBoot = await boot([bridge]);
330
+ expect(refusedBoot[machineTable.name]).toMatchObject({ inserts: 0, updates: 0, adopted: 0, refused: 1 });
331
+ const after = await db.get(machineTable, { id: owned.id });
332
+ expect(after).toMatchObject({
333
+ email: 'bridge@test.local',
334
+ displayName: 'Ops bridge',
335
+ status: 'active',
336
+ runtimeNote: 'the-minted-hash',
337
+ isLoadedFromSource: false,
338
+ });
339
+ expect(await db.get(machineTable, { id: 'declared-lost-id-other' })).toBeUndefined();
340
+
341
+ // The recovery is the marker set back: the next boot owns the row again and writes nothing else.
342
+ await db.update(machineTable, { id: owned.id, isLoadedFromSource: true });
343
+ expect((await boot([bridge]))[machineTable.name]).toMatchObject({ inserts: 0, refused: 0, unchanged: 1 });
344
+ expect(await db.get(machineTable, { id: owned.id })).toMatchObject({
345
+ runtimeNote: 'the-minted-hash',
346
+ isLoadedFromSource: true,
347
+ });
348
+ });
349
+
350
+ test('an id-keyed declaration still claims a runtime row holding its own declared id — the id is minted by the declaration, never shared', async () => {
351
+ const db = getDbAsSystem();
352
+ // A row written before the table was source-loaded, under the id the declaration carries
353
+ // (the insert type omits `id`; the loader's own inserts pass it the same way).
354
+ await db.insert(defaultPolicyTable, {
355
+ id: 'legacy-id',
356
+ email: 'legacy@test.local',
357
+ displayName: 'Old name',
358
+ } as unknown as { email: string });
359
+
360
+ const summary = await boot([
361
+ { table: defaultPolicyTable, record: { id: 'legacy-id', email: 'legacy@test.local', displayName: 'Declared' } },
362
+ ]);
363
+
364
+ expect(summary[defaultPolicyTable.name]).toMatchObject({ adopted: 1, refused: 0 });
365
+ expect(await db.get(defaultPolicyTable, { id: 'legacy-id' })).toMatchObject({
366
+ displayName: 'Declared',
367
+ isLoadedFromSource: true,
368
+ sourcePackage: DEFAULT_TEST_SOURCE,
369
+ });
237
370
  });
238
371
 
239
372
  test('onSourceRemoved update: removed rows are flagged through Db.update (watchers fire), never deleted; re-declaring reverts', async () => {
@@ -520,10 +653,16 @@ export const sourceRecordSyncTests = (
520
653
  expect(deactivations).toHaveLength(0);
521
654
  });
522
655
 
523
- test('cross-source boots leave a natural-key-adopted row in place — adopted id preserved', async () => {
656
+ test('cross-source boots leave a natural-key-synced row in place — its environment id preserved', async () => {
524
657
  const db = getDbAsSystem();
525
- // Hand-made row with an environment-random id, adopted by pkg-b via natural key.
526
- const handMade = await db.insert(machineTable, { email: 'bridge2@test.local', runtimeNote: 'cred' });
658
+ // A row pkg-b owns under an environment-random id (adopted by an earlier build), synced
659
+ // by natural key.
660
+ const handMade = await db.insert(machineTable, {
661
+ email: 'bridge2@test.local',
662
+ runtimeNote: 'cred',
663
+ isLoadedFromSource: true,
664
+ sourcePackage: '@test/pkg-b',
665
+ });
527
666
  await bootAsSource('@test/pkg-b', [
528
667
  { table: machineTable, record: { id: 'declared-bridge2', email: 'bridge2@test.local', status: 'active' } },
529
668
  ]);
@@ -612,9 +751,9 @@ export const sourceRecordSyncTests = (
612
751
 
613
752
  /**
614
753
  * Version-scoped pruning WITHIN one package: two servers running different VERSIONS of the
615
- * same package against one shared database (the actual brent-dev-2 incident — the main app
616
- * at thought-common 3.29.0 and a feature worktree at 3.32.0, whose added thought types the
617
- * older build deleted on every boot). Every row carries the declaring package's version;
754
+ * same package against one shared database (a real incident — the main application at one
755
+ * version of a shared package and a feature worktree at a later one, whose added declarations
756
+ * the older build deleted on every boot). Every row carries the declaring package's version;
618
757
  * a boot never prunes (or flags) a row stamped by a NEWER version of the same package.
619
758
  * Within the same or an older stamped version, the package stays authoritative:
620
759
  * removals still land, and equal-version skew is last-writer-wins by design.
@@ -625,7 +764,7 @@ export const sourceRecordSyncTests = (
625
764
  test('an older build of a package never prunes rows a newer build of the same package declared (the incident)', async () => {
626
765
  const db = getDbAsSystem();
627
766
 
628
- // The newer build (e.g. thought-common 3.32.0) declares an added type k-2.
767
+ // The newer build (3.32.0) declares an added type k-2.
629
768
  await bootAsSource('@test/pkg-a', [decl('k-1', 'k1@test.local'), decl('k-2', 'k2@test.local')], '3.32.0');
630
769
  // The older build (3.29.0) boots without k-2 — pre-fix this deleted k-2 every boot.
631
770
  await bootAsSource('@test/pkg-a', [decl('k-1', 'k1@test.local')], '3.29.0');
@@ -9,7 +9,7 @@ import {
9
9
 
10
10
  /**
11
11
  * Fixtures for the source-record sync suites ({@link sourceRecordSyncTests}): a machine-account-
12
- * shaped mixed table (natural-key adoption + flag-on-removed), a default-policy table (removed =
12
+ * shaped mixed table (natural-key sync + flag-on-removed), a default-policy table (removed =
13
13
  * deleted), a misdeclared natural-key table (non-unique column), and the two-generation table
14
14
  * pair driving the unique-index duplicate preflight.
15
15
  *
@@ -27,7 +27,7 @@ export interface SyncMachineAccount extends SourceRecord {
27
27
  runtimeNote?: string | null;
28
28
  }
29
29
 
30
- /** The machine-account shape: natural-key adoption by email, removed rows flagged not deleted. */
30
+ /** The machine-account shape: synced by email, removed rows flagged not deleted. */
31
31
  export class SyncMachineAccountTable extends Table<SyncMachineAccount> {
32
32
  name = 'db_test_sync_machine_account';
33
33
  columns: Table<SyncMachineAccount>['columns'] = withSourceRecordColumns<SyncMachineAccount>({