videobook-engine 5.0.1 → 5.3.2

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 (207) hide show
  1. package/README.md +73 -13
  2. package/dist/artifacts.d.ts +2 -2
  3. package/dist/artifacts.d.ts.map +1 -1
  4. package/dist/artifacts.js +42 -11
  5. package/dist/artifacts.js.map +1 -1
  6. package/dist/cas.d.ts +2 -1
  7. package/dist/cas.d.ts.map +1 -1
  8. package/dist/cas.js +33 -4
  9. package/dist/cas.js.map +1 -1
  10. package/dist/catalog-gc-state.d.ts +4 -0
  11. package/dist/catalog-gc-state.d.ts.map +1 -0
  12. package/dist/catalog-gc-state.js +49 -0
  13. package/dist/catalog-gc-state.js.map +1 -0
  14. package/dist/catalog-metadata.d.ts +1 -1
  15. package/dist/catalog-metadata.js +1 -1
  16. package/dist/context.d.ts +2 -0
  17. package/dist/context.d.ts.map +1 -1
  18. package/dist/context.js +7 -1
  19. package/dist/context.js.map +1 -1
  20. package/dist/domain.d.ts.map +1 -1
  21. package/dist/domain.js +2 -2
  22. package/dist/domain.js.map +1 -1
  23. package/dist/edits.js +2 -2
  24. package/dist/edits.js.map +1 -1
  25. package/dist/engine-types.d.ts +97 -12
  26. package/dist/engine-types.d.ts.map +1 -1
  27. package/dist/engine-types.js +2 -0
  28. package/dist/engine-types.js.map +1 -1
  29. package/dist/engine.d.ts +11 -3
  30. package/dist/engine.d.ts.map +1 -1
  31. package/dist/engine.js +43 -1
  32. package/dist/engine.js.map +1 -1
  33. package/dist/files.d.ts +10 -0
  34. package/dist/files.d.ts.map +1 -1
  35. package/dist/files.js +48 -1
  36. package/dist/files.js.map +1 -1
  37. package/dist/fork.d.ts +11 -28
  38. package/dist/fork.d.ts.map +1 -1
  39. package/dist/fork.js +5 -7
  40. package/dist/fork.js.map +1 -1
  41. package/dist/history.js +10 -9
  42. package/dist/history.js.map +1 -1
  43. package/dist/index.d.ts +1 -1
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +1 -1
  46. package/dist/index.js.map +1 -1
  47. package/dist/inline-temporal-models.d.ts +43 -0
  48. package/dist/inline-temporal-models.d.ts.map +1 -0
  49. package/dist/inline-temporal-models.js +236 -0
  50. package/dist/inline-temporal-models.js.map +1 -0
  51. package/dist/isolated-models.d.ts +8 -0
  52. package/dist/isolated-models.d.ts.map +1 -0
  53. package/dist/isolated-models.js +25 -0
  54. package/dist/isolated-models.js.map +1 -0
  55. package/dist/job-queue.d.ts +1 -1
  56. package/dist/job-queue.d.ts.map +1 -1
  57. package/dist/job-queue.js +20 -6
  58. package/dist/job-queue.js.map +1 -1
  59. package/dist/media-image.d.ts +8 -0
  60. package/dist/media-image.d.ts.map +1 -0
  61. package/dist/media-image.js +41 -0
  62. package/dist/media-image.js.map +1 -0
  63. package/dist/media-process.d.ts +13 -0
  64. package/dist/media-process.d.ts.map +1 -0
  65. package/dist/media-process.js +89 -0
  66. package/dist/media-process.js.map +1 -0
  67. package/dist/merge-policy.d.ts +6 -10
  68. package/dist/merge-policy.d.ts.map +1 -1
  69. package/dist/merge-policy.js +5 -10
  70. package/dist/merge-policy.js.map +1 -1
  71. package/dist/metadata.d.ts +1 -0
  72. package/dist/metadata.d.ts.map +1 -1
  73. package/dist/metadata.js +11 -0
  74. package/dist/metadata.js.map +1 -1
  75. package/dist/migrate-grid-text.d.ts +17 -0
  76. package/dist/migrate-grid-text.d.ts.map +1 -0
  77. package/dist/migrate-grid-text.js +117 -0
  78. package/dist/migrate-grid-text.js.map +1 -0
  79. package/dist/migrate-grid-v22.d.ts +12 -0
  80. package/dist/migrate-grid-v22.d.ts.map +1 -0
  81. package/dist/migrate-grid-v22.js +166 -0
  82. package/dist/migrate-grid-v22.js.map +1 -0
  83. package/dist/migrate-grid-v23.d.ts +10 -0
  84. package/dist/migrate-grid-v23.d.ts.map +1 -0
  85. package/dist/migrate-grid-v23.js +31 -0
  86. package/dist/migrate-grid-v23.js.map +1 -0
  87. package/dist/migration-notebooks.d.ts +25 -0
  88. package/dist/migration-notebooks.d.ts.map +1 -0
  89. package/dist/migration-notebooks.js +131 -0
  90. package/dist/migration-notebooks.js.map +1 -0
  91. package/dist/migration-source.d.ts +6 -0
  92. package/dist/migration-source.d.ts.map +1 -0
  93. package/dist/migration-source.js +37 -0
  94. package/dist/migration-source.js.map +1 -0
  95. package/dist/migration-timeline.d.ts +36 -0
  96. package/dist/migration-timeline.d.ts.map +1 -0
  97. package/dist/migration-timeline.js +187 -0
  98. package/dist/migration-timeline.js.map +1 -0
  99. package/dist/migration-validation.d.ts +12 -0
  100. package/dist/migration-validation.d.ts.map +1 -0
  101. package/dist/migration-validation.js +68 -0
  102. package/dist/migration-validation.js.map +1 -0
  103. package/dist/migration.d.ts.map +1 -1
  104. package/dist/migration.js +188 -117
  105. package/dist/migration.js.map +1 -1
  106. package/dist/model-cache-paths.d.ts +2 -0
  107. package/dist/model-cache-paths.d.ts.map +1 -0
  108. package/dist/model-cache-paths.js +5 -0
  109. package/dist/model-cache-paths.js.map +1 -0
  110. package/dist/model-checksums.json +338 -0
  111. package/dist/model-file-resolver.d.ts +30 -0
  112. package/dist/model-file-resolver.d.ts.map +1 -0
  113. package/dist/model-file-resolver.js +304 -0
  114. package/dist/model-file-resolver.js.map +1 -0
  115. package/dist/model-identity.d.ts +13 -0
  116. package/dist/model-identity.d.ts.map +1 -0
  117. package/dist/model-identity.js +44 -0
  118. package/dist/model-identity.js.map +1 -0
  119. package/dist/model-worker-errors.d.ts +3 -0
  120. package/dist/model-worker-errors.d.ts.map +1 -0
  121. package/dist/model-worker-errors.js +12 -0
  122. package/dist/model-worker-errors.js.map +1 -0
  123. package/dist/model-worker-pool.d.ts +32 -0
  124. package/dist/model-worker-pool.d.ts.map +1 -0
  125. package/dist/model-worker-pool.js +269 -0
  126. package/dist/model-worker-pool.js.map +1 -0
  127. package/dist/model-worker-protocol.d.ts +42 -0
  128. package/dist/model-worker-protocol.d.ts.map +1 -0
  129. package/dist/model-worker-protocol.js +2 -0
  130. package/dist/model-worker-protocol.js.map +1 -0
  131. package/dist/model-worker.d.ts +2 -0
  132. package/dist/model-worker.d.ts.map +1 -0
  133. package/dist/model-worker.js +91 -0
  134. package/dist/model-worker.js.map +1 -0
  135. package/dist/mvp-contracts.d.ts +31 -7
  136. package/dist/mvp-contracts.d.ts.map +1 -1
  137. package/dist/mvp-contracts.js +1 -1
  138. package/dist/mvp-contracts.js.map +1 -1
  139. package/dist/notebook-grid.d.ts +13 -4
  140. package/dist/notebook-grid.d.ts.map +1 -1
  141. package/dist/notebook-grid.js +35 -8
  142. package/dist/notebook-grid.js.map +1 -1
  143. package/dist/notebook-mentions.d.ts +1 -1
  144. package/dist/notebook-mentions.d.ts.map +1 -1
  145. package/dist/notebook-mentions.js +2 -1
  146. package/dist/notebook-mentions.js.map +1 -1
  147. package/dist/schema.d.ts +2 -1
  148. package/dist/schema.d.ts.map +1 -1
  149. package/dist/schema.js +7 -4
  150. package/dist/schema.js.map +1 -1
  151. package/dist/search-provider-access.d.ts +11 -0
  152. package/dist/search-provider-access.d.ts.map +1 -0
  153. package/dist/search-provider-access.js +42 -0
  154. package/dist/search-provider-access.js.map +1 -0
  155. package/dist/similarity.d.ts +60 -1
  156. package/dist/similarity.d.ts.map +1 -1
  157. package/dist/similarity.js +165 -145
  158. package/dist/similarity.js.map +1 -1
  159. package/dist/store.d.ts +15 -1
  160. package/dist/store.d.ts.map +1 -1
  161. package/dist/store.js +168 -14
  162. package/dist/store.js.map +1 -1
  163. package/dist/temporal-index-cache.d.ts +13 -0
  164. package/dist/temporal-index-cache.d.ts.map +1 -0
  165. package/dist/temporal-index-cache.js +59 -0
  166. package/dist/temporal-index-cache.js.map +1 -0
  167. package/dist/temporal-model-manifests.d.ts +17 -0
  168. package/dist/temporal-model-manifests.d.ts.map +1 -0
  169. package/dist/temporal-model-manifests.js +33 -0
  170. package/dist/temporal-model-manifests.js.map +1 -0
  171. package/dist/temporal-models.d.ts +20 -35
  172. package/dist/temporal-models.d.ts.map +1 -1
  173. package/dist/temporal-models.js +23 -257
  174. package/dist/temporal-models.js.map +1 -1
  175. package/dist/temporal-search.d.ts +6 -3
  176. package/dist/temporal-search.d.ts.map +1 -1
  177. package/dist/temporal-search.js +404 -132
  178. package/dist/temporal-search.js.map +1 -1
  179. package/dist/temporal-vector-index.d.ts +46 -0
  180. package/dist/temporal-vector-index.d.ts.map +1 -0
  181. package/dist/temporal-vector-index.js +220 -0
  182. package/dist/temporal-vector-index.js.map +1 -0
  183. package/dist/third-party/@huggingface-jinja.LICENSE +21 -0
  184. package/dist/third-party/@huggingface-tokenizers.LICENSE +201 -0
  185. package/dist/third-party/@huggingface-transformers.LICENSE +202 -0
  186. package/dist/third-party/onnxruntime-common.LICENSE +21 -0
  187. package/dist/third-party/versions.json +23 -0
  188. package/dist/transformers-runtime.d.ts +2 -0
  189. package/dist/transformers-runtime.d.ts.map +1 -0
  190. package/dist/transformers-runtime.js +33014 -0
  191. package/dist/transformers-runtime.js.LEGAL.txt +12 -0
  192. package/dist/transformers-runtime.js.map +7 -0
  193. package/docs/doltlite-staging.md +69 -0
  194. package/docs/edit-performance.md +83 -0
  195. package/docs/engine-layout.md +42 -31
  196. package/docs/media-limits.md +57 -0
  197. package/docs/model-download-policy.md +15 -0
  198. package/docs/model-integrity.md +72 -0
  199. package/docs/model-isolation.md +72 -0
  200. package/docs/model-revisions.md +54 -0
  201. package/docs/release-evidence.md +300 -0
  202. package/docs/search-provider-consent.md +67 -0
  203. package/docs/semantic-durability.md +55 -0
  204. package/docs/temporal-search-performance.md +260 -0
  205. package/docs/v4-migration.md +83 -0
  206. package/fixtures/v5/contract-fixtures.json +3 -3
  207. package/package.json +19 -13
