mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 18:41:08 +02:00
refactor(workbench-seeding): drop transitional code paths, finalise via v50
With the smart hook always stamping the active-Space id and the seeder running the deterministic-id contract, the transitional concessions introduced for the soft-cleanup window are no longer load-bearing: - `seedWorkbenchHomeOn` no longer scans for a legacy random-uuid Home in the same Space and defers to it. It just no-ops on a present deterministic-id row, otherwise inserts. The corresponding three transitional unit tests are dropped. - `(app)/+layout.svelte` no longer runs a post-`reconcileSentinels` dedup sweep. The sweep was belt-and-suspenders for an edge case that the smart hook + deterministic id structurally prevent. D-hard via Dexie v50: soft-deletes every uncustomised "Home" row whose id is NOT `seed-home-<spaceId>`. The per-space-seeds registry recreates a fresh deterministic-id row on the next `setActiveSpace` for any Space that lost its uncustomised Home, so the system self- heals. Customised Homes (description / wallpaper / agent / scope tags) are preserved. Combined with v48 this leaves zero legacy duplicates and zero random-UUID seeds in `workbenchScenes` once the upgrade runs. New devices coming up against an empty IndexedDB walk through both upgrades as no-ops and land on the clean state directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a6c5397d10
commit
fa71269fc8
4 changed files with 41 additions and 115 deletions
|
|
@ -1144,6 +1144,43 @@ db.version(49).stores({
|
|||
comicCharacters: 'id, createdAt, style, isFavorite, isArchived',
|
||||
});
|
||||
|
||||
// v50 — Final normalisation step for the workbench-Home seeding
|
||||
// cleanup. Soft-deletes every uncustomised "Home" row whose id is NOT
|
||||
// the deterministic `seed-home-<spaceId>`. After this upgrade runs,
|
||||
// the only surviving Home rows in any Space are either user-customised
|
||||
// (description / wallpaper / agent / scope) or already on the
|
||||
// deterministic-id contract. The per-space-seeds registry recreates a
|
||||
// fresh `seed-home-<spaceId>` row on the next `setActiveSpace` for
|
||||
// any Space that lost its uncustomised Home, making the system self-
|
||||
// healing. Together with v48 this leaves zero legacy duplicates or
|
||||
// random-UUID seeds in the table. See
|
||||
// docs/plans/workbench-seeding-cleanup.md.
|
||||
db.version(50).upgrade(async (tx) => {
|
||||
const now = new Date().toISOString();
|
||||
let removed = 0;
|
||||
await tx
|
||||
.table('workbenchScenes')
|
||||
.toCollection()
|
||||
.modify((row: Record<string, unknown>) => {
|
||||
if (row.deletedAt) return;
|
||||
if (row.name !== 'Home') return;
|
||||
if (typeof row.id !== 'string' || row.id.startsWith('seed-home-')) return;
|
||||
if (row.description) return;
|
||||
if (row.wallpaper) return;
|
||||
if (row.viewingAsAgentId) return;
|
||||
const scope = row.scopeTagIds;
|
||||
if (Array.isArray(scope) && scope.length > 0) return;
|
||||
row.deletedAt = now;
|
||||
row.updatedAt = now;
|
||||
removed += 1;
|
||||
});
|
||||
if (removed > 0) {
|
||||
console.info(
|
||||
`[workbench-scenes v50] retired ${removed} legacy Home rows to deterministic-id contract`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Sync Routing ──────────────────────────────────────────
|
||||
// SYNC_APP_MAP, TABLE_TO_SYNC_NAME, TABLE_TO_APP, SYNC_NAME_TO_TABLE,
|
||||
// toSyncName() and fromSyncName() are now derived from per-module
|
||||
|
|
|
|||
|
|
@ -103,68 +103,6 @@ describe('seedWorkbenchHomeOn', () => {
|
|||
expect(row?.openApps).toEqual([{ appId: 'todo' }, { appId: 'mood' }, { appId: 'meditate' }]);
|
||||
});
|
||||
|
||||
it('defers to a legacy random-uuid Home in the same Space (transitional)', async () => {
|
||||
// Simulates a user coming from the pre-deterministic-id world —
|
||||
// e.g. a Schicht D-soft dedup survivor with a random UUID and
|
||||
// the default openApps shape. The new seeder must NOT create a
|
||||
// second deterministic-id row alongside it, otherwise +layout's
|
||||
// dedup pass would just churn through soft-deleting one of them.
|
||||
await db.workbenchScenes.add({
|
||||
id: 'legacy-random-uuid-1234',
|
||||
name: 'Home',
|
||||
order: 0,
|
||||
openApps: [{ appId: 'todo' }, { appId: 'calendar' }, { appId: 'notes' }],
|
||||
createdAt: '2026-04-23T08:00:00.000Z',
|
||||
updatedAt: '2026-04-23T08:00:00.000Z',
|
||||
spaceId: 'space-abc',
|
||||
} as LocalWorkbenchScene & { spaceId: string });
|
||||
|
||||
const inserted = await seedWorkbenchHomeOn(db.workbenchScenes, 'space-abc');
|
||||
expect(inserted).toBe(false);
|
||||
|
||||
const all = await db.workbenchScenes.toArray();
|
||||
expect(all).toHaveLength(1);
|
||||
expect(all[0].id).toBe('legacy-random-uuid-1234');
|
||||
});
|
||||
|
||||
it('still seeds when the existing Home is in a different Space', async () => {
|
||||
await db.workbenchScenes.add({
|
||||
id: 'legacy-random-uuid-other-space',
|
||||
name: 'Home',
|
||||
order: 0,
|
||||
openApps: DEFAULT_HOME_APPS,
|
||||
createdAt: '2026-04-23T08:00:00.000Z',
|
||||
updatedAt: '2026-04-23T08:00:00.000Z',
|
||||
spaceId: 'space-different',
|
||||
} as LocalWorkbenchScene & { spaceId: string });
|
||||
|
||||
const inserted = await seedWorkbenchHomeOn(db.workbenchScenes, 'space-abc');
|
||||
expect(inserted).toBe(true);
|
||||
|
||||
const seeded = await db.workbenchScenes.get('seed-home-space-abc');
|
||||
expect(seeded).toBeDefined();
|
||||
});
|
||||
|
||||
it('still seeds when the existing Home in this Space is customised', async () => {
|
||||
// A user-customised "Home" (renamed-back, with a description or
|
||||
// wallpaper) shouldn't block fresh seeds — the dedup heuristic
|
||||
// already excludes such rows from merging, so the symmetric
|
||||
// behaviour here is to seed anyway. Schicht D-hard will normalise.
|
||||
await db.workbenchScenes.add({
|
||||
id: 'user-custom-home',
|
||||
name: 'Home',
|
||||
description: 'My personal layout',
|
||||
order: 0,
|
||||
openApps: [{ appId: 'todo' }],
|
||||
createdAt: '2026-04-23T08:00:00.000Z',
|
||||
updatedAt: '2026-04-23T08:00:00.000Z',
|
||||
spaceId: 'space-abc',
|
||||
} as LocalWorkbenchScene & { spaceId: string });
|
||||
|
||||
const inserted = await seedWorkbenchHomeOn(db.workbenchScenes, 'space-abc');
|
||||
expect(inserted).toBe(true);
|
||||
});
|
||||
|
||||
it('seeds independently per Space (no cross-pollination)', async () => {
|
||||
await seedWorkbenchHomeOn(db.workbenchScenes, 'space-A');
|
||||
await seedWorkbenchHomeOn(db.workbenchScenes, 'space-B');
|
||||
|
|
|
|||
|
|
@ -8,14 +8,13 @@
|
|||
* Idempotency is structural: the row id is `seed-home-${spaceId}`
|
||||
* (deterministic) and the seeder no-ops if the row already exists.
|
||||
* Re-running for the same Space is a no-op — no duplicate possible
|
||||
* regardless of how the boot/replay timing shakes out. This is the
|
||||
* structural answer to the bug Schicht D-soft cleaned up after.
|
||||
* regardless of how the boot/replay timing shakes out.
|
||||
*
|
||||
* The actual write is split out into `seedWorkbenchHomeOn(table, ...)`
|
||||
* so unit tests can drive it against a fixture Dexie instance without
|
||||
* pulling in `database.ts`'s side-effect imports.
|
||||
*
|
||||
* See docs/plans/workbench-seeding-cleanup.md §"Schicht B + C".
|
||||
* See docs/plans/workbench-seeding-cleanup.md.
|
||||
*/
|
||||
|
||||
import type { Table } from 'dexie';
|
||||
|
|
@ -49,18 +48,9 @@ export function workbenchHomeSeedId(spaceId: string): string {
|
|||
/**
|
||||
* Pure-ish: takes a Dexie Table reference, ensures a Home scene exists
|
||||
* for the given Space. Returns true when a new row was inserted, false
|
||||
* when an existing Home is honoured. The creating-hook stamps the
|
||||
* actor / timestamps fields; this function only owns the
|
||||
* when the deterministic-id row was already there. The creating-hook
|
||||
* stamps the actor / timestamps fields; this function only owns the
|
||||
* deterministic-id + default-shape contract.
|
||||
*
|
||||
* Two reasons we may skip the insert:
|
||||
* 1. The deterministic-id row already exists — the structural
|
||||
* idempotency case. Re-running for the same Space is a no-op.
|
||||
* 2. A legacy random-uuid Home already exists for this Space — the
|
||||
* transitional case for users coming from the pre-deterministic
|
||||
* world (Schicht D-soft survivors). We defer to the user's
|
||||
* existing layout. Schicht D-hard will rename such rows to the
|
||||
* deterministic id and this branch becomes dead code.
|
||||
*/
|
||||
export async function seedWorkbenchHomeOn(
|
||||
table: Table<LocalWorkbenchScene, string>,
|
||||
|
|
@ -69,28 +59,6 @@ export async function seedWorkbenchHomeOn(
|
|||
const id = workbenchHomeSeedId(spaceId);
|
||||
if (await table.get(id)) return false;
|
||||
|
||||
// Transitional check: a Home scene already exists for this Space
|
||||
// under a different (legacy random) id. Skipping here avoids an
|
||||
// unnecessary create-then-soft-delete roundtrip via the dedup pass
|
||||
// in `+layout.svelte`, which would otherwise pick the customised
|
||||
// legacy row as the survivor and nuke our just-inserted seed.
|
||||
// Looks at the same "uncustomised default seed" shape the dedup
|
||||
// function uses, so a deliberately-named "Home" with description /
|
||||
// wallpaper / agent / scope still triggers a fresh seed.
|
||||
const legacy = await table
|
||||
.filter((r) => {
|
||||
if (r.deletedAt) return false;
|
||||
if (r.name !== 'Home') return false;
|
||||
if ((r as { spaceId?: unknown }).spaceId !== spaceId) return false;
|
||||
if (r.description) return false;
|
||||
if (r.wallpaper) return false;
|
||||
if (r.viewingAsAgentId) return false;
|
||||
if (r.scopeTagIds && r.scopeTagIds.length > 0) return false;
|
||||
return true;
|
||||
})
|
||||
.first();
|
||||
if (legacy) return false;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const row: LocalWorkbenchScene & { spaceId: string } = {
|
||||
id,
|
||||
|
|
|
|||
|
|
@ -620,23 +620,6 @@
|
|||
if (rewritten > 0) {
|
||||
console.info(`[spaces] reconciled ${rewritten} sentinel records to active space`);
|
||||
}
|
||||
|
||||
// Belt-and-suspenders dedup of duplicate "Home" workbench
|
||||
// scenes. The Dexie v48 upgrade already does one pass at
|
||||
// schema-bump time; this second pass covers the edge case
|
||||
// where reconcileSentinels just collapsed sentinel-stamped
|
||||
// rows into the same space-id as already-reconciled rows,
|
||||
// producing fresh duplicates. Idempotent — a no-op when
|
||||
// nothing matches. The structural fix that prevents new
|
||||
// duplicates ships separately, see
|
||||
// docs/plans/workbench-seeding-cleanup.md.
|
||||
const { dedupHomeScenesOn } = await import('$lib/data/scope/dedup-workbench-scenes');
|
||||
const dedupedCount = await db.transaction('rw', 'workbenchScenes', () =>
|
||||
dedupHomeScenesOn(db.table('workbenchScenes'))
|
||||
);
|
||||
if (dedupedCount > 0) {
|
||||
console.info(`[workbench-scenes] deduped ${dedupedCount} duplicate Home scenes`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[spaces] active-space boot failed — sync will use sentinel scope', err);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue