@@ -387,3 +387,178 @@ describe("integration: ingest and query", () => {
387387 expect ( hits3 [ 0 ] . page . content ) . toBe ( astronomyChunks [ 0 ] ) ;
388388 } ) ;
389389} ) ;
390+
391+ // ---------------------------------------------------------------------------
392+ // P1-F: Hierarchical + Dialectical integration tests (v0.5)
393+ // ---------------------------------------------------------------------------
394+
395+ describe ( "integration (v0.5): hierarchical and dialectical ingest/query" , ( ) => {
396+ beforeEach ( ( ) => {
397+ ( globalThis as Record < string , unknown > ) [ "indexedDB" ] = new IDBFactory ( ) ;
398+ ( globalThis as Record < string , unknown > ) [ "IDBKeyRange" ] = FakeIDBKeyRange ;
399+ } ) ;
400+
401+ it ( "ingest produces full Page → Book → Volume → Shelf hierarchy" , async ( ) => {
402+ const dbName = freshDbName ( ) ;
403+ const metadataStore = await IndexedDbMetadataStore . open ( dbName ) ;
404+ const vectorStore = new MemoryVectorStore ( ) ;
405+ const keyPair = await generateKeyPair ( ) ;
406+ const profile = makeProfile ( ) ;
407+ const runner = makeRunner ( makeBackend ( ) ) ;
408+
409+ const result = await ingestText ( ASTRONOMY_TEXT + " " + BIOLOGY_TEXT , {
410+ modelProfile : profile ,
411+ embeddingRunner : runner ,
412+ vectorStore,
413+ metadataStore,
414+ keyPair,
415+ } ) ;
416+
417+ // Pages were created
418+ expect ( result . pages . length ) . toBeGreaterThanOrEqual ( 1 ) ;
419+
420+ // Book was created and accessible
421+ expect ( result . book ) . toBeDefined ( ) ;
422+ const storedBook = await metadataStore . getBook ( result . book ! . bookId ) ;
423+ expect ( storedBook ) . toBeDefined ( ) ;
424+ expect ( storedBook ! . medoidPageId ) . toBeDefined ( ) ;
425+ expect ( storedBook ! . pageIds ) . toContain ( storedBook ! . medoidPageId ) ;
426+
427+ // Volumes were created (at least one)
428+ expect ( result . volumes ) . toBeDefined ( ) ;
429+ expect ( result . volumes ! . length ) . toBeGreaterThanOrEqual ( 1 ) ;
430+ for ( const volume of result . volumes ! ) {
431+ const stored = await metadataStore . getVolume ( volume . volumeId ) ;
432+ expect ( stored ) . toBeDefined ( ) ;
433+ expect ( stored ! . bookIds . length ) . toBeGreaterThanOrEqual ( 1 ) ;
434+ expect ( stored ! . prototypeOffsets . length ) . toBeGreaterThanOrEqual ( 1 ) ;
435+ }
436+
437+ // Shelves were created (at least one)
438+ expect ( result . shelves ) . toBeDefined ( ) ;
439+ expect ( result . shelves ! . length ) . toBeGreaterThanOrEqual ( 1 ) ;
440+ for ( const shelf of result . shelves ! ) {
441+ const stored = await metadataStore . getShelf ( shelf . shelfId ) ;
442+ expect ( stored ) . toBeDefined ( ) ;
443+ expect ( stored ! . volumeIds . length ) . toBeGreaterThanOrEqual ( 1 ) ;
444+ expect ( stored ! . routingPrototypeOffsets . length ) . toBeGreaterThanOrEqual ( 1 ) ;
445+ }
446+ } ) ;
447+
448+ it ( "hotpath entries exist for hierarchy prototypes after ingest" , async ( ) => {
449+ const dbName = freshDbName ( ) ;
450+ const metadataStore = await IndexedDbMetadataStore . open ( dbName ) ;
451+ const vectorStore = new MemoryVectorStore ( ) ;
452+ const keyPair = await generateKeyPair ( ) ;
453+ const profile = makeProfile ( ) ;
454+ const runner = makeRunner ( makeBackend ( ) ) ;
455+
456+ await ingestText ( ASTRONOMY_TEXT + " " + BIOLOGY_TEXT + " " + HISTORY_TEXT , {
457+ modelProfile : profile ,
458+ embeddingRunner : runner ,
459+ vectorStore,
460+ metadataStore,
461+ keyPair,
462+ } ) ;
463+
464+ // At least some hotpath entries should exist
465+ const allEntries = await metadataStore . getHotpathEntries ( ) ;
466+ expect ( allEntries . length ) . toBeGreaterThan ( 0 ) ;
467+
468+ // Page-tier entries should exist
469+ const pageEntries = await metadataStore . getHotpathEntries ( "page" ) ;
470+ expect ( pageEntries . length ) . toBeGreaterThan ( 0 ) ;
471+ } ) ;
472+
473+ it ( "semantic neighbor graph is populated after ingest" , async ( ) => {
474+ const dbName = freshDbName ( ) ;
475+ const metadataStore = await IndexedDbMetadataStore . open ( dbName ) ;
476+ const vectorStore = new MemoryVectorStore ( ) ;
477+ const keyPair = await generateKeyPair ( ) ;
478+ const profile = makeProfile ( ) ;
479+ const runner = makeRunner ( makeBackend ( ) ) ;
480+
481+ const result = await ingestText ( ASTRONOMY_TEXT + " " + BIOLOGY_TEXT , {
482+ modelProfile : profile ,
483+ embeddingRunner : runner ,
484+ vectorStore,
485+ metadataStore,
486+ keyPair,
487+ } ) ;
488+
489+ // Verify that semantic neighbor records are structurally valid when present.
490+ // With content-hash-based embeddings, pages may not meet the cosine-similarity
491+ // threshold, so we only validate structure — not that neighbors must exist.
492+ for ( const page of result . pages ) {
493+ const neighbors = await metadataStore . getSemanticNeighbors ( page . pageId ) ;
494+ for ( const n of neighbors ) {
495+ expect ( n . neighborPageId ) . toBeDefined ( ) ;
496+ expect ( typeof n . neighborPageId ) . toBe ( "string" ) ;
497+ expect ( n . cosineSimilarity ) . toBeGreaterThanOrEqual ( - 1 ) ;
498+ expect ( n . cosineSimilarity ) . toBeLessThanOrEqual ( 1 ) ;
499+ expect ( n . distance ) . toBeCloseTo ( 1 - n . cosineSimilarity , 5 ) ;
500+ }
501+ }
502+ } ) ;
503+
504+ it ( "Williams Bound: resident count never exceeds H(t) after ingest" , async ( ) => {
505+ const dbName = freshDbName ( ) ;
506+ const metadataStore = await IndexedDbMetadataStore . open ( dbName ) ;
507+ const vectorStore = new MemoryVectorStore ( ) ;
508+ const keyPair = await generateKeyPair ( ) ;
509+ const profile = makeProfile ( ) ;
510+ const runner = makeRunner ( makeBackend ( ) ) ;
511+
512+ await ingestText ( ASTRONOMY_TEXT + " " + BIOLOGY_TEXT + " " + HISTORY_TEXT , {
513+ modelProfile : profile ,
514+ embeddingRunner : runner ,
515+ vectorStore,
516+ metadataStore,
517+ keyPair,
518+ } ) ;
519+
520+ // Williams Bound: H(t) = ceil(c * sqrt(t * log2(1+t)))
521+ const allPages = await metadataStore . getAllPages ( ) ;
522+ const graphMass = allPages . length ;
523+ const c = 0.5 ;
524+ const capacity = Math . max ( 1 , Math . ceil ( c * Math . sqrt ( graphMass * Math . log2 ( 1 + graphMass ) ) ) ) ;
525+
526+ const residentCount = await metadataStore . getResidentCount ( ) ;
527+ expect ( residentCount ) . toBeLessThanOrEqual ( capacity ) ;
528+ } ) ;
529+
530+ it ( "knowledge gap is signalled for a model without Matryoshka dims" , async ( ) => {
531+ const dbName = freshDbName ( ) ;
532+ const metadataStore = await IndexedDbMetadataStore . open ( dbName ) ;
533+ const vectorStore = new MemoryVectorStore ( ) ;
534+ const keyPair = await generateKeyPair ( ) ;
535+ // Non-Matryoshka model: no matryoshkaProtectedDim
536+ const profile = makeProfile ( ) ;
537+ const runner = makeRunner ( makeBackend ( ) ) ;
538+ const { WasmVectorBackend } = await import ( "../../WasmVectorBackend" ) ;
539+ const vectorBackend = new WasmVectorBackend ( ) ;
540+ const { query } = await import ( "../../cortex/Query" ) ;
541+
542+ await ingestText ( ASTRONOMY_TEXT , {
543+ modelProfile : profile ,
544+ embeddingRunner : runner ,
545+ vectorStore,
546+ metadataStore,
547+ keyPair,
548+ } ) ;
549+
550+ const result = await query ( ASTRONOMY_TEXT . slice ( 0 , 50 ) , {
551+ modelProfile : profile ,
552+ embeddingRunner : runner ,
553+ vectorStore,
554+ metadataStore,
555+ vectorBackend,
556+ topK : 3 ,
557+ } ) ;
558+
559+ // Profile has no matryoshkaProtectedDim → MetroidBuilder always declares a gap
560+ expect ( result . metroid ) . not . toBeNull ( ) ;
561+ expect ( result . metroid ! . knowledgeGap ) . toBe ( true ) ;
562+ expect ( result . knowledgeGap ) . not . toBeNull ( ) ;
563+ } ) ;
564+ } ) ;
0 commit comments