@camstack/system 1.2.47 → 1.2.48
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.
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +1 -1
- package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.ts +10 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +516 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +516 -1
- package/dist/builtins/sqlite-storage/vector-index.d.ts +172 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-BRBfh1_9.mjs → dist-C7rPTQLR.mjs} +33 -1
- package/dist/{dist-CKjrL1qP.js → dist-CH0DyAwh.js} +56 -0
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as
|
|
1
|
+
import { C as cosineSimilarity, Nt as parseJsonUnknown, St as asJsonObject, T as decodeVectorBase64, dt as errMsg, ft as BaseAddon, l as RUNTIME_DEFAULTS, lt as vectorDimFromBase64, ut as vectorStoreCapability, w as dataStoreProviderCapability } from "../../dist-C7rPTQLR.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import Database from "better-sqlite3";
|
|
4
4
|
//#region src/builtins/sqlite-storage/filter-compiler.ts
|
|
@@ -641,6 +641,484 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
|
|
|
641
641
|
}
|
|
642
642
|
};
|
|
643
643
|
//#endregion
|
|
644
|
+
//#region src/builtins/sqlite-storage/vector-index.ts
|
|
645
|
+
/**
|
|
646
|
+
* SQLite-backed vector index — compact rows, exhaustive cosine ranking.
|
|
647
|
+
*
|
|
648
|
+
* Lives INSIDE the sqlite-settings builtin, against `SqliteSettingsBackend`
|
|
649
|
+
* directly, and that placement is the point. It first shipped as its own addon
|
|
650
|
+
* and that cost two round trips per write — caller → the `settings-store` door
|
|
651
|
+
* → the engine ([D44](../../../../../docs/decisions/adr-0044.md)) — plus a
|
|
652
|
+
* whole runner process, to reach the very database this addon already holds
|
|
653
|
+
* open. Here an upsert is a function call.
|
|
654
|
+
*
|
|
655
|
+
* Replaceability is unaffected: it is the CAPABILITY that makes the backend
|
|
656
|
+
* swappable, not the package. An ANN provider registers `vector-store` from
|
|
657
|
+
* wherever it likes and `setActiveSingleton` picks it.
|
|
658
|
+
*
|
|
659
|
+
* This is the DEFAULT `vector-store` provider, not the final one. It exists to
|
|
660
|
+
* make a row cheap; replacing exhaustive scan with a real ANN index is a
|
|
661
|
+
* different provider registered against the same capability.
|
|
662
|
+
*
|
|
663
|
+
* What it changes versus the per-store JSON columns it replaces:
|
|
664
|
+
*
|
|
665
|
+
* - the vector is a base64 Float32 TEXT column (~2.7 KB for 512 dims) instead
|
|
666
|
+
* of a JSON array of 512 numbers (~11.7 KB measured on the live hub), and
|
|
667
|
+
* decoding is a `readFloatLE` loop instead of `JSON.parse` of 512 tokens;
|
|
668
|
+
* - filterable metadata is promoted to real columns, so `deviceId` /
|
|
669
|
+
* `timestamp` / `className` prefilters are pushed into SQL and indexed
|
|
670
|
+
* rather than applied after loading everything;
|
|
671
|
+
* - a truncated scan is REPORTED (`truncated`), because a search that silently
|
|
672
|
+
* considered a subset reads as "nothing else matched".
|
|
673
|
+
*
|
|
674
|
+
* Metadata that is not one of the promoted columns rides in a JSON blob and is
|
|
675
|
+
* returned verbatim; it cannot be filtered on. That is deliberate — a filter a
|
|
676
|
+
* backend cannot push down is a filter that gets forgotten.
|
|
677
|
+
*/
|
|
678
|
+
/** Metadata keys promoted to real, indexable columns. */
|
|
679
|
+
var PROMOTED_COLUMNS = [
|
|
680
|
+
"deviceId",
|
|
681
|
+
"timestamp",
|
|
682
|
+
"className",
|
|
683
|
+
"modelId"
|
|
684
|
+
];
|
|
685
|
+
/** Ceiling on rows compared in one query, so a pathological index cannot wedge the runner. */
|
|
686
|
+
var MAX_SCAN_ROWS = 2e5;
|
|
687
|
+
var collectionFor = (index) => `vector-store:${index}`;
|
|
688
|
+
/**
|
|
689
|
+
* Where index declarations live.
|
|
690
|
+
*
|
|
691
|
+
* They MUST be durable. The rows survive a restart in SQLite but the `dim` and
|
|
692
|
+
* `metric` that make them queryable were only ever in memory, so after a hub
|
|
693
|
+
* restart `query` and `stats` both threw "index was never declared" until some
|
|
694
|
+
* caller happened to write — which meant semantic search was dead from boot
|
|
695
|
+
* until the next track closed. Callers declare lazily (the dimension comes from
|
|
696
|
+
* the active encoder, unknown at construction), so nothing re-declared on boot.
|
|
697
|
+
*/
|
|
698
|
+
var INDEX_REGISTRY_COLLECTION = "vector-store:__indexes";
|
|
699
|
+
var SqliteVectorIndex = class {
|
|
700
|
+
store;
|
|
701
|
+
logger;
|
|
702
|
+
specs = /* @__PURE__ */ new Map();
|
|
703
|
+
constructor(deps) {
|
|
704
|
+
this.store = deps.store;
|
|
705
|
+
this.logger = deps.logger;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Load persisted index declarations. Call once at startup, BEFORE serving:
|
|
709
|
+
* without it every method on an existing index throws until a write
|
|
710
|
+
* re-declares it.
|
|
711
|
+
*/
|
|
712
|
+
async loadIndexRegistry() {
|
|
713
|
+
try {
|
|
714
|
+
await this.store.declareCollection({
|
|
715
|
+
collection: INDEX_REGISTRY_COLLECTION,
|
|
716
|
+
columns: [
|
|
717
|
+
{
|
|
718
|
+
name: "id",
|
|
719
|
+
type: "TEXT",
|
|
720
|
+
primaryKey: true,
|
|
721
|
+
notNull: true
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
name: "dim",
|
|
725
|
+
type: "INTEGER",
|
|
726
|
+
notNull: true
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
name: "metric",
|
|
730
|
+
type: "TEXT",
|
|
731
|
+
notNull: true
|
|
732
|
+
}
|
|
733
|
+
]
|
|
734
|
+
});
|
|
735
|
+
const rows = await this.store.query({ collection: INDEX_REGISTRY_COLLECTION });
|
|
736
|
+
for (const row of rows) {
|
|
737
|
+
const dim = row.data["dim"];
|
|
738
|
+
const metric = row.data["metric"];
|
|
739
|
+
if (typeof dim !== "number") continue;
|
|
740
|
+
this.specs.set(row.id, {
|
|
741
|
+
dim,
|
|
742
|
+
metric: isMetric(metric) ? metric : "cosine"
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
this.logger.info("vector-store index registry loaded", { meta: { indexes: this.specs.size } });
|
|
746
|
+
} catch (err) {
|
|
747
|
+
this.logger.error("vector-store index registry failed to load", { meta: { error: String(err) } });
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Idempotent. Re-declaring an index with a DIFFERENT dim throws rather than
|
|
752
|
+
* wiping: silently dropping a populated index because a caller shipped a new
|
|
753
|
+
* model would destroy history that cannot be rebuilt.
|
|
754
|
+
*/
|
|
755
|
+
async declareIndex(index, dim, metric) {
|
|
756
|
+
const existing = this.specs.get(index);
|
|
757
|
+
if (existing !== void 0 && existing.dim !== dim) throw new Error(`vector-store: index "${index}" is declared with dim ${existing.dim}; refusing to redeclare as ${dim}`);
|
|
758
|
+
await this.store.declareCollection({
|
|
759
|
+
collection: collectionFor(index),
|
|
760
|
+
columns: [
|
|
761
|
+
{
|
|
762
|
+
name: "id",
|
|
763
|
+
type: "TEXT",
|
|
764
|
+
primaryKey: true,
|
|
765
|
+
notNull: true
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
name: "vector",
|
|
769
|
+
type: "TEXT",
|
|
770
|
+
notNull: true
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
name: "deviceId",
|
|
774
|
+
type: "INTEGER"
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
name: "timestamp",
|
|
778
|
+
type: "INTEGER"
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
name: "className",
|
|
782
|
+
type: "TEXT"
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
name: "modelId",
|
|
786
|
+
type: "TEXT"
|
|
787
|
+
},
|
|
788
|
+
{
|
|
789
|
+
name: "extra",
|
|
790
|
+
type: "JSON"
|
|
791
|
+
}
|
|
792
|
+
],
|
|
793
|
+
indexes: [
|
|
794
|
+
{
|
|
795
|
+
name: `idx_vec_${index}_dev_ts`,
|
|
796
|
+
columns: ["deviceId", "timestamp"]
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
name: `idx_vec_${index}_ts`,
|
|
800
|
+
columns: ["timestamp"]
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
name: `idx_vec_${index}_class`,
|
|
804
|
+
columns: ["className"]
|
|
805
|
+
}
|
|
806
|
+
]
|
|
807
|
+
});
|
|
808
|
+
this.specs.set(index, {
|
|
809
|
+
dim,
|
|
810
|
+
metric
|
|
811
|
+
});
|
|
812
|
+
try {
|
|
813
|
+
await this.store.set({
|
|
814
|
+
collection: INDEX_REGISTRY_COLLECTION,
|
|
815
|
+
key: index,
|
|
816
|
+
value: {
|
|
817
|
+
dim,
|
|
818
|
+
metric
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
} catch (err) {
|
|
822
|
+
this.logger.warn("vector-store index declaration not persisted", { meta: {
|
|
823
|
+
index,
|
|
824
|
+
dim,
|
|
825
|
+
error: String(err)
|
|
826
|
+
} });
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Resolve an index's spec, recovering it from stored rows when the registry
|
|
831
|
+
* has no entry.
|
|
832
|
+
*
|
|
833
|
+
* The recovery is not belt-and-braces, it is required: indexes created before
|
|
834
|
+
* the registry existed have rows and no declaration, and so would be
|
|
835
|
+
* permanently unqueryable — which is exactly how this was found, with 8,494
|
|
836
|
+
* live vectors that `stats` refused to look at. The dimension is derivable
|
|
837
|
+
* from any stored vector, so derive it and persist it rather than demanding
|
|
838
|
+
* the caller redeclare. The metric is NOT derivable and falls back to
|
|
839
|
+
* `cosine`, which is what every current caller declares.
|
|
840
|
+
*/
|
|
841
|
+
async resolveSpec(index) {
|
|
842
|
+
const known = this.specs.get(index);
|
|
843
|
+
if (known !== void 0) return known;
|
|
844
|
+
const sample = (await this.store.query({
|
|
845
|
+
collection: collectionFor(index),
|
|
846
|
+
filter: { limit: 1 }
|
|
847
|
+
}))[0];
|
|
848
|
+
const vector = sample === void 0 ? void 0 : readRow(sample.data)?.vector;
|
|
849
|
+
if (vector === void 0) throw new Error(`vector-store: index "${index}" was never declared`);
|
|
850
|
+
const dim = vectorDimFromBase64(vector);
|
|
851
|
+
this.logger.warn("vector-store recovered an undeclared index from its rows", { meta: {
|
|
852
|
+
index,
|
|
853
|
+
dim,
|
|
854
|
+
metric: "cosine"
|
|
855
|
+
} });
|
|
856
|
+
await this.declareIndex(index, dim, "cosine");
|
|
857
|
+
return {
|
|
858
|
+
dim,
|
|
859
|
+
metric: "cosine"
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
/** Upsert items, refusing any whose vector length disagrees with the index. */
|
|
863
|
+
async upsert(index, items) {
|
|
864
|
+
const spec = await this.resolveSpec(index);
|
|
865
|
+
const collection = collectionFor(index);
|
|
866
|
+
let upserted = 0;
|
|
867
|
+
let rejected = 0;
|
|
868
|
+
for (const item of items) {
|
|
869
|
+
if (vectorDimFromBase64(item.vector) !== spec.dim) {
|
|
870
|
+
rejected += 1;
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
const { deviceId, timestamp, className, modelId, extra } = splitMetadata(item.metadata);
|
|
874
|
+
try {
|
|
875
|
+
await this.store.set({
|
|
876
|
+
collection,
|
|
877
|
+
key: item.id,
|
|
878
|
+
value: {
|
|
879
|
+
vector: item.vector,
|
|
880
|
+
deviceId,
|
|
881
|
+
timestamp,
|
|
882
|
+
className,
|
|
883
|
+
modelId,
|
|
884
|
+
extra
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
upserted += 1;
|
|
888
|
+
} catch (err) {
|
|
889
|
+
rejected += 1;
|
|
890
|
+
this.logger.warn("vector-store upsert failed", {
|
|
891
|
+
...deviceId !== null ? { tags: { deviceId } } : {},
|
|
892
|
+
meta: {
|
|
893
|
+
index,
|
|
894
|
+
id: item.id,
|
|
895
|
+
error: String(err)
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
if (rejected > 0) this.logger.warn("vector-store rejected items on upsert", { meta: {
|
|
901
|
+
index,
|
|
902
|
+
rejected,
|
|
903
|
+
expectedDim: spec.dim
|
|
904
|
+
} });
|
|
905
|
+
return {
|
|
906
|
+
upserted,
|
|
907
|
+
rejected
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
/** Exhaustive ranked search over the rows a prefilter admits. */
|
|
911
|
+
async query(params) {
|
|
912
|
+
const spec = await this.resolveSpec(params.index);
|
|
913
|
+
const queryVec = decodeVectorBase64(params.vector);
|
|
914
|
+
if (queryVec.length !== spec.dim) throw new Error(`vector-store: query vector has ${queryVec.length} dims, index "${params.index}" has ${spec.dim}`);
|
|
915
|
+
const rows = await this.store.query({
|
|
916
|
+
collection: collectionFor(params.index),
|
|
917
|
+
filter: {
|
|
918
|
+
...buildSqlFilter(params.filter),
|
|
919
|
+
orderBy: {
|
|
920
|
+
field: "timestamp",
|
|
921
|
+
direction: "desc"
|
|
922
|
+
},
|
|
923
|
+
limit: MAX_SCAN_ROWS
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
const minScore = params.minScore ?? Number.NEGATIVE_INFINITY;
|
|
927
|
+
const scored = [];
|
|
928
|
+
for (const row of rows) {
|
|
929
|
+
const stored = readRow(row.data);
|
|
930
|
+
if (stored === null) continue;
|
|
931
|
+
if (!passesUnpushedFilter(stored, params.filter)) continue;
|
|
932
|
+
const vec = decodeVectorBase64(stored.vector);
|
|
933
|
+
if (vec.length !== spec.dim) continue;
|
|
934
|
+
const score = scoreFor(spec.metric, queryVec, vec);
|
|
935
|
+
if (score < minScore) continue;
|
|
936
|
+
scored.push({
|
|
937
|
+
id: row.id,
|
|
938
|
+
score,
|
|
939
|
+
metadata: rebuildMetadata(stored)
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
scored.sort((a, b) => b.score - a.score);
|
|
943
|
+
return {
|
|
944
|
+
matches: scored.slice(0, params.topK),
|
|
945
|
+
scanned: rows.length,
|
|
946
|
+
truncated: rows.length >= MAX_SCAN_ROWS
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Metadata for the given ids, WITHOUT decoding their vectors.
|
|
951
|
+
*
|
|
952
|
+
* The caller is a best-of gate comparing confidences, so the vector is dead
|
|
953
|
+
* weight on that path — reading it would reintroduce exactly the per-row cost
|
|
954
|
+
* this index exists to remove. Missing ids are absent from the result rather
|
|
955
|
+
* than present with an empty metadata object.
|
|
956
|
+
*/
|
|
957
|
+
async getByIds(index, ids) {
|
|
958
|
+
await this.resolveSpec(index);
|
|
959
|
+
if (ids.length === 0) return [];
|
|
960
|
+
const rows = await this.store.query({
|
|
961
|
+
collection: collectionFor(index),
|
|
962
|
+
filter: { limit: MAX_SCAN_ROWS }
|
|
963
|
+
});
|
|
964
|
+
const wanted = new Set(ids);
|
|
965
|
+
const out = [];
|
|
966
|
+
for (const row of rows) {
|
|
967
|
+
if (!wanted.has(row.id)) continue;
|
|
968
|
+
const stored = readRow(row.data);
|
|
969
|
+
if (stored === null) continue;
|
|
970
|
+
out.push({
|
|
971
|
+
id: row.id,
|
|
972
|
+
metadata: rebuildMetadata(stored)
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
return out;
|
|
976
|
+
}
|
|
977
|
+
async deleteByIds(index, ids) {
|
|
978
|
+
const collection = collectionFor(index);
|
|
979
|
+
let deleted = 0;
|
|
980
|
+
for (const id of ids) try {
|
|
981
|
+
await this.store.delete({
|
|
982
|
+
collection,
|
|
983
|
+
key: id
|
|
984
|
+
});
|
|
985
|
+
deleted += 1;
|
|
986
|
+
} catch (err) {
|
|
987
|
+
this.logger.warn("vector-store delete failed", { meta: {
|
|
988
|
+
index,
|
|
989
|
+
id,
|
|
990
|
+
error: String(err)
|
|
991
|
+
} });
|
|
992
|
+
}
|
|
993
|
+
return deleted;
|
|
994
|
+
}
|
|
995
|
+
async deleteByFilter(index, filter) {
|
|
996
|
+
const ids = (await this.store.query({
|
|
997
|
+
collection: collectionFor(index),
|
|
998
|
+
filter: {
|
|
999
|
+
...buildSqlFilter(filter),
|
|
1000
|
+
limit: MAX_SCAN_ROWS
|
|
1001
|
+
}
|
|
1002
|
+
})).filter((r) => {
|
|
1003
|
+
const stored = readRow(r.data);
|
|
1004
|
+
return stored !== null && passesUnpushedFilter(stored, filter);
|
|
1005
|
+
}).map((r) => r.id);
|
|
1006
|
+
return this.deleteByIds(index, ids);
|
|
1007
|
+
}
|
|
1008
|
+
async stats(index) {
|
|
1009
|
+
const spec = await this.resolveSpec(index);
|
|
1010
|
+
return {
|
|
1011
|
+
backend: "sqlite-bruteforce",
|
|
1012
|
+
count: (await this.store.query({
|
|
1013
|
+
collection: collectionFor(index),
|
|
1014
|
+
filter: { limit: MAX_SCAN_ROWS }
|
|
1015
|
+
})).length,
|
|
1016
|
+
dim: spec.dim,
|
|
1017
|
+
metric: spec.metric,
|
|
1018
|
+
exact: true
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
/** Split caller metadata into promoted columns plus a verbatim remainder. */
|
|
1023
|
+
function splitMetadata(metadata) {
|
|
1024
|
+
const extra = {};
|
|
1025
|
+
for (const [key, value] of Object.entries(metadata)) if (!isPromoted(key)) extra[key] = value;
|
|
1026
|
+
return {
|
|
1027
|
+
deviceId: numberOrNull(metadata["deviceId"]),
|
|
1028
|
+
timestamp: numberOrNull(metadata["timestamp"]),
|
|
1029
|
+
className: stringOrNull(metadata["className"]),
|
|
1030
|
+
modelId: stringOrNull(metadata["modelId"]),
|
|
1031
|
+
extra
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
function isPromoted(key) {
|
|
1035
|
+
for (const promoted of PROMOTED_COLUMNS) if (promoted === key) return true;
|
|
1036
|
+
return false;
|
|
1037
|
+
}
|
|
1038
|
+
function numberOrNull(value) {
|
|
1039
|
+
return typeof value === "number" ? value : null;
|
|
1040
|
+
}
|
|
1041
|
+
function stringOrNull(value) {
|
|
1042
|
+
return typeof value === "string" ? value : null;
|
|
1043
|
+
}
|
|
1044
|
+
/** Rebuild the caller's metadata from the promoted columns plus the remainder. */
|
|
1045
|
+
function rebuildMetadata(row) {
|
|
1046
|
+
const out = { ...row.extra };
|
|
1047
|
+
if (row.deviceId !== null) out["deviceId"] = row.deviceId;
|
|
1048
|
+
if (row.timestamp !== null) out["timestamp"] = row.timestamp;
|
|
1049
|
+
if (row.className !== null) out["className"] = row.className;
|
|
1050
|
+
if (row.modelId !== null) out["modelId"] = row.modelId;
|
|
1051
|
+
return out;
|
|
1052
|
+
}
|
|
1053
|
+
/** The part of a filter SQLite can apply: promoted columns only. */
|
|
1054
|
+
function buildSqlFilter(filter) {
|
|
1055
|
+
if (filter === void 0) return {};
|
|
1056
|
+
const where = {};
|
|
1057
|
+
const whereBetween = {};
|
|
1058
|
+
for (const [key, value] of Object.entries(filter.equals ?? {})) if (isPromoted(key)) where[key] = value;
|
|
1059
|
+
for (const [key, range] of Object.entries(filter.between ?? {})) if (isPromoted(key)) whereBetween[key] = range;
|
|
1060
|
+
return {
|
|
1061
|
+
...Object.keys(where).length > 0 ? { where } : {},
|
|
1062
|
+
...Object.keys(whereBetween).length > 0 ? { whereBetween } : {}
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Apply the clauses SQL could not.
|
|
1067
|
+
*
|
|
1068
|
+
* A filter key that is not a promoted column lives inside the `extra` JSON
|
|
1069
|
+
* blob, so it is checked here in JS. It is applied rather than ignored — an
|
|
1070
|
+
* unapplied filter returns rows the caller will treat as matches.
|
|
1071
|
+
*/
|
|
1072
|
+
function passesUnpushedFilter(row, filter) {
|
|
1073
|
+
if (filter === void 0) return true;
|
|
1074
|
+
for (const [key, expected] of Object.entries(filter.equals ?? {})) {
|
|
1075
|
+
if (isPromoted(key)) continue;
|
|
1076
|
+
if (row.extra[key] !== expected) return false;
|
|
1077
|
+
}
|
|
1078
|
+
for (const [key, [lo, hi]] of Object.entries(filter.between ?? {})) {
|
|
1079
|
+
if (isPromoted(key)) continue;
|
|
1080
|
+
const actual = row.extra[key];
|
|
1081
|
+
if (typeof actual !== "number" || actual < lo || actual > hi) return false;
|
|
1082
|
+
}
|
|
1083
|
+
return true;
|
|
1084
|
+
}
|
|
1085
|
+
function readRow(data) {
|
|
1086
|
+
const vector = data["vector"];
|
|
1087
|
+
if (typeof vector !== "string" || vector.length === 0) return null;
|
|
1088
|
+
const extra = data["extra"];
|
|
1089
|
+
return {
|
|
1090
|
+
vector,
|
|
1091
|
+
deviceId: numberOrNull(data["deviceId"]),
|
|
1092
|
+
timestamp: numberOrNull(data["timestamp"]),
|
|
1093
|
+
className: stringOrNull(data["className"]),
|
|
1094
|
+
modelId: stringOrNull(data["modelId"]),
|
|
1095
|
+
extra: isMetadata(extra) ? extra : {}
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
function isMetadata(value) {
|
|
1099
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1100
|
+
}
|
|
1101
|
+
/** Narrow a stored metric string back to the enum. */
|
|
1102
|
+
function isMetric(value) {
|
|
1103
|
+
return value === "cosine" || value === "dot" || value === "euclidean";
|
|
1104
|
+
}
|
|
1105
|
+
/** Score two vectors under the index's metric. Higher is nearer in every case. */
|
|
1106
|
+
function scoreFor(metric, a, b) {
|
|
1107
|
+
if (metric === "cosine") return cosineSimilarity(a, b);
|
|
1108
|
+
if (metric === "dot") {
|
|
1109
|
+
let sum = 0;
|
|
1110
|
+
for (let i = 0; i < a.length; i += 1) sum += (a[i] ?? 0) * (b[i] ?? 0);
|
|
1111
|
+
return sum;
|
|
1112
|
+
}
|
|
1113
|
+
let sum = 0;
|
|
1114
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
1115
|
+
const d = (a[i] ?? 0) - (b[i] ?? 0);
|
|
1116
|
+
sum += d * d;
|
|
1117
|
+
}
|
|
1118
|
+
const distance = Math.sqrt(sum);
|
|
1119
|
+
return distance === 0 ? 0 : -distance;
|
|
1120
|
+
}
|
|
1121
|
+
//#endregion
|
|
644
1122
|
//#region src/builtins/sqlite-storage/sqlite-settings.addon.ts
|
|
645
1123
|
/**
|
|
646
1124
|
* SQLite Settings addon — the relational ENGINE behind the data door.
|
|
@@ -651,9 +1129,16 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
|
|
|
651
1129
|
* ([D44](../../../../../docs/decisions/adr-0044.md)). Callers reach this
|
|
652
1130
|
* engine only through the door — which is the point, because it is what
|
|
653
1131
|
* makes the engine replaceable.
|
|
1132
|
+
*
|
|
1133
|
+
* It ALSO serves `vector-store` (singleton, internal), from the same database
|
|
1134
|
+
* file and the same open handle. That capability first shipped as its own addon
|
|
1135
|
+
* and the separation cost two round trips per write — caller → door → engine —
|
|
1136
|
+
* plus a runner process, to reach a database this addon already has open. The
|
|
1137
|
+
* capability is what keeps the backend swappable; the package never was.
|
|
654
1138
|
*/
|
|
655
1139
|
var SqliteSettingsAddon = class extends BaseAddon {
|
|
656
1140
|
backend = null;
|
|
1141
|
+
vectorIndex = null;
|
|
657
1142
|
constructor() {
|
|
658
1143
|
super({});
|
|
659
1144
|
}
|
|
@@ -677,10 +1162,36 @@ var SqliteSettingsAddon = class extends BaseAddon {
|
|
|
677
1162
|
this.ctx.logger.info("DB file status", { meta: { dbExists } });
|
|
678
1163
|
this.backend = new SqliteSettingsBackend(dbPath, { ...RUNTIME_DEFAULTS });
|
|
679
1164
|
await this.backend.initialize();
|
|
1165
|
+
const vectorIndex = new SqliteVectorIndex({
|
|
1166
|
+
store: this.backend,
|
|
1167
|
+
logger: this.ctx.logger.child("VectorIndex")
|
|
1168
|
+
});
|
|
1169
|
+
await vectorIndex.loadIndexRegistry();
|
|
1170
|
+
this.vectorIndex = vectorIndex;
|
|
1171
|
+
const vectorProvider = {
|
|
1172
|
+
declareIndex: async (input) => {
|
|
1173
|
+
await vectorIndex.declareIndex(input.index, input.dim, input.metric);
|
|
1174
|
+
},
|
|
1175
|
+
upsert: async (input) => vectorIndex.upsert(input.index, input.items),
|
|
1176
|
+
query: async (input) => vectorIndex.query({
|
|
1177
|
+
index: input.index,
|
|
1178
|
+
vector: input.vector,
|
|
1179
|
+
topK: input.topK,
|
|
1180
|
+
...input.minScore !== void 0 ? { minScore: input.minScore } : {},
|
|
1181
|
+
...input.filter !== void 0 ? { filter: input.filter } : {}
|
|
1182
|
+
}),
|
|
1183
|
+
getByIds: async (input) => ({ items: await vectorIndex.getByIds(input.index, input.ids) }),
|
|
1184
|
+
deleteByIds: async (input) => ({ deleted: await vectorIndex.deleteByIds(input.index, input.ids) }),
|
|
1185
|
+
deleteByFilter: async (input) => ({ deleted: await vectorIndex.deleteByFilter(input.index, input.filter) }),
|
|
1186
|
+
stats: async (input) => vectorIndex.stats(input.index)
|
|
1187
|
+
};
|
|
680
1188
|
this.ctx.logger.info("Initialized successfully");
|
|
681
1189
|
return [{
|
|
682
1190
|
capability: dataStoreProviderCapability,
|
|
683
1191
|
provider: this.backend
|
|
1192
|
+
}, {
|
|
1193
|
+
capability: vectorStoreCapability,
|
|
1194
|
+
provider: vectorProvider
|
|
684
1195
|
}];
|
|
685
1196
|
}
|
|
686
1197
|
async onShutdown() {
|
|
@@ -689,6 +1200,10 @@ var SqliteSettingsAddon = class extends BaseAddon {
|
|
|
689
1200
|
getBackend() {
|
|
690
1201
|
return this.backend;
|
|
691
1202
|
}
|
|
1203
|
+
/** Exposed for tests; null before `onInitialize`. */
|
|
1204
|
+
getVectorIndex() {
|
|
1205
|
+
return this.vectorIndex;
|
|
1206
|
+
}
|
|
692
1207
|
};
|
|
693
1208
|
//#endregion
|
|
694
1209
|
export { SqliteSettingsAddon, SqliteSettingsAddon as default, SqliteSettingsBackend as t };
|