From 82b74dba2479d2fd970576e1b18f6abf8518508e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 22:13:22 +0000 Subject: [PATCH 1/3] Initial plan From 75fe21c237a375c5f94bc0349cbd4e08595d3d63 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 22:23:07 +0000 Subject: [PATCH 2/3] fix(P0-X): rename proximity graph from Metroid* to SemanticNeighbor* MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves #56 — fixes naming collision where 'Metroid' was applied to the sparse proximity/neighbor graph. In CORTEX, 'Metroid' is reserved exclusively for the dialectical search probe { m1, m2, c }. Changes: - core/types.ts: MetroidNeighbor → SemanticNeighbor, MetroidSubgraph → SemanticNeighborSubgraph; 6 MetadataStore interface methods renamed (putSemanticNeighbors, getSemanticNeighbors, getInducedNeighborSubgraph, needsNeighborRecalc, flagVolumeForNeighborRecalc, clearNeighborRecalcFlag) - storage/IndexedDbMetadataStore.ts: matching implementation renames, DB_VERSION 2→3, STORE key metroid_neighbors→neighbor_graph, v3 upgrade migration (cursor copy + deleteObjectStore) - tests/Persistence.test.ts: all references updated - tests/SalienceEngine.test.ts: all stub method names updated All 211 tests pass. Co-authored-by: devlux76 <86517969+devlux76@users.noreply.github.com> --- core/types.ts | 24 +++++------ storage/IndexedDbMetadataStore.ts | 67 ++++++++++++++++++---------- tests/Persistence.test.ts | 72 +++++++++++++++---------------- tests/SalienceEngine.test.ts | 12 +++--- 4 files changed, 99 insertions(+), 76 deletions(-) diff --git a/core/types.ts b/core/types.ts index 7271e8a..d8cc52b 100644 --- a/core/types.ts +++ b/core/types.ts @@ -64,16 +64,16 @@ export interface Edge { } // --------------------------------------------------------------------------- -// Metroid nearest-neighbour graph (project term; medoid-inspired) +// Semantic nearest-neighbour graph // --------------------------------------------------------------------------- -export interface MetroidNeighbor { +export interface SemanticNeighbor { neighborPageId: Hash; cosineSimilarity: number; // threshold is defined by runtime policy distance: number; // 1 - cosineSimilarity (ready for TSP) } -export interface MetroidSubgraph { +export interface SemanticNeighborSubgraph { nodes: Hash[]; edges: { from: Hash; to: Hash; distance: number }[]; } @@ -175,20 +175,20 @@ export interface MetadataStore { getVolumesByBook(bookId: Hash): Promise; getShelvesByVolume(volumeId: Hash): Promise; - // --- Metroid NN radius index --- - putMetroidNeighbors(pageId: Hash, neighbors: MetroidNeighbor[]): Promise; - getMetroidNeighbors(pageId: Hash, maxDegree?: number): Promise; + // --- Semantic neighbor radius index --- + putSemanticNeighbors(pageId: Hash, neighbors: SemanticNeighbor[]): Promise; + getSemanticNeighbors(pageId: Hash, maxDegree?: number): Promise; - /** BFS expansion of the Metroid subgraph up to `maxHops` levels deep. */ - getInducedMetroidSubgraph( + /** BFS expansion of the semantic neighbor subgraph up to `maxHops` levels deep. */ + getInducedNeighborSubgraph( seedPageIds: Hash[], maxHops: number, - ): Promise; + ): Promise; // --- Dirty-volume recalc flags --- - needsMetroidRecalc(volumeId: Hash): Promise; - flagVolumeForMetroidRecalc(volumeId: Hash): Promise; - clearMetroidRecalcFlag(volumeId: Hash): Promise; + needsNeighborRecalc(volumeId: Hash): Promise; + flagVolumeForNeighborRecalc(volumeId: Hash): Promise; + clearNeighborRecalcFlag(volumeId: Hash): Promise; // --- Hotpath index --- putHotpathEntry(entry: HotpathEntry): Promise; diff --git a/storage/IndexedDbMetadataStore.ts b/storage/IndexedDbMetadataStore.ts index 8cd21e0..8bf09c0 100644 --- a/storage/IndexedDbMetadataStore.ts +++ b/storage/IndexedDbMetadataStore.ts @@ -4,8 +4,8 @@ import type { Hash, HotpathEntry, MetadataStore, - MetroidNeighbor, - MetroidSubgraph, + SemanticNeighbor, + SemanticNeighborSubgraph, Page, PageActivity, Shelf, @@ -16,7 +16,7 @@ import type { // Schema constants // --------------------------------------------------------------------------- -const DB_VERSION = 2; +const DB_VERSION = 3; /** Object-store names used across the schema. */ const STORE = { @@ -25,7 +25,7 @@ const STORE = { volumes: "volumes", shelves: "shelves", edges: "edges_hebbian", - metroidNeighbors: "metroid_neighbors", + neighborGraph: "neighbor_graph", flags: "flags", pageToBook: "page_to_book", bookToVolume: "book_to_volume", @@ -50,7 +50,7 @@ function promisifyTransaction(tx: IDBTransaction): Promise { // Schema upgrade // --------------------------------------------------------------------------- -function applyUpgrade(db: IDBDatabase): void { +function applyUpgrade(db: IDBDatabase, upgradeTx: IDBTransaction): void { // v1 stores if (!db.objectStoreNames.contains(STORE.pages)) { db.createObjectStore(STORE.pages, { keyPath: "pageId" }); @@ -72,9 +72,6 @@ function applyUpgrade(db: IDBDatabase): void { edgeStore.createIndex("by-from", "fromPageId"); } - if (!db.objectStoreNames.contains(STORE.metroidNeighbors)) { - db.createObjectStore(STORE.metroidNeighbors, { keyPath: "pageId" }); - } if (!db.objectStoreNames.contains(STORE.flags)) { db.createObjectStore(STORE.flags, { keyPath: "volumeId" }); } @@ -97,6 +94,30 @@ function applyUpgrade(db: IDBDatabase): void { if (!db.objectStoreNames.contains(STORE.pageActivity)) { db.createObjectStore(STORE.pageActivity, { keyPath: "pageId" }); } + + // v3 stores — rename metroid_neighbors → neighbor_graph + if (!db.objectStoreNames.contains(STORE.neighborGraph)) { + db.createObjectStore(STORE.neighborGraph, { keyPath: "pageId" }); + } + if (db.objectStoreNames.contains("metroid_neighbors")) { + // Copy all records from the old store to the new one via an IDB cursor. + // Each cursor.continue() call keeps the upgrade transaction alive; the + // transaction cannot commit until the cursor is exhausted (cursor === null). + // deleteObjectStore is invoked only once, in the final else-branch, after + // every record has been migrated. + const oldStore = upgradeTx.objectStore("metroid_neighbors"); + const newStore = upgradeTx.objectStore(STORE.neighborGraph); + oldStore.openCursor().onsuccess = (event) => { + const cursor = (event.target as IDBRequest).result; + if (cursor) { + newStore.put(cursor.value); + cursor.continue(); + } else { + // cursor is null — all records migrated; safe to drop the old store now. + db.deleteObjectStore("metroid_neighbors"); + } + }; + } } // --------------------------------------------------------------------------- @@ -125,7 +146,9 @@ export class IndexedDbMetadataStore implements MetadataStore { const req = indexedDB.open(dbName, DB_VERSION); req.onupgradeneeded = (event) => { - applyUpgrade((event.target as IDBOpenDBRequest).result); + const db = (event.target as IDBOpenDBRequest).result; + const tx = (event.target as IDBOpenDBRequest).transaction!; + applyUpgrade(db, tx); }; req.onsuccess = () => resolve(new IndexedDbMetadataStore(req.result)); @@ -328,19 +351,19 @@ export class IndexedDbMetadataStore implements MetadataStore { } // ------------------------------------------------------------------------- - // Metroid NN radius index + // Semantic neighbor radius index // ------------------------------------------------------------------------- - putMetroidNeighbors(pageId: Hash, neighbors: MetroidNeighbor[]): Promise { - return this._put(STORE.metroidNeighbors, { pageId, neighbors }); + putSemanticNeighbors(pageId: Hash, neighbors: SemanticNeighbor[]): Promise { + return this._put(STORE.neighborGraph, { pageId, neighbors }); } - async getMetroidNeighbors( + async getSemanticNeighbors( pageId: Hash, maxDegree?: number, - ): Promise { - const row = await this._get<{ pageId: Hash; neighbors: MetroidNeighbor[] }>( - STORE.metroidNeighbors, + ): Promise { + const row = await this._get<{ pageId: Hash; neighbors: SemanticNeighbor[] }>( + STORE.neighborGraph, pageId, ); if (!row) return []; @@ -348,10 +371,10 @@ export class IndexedDbMetadataStore implements MetadataStore { return maxDegree !== undefined ? list.slice(0, maxDegree) : list; } - async getInducedMetroidSubgraph( + async getInducedNeighborSubgraph( seedPageIds: Hash[], maxHops: number, - ): Promise { + ): Promise { const visited = new Set(seedPageIds); const nodeSet = new Set(seedPageIds); const edgeMap = new Map(); @@ -362,7 +385,7 @@ export class IndexedDbMetadataStore implements MetadataStore { const nextFrontier: Hash[] = []; for (const pageId of frontier) { - const neighbors = await this.getMetroidNeighbors(pageId); + const neighbors = await this.getSemanticNeighbors(pageId); for (const n of neighbors) { const key = `${pageId}\x00${n.neighborPageId}`; if (!edgeMap.has(key)) { @@ -393,7 +416,7 @@ export class IndexedDbMetadataStore implements MetadataStore { // Dirty-recalc flags // ------------------------------------------------------------------------- - async needsMetroidRecalc(volumeId: Hash): Promise { + async needsNeighborRecalc(volumeId: Hash): Promise { const row = await this._get<{ volumeId: Hash; needsRecalc: boolean }>( STORE.flags, volumeId, @@ -401,11 +424,11 @@ export class IndexedDbMetadataStore implements MetadataStore { return row?.needsRecalc === true; } - flagVolumeForMetroidRecalc(volumeId: Hash): Promise { + flagVolumeForNeighborRecalc(volumeId: Hash): Promise { return this._put(STORE.flags, { volumeId, needsRecalc: true }); } - clearMetroidRecalcFlag(volumeId: Hash): Promise { + clearNeighborRecalcFlag(volumeId: Hash): Promise { return this._put(STORE.flags, { volumeId, needsRecalc: false }); } diff --git a/tests/Persistence.test.ts b/tests/Persistence.test.ts index e38ea29..360bc04 100644 --- a/tests/Persistence.test.ts +++ b/tests/Persistence.test.ts @@ -19,7 +19,7 @@ import type { Book, Edge, HotpathEntry, - MetroidNeighbor, + SemanticNeighbor, Page, PageActivity, Shelf, @@ -286,7 +286,7 @@ const EDGE_B: Edge = { lastUpdatedAt: "2026-03-11T00:00:00.000Z", }; -const NEIGHBORS: MetroidNeighbor[] = [ +const NEIGHBORS: SemanticNeighbor[] = [ { neighborPageId: "page-def", cosineSimilarity: 0.9, distance: 0.1 }, { neighborPageId: "page-ghi", cosineSimilarity: 0.7, distance: 0.3 }, ]; @@ -415,72 +415,72 @@ describe("IndexedDbMetadataStore", () => { expect(neighbors).toEqual([]); }); - // --- MetroidNeighbors --- + // --- SemanticNeighbors --- - it("putMetroidNeighbors / getMetroidNeighbors round-trips neighbor list", async () => { + it("putSemanticNeighbors / getSemanticNeighbors round-trips neighbor list", async () => { const store = await IndexedDbMetadataStore.open(freshDbName()); - await store.putMetroidNeighbors("page-abc", NEIGHBORS); - const result = await store.getMetroidNeighbors("page-abc"); + await store.putSemanticNeighbors("page-abc", NEIGHBORS); + const result = await store.getSemanticNeighbors("page-abc"); expect(result).toEqual(NEIGHBORS); }); - it("getMetroidNeighbors respects maxDegree", async () => { + it("getSemanticNeighbors respects maxDegree", async () => { const store = await IndexedDbMetadataStore.open(freshDbName()); - await store.putMetroidNeighbors("page-abc", NEIGHBORS); - const result = await store.getMetroidNeighbors("page-abc", 1); + await store.putSemanticNeighbors("page-abc", NEIGHBORS); + const result = await store.getSemanticNeighbors("page-abc", 1); expect(result).toHaveLength(1); expect(result[0].neighborPageId).toBe("page-def"); }); - it("getMetroidNeighbors returns empty array for unknown page", async () => { + it("getSemanticNeighbors returns empty array for unknown page", async () => { const store = await IndexedDbMetadataStore.open(freshDbName()); - const result = await store.getMetroidNeighbors("no-such-page"); + const result = await store.getSemanticNeighbors("no-such-page"); expect(result).toEqual([]); }); - it("putMetroidNeighbors overwrites existing list", async () => { + it("putSemanticNeighbors overwrites existing list", async () => { const store = await IndexedDbMetadataStore.open(freshDbName()); - await store.putMetroidNeighbors("page-abc", NEIGHBORS); - const updated: MetroidNeighbor[] = [ + await store.putSemanticNeighbors("page-abc", NEIGHBORS); + const updated: SemanticNeighbor[] = [ { neighborPageId: "page-new", cosineSimilarity: 0.95, distance: 0.05 }, ]; - await store.putMetroidNeighbors("page-abc", updated); - const result = await store.getMetroidNeighbors("page-abc"); + await store.putSemanticNeighbors("page-abc", updated); + const result = await store.getSemanticNeighbors("page-abc"); expect(result).toHaveLength(1); expect(result[0].neighborPageId).toBe("page-new"); }); - // --- Induced Metroid subgraph (BFS) --- + // --- Induced semantic neighbor subgraph (BFS) --- - it("getInducedMetroidSubgraph returns seed nodes with zero hops", async () => { + it("getInducedNeighborSubgraph returns seed nodes with zero hops", async () => { const store = await IndexedDbMetadataStore.open(freshDbName()); - await store.putMetroidNeighbors("page-abc", NEIGHBORS); - const subgraph = await store.getInducedMetroidSubgraph(["page-abc"], 0); + await store.putSemanticNeighbors("page-abc", NEIGHBORS); + const subgraph = await store.getInducedNeighborSubgraph(["page-abc"], 0); expect(subgraph.nodes).toEqual(["page-abc"]); expect(subgraph.edges).toHaveLength(0); }); - it("getInducedMetroidSubgraph expands one hop correctly", async () => { + it("getInducedNeighborSubgraph expands one hop correctly", async () => { const store = await IndexedDbMetadataStore.open(freshDbName()); - await store.putMetroidNeighbors("page-abc", NEIGHBORS); + await store.putSemanticNeighbors("page-abc", NEIGHBORS); // page-def and page-ghi have no further neighbors - const subgraph = await store.getInducedMetroidSubgraph(["page-abc"], 1); + const subgraph = await store.getInducedNeighborSubgraph(["page-abc"], 1); expect(subgraph.nodes.sort()).toEqual( ["page-abc", "page-def", "page-ghi"].sort(), ); expect(subgraph.edges).toHaveLength(2); }); - it("getInducedMetroidSubgraph does not revisit nodes", async () => { + it("getInducedNeighborSubgraph does not revisit nodes", async () => { const store = await IndexedDbMetadataStore.open(freshDbName()); // Triangle: abc → def → abc (cycle) - await store.putMetroidNeighbors("page-abc", [ + await store.putSemanticNeighbors("page-abc", [ { neighborPageId: "page-def", cosineSimilarity: 0.9, distance: 0.1 }, ]); - await store.putMetroidNeighbors("page-def", [ + await store.putSemanticNeighbors("page-def", [ { neighborPageId: "page-abc", cosineSimilarity: 0.9, distance: 0.1 }, ]); - const subgraph = await store.getInducedMetroidSubgraph(["page-abc"], 5); + const subgraph = await store.getInducedNeighborSubgraph(["page-abc"], 5); const uniqueNodes = new Set(subgraph.nodes); expect(uniqueNodes.size).toBe(subgraph.nodes.length); // no duplicates expect(subgraph.nodes.sort()).toEqual(["page-abc", "page-def"].sort()); @@ -488,22 +488,22 @@ describe("IndexedDbMetadataStore", () => { // --- Dirty-recalc flags --- - it("needsMetroidRecalc returns false before any flag is set", async () => { + it("needsNeighborRecalc returns false before any flag is set", async () => { const store = await IndexedDbMetadataStore.open(freshDbName()); - expect(await store.needsMetroidRecalc("vol-001")).toBe(false); + expect(await store.needsNeighborRecalc("vol-001")).toBe(false); }); - it("flagVolumeForMetroidRecalc / needsMetroidRecalc round-trips", async () => { + it("flagVolumeForNeighborRecalc / needsNeighborRecalc round-trips", async () => { const store = await IndexedDbMetadataStore.open(freshDbName()); - await store.flagVolumeForMetroidRecalc("vol-001"); - expect(await store.needsMetroidRecalc("vol-001")).toBe(true); + await store.flagVolumeForNeighborRecalc("vol-001"); + expect(await store.needsNeighborRecalc("vol-001")).toBe(true); }); - it("clearMetroidRecalcFlag resets the flag", async () => { + it("clearNeighborRecalcFlag resets the flag", async () => { const store = await IndexedDbMetadataStore.open(freshDbName()); - await store.flagVolumeForMetroidRecalc("vol-001"); - await store.clearMetroidRecalcFlag("vol-001"); - expect(await store.needsMetroidRecalc("vol-001")).toBe(false); + await store.flagVolumeForNeighborRecalc("vol-001"); + await store.clearNeighborRecalcFlag("vol-001"); + expect(await store.needsNeighborRecalc("vol-001")).toBe(false); }); // --- HotpathEntry CRUD --- diff --git a/tests/SalienceEngine.test.ts b/tests/SalienceEngine.test.ts index 0618a33..22dfc96 100644 --- a/tests/SalienceEngine.test.ts +++ b/tests/SalienceEngine.test.ts @@ -115,12 +115,12 @@ class MockMetadataStore implements MetadataStore { async getBooksByPage(): Promise { return []; } async getVolumesByBook(): Promise { return []; } async getShelvesByVolume(): Promise { return []; } - async putMetroidNeighbors(): Promise { /* stub */ } - async getMetroidNeighbors(): Promise { return []; } - async getInducedMetroidSubgraph() { return { nodes: [], edges: [] }; } - async needsMetroidRecalc(): Promise { return false; } - async flagVolumeForMetroidRecalc(): Promise { /* stub */ } - async clearMetroidRecalcFlag(): Promise { /* stub */ } + async putSemanticNeighbors(): Promise { /* stub */ } + async getSemanticNeighbors(): Promise { return []; } + async getInducedNeighborSubgraph() { return { nodes: [], edges: [] }; } + async needsNeighborRecalc(): Promise { return false; } + async flagVolumeForNeighborRecalc(): Promise { /* stub */ } + async clearNeighborRecalcFlag(): Promise { /* stub */ } } // --------------------------------------------------------------------------- From 9e4e0fcf26bb30c81dda77fbe8ce2ec200a0d757 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 22:36:47 +0000 Subject: [PATCH 3/3] =?UTF-8?q?fix(P0-X):=20drop=20migration=20cruft=20fro?= =?UTF-8?q?m=20applyUpgrade=20=E2=80=94=20no=20installed=20base?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the cursor-based metroid_neighbors → neighbor_graph migration from the v3 upgrade path. The project has no installed base, so the migration is unnecessary overhead. applyUpgrade simply creates neighbor_graph fresh. Also reverts open() back to the simpler single-argument applyUpgrade(db) call. Co-authored-by: devlux76 <86517969+devlux76@users.noreply.github.com> --- storage/IndexedDbMetadataStore.ts | 27 +++------------------------ 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/storage/IndexedDbMetadataStore.ts b/storage/IndexedDbMetadataStore.ts index 8bf09c0..212523d 100644 --- a/storage/IndexedDbMetadataStore.ts +++ b/storage/IndexedDbMetadataStore.ts @@ -50,7 +50,7 @@ function promisifyTransaction(tx: IDBTransaction): Promise { // Schema upgrade // --------------------------------------------------------------------------- -function applyUpgrade(db: IDBDatabase, upgradeTx: IDBTransaction): void { +function applyUpgrade(db: IDBDatabase): void { // v1 stores if (!db.objectStoreNames.contains(STORE.pages)) { db.createObjectStore(STORE.pages, { keyPath: "pageId" }); @@ -95,29 +95,10 @@ function applyUpgrade(db: IDBDatabase, upgradeTx: IDBTransaction): void { db.createObjectStore(STORE.pageActivity, { keyPath: "pageId" }); } - // v3 stores — rename metroid_neighbors → neighbor_graph + // v3 stores — neighbor_graph (replaces the old metroid_neighbors name) if (!db.objectStoreNames.contains(STORE.neighborGraph)) { db.createObjectStore(STORE.neighborGraph, { keyPath: "pageId" }); } - if (db.objectStoreNames.contains("metroid_neighbors")) { - // Copy all records from the old store to the new one via an IDB cursor. - // Each cursor.continue() call keeps the upgrade transaction alive; the - // transaction cannot commit until the cursor is exhausted (cursor === null). - // deleteObjectStore is invoked only once, in the final else-branch, after - // every record has been migrated. - const oldStore = upgradeTx.objectStore("metroid_neighbors"); - const newStore = upgradeTx.objectStore(STORE.neighborGraph); - oldStore.openCursor().onsuccess = (event) => { - const cursor = (event.target as IDBRequest).result; - if (cursor) { - newStore.put(cursor.value); - cursor.continue(); - } else { - // cursor is null — all records migrated; safe to drop the old store now. - db.deleteObjectStore("metroid_neighbors"); - } - }; - } } // --------------------------------------------------------------------------- @@ -146,9 +127,7 @@ export class IndexedDbMetadataStore implements MetadataStore { const req = indexedDB.open(dbName, DB_VERSION); req.onupgradeneeded = (event) => { - const db = (event.target as IDBOpenDBRequest).result; - const tx = (event.target as IDBOpenDBRequest).transaction!; - applyUpgrade(db, tx); + applyUpgrade((event.target as IDBOpenDBRequest).result); }; req.onsuccess = () => resolve(new IndexedDbMetadataStore(req.result));