Skip to content

Commit 5d5a91f

Browse files
authored
Merge pull request #67 from devlux76/copilot/p0-task-subtask-creation
2 parents 30362d2 + 9e4e0fc commit 5d5a91f

4 files changed

Lines changed: 76 additions & 74 deletions

File tree

core/types.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,16 @@ export interface Edge {
6464
}
6565

6666
// ---------------------------------------------------------------------------
67-
// Metroid nearest-neighbour graph (project term; medoid-inspired)
67+
// Semantic nearest-neighbour graph
6868
// ---------------------------------------------------------------------------
6969

70-
export interface MetroidNeighbor {
70+
export interface SemanticNeighbor {
7171
neighborPageId: Hash;
7272
cosineSimilarity: number; // threshold is defined by runtime policy
7373
distance: number; // 1 - cosineSimilarity (ready for TSP)
7474
}
7575

76-
export interface MetroidSubgraph {
76+
export interface SemanticNeighborSubgraph {
7777
nodes: Hash[];
7878
edges: { from: Hash; to: Hash; distance: number }[];
7979
}
@@ -175,20 +175,20 @@ export interface MetadataStore {
175175
getVolumesByBook(bookId: Hash): Promise<Volume[]>;
176176
getShelvesByVolume(volumeId: Hash): Promise<Shelf[]>;
177177

178-
// --- Metroid NN radius index ---
179-
putMetroidNeighbors(pageId: Hash, neighbors: MetroidNeighbor[]): Promise<void>;
180-
getMetroidNeighbors(pageId: Hash, maxDegree?: number): Promise<MetroidNeighbor[]>;
178+
// --- Semantic neighbor radius index ---
179+
putSemanticNeighbors(pageId: Hash, neighbors: SemanticNeighbor[]): Promise<void>;
180+
getSemanticNeighbors(pageId: Hash, maxDegree?: number): Promise<SemanticNeighbor[]>;
181181

182-
/** BFS expansion of the Metroid subgraph up to `maxHops` levels deep. */
183-
getInducedMetroidSubgraph(
182+
/** BFS expansion of the semantic neighbor subgraph up to `maxHops` levels deep. */
183+
getInducedNeighborSubgraph(
184184
seedPageIds: Hash[],
185185
maxHops: number,
186-
): Promise<MetroidSubgraph>;
186+
): Promise<SemanticNeighborSubgraph>;
187187

188188
// --- Dirty-volume recalc flags ---
189-
needsMetroidRecalc(volumeId: Hash): Promise<boolean>;
190-
flagVolumeForMetroidRecalc(volumeId: Hash): Promise<void>;
191-
clearMetroidRecalcFlag(volumeId: Hash): Promise<void>;
189+
needsNeighborRecalc(volumeId: Hash): Promise<boolean>;
190+
flagVolumeForNeighborRecalc(volumeId: Hash): Promise<void>;
191+
clearNeighborRecalcFlag(volumeId: Hash): Promise<void>;
192192

193193
// --- Hotpath index ---
194194
putHotpathEntry(entry: HotpathEntry): Promise<void>;

storage/IndexedDbMetadataStore.ts

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import type {
44
Hash,
55
HotpathEntry,
66
MetadataStore,
7-
MetroidNeighbor,
8-
MetroidSubgraph,
7+
SemanticNeighbor,
8+
SemanticNeighborSubgraph,
99
Page,
1010
PageActivity,
1111
Shelf,
@@ -16,7 +16,7 @@ import type {
1616
// Schema constants
1717
// ---------------------------------------------------------------------------
1818

19-
const DB_VERSION = 2;
19+
const DB_VERSION = 3;
2020

2121
/** Object-store names used across the schema. */
2222
const STORE = {
@@ -25,7 +25,7 @@ const STORE = {
2525
volumes: "volumes",
2626
shelves: "shelves",
2727
edges: "edges_hebbian",
28-
metroidNeighbors: "metroid_neighbors",
28+
neighborGraph: "neighbor_graph",
2929
flags: "flags",
3030
pageToBook: "page_to_book",
3131
bookToVolume: "book_to_volume",
@@ -72,9 +72,6 @@ function applyUpgrade(db: IDBDatabase): void {
7272
edgeStore.createIndex("by-from", "fromPageId");
7373
}
7474

75-
if (!db.objectStoreNames.contains(STORE.metroidNeighbors)) {
76-
db.createObjectStore(STORE.metroidNeighbors, { keyPath: "pageId" });
77-
}
7875
if (!db.objectStoreNames.contains(STORE.flags)) {
7976
db.createObjectStore(STORE.flags, { keyPath: "volumeId" });
8077
}
@@ -97,6 +94,11 @@ function applyUpgrade(db: IDBDatabase): void {
9794
if (!db.objectStoreNames.contains(STORE.pageActivity)) {
9895
db.createObjectStore(STORE.pageActivity, { keyPath: "pageId" });
9996
}
97+
98+
// v3 stores — neighbor_graph (replaces the old metroid_neighbors name)
99+
if (!db.objectStoreNames.contains(STORE.neighborGraph)) {
100+
db.createObjectStore(STORE.neighborGraph, { keyPath: "pageId" });
101+
}
100102
}
101103

102104
// ---------------------------------------------------------------------------
@@ -328,30 +330,30 @@ export class IndexedDbMetadataStore implements MetadataStore {
328330
}
329331

330332
// -------------------------------------------------------------------------
331-
// Metroid NN radius index
333+
// Semantic neighbor radius index
332334
// -------------------------------------------------------------------------
333335

334-
putMetroidNeighbors(pageId: Hash, neighbors: MetroidNeighbor[]): Promise<void> {
335-
return this._put(STORE.metroidNeighbors, { pageId, neighbors });
336+
putSemanticNeighbors(pageId: Hash, neighbors: SemanticNeighbor[]): Promise<void> {
337+
return this._put(STORE.neighborGraph, { pageId, neighbors });
336338
}
337339

338-
async getMetroidNeighbors(
340+
async getSemanticNeighbors(
339341
pageId: Hash,
340342
maxDegree?: number,
341-
): Promise<MetroidNeighbor[]> {
342-
const row = await this._get<{ pageId: Hash; neighbors: MetroidNeighbor[] }>(
343-
STORE.metroidNeighbors,
343+
): Promise<SemanticNeighbor[]> {
344+
const row = await this._get<{ pageId: Hash; neighbors: SemanticNeighbor[] }>(
345+
STORE.neighborGraph,
344346
pageId,
345347
);
346348
if (!row) return [];
347349
const list = row.neighbors;
348350
return maxDegree !== undefined ? list.slice(0, maxDegree) : list;
349351
}
350352

351-
async getInducedMetroidSubgraph(
353+
async getInducedNeighborSubgraph(
352354
seedPageIds: Hash[],
353355
maxHops: number,
354-
): Promise<MetroidSubgraph> {
356+
): Promise<SemanticNeighborSubgraph> {
355357
const visited = new Set<Hash>(seedPageIds);
356358
const nodeSet = new Set<Hash>(seedPageIds);
357359
const edgeMap = new Map<string, { from: Hash; to: Hash; distance: number }>();
@@ -362,7 +364,7 @@ export class IndexedDbMetadataStore implements MetadataStore {
362364
const nextFrontier: Hash[] = [];
363365

364366
for (const pageId of frontier) {
365-
const neighbors = await this.getMetroidNeighbors(pageId);
367+
const neighbors = await this.getSemanticNeighbors(pageId);
366368
for (const n of neighbors) {
367369
const key = `${pageId}\x00${n.neighborPageId}`;
368370
if (!edgeMap.has(key)) {
@@ -393,19 +395,19 @@ export class IndexedDbMetadataStore implements MetadataStore {
393395
// Dirty-recalc flags
394396
// -------------------------------------------------------------------------
395397

396-
async needsMetroidRecalc(volumeId: Hash): Promise<boolean> {
398+
async needsNeighborRecalc(volumeId: Hash): Promise<boolean> {
397399
const row = await this._get<{ volumeId: Hash; needsRecalc: boolean }>(
398400
STORE.flags,
399401
volumeId,
400402
);
401403
return row?.needsRecalc === true;
402404
}
403405

404-
flagVolumeForMetroidRecalc(volumeId: Hash): Promise<void> {
406+
flagVolumeForNeighborRecalc(volumeId: Hash): Promise<void> {
405407
return this._put(STORE.flags, { volumeId, needsRecalc: true });
406408
}
407409

408-
clearMetroidRecalcFlag(volumeId: Hash): Promise<void> {
410+
clearNeighborRecalcFlag(volumeId: Hash): Promise<void> {
409411
return this._put(STORE.flags, { volumeId, needsRecalc: false });
410412
}
411413

tests/Persistence.test.ts

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import type {
1919
Book,
2020
Edge,
2121
HotpathEntry,
22-
MetroidNeighbor,
22+
SemanticNeighbor,
2323
Page,
2424
PageActivity,
2525
Shelf,
@@ -286,7 +286,7 @@ const EDGE_B: Edge = {
286286
lastUpdatedAt: "2026-03-11T00:00:00.000Z",
287287
};
288288

289-
const NEIGHBORS: MetroidNeighbor[] = [
289+
const NEIGHBORS: SemanticNeighbor[] = [
290290
{ neighborPageId: "page-def", cosineSimilarity: 0.9, distance: 0.1 },
291291
{ neighborPageId: "page-ghi", cosineSimilarity: 0.7, distance: 0.3 },
292292
];
@@ -415,95 +415,95 @@ describe("IndexedDbMetadataStore", () => {
415415
expect(neighbors).toEqual([]);
416416
});
417417

418-
// --- MetroidNeighbors ---
418+
// --- SemanticNeighbors ---
419419

420-
it("putMetroidNeighbors / getMetroidNeighbors round-trips neighbor list", async () => {
420+
it("putSemanticNeighbors / getSemanticNeighbors round-trips neighbor list", async () => {
421421
const store = await IndexedDbMetadataStore.open(freshDbName());
422-
await store.putMetroidNeighbors("page-abc", NEIGHBORS);
423-
const result = await store.getMetroidNeighbors("page-abc");
422+
await store.putSemanticNeighbors("page-abc", NEIGHBORS);
423+
const result = await store.getSemanticNeighbors("page-abc");
424424
expect(result).toEqual(NEIGHBORS);
425425
});
426426

427-
it("getMetroidNeighbors respects maxDegree", async () => {
427+
it("getSemanticNeighbors respects maxDegree", async () => {
428428
const store = await IndexedDbMetadataStore.open(freshDbName());
429-
await store.putMetroidNeighbors("page-abc", NEIGHBORS);
430-
const result = await store.getMetroidNeighbors("page-abc", 1);
429+
await store.putSemanticNeighbors("page-abc", NEIGHBORS);
430+
const result = await store.getSemanticNeighbors("page-abc", 1);
431431
expect(result).toHaveLength(1);
432432
expect(result[0].neighborPageId).toBe("page-def");
433433
});
434434

435-
it("getMetroidNeighbors returns empty array for unknown page", async () => {
435+
it("getSemanticNeighbors returns empty array for unknown page", async () => {
436436
const store = await IndexedDbMetadataStore.open(freshDbName());
437-
const result = await store.getMetroidNeighbors("no-such-page");
437+
const result = await store.getSemanticNeighbors("no-such-page");
438438
expect(result).toEqual([]);
439439
});
440440

441-
it("putMetroidNeighbors overwrites existing list", async () => {
441+
it("putSemanticNeighbors overwrites existing list", async () => {
442442
const store = await IndexedDbMetadataStore.open(freshDbName());
443-
await store.putMetroidNeighbors("page-abc", NEIGHBORS);
444-
const updated: MetroidNeighbor[] = [
443+
await store.putSemanticNeighbors("page-abc", NEIGHBORS);
444+
const updated: SemanticNeighbor[] = [
445445
{ neighborPageId: "page-new", cosineSimilarity: 0.95, distance: 0.05 },
446446
];
447-
await store.putMetroidNeighbors("page-abc", updated);
448-
const result = await store.getMetroidNeighbors("page-abc");
447+
await store.putSemanticNeighbors("page-abc", updated);
448+
const result = await store.getSemanticNeighbors("page-abc");
449449
expect(result).toHaveLength(1);
450450
expect(result[0].neighborPageId).toBe("page-new");
451451
});
452452

453-
// --- Induced Metroid subgraph (BFS) ---
453+
// --- Induced semantic neighbor subgraph (BFS) ---
454454

455-
it("getInducedMetroidSubgraph returns seed nodes with zero hops", async () => {
455+
it("getInducedNeighborSubgraph returns seed nodes with zero hops", async () => {
456456
const store = await IndexedDbMetadataStore.open(freshDbName());
457-
await store.putMetroidNeighbors("page-abc", NEIGHBORS);
458-
const subgraph = await store.getInducedMetroidSubgraph(["page-abc"], 0);
457+
await store.putSemanticNeighbors("page-abc", NEIGHBORS);
458+
const subgraph = await store.getInducedNeighborSubgraph(["page-abc"], 0);
459459
expect(subgraph.nodes).toEqual(["page-abc"]);
460460
expect(subgraph.edges).toHaveLength(0);
461461
});
462462

463-
it("getInducedMetroidSubgraph expands one hop correctly", async () => {
463+
it("getInducedNeighborSubgraph expands one hop correctly", async () => {
464464
const store = await IndexedDbMetadataStore.open(freshDbName());
465-
await store.putMetroidNeighbors("page-abc", NEIGHBORS);
465+
await store.putSemanticNeighbors("page-abc", NEIGHBORS);
466466
// page-def and page-ghi have no further neighbors
467-
const subgraph = await store.getInducedMetroidSubgraph(["page-abc"], 1);
467+
const subgraph = await store.getInducedNeighborSubgraph(["page-abc"], 1);
468468
expect(subgraph.nodes.sort()).toEqual(
469469
["page-abc", "page-def", "page-ghi"].sort(),
470470
);
471471
expect(subgraph.edges).toHaveLength(2);
472472
});
473473

474-
it("getInducedMetroidSubgraph does not revisit nodes", async () => {
474+
it("getInducedNeighborSubgraph does not revisit nodes", async () => {
475475
const store = await IndexedDbMetadataStore.open(freshDbName());
476476
// Triangle: abc → def → abc (cycle)
477-
await store.putMetroidNeighbors("page-abc", [
477+
await store.putSemanticNeighbors("page-abc", [
478478
{ neighborPageId: "page-def", cosineSimilarity: 0.9, distance: 0.1 },
479479
]);
480-
await store.putMetroidNeighbors("page-def", [
480+
await store.putSemanticNeighbors("page-def", [
481481
{ neighborPageId: "page-abc", cosineSimilarity: 0.9, distance: 0.1 },
482482
]);
483-
const subgraph = await store.getInducedMetroidSubgraph(["page-abc"], 5);
483+
const subgraph = await store.getInducedNeighborSubgraph(["page-abc"], 5);
484484
const uniqueNodes = new Set(subgraph.nodes);
485485
expect(uniqueNodes.size).toBe(subgraph.nodes.length); // no duplicates
486486
expect(subgraph.nodes.sort()).toEqual(["page-abc", "page-def"].sort());
487487
});
488488

489489
// --- Dirty-recalc flags ---
490490

491-
it("needsMetroidRecalc returns false before any flag is set", async () => {
491+
it("needsNeighborRecalc returns false before any flag is set", async () => {
492492
const store = await IndexedDbMetadataStore.open(freshDbName());
493-
expect(await store.needsMetroidRecalc("vol-001")).toBe(false);
493+
expect(await store.needsNeighborRecalc("vol-001")).toBe(false);
494494
});
495495

496-
it("flagVolumeForMetroidRecalc / needsMetroidRecalc round-trips", async () => {
496+
it("flagVolumeForNeighborRecalc / needsNeighborRecalc round-trips", async () => {
497497
const store = await IndexedDbMetadataStore.open(freshDbName());
498-
await store.flagVolumeForMetroidRecalc("vol-001");
499-
expect(await store.needsMetroidRecalc("vol-001")).toBe(true);
498+
await store.flagVolumeForNeighborRecalc("vol-001");
499+
expect(await store.needsNeighborRecalc("vol-001")).toBe(true);
500500
});
501501

502-
it("clearMetroidRecalcFlag resets the flag", async () => {
502+
it("clearNeighborRecalcFlag resets the flag", async () => {
503503
const store = await IndexedDbMetadataStore.open(freshDbName());
504-
await store.flagVolumeForMetroidRecalc("vol-001");
505-
await store.clearMetroidRecalcFlag("vol-001");
506-
expect(await store.needsMetroidRecalc("vol-001")).toBe(false);
504+
await store.flagVolumeForNeighborRecalc("vol-001");
505+
await store.clearNeighborRecalcFlag("vol-001");
506+
expect(await store.needsNeighborRecalc("vol-001")).toBe(false);
507507
});
508508

509509
// --- HotpathEntry CRUD ---

tests/SalienceEngine.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,12 @@ class MockMetadataStore implements MetadataStore {
115115
async getBooksByPage(): Promise<never[]> { return []; }
116116
async getVolumesByBook(): Promise<never[]> { return []; }
117117
async getShelvesByVolume(): Promise<never[]> { return []; }
118-
async putMetroidNeighbors(): Promise<void> { /* stub */ }
119-
async getMetroidNeighbors(): Promise<never[]> { return []; }
120-
async getInducedMetroidSubgraph() { return { nodes: [], edges: [] }; }
121-
async needsMetroidRecalc(): Promise<boolean> { return false; }
122-
async flagVolumeForMetroidRecalc(): Promise<void> { /* stub */ }
123-
async clearMetroidRecalcFlag(): Promise<void> { /* stub */ }
118+
async putSemanticNeighbors(): Promise<void> { /* stub */ }
119+
async getSemanticNeighbors(): Promise<never[]> { return []; }
120+
async getInducedNeighborSubgraph() { return { nodes: [], edges: [] }; }
121+
async needsNeighborRecalc(): Promise<boolean> { return false; }
122+
async flagVolumeForNeighborRecalc(): Promise<void> { /* stub */ }
123+
async clearNeighborRecalcFlag(): Promise<void> { /* stub */ }
124124
}
125125

126126
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)