@@ -1,7 +1,28 @@
1
+ import { guardSearchProvider } from "./search-provider-access.js";
1
2
  import { createHash } from "node:crypto";
3
+ import { join } from "node:path";
2
4
  import { normalizeSourcePoint, normalizeSourceRange, rationalEquals, sourceRangeEndTick, } from "./mvp-time.js";
3
5
  import { resultOf, syncResultOf } from "./context.js";
4
6
  import { canonicalJson, EngineFault, parseJson } from "./store.js";
7
+ import { TemporalVectorIndex } from "./temporal-vector-index.js";
8
+ const retrievalStates = new WeakMap();
9
+ const VECTOR_CANDIDATES = 1_000;
10
+ const continuityPrefixes = new WeakMap();
11
+ const SEGMENT_SELECT = `SELECT s.segment_id, s.artifact_id, a.label AS artifact_label,
12
+ a.kind AS artifact_kind, a.created_at AS artifact_created_at,
13
+ s.stream_id, s.object_hash, s.source_range_json, s.source_path, s.segment_kind,
14
+ s.representative_tick, s.generation, s.manifest_id
15
+ FROM runtime_media_segments s JOIN artifacts a ON a.artifact_id=s.artifact_id`;
16
+ export function clearTemporalSearchCache(context) {
17
+ const state = retrievalStates.get(context);
18
+ retrievalStates.delete(context);
19
+ for (const index of state?.vectors.values() ?? []) {
20
+ try {
21
+ index.dispose();
22
+ }
23
+ catch { /* Cache writes cannot prevent closing the catalog. */ }
24
+ }
25
+ }
5
26
  const INDEX_PHASES = [
6
27
  "probe",
7
28
  "segment",
@@ -14,13 +35,28 @@ const INDEX_PHASES = [
14
35
  ];
15
36
  export function createTemporalSearchApi(context) {
16
37
  const providers = new Map();
38
+ const registrations = new Map();
17
39
  return {
18
40
  providers: {
19
- register: (provider) => {
20
- requiredText(provider.manifestId, "Provider manifest ID");
21
- providers.set(provider.manifestId, provider);
41
+ register: (provider, consent = {}) => {
42
+ const id = requiredText(provider.manifestId, "Provider manifest ID");
43
+ const existing = registrations.get(id);
44
+ if (existing?.matches(provider, consent)) {
45
+ existing.validate();
46
+ return;
47
+ }
48
+ let registered = false;
49
+ const guarded = guardSearchProvider(provider, consent, () => !registered || providers.get(id) === guarded.provider);
50
+ guarded.validate();
51
+ providers.set(id, guarded.provider);
52
+ registrations.set(id, guarded);
53
+ registered = true;
22
54
  },
23
55
  list: () => [...providers.keys()].sort(),
56
+ unregister: (manifestId) => {
57
+ registrations.delete(manifestId);
58
+ return providers.delete(manifestId);
59
+ },
24
60
  },
25
61
  manifests: {
26
62
  register: (manifest) => syncResultOf(() => registerManifest(context, manifest)),
@@ -31,8 +67,9 @@ export function createTemporalSearchApi(context) {
31
67
  commitBatch: (input) => syncResultOf(() => commitIndexBatch(context, input)),
32
68
  activate: (manifestId, generation) => syncResultOf(() => activateGeneration(context, manifestId, generation)),
33
69
  coverage: (artifact) => syncResultOf(() => searchCoverage(context, artifact)),
34
- query: (query) => queryTemporalIndex(context, providers, query),
35
- queryPrepared: (query, reference, options = {}) => queryTemporalIndex(context, providers, query, { reference, options }),
70
+ prepare: (options = {}) => context.withLocalMedia(() => resultOf(() => prepareTemporalIndexes(context, options))),
71
+ query: (query) => context.withLocalMedia(() => queryTemporalIndex(context, providers, query)),
72
+ queryPrepared: (query, reference, options = {}) => context.withLocalMedia(() => queryTemporalIndex(context, providers, query, { reference, options })),
36
73
  invalidate: (artifact, objectHash) => syncResultOf(() => invalidateCoverage(context, artifact, objectHash)),
37
74
  cleanup: () => syncResultOf(() => cleanupRetired(context)),
38
75
  stats: () => temporalStats(context),
@@ -226,6 +263,11 @@ function commitIndexBatch(context, input) {
226
263
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
227
264
  .run(batchKey, input.artifactId, input.objectHash, input.manifestId, input.generation, input.phase, input.cursor ?? null, canonicalJson(result), now);
228
265
  });
266
+ const cache = retrievalStates.get(context);
267
+ if (cache?.generations.has(input.generation)) {
268
+ for (const observation of observations)
269
+ cache.pending.add(observation.segmentId);
270
+ }
229
271
  return result;
230
272
  }
231
273
  function normalizeObservation(context, manifest, batch, input, index, streamCache) {
@@ -445,7 +487,8 @@ async function queryTemporalIndex(context, providers, query, prepared) {
445
487
  const active = activeGenerations(context);
446
488
  const generationKey = hashJson(active.map((item) => [item.embedding_space, item.generation]));
447
489
  const offset = decodeCursor(query.cursor, generationKey);
448
- const segments = querySegments(context, active, query);
490
+ const orderedVideo = prepared?.reference.kind === "video" || query.reference?.kind === "video";
491
+ const segments = querySegments(context, active, orderedVideo ? { ...query, durationMs: undefined } : query);
449
492
  const candidates = new Map(segments.map((segment) => [
450
493
  segment.segment_id,
451
494
  { segment, signals: [], score: 0 },
@@ -468,7 +511,8 @@ async function queryTemporalIndex(context, providers, query, prepared) {
468
511
  .filter((candidate) => (!query.sourceArtifactIds
469
512
  || query.sourceArtifactIds.includes(candidate.segment.artifact_id))
470
513
  && (!query.artifactKinds
471
- || query.artifactKinds.includes(candidate.segment.artifact_kind)))
514
+ || query.artifactKinds.includes(candidate.segment.artifact_kind))
515
+ && segmentMatchesDuration(candidate.segment, query.durationMs))
472
516
  .map((candidate) => ({
473
517
  ...candidate,
474
518
  signals: candidate.signals.filter((signal) => allowsSignal(query.modalities, signal.kind)),
@@ -519,22 +563,200 @@ function activeGenerations(context) {
519
563
  ORDER BY embedding_space, generation`)
520
564
  .all();
521
565
  }
566
+ async function prepareTemporalIndexes(context, options) {
567
+ const active = activeGenerations(context);
568
+ const selected = active.filter((generation) => (!options.manifestId || generation.manifest_id === options.manifestId)
569
+ && (!options.generation || generation.generation === options.generation));
570
+ if ((options.manifestId || options.generation) && selected.length === 0) {
571
+ throw new EngineFault({ code: "NOT_READY", message: "Activate a generation before preparing its search index" });
572
+ }
573
+ const state = retrievalState(context, active, false);
574
+ const result = { indexes: 0, vectors: 0, updatedVectors: 0, loadedIndexes: 0, persistedIndexes: 0 };
575
+ for (const generation of selected) {
576
+ for (const modality of requiredManifest(context, generation.manifest_id).modalities) {
577
+ if (modality === "metadata")
578
+ continue;
579
+ const prepared = await vectorIndex(context, generation, modality, state).prepare(options.signal, options.checkpoint === "periodic");
580
+ result.indexes++;
581
+ result.vectors += prepared.vectors;
582
+ result.updatedVectors += prepared.updatedVectors;
583
+ result.loadedIndexes += Number(prepared.loaded);
584
+ result.persistedIndexes += Number(prepared.persisted);
585
+ }
586
+ }
587
+ return result;
588
+ }
589
+ function retrievalState(context, active = activeGenerations(context), refreshMetadata = true) {
590
+ const generationKey = canonicalJson(active.map((item) => [item.generation, item.embedding_space, item.manifest_id]));
591
+ let state = retrievalStates.get(context);
592
+ if (!state || state.generationKey !== generationKey) {
593
+ const generations = [...new Set(active.map((item) => item.generation))];
594
+ const rows = generations.length === 0 ? [] : context.store.db.prepare(`${SEGMENT_SELECT} WHERE s.generation IN (${generations.map(() => "?").join(",")}) ORDER BY s.segment_id`).all(...generations);
595
+ const segments = new Map(rows.map((row) => [row.segment_id, row]));
596
+ const vectors = state?.vectors ?? new Map();
597
+ for (const [key, index] of vectors) {
598
+ const [generation, space] = JSON.parse(key);
599
+ if (!active.some((item) => item.generation === generation && item.embedding_space === space)) {
600
+ try {
601
+ index.dispose();
602
+ }
603
+ catch { /* Retired cache persistence is optional. */ }
604
+ vectors.delete(key);
605
+ continue;
606
+ }
607
+ for (const [segmentId, entry] of index.entries) {
608
+ const segment = segments.get(segmentId);
609
+ if (segment)
610
+ entry.value.segment = segment;
611
+ else
612
+ index.delete(segmentId);
613
+ }
614
+ }
615
+ state = {
616
+ semanticVersion: context.store.semanticVersion, generationKey, generations: new Set(generations),
617
+ segments, vectors, ordered: new Map(), pending: state?.pending ?? new Set(),
618
+ };
619
+ retrievalStates.set(context, state);
620
+ }
621
+ if (refreshMetadata && state.semanticVersion !== context.store.semanticVersion) {
622
+ const generations = [...state.generations];
623
+ const rows = generations.length === 0 ? [] : context.store.db.prepare(`${SEGMENT_SELECT} WHERE s.generation IN (${generations.map(() => "?").join(",")}) ORDER BY s.segment_id`).all(...generations);
624
+ state.segments = new Map(rows.map((row) => [row.segment_id, row]));
625
+ for (const [key, index] of state.vectors) {
626
+ for (const [segmentId, entry] of index.entries) {
627
+ const segment = state.segments.get(segmentId);
628
+ if (segment)
629
+ entry.value.segment = segment;
630
+ else
631
+ index.delete(segmentId);
632
+ }
633
+ if (index.entries.size === 0) {
634
+ try {
635
+ index.dispose();
636
+ }
637
+ catch { /* Empty cache persistence is optional. */ }
638
+ state.vectors.delete(key);
639
+ }
640
+ }
641
+ state.texts = undefined;
642
+ state.ordered.clear();
643
+ state.semanticVersion = context.store.semanticVersion;
644
+ }
645
+ if (state.pending.size > 0)
646
+ refreshRetrievalState(context, state);
647
+ return state;
648
+ }
649
+ function refreshRetrievalState(context, state) {
650
+ const pending = [...state.pending];
651
+ for (let start = 0; start < pending.length; start += 400) {
652
+ const keys = pending.slice(start, start + 400);
653
+ const placeholders = keys.map(() => "?").join(",");
654
+ for (const key of keys) {
655
+ state.segments.delete(key);
656
+ state.texts?.delete(key);
657
+ for (const index of state.vectors.values())
658
+ index.delete(key);
659
+ }
660
+ const rows = context.store.db.prepare(`${SEGMENT_SELECT} WHERE s.segment_id IN (${placeholders}) ORDER BY s.segment_id`).all(...keys);
661
+ for (const row of rows) {
662
+ if (state.generations.has(row.generation))
663
+ state.segments.set(row.segment_id, row);
664
+ }
665
+ if (state.texts) {
666
+ const texts = context.store.db.prepare(`SELECT segment_id, kind, text FROM runtime_segment_text
667
+ WHERE segment_id IN (${placeholders}) ORDER BY segment_id, kind, text`).all(...keys);
668
+ for (const row of texts)
669
+ addCachedText(state.texts, row);
670
+ }
671
+ for (const [key, index] of state.vectors) {
672
+ const [generation, embeddingSpace, modality] = JSON.parse(key);
673
+ const vectors = context.store.db.prepare(`SELECT segment_id, dimensions, vector_blob FROM runtime_segment_embeddings
674
+ WHERE generation=? AND embedding_space=? AND modality=?
675
+ AND segment_id IN (${placeholders}) ORDER BY segment_id`).all(generation, embeddingSpace, modality, ...keys);
676
+ for (const row of vectors)
677
+ addCachedVector(state, index, row);
678
+ }
679
+ }
680
+ state.ordered.clear();
681
+ state.pending.clear();
682
+ }
683
+ function cachedTextRows(context, active) {
684
+ const state = retrievalState(context, active);
685
+ if (!state.texts) {
686
+ state.texts = new Map();
687
+ const generations = [...state.generations];
688
+ const rows = generations.length === 0 ? [] : context.store.db.prepare(`SELECT segment_id, kind, text FROM runtime_segment_text
689
+ WHERE generation IN (${generations.map(() => "?").join(",")}) ORDER BY segment_id, kind, text`).all(...generations);
690
+ for (const row of rows)
691
+ addCachedText(state.texts, row);
692
+ }
693
+ return [...state.texts.values()].flat();
694
+ }
695
+ function addCachedText(texts, row) {
696
+ const rows = texts.get(row.segment_id) ?? [];
697
+ rows.push(row);
698
+ texts.set(row.segment_id, rows);
699
+ }
700
+ function addCachedVector(state, index, row) {
701
+ const segment = state.segments.get(row.segment_id);
702
+ if (!segment)
703
+ return;
704
+ if (row.dimensions !== index.dimensions) {
705
+ throw new EngineFault({ code: "MANIFEST_INCOMPATIBLE", message: "Stored vector dimensions do not match the index manifest" });
706
+ }
707
+ const vector = blobToVector(row.vector_blob, row.dimensions);
708
+ index.set(row.segment_id, vector, { segment, vector, range: segmentRange(segment) });
709
+ }
710
+ function vectorIndex(context, generation, modality, state = retrievalState(context)) {
711
+ const key = canonicalJson([generation.generation, generation.embedding_space, modality]);
712
+ const cached = state.vectors.get(key);
713
+ if (cached)
714
+ return cached;
715
+ const manifest = requiredManifest(context, generation.manifest_id);
716
+ const identity = canonicalJson([key, manifest]);
717
+ const index = new TemporalVectorIndex(manifest.dimensions, {
718
+ identity, basePath: join(context.store.dataDir, "runtime-search", hashJson(identity)),
719
+ });
720
+ let after = "";
721
+ for (;;) {
722
+ const rows = context.store.db.prepare(`SELECT segment_id, dimensions, vector_blob FROM runtime_segment_embeddings
723
+ WHERE generation=? AND embedding_space=? AND modality=? AND segment_id>?
724
+ ORDER BY segment_id LIMIT 512`).all(generation.generation, generation.embedding_space, modality, after);
725
+ for (const row of rows)
726
+ addCachedVector(state, index, row);
727
+ if (rows.length < 512)
728
+ break;
729
+ after = rows.at(-1).segment_id;
730
+ }
731
+ state.vectors.set(key, index);
732
+ return index;
733
+ }
734
+ function orderedStreams(context, generation) {
735
+ const state = retrievalState(context);
736
+ const key = canonicalJson([generation.generation, generation.embedding_space]);
737
+ const cached = state.ordered.get(key);
738
+ if (cached)
739
+ return cached;
740
+ const streams = new Map();
741
+ for (const { value } of vectorIndex(context, generation, "visual").entries.values()) {
742
+ if (!value.range)
743
+ continue;
744
+ const stream = streams.get(value.range.streamId) ?? [];
745
+ stream.push({ segment: value.segment, vector: value.vector, range: value.range });
746
+ streams.set(value.range.streamId, stream);
747
+ }
748
+ for (const stream of streams.values())
749
+ stream.sort((left, right) => left.range.startTick - right.range.startTick || left.segment.segment_id.localeCompare(right.segment.segment_id));
750
+ state.ordered.set(key, streams);
751
+ return streams;
752
+ }
522
753
  function querySegments(context, active, query) {
523
754
  if (active.length === 0)
524
755
  return [];
525
- const generations = active.map((item) => item.generation);
526
- const placeholders = generations.map(() => "?").join(",");
527
- const rows = context.store.db
528
- .prepare(`SELECT s.segment_id, s.artifact_id, a.label AS artifact_label,
529
- a.kind AS artifact_kind, a.created_at AS artifact_created_at,
530
- s.stream_id, s.object_hash,
531
- s.source_range_json, s.source_path, s.segment_kind,
532
- s.representative_tick, s.generation, s.manifest_id
533
- FROM runtime_media_segments s
534
- JOIN artifacts a ON a.artifact_id=s.artifact_id
535
- WHERE s.generation IN (${placeholders})
536
- ORDER BY s.artifact_id, s.stream_id, s.representative_tick, s.segment_id`)
537
- .all(...generations);
756
+ const rows = [...retrievalState(context, active).segments.values()];
757
+ const labelMatches = new Map();
758
+ const orientations = new Map();
759
+ const indexingStates = new Map();
538
760
  return rows.filter((row) => {
539
761
  if (query.artifactKinds && !query.artifactKinds.includes(row.artifact_kind)) {
540
762
  return false;
@@ -551,33 +773,38 @@ function querySegments(context, active, query) {
551
773
  && row.artifact_created_at >= query.createdBefore) {
552
774
  return false;
553
775
  }
554
- if (query.labels && !artifactHasLabels(context, row.artifact_id, query.labels)) {
555
- return false;
776
+ if (query.labels) {
777
+ if (!labelMatches.has(row.artifact_id))
778
+ labelMatches.set(row.artifact_id, artifactHasLabels(context, row.artifact_id, query.labels));
779
+ if (!labelMatches.get(row.artifact_id))
780
+ return false;
556
781
  }
557
- if (query.orientations
558
- && !query.orientations.includes(artifactOrientation(context, row.artifact_id))) {
559
- return false;
782
+ if (query.orientations) {
783
+ if (!orientations.has(row.artifact_id))
784
+ orientations.set(row.artifact_id, artifactOrientation(context, row.artifact_id));
785
+ if (!query.orientations.includes(orientations.get(row.artifact_id)))
786
+ return false;
560
787
  }
561
- if (query.indexingStates
562
- && !query.indexingStates.includes(artifactGenerationState(context, row.artifact_id, row.generation))) {
563
- return false;
788
+ if (query.indexingStates) {
789
+ const key = `${row.artifact_id}:${row.generation}`;
790
+ if (!indexingStates.has(key))
791
+ indexingStates.set(key, artifactGenerationState(context, row.artifact_id, row.generation));
792
+ if (!query.indexingStates.includes(indexingStates.get(key)))
793
+ return false;
564
794
  }
565
- if (query.durationMs) {
566
- const range = segmentRange(row);
567
- if (range) {
568
- const durationMs = (range.durationTicks * range.timeBase.numerator * 1_000)
569
- / range.timeBase.denominator;
570
- if ((query.durationMs.min !== undefined
571
- && durationMs < query.durationMs.min)
572
- || (query.durationMs.max !== undefined
573
- && durationMs > query.durationMs.max)) {
574
- return false;
575
- }
576
- }
577
- }
578
- return true;
795
+ return segmentMatchesDuration(row, query.durationMs);
579
796
  });
580
797
  }
798
+ function segmentMatchesDuration(segment, requested) {
799
+ if (!requested)
800
+ return true;
801
+ const range = segmentRange(segment);
802
+ if (!range)
803
+ return true;
804
+ const durationMs = sourceTickToMs(range.durationTicks, range);
805
+ return (requested.min === undefined || durationMs >= requested.min)
806
+ && (requested.max === undefined || durationMs <= requested.max);
807
+ }
581
808
  function artifactHasLabels(context, artifactId, requested) {
582
809
  const row = context.store.db
583
810
  .prepare(`SELECT value_json FROM artifact_metadata
@@ -612,12 +839,7 @@ function addLexicalSignals(context, candidates, text, active) {
612
839
  const generations = active.map((item) => item.generation);
613
840
  if (generations.length === 0)
614
841
  return;
615
- const rows = context.store.db
616
- .prepare(`SELECT segment_id, kind, text
617
- FROM runtime_segment_text
618
- WHERE generation IN (${generations.map(() => "?").join(",")})
619
- ORDER BY segment_id, kind, text`)
620
- .all(...generations);
842
+ const rows = cachedTextRows(context, active);
621
843
  const normalized = text.trim().toLocaleLowerCase();
622
844
  const quoted = [...text.matchAll(/"([^"]+)"/g)]
623
845
  .map((match) => match[1]?.trim().toLocaleLowerCase())
@@ -705,8 +927,11 @@ function addOrderedVideoSignals(context, candidates, rangeInput, generation) {
705
927
  && rangesOverlap(segmentRangeValue.startTick, sourceRangeEndTick(segmentRangeValue), range.startTick, sourceRangeEndTick(range)));
706
928
  if (query.length === 0)
707
929
  return;
708
- const sample = evenlySample(query, Math.min(query.length, 16));
709
- addOrderedVideoVectorSignals(context, candidates, sample, generation);
930
+ const sample = evenlySample(query, Math.min(query.length, 16)).map((item) => ({
931
+ ...item,
932
+ offsetMs: sourceTickToMs(item.range.startTick - range.startTick, range),
933
+ }));
934
+ addOrderedVideoVectorSignals(context, candidates, sample, generation, sourceTickToMs(range.durationTicks, range));
710
935
  }
711
936
  function addPreparedReferenceSignals(context, candidates, reference, options, active) {
712
937
  for (const generation of active) {
@@ -720,39 +945,62 @@ function addPreparedReferenceSignals(context, candidates, reference, options, ac
720
945
  continue;
721
946
  }
722
947
  const samples = preparedVideoSamples(reference, options);
948
+ const startMs = options.range?.startMs ?? 0;
949
+ const durationMs = Math.min(options.range?.durationMs ?? Infinity, preparedVideoDuration(reference) - startMs);
723
950
  for (const sample of samples) {
724
951
  preparedVector(sample.vector, manifest.dimensions);
725
952
  }
726
953
  addOrderedVideoVectorSignals(context, candidates, samples.map((sample) => ({
727
954
  vector: Float32Array.from(sample.vector),
728
- })), generation);
955
+ offsetMs: sample.offsetMs - startMs,
956
+ })), generation, durationMs);
729
957
  }
730
958
  }
731
- function addOrderedVideoVectorSignals(context, candidates, sample, generation) {
959
+ function addOrderedVideoVectorSignals(context, candidates, sample, generation, durationMs) {
732
960
  if (sample.length === 0)
733
961
  return;
734
- const all = orderedEmbeddings(context, generation, () => true);
735
- const byStream = new Map();
736
- for (const item of all) {
737
- const values = byStream.get(item.range.streamId) ?? [];
738
- values.push(item);
739
- byStream.set(item.range.streamId, values);
740
- }
962
+ const byStream = orderedStreams(context, generation);
963
+ const starts = orderedCandidateStarts(context, candidates, sample, generation, byStream);
964
+ const queryVectors = new Map();
741
965
  const windows = [];
742
- for (const values of byStream.values()) {
743
- values.sort((left, right) => left.range.startTick - right.range.startTick
744
- || left.segment.segment_id.localeCompare(right.segment.segment_id));
745
- if (values.length < sample.length)
746
- continue;
747
- for (let start = 0; start <= values.length - sample.length; start += 1) {
748
- const window = values.slice(start, start + sample.length);
749
- const aligned = sample.reduce((sum, item, index) => sum + cosineSimilarity(item.vector, window[index].vector), 0) / sample.length;
750
- const coherence = transitionCoherence(sample, window) * 0.75
751
- + temporalContinuity(window) * 0.25;
966
+ for (const [streamId, positions] of starts) {
967
+ const values = byStream.get(streamId);
968
+ for (const start of [...positions].sort((left, right) => left - right)) {
969
+ const first = values[start];
970
+ const startMs = sourceTickToMs(first.range.startTick, first.range);
971
+ const end = orderedPosition(values, startMs + durationMs);
972
+ const count = end - start;
973
+ const window = Array.from({ length: Math.min(count, 16) }, (_, index) => values[start + (count === 1 ? 0 : Math.round(index * (count - 1) / (Math.min(count, 16) - 1)))]);
974
+ const last = values[end - 1];
975
+ const availableMs = Math.min(durationMs, sourceTickToMs(sourceRangeEndTick(last.range), last.range) - startMs);
976
+ const timedWindow = window.map((item) => ({
977
+ ...item,
978
+ offsetMs: sourceTickToMs(item.range.startTick, item.range) - startMs,
979
+ }));
980
+ const offsets = [...new Set([...sample, ...timedWindow]
981
+ .map((item) => item.offsetMs)
982
+ .filter((offset) => offset >= 0 && offset < availableMs))]
983
+ .sort((left, right) => left - right);
984
+ if (offsets.length === 0)
985
+ continue;
986
+ const query = offsets.map((offset) => {
987
+ let vector = queryVectors.get(offset);
988
+ if (!vector) {
989
+ vector = videoVectorAt(sample, offset);
990
+ queryVectors.set(offset, vector);
991
+ }
992
+ return { vector };
993
+ });
994
+ const target = offsets.map((offset) => ({ vector: videoVectorAt(timedWindow, offset) }));
995
+ const aligned = query.reduce((sum, item, index) => sum + cosineSimilarity(item.vector, target[index].vector), 0) / query.length;
996
+ const coherence = transitionCoherence(query, target) * 0.75
997
+ + temporalContinuity(values, start, end) * 0.25;
752
998
  windows.push({
753
999
  values: window,
754
- score: aligned * 0.75 + coherence * 0.25,
1000
+ score: (aligned * 0.75 + coherence * 0.25) * availableMs / durationMs,
755
1001
  coherence,
1002
+ durationTicks: Math.max(1, Math.min(sourceRangeEndTick(last.range) - first.range.startTick, Math.round(availableMs * first.range.timeBase.denominator
1003
+ / (first.range.timeBase.numerator * 1_000)))),
756
1004
  });
757
1005
  }
758
1006
  }
@@ -764,7 +1012,7 @@ function addOrderedVideoVectorSignals(context, candidates, sample, generation) {
764
1012
  const last = window.values.at(-1);
765
1013
  const combined = {
766
1014
  ...first.range,
767
- durationTicks: sourceRangeEndTick(last.range) - first.range.startTick,
1015
+ durationTicks: window.durationTicks,
768
1016
  };
769
1017
  const segmentId = `ordered:${generation.generation}:${first.segment.segment_id}:${last.segment.segment_id}`;
770
1018
  const candidate = {
@@ -795,30 +1043,56 @@ function addOrderedVideoVectorSignals(context, candidates, sample, generation) {
795
1043
  candidates.set(segmentId, candidate);
796
1044
  });
797
1045
  }
1046
+ function orderedCandidateStarts(context, candidates, sample, generation, streams) {
1047
+ const starts = new Map();
1048
+ const index = vectorIndex(context, generation, "visual");
1049
+ const timedCandidates = new Map([...candidates].filter(([, candidate]) => candidate.segment.source_range_json !== null));
1050
+ const seedCount = Math.max(32, Math.floor(512 / sample.length));
1051
+ for (const querySample of sample) {
1052
+ for (const { value } of index.nearest(querySample.vector, seedCount, timedCandidates)) {
1053
+ if (!value.range)
1054
+ continue;
1055
+ const stream = streams.get(value.range.streamId);
1056
+ const desiredMs = sourceTickToMs(value.range.startTick, value.range) - querySample.offsetMs;
1057
+ const low = orderedPosition(stream, desiredMs);
1058
+ const positions = starts.get(value.range.streamId) ?? new Set();
1059
+ for (const position of [low - 1, low, low + 1]) {
1060
+ const item = stream[position];
1061
+ if (item && candidates.has(item.segment.segment_id))
1062
+ positions.add(position);
1063
+ }
1064
+ starts.set(value.range.streamId, positions);
1065
+ }
1066
+ }
1067
+ return starts;
1068
+ }
1069
+ function orderedPosition(stream, milliseconds) {
1070
+ let low = 0;
1071
+ let high = stream.length;
1072
+ while (low < high) {
1073
+ const middle = (low + high) >>> 1;
1074
+ const item = stream[middle];
1075
+ if (sourceTickToMs(item.range.startTick, item.range) < milliseconds)
1076
+ low = middle + 1;
1077
+ else
1078
+ high = middle;
1079
+ }
1080
+ return low;
1081
+ }
1082
+ function videoVectorAt(samples, offsetMs) {
1083
+ const next = samples.findIndex((sample) => sample.offsetMs >= offsetMs);
1084
+ if (next === -1)
1085
+ return samples.at(-1).vector;
1086
+ if (next === 0 || samples[next].offsetMs === offsetMs)
1087
+ return samples[next].vector;
1088
+ const before = samples[next - 1];
1089
+ const after = samples[next];
1090
+ const fraction = (offsetMs - before.offsetMs) / (after.offsetMs - before.offsetMs);
1091
+ return before.vector.map((value, index) => value * (1 - fraction) + after.vector[index] * fraction);
1092
+ }
798
1093
  function orderedEmbeddings(context, generation, include) {
799
- const rows = context.store.db
800
- .prepare(`SELECT s.segment_id, s.artifact_id, a.label AS artifact_label,
801
- a.kind AS artifact_kind, a.created_at AS artifact_created_at,
802
- s.stream_id, s.object_hash, s.source_range_json,
803
- s.source_path, s.segment_kind, s.representative_tick,
804
- s.generation, s.manifest_id, e.dimensions, e.vector_blob
805
- FROM runtime_media_segments s
806
- JOIN artifacts a ON a.artifact_id=s.artifact_id
807
- JOIN runtime_segment_embeddings e ON e.segment_id=s.segment_id
808
- WHERE s.generation=? AND e.embedding_space=?
809
- AND e.modality='visual' AND s.source_range_json IS NOT NULL
810
- ORDER BY s.stream_id, s.representative_tick, s.segment_id`)
811
- .all(generation.generation, generation.embedding_space);
812
- return rows.flatMap((row) => {
813
- const range = segmentRange(row);
814
- return range && include(range)
815
- ? [{
816
- segment: row,
817
- range,
818
- vector: blobToVector(row.vector_blob, row.dimensions),
819
- }]
820
- : [];
821
- });
1094
+ return [...orderedStreams(context, generation).values()]
1095
+ .flatMap((rows) => rows.filter((row) => include(row.range)));
822
1096
  }
823
1097
  function transitionCoherence(query, candidate) {
824
1098
  if (query.length < 2)
@@ -829,20 +1103,24 @@ function transitionCoherence(query, candidate) {
829
1103
  }
830
1104
  return score / (query.length - 1);
831
1105
  }
832
- function temporalContinuity(candidate) {
833
- if (candidate.length < 2)
1106
+ function temporalContinuity(candidate, start, end) {
1107
+ if (end - start < 2)
834
1108
  return 1;
835
- let score = 0;
836
- for (let index = 1; index < candidate.length; index += 1) {
837
- const previous = candidate[index - 1].range;
838
- const current = candidate[index].range;
839
- const gapMs = Math.max(0, sourceTickToMs(current.startTick, current)
840
- - sourceTickToMs(sourceRangeEndTick(previous), previous));
841
- const scaleMs = Math.max(1, (sourceTickToMs(previous.durationTicks, previous)
842
- + sourceTickToMs(current.durationTicks, current)) / 2);
843
- score += 1 / (1 + gapMs / scaleMs);
1109
+ let prefix = continuityPrefixes.get(candidate);
1110
+ if (!prefix) {
1111
+ prefix = new Float64Array(candidate.length);
1112
+ for (let index = 1; index < candidate.length; index++) {
1113
+ const previous = candidate[index - 1].range;
1114
+ const current = candidate[index].range;
1115
+ const gapMs = Math.max(0, sourceTickToMs(current.startTick, current)
1116
+ - sourceTickToMs(sourceRangeEndTick(previous), previous));
1117
+ const scaleMs = Math.max(1, (sourceTickToMs(previous.durationTicks, previous)
1118
+ + sourceTickToMs(current.durationTicks, current)) / 2);
1119
+ prefix[index] = prefix[index - 1] + 1 / (1 + gapMs / scaleMs);
1120
+ }
1121
+ continuityPrefixes.set(candidate, prefix);
844
1122
  }
845
- return score / (candidate.length - 1);
1123
+ return (prefix[end - 1] - prefix[start]) / (end - start - 1);
846
1124
  }
847
1125
  function isExactOrderedWindow(query, candidate) {
848
1126
  return query.length === candidate.length && query.every((item, index) => {
@@ -879,31 +1157,13 @@ function evenlySample(values, count) {
879
1157
  });
880
1158
  }
881
1159
  function rankVectorRows(context, candidates, generation, queryVector, modality, explanation) {
882
- const rows = context.store.db
883
- .prepare(`SELECT segment_id, embedding_space, modality, dimensions, vector_blob
884
- FROM runtime_segment_embeddings
885
- WHERE generation=? AND embedding_space=?
886
- AND modality=?
887
- ORDER BY segment_id`)
888
- .all(generation.generation, generation.embedding_space, modality);
889
- rows
890
- .map((row) => ({
891
- row,
892
- score: cosineSimilarity(queryVector, blobToVector(row.vector_blob, row.dimensions)),
893
- }))
894
- .filter((item) => Number.isFinite(item.score))
895
- .sort((left, right) => right.score - left.score
896
- || left.row.segment_id.localeCompare(right.row.segment_id))
1160
+ vectorIndex(context, generation, modality)
1161
+ .nearest(queryVector, VECTOR_CANDIDATES, candidates)
897
1162
  .forEach((item, index) => {
898
- const candidate = candidates.get(item.row.segment_id);
1163
+ const candidate = candidates.get(item.key);
899
1164
  if (!candidate)
900
1165
  return;
901
- candidate.signals.push({
902
- kind: modality,
903
- rank: index + 1,
904
- score: item.score,
905
- explanation,
906
- });
1166
+ candidate.signals.push({ kind: modality, rank: index + 1, score: item.score, explanation });
907
1167
  });
908
1168
  }
909
1169
  function referenceEmbeddingRows(context, reference, generation) {
@@ -1341,6 +1601,13 @@ function validatePreparedReference(reference, options) {
1341
1601
  message: "Prepared video references require between 1 and 16 samples",
1342
1602
  });
1343
1603
  }
1604
+ if (reference.durationMs !== undefined && (!Number.isFinite(reference.durationMs) || reference.durationMs <= 0
1605
+ || reference.samples.some((sample) => sample.offsetMs >= reference.durationMs))) {
1606
+ throw new EngineFault({
1607
+ code: "INVALID_INPUT",
1608
+ message: "Prepared video duration must be finite, positive, and contain every sample",
1609
+ });
1610
+ }
1344
1611
  let previousOffset = -1;
1345
1612
  for (const sample of reference.samples) {
1346
1613
  if (!Number.isFinite(sample.offsetMs) || sample.offsetMs < 0) {
@@ -1378,11 +1645,9 @@ function validatePreparedReference(reference, options) {
1378
1645
  }
1379
1646
  function preparedVideoSamples(reference, options) {
1380
1647
  const range = options.range;
1381
- const endMs = range?.durationMs === undefined
1382
- ? Number.POSITIVE_INFINITY
1383
- : range.startMs + range.durationMs;
1648
+ const endMs = Math.min(preparedVideoDuration(reference), range?.durationMs === undefined ? Infinity : range.startMs + range.durationMs);
1384
1649
  const samples = range
1385
- ? reference.samples.filter((sample) => sample.offsetMs >= range.startMs && sample.offsetMs <= endMs)
1650
+ ? reference.samples.filter((sample) => sample.offsetMs >= range.startMs && sample.offsetMs < endMs)
1386
1651
  : reference.samples;
1387
1652
  if (samples.length === 0) {
1388
1653
  throw new EngineFault({
@@ -1392,6 +1657,13 @@ function preparedVideoSamples(reference, options) {
1392
1657
  }
1393
1658
  return samples;
1394
1659
  }
1660
+ function preparedVideoDuration(reference) {
1661
+ if (reference.durationMs !== undefined)
1662
+ return reference.durationMs;
1663
+ const last = reference.samples.at(-1);
1664
+ const intervals = reference.samples.slice(1).map((sample, index) => sample.offsetMs - reference.samples[index].offsetMs).sort((a, b) => a - b);
1665
+ return last.offsetMs + (intervals[Math.floor(intervals.length / 2)] ?? 1);
1666
+ }
1395
1667
  function preparedVector(vector, dimensions) {
1396
1668
  if (!Array.isArray(vector)
1397
1669
  || vector.length === 0