pilotswarm-horizon-store 0.2.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 (71) hide show
  1. package/README.md +154 -0
  2. package/dist/src/agent-tools.d.ts +32 -0
  3. package/dist/src/agent-tools.d.ts.map +1 -0
  4. package/dist/src/agent-tools.js +263 -0
  5. package/dist/src/agent-tools.js.map +1 -0
  6. package/dist/src/config.d.ts +93 -0
  7. package/dist/src/config.d.ts.map +1 -0
  8. package/dist/src/config.js +120 -0
  9. package/dist/src/config.js.map +1 -0
  10. package/dist/src/db-retry.d.ts +21 -0
  11. package/dist/src/db-retry.d.ts.map +1 -0
  12. package/dist/src/db-retry.js +94 -0
  13. package/dist/src/db-retry.js.map +1 -0
  14. package/dist/src/embedding-client.d.ts +13 -0
  15. package/dist/src/embedding-client.d.ts.map +1 -0
  16. package/dist/src/embedding-client.js +79 -0
  17. package/dist/src/embedding-client.js.map +1 -0
  18. package/dist/src/graph-model.d.ts +49 -0
  19. package/dist/src/graph-model.d.ts.map +1 -0
  20. package/dist/src/graph-model.js +112 -0
  21. package/dist/src/graph-model.js.map +1 -0
  22. package/dist/src/graph-queries.d.ts +83 -0
  23. package/dist/src/graph-queries.d.ts.map +1 -0
  24. package/dist/src/graph-queries.js +727 -0
  25. package/dist/src/graph-queries.js.map +1 -0
  26. package/dist/src/graph-store.d.ts +57 -0
  27. package/dist/src/graph-store.d.ts.map +1 -0
  28. package/dist/src/graph-store.js +327 -0
  29. package/dist/src/graph-store.js.map +1 -0
  30. package/dist/src/horizon-migrator.d.ts +49 -0
  31. package/dist/src/horizon-migrator.d.ts.map +1 -0
  32. package/dist/src/horizon-migrator.js +231 -0
  33. package/dist/src/horizon-migrator.js.map +1 -0
  34. package/dist/src/horizon-store.d.ts +116 -0
  35. package/dist/src/horizon-store.d.ts.map +1 -0
  36. package/dist/src/horizon-store.js +563 -0
  37. package/dist/src/horizon-store.js.map +1 -0
  38. package/dist/src/index.d.ts +13 -0
  39. package/dist/src/index.d.ts.map +1 -0
  40. package/dist/src/index.js +21 -0
  41. package/dist/src/index.js.map +1 -0
  42. package/dist/src/preconditions.d.ts +16 -0
  43. package/dist/src/preconditions.d.ts.map +1 -0
  44. package/dist/src/preconditions.js +99 -0
  45. package/dist/src/preconditions.js.map +1 -0
  46. package/dist/src/query-builder.d.ts +37 -0
  47. package/dist/src/query-builder.d.ts.map +1 -0
  48. package/dist/src/query-builder.js +82 -0
  49. package/dist/src/query-builder.js.map +1 -0
  50. package/dist/src/sql-util.d.ts +20 -0
  51. package/dist/src/sql-util.d.ts.map +1 -0
  52. package/dist/src/sql-util.js +40 -0
  53. package/dist/src/sql-util.js.map +1 -0
  54. package/dist/src/types.d.ts +16 -0
  55. package/dist/src/types.d.ts.map +1 -0
  56. package/dist/src/types.js +35 -0
  57. package/dist/src/types.js.map +1 -0
  58. package/migrations/0001_facts_table.sql +77 -0
  59. package/migrations/0002_indexes.sql +27 -0
  60. package/migrations/0003_age_bootstrap.sql +13 -0
  61. package/migrations/0004_facts_procs.sql +253 -0
  62. package/migrations/0005_embedder_workflow.sql +119 -0
  63. package/migrations/0006_facts_read_uncrawled_embedded_gate.sql +33 -0
  64. package/migrations/0007_embed_key_value_text.sql +122 -0
  65. package/migrations/0008_embedder_failure_recovery.sql +409 -0
  66. package/migrations/0009_search_text_full_value.sql +52 -0
  67. package/migrations/0010_minimal_two_loop_embedder.sql +392 -0
  68. package/migrations/0011_unified_api_embedder_workflow.sql +273 -0
  69. package/migrations/0012_facts_soft_delete.sql +587 -0
  70. package/migrations/0013_graph_namespaces.sql +60 -0
  71. package/package.json +69 -0
@@ -0,0 +1,120 @@
1
+ // pilotswarm-horizon-store — provider configuration.
2
+ //
3
+ // The EnhancedFactStore is configured with a connection string and, optionally,
4
+ // an EMBEDDING ENDPOINT. The embedding endpoint is a DATABASE-AGNOSTIC contract:
5
+ // it describes an OpenAI/Azure-OpenAI-compatible HTTP embeddings API and says
6
+ // nothing about how any particular provider invokes it. A provider records this
7
+ // endpoint and embeds facts however it likes — the HorizonDB provider calls it
8
+ // in-database via pg_durable's df.http() background loop, but an AlloyDB- or
9
+ // any-Postgres-based provider could consume the exact same config and call it
10
+ // from a trigger, a sidecar, or host-side code. PilotSwarm only passes the
11
+ // endpoint; the mechanism is the provider's concern.
12
+ //
13
+ // The SAME endpoint config is also usable Node-side (see embedding-client.ts)
14
+ // for query-time embedding and for tests that don't depend on any in-database
15
+ // HTTP path.
16
+ export const DEFAULT_SCHEMA = "horizon_facts";
17
+ export const DEFAULT_GRAPH = "horizon_facts";
18
+ export const DEFAULT_POOL_MAX = 16;
19
+ export const DEFAULT_CONNECTION_TIMEOUT_MS = 15_000;
20
+ export const DEFAULT_NAMESPACE_CACHE_TTL_MS = 60_000;
21
+ function resolveConnectionTimeoutMillis() {
22
+ const raw = process.env.HORIZON_CONNECTION_TIMEOUT_MS;
23
+ if (raw == null || raw === "")
24
+ return DEFAULT_CONNECTION_TIMEOUT_MS;
25
+ const value = Number(raw);
26
+ if (!Number.isFinite(value) || value < 0) {
27
+ throw new Error(`HORIZON_CONNECTION_TIMEOUT_MS must be a non-negative number, got ${JSON.stringify(raw)}`);
28
+ }
29
+ return Math.trunc(value);
30
+ }
31
+ /** Resolve + validate the namespace cache TTL (ms). Rejects NaN / negative. */
32
+ function resolveNamespaceCacheTtl(explicit) {
33
+ const envRaw = process.env.HORIZON_NAMESPACE_CACHE_TTL_MS;
34
+ const raw = explicit ?? (envRaw != null && envRaw !== "" ? Number(envRaw) : undefined);
35
+ if (raw == null)
36
+ return DEFAULT_NAMESPACE_CACHE_TTL_MS;
37
+ if (!Number.isFinite(raw) || raw < 0) {
38
+ throw new Error(`namespaceCacheTtlMs / HORIZON_NAMESPACE_CACHE_TTL_MS must be a non-negative number, got ${JSON.stringify(envRaw ?? explicit)}`);
39
+ }
40
+ return raw;
41
+ }
42
+ /** Resolve config from explicit values, falling back to HORIZON_* env vars. */
43
+ export function resolveConfig(partial = {}) {
44
+ const connectionString = partial.connectionString ?? process.env.HORIZON_DATABASE_URL ?? "";
45
+ if (!connectionString) {
46
+ throw new Error("HorizonDBFactStore requires a connectionString (or HORIZON_DATABASE_URL env var).");
47
+ }
48
+ // The embedding endpoint must be passed EXPLICITLY (no env-var fallback):
49
+ // an implicit endpoint can silently conflict with the store's vector(N)
50
+ // dimension, and config provenance should be the host's choice (02 §1b).
51
+ const embedding = partial.embedding;
52
+ const graphName = partial.graphName ?? DEFAULT_GRAPH;
53
+ return {
54
+ connectionString,
55
+ schema: partial.schema ?? DEFAULT_SCHEMA,
56
+ graphName,
57
+ registrySchema: partial.registrySchema ?? process.env.HORIZON_GRAPH_REGISTRY_SCHEMA ?? `${graphName}_registry`,
58
+ embedding,
59
+ embeddingDim: partial.embeddingDim ?? embedding?.dim ?? 1536,
60
+ annIndex: partial.annIndex
61
+ ?? process.env.HORIZON_ANN_INDEX
62
+ ?? "auto",
63
+ poolMax: partial.poolMax
64
+ ?? (process.env.HORIZON_POOL_MAX ? Number(process.env.HORIZON_POOL_MAX) : undefined)
65
+ ?? DEFAULT_POOL_MAX,
66
+ namespaceCacheTtlMs: resolveNamespaceCacheTtl(partial.namespaceCacheTtlMs),
67
+ useManagedIdentity: partial.useManagedIdentity,
68
+ aadUser: partial.aadUser,
69
+ };
70
+ }
71
+ /** sslmode values that mean "use TLS" (mirrors the PilotSwarm SDK pg-pool-factory). */
72
+ const SSL_REQUIRING_MODES = ["require", "prefer", "verify-ca", "verify-full"];
73
+ /**
74
+ * Build a `pg.PoolConfig` from a HorizonDB connection string, normalizing TLS
75
+ * the same way the PilotSwarm SDK's `pg-pool-factory` does for the CMS / facts
76
+ * pools, so a HorizonDB URL behaves here exactly like `DATABASE_URL` does there.
77
+ *
78
+ * Why this is needed: HorizonDB requires SSL, but its certificate chain is not in
79
+ * Node's default trust store, and `pg` v8 treats `sslmode=require` (and
80
+ * `prefer` / `verify-ca` / `verify-full`) as `verify-full` — which rejects the
81
+ * chain with `self-signed certificate in certificate chain`. The SDK factory
82
+ * handles this for `DATABASE_URL` by stripping `sslmode` from the URL and setting
83
+ * `ssl: { rejectUnauthorized: false }` on the pool config. This provider builds
84
+ * its OWN raw pools (it keeps its runtime dep surface to `pg` only and does not
85
+ * import the SDK), so without this helper a natural `?sslmode=require` URL fails
86
+ * here while the identical URL works for `DATABASE_URL` — the asymmetry that
87
+ * previously forced callers to hand-append `uselibpqcompat=true`.
88
+ *
89
+ * Encrypt-but-don't-verify matches libpq's `sslmode=require` semantics and the
90
+ * SDK's existing posture (this is not a new weakening — it is the same choice the
91
+ * CMS/facts pools already make). For full CA verification against a non-preview
92
+ * cluster, present a trusted chain and pass a URL without an `sslmode` param.
93
+ */
94
+ export function buildPoolConfig(connectionString, max) {
95
+ let needsSsl = false;
96
+ let sanitized = connectionString;
97
+ try {
98
+ const url = new URL(connectionString);
99
+ needsSsl = SSL_REQUIRING_MODES.includes(url.searchParams.get("sslmode") ?? "");
100
+ if (needsSsl) {
101
+ // Control SSL via the config object, not the URL. Drop sslmode so pg
102
+ // does not re-apply verify-full, and drop the now-redundant
103
+ // uselibpqcompat hint so the effective URL is clean either way.
104
+ url.searchParams.delete("sslmode");
105
+ url.searchParams.delete("uselibpqcompat");
106
+ sanitized = url.toString();
107
+ }
108
+ }
109
+ catch {
110
+ // Not a parseable URL (unexpected for a real DSN). Leave it untouched and
111
+ // let pg surface a clear connection error rather than masking it here.
112
+ }
113
+ return {
114
+ connectionString: sanitized,
115
+ max,
116
+ connectionTimeoutMillis: resolveConnectionTimeoutMillis(),
117
+ ...(needsSsl ? { ssl: { rejectUnauthorized: false } } : {}),
118
+ };
119
+ }
120
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ export declare function isTransientDbError(err: any): boolean;
2
+ /** The AGE lazy-label-creation race, in BOTH manifestations (42P07 duplicate_table
3
+ * and 23505 unique_violation on a pg_catalog index). Re-running the same
4
+ * idempotent graph statement once the label exists succeeds. Label-agnostic. */
5
+ export declare function isLabelCreationRaceError(err: any): boolean;
6
+ export interface DbRetryHooks {
7
+ onRetry?: (info: {
8
+ label: string;
9
+ attempt: number;
10
+ error: any;
11
+ }) => void;
12
+ }
13
+ /** Install process-wide retry observers (used by the eval perf/error report). */
14
+ export declare function setDbRetryHooks(hooks: DbRetryHooks): void;
15
+ export interface DbRetryOpts {
16
+ tries?: number;
17
+ /** Which errors are retryable. Default: isTransientDbError (connection class). */
18
+ isRetryable?: (err: any) => boolean;
19
+ }
20
+ export declare function withDbRetry<T>(label: string, fn: () => Promise<T>, opts?: DbRetryOpts): Promise<T>;
21
+ //# sourceMappingURL=db-retry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db-retry.d.ts","sourceRoot":"","sources":["../../src/db-retry.ts"],"names":[],"mappings":"AAwDA,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAIpD;AAED;;gFAEgF;AAChF,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAS1D;AAED,MAAM,WAAW,YAAY;IACzB,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,GAAG,CAAA;KAAE,KAAK,IAAI,CAAC;CAC5E;AAGD,iFAAiF;AACjF,wBAAgB,eAAe,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAAkC;AAE5F,MAAM,WAAW,WAAW;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kFAAkF;IAClF,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC;CACvC;AAED,wBAAsB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,CAAC,CAAC,CAe5G"}
@@ -0,0 +1,94 @@
1
+ // pilotswarm-horizon-store — retry helpers (HorizonDB preview hardening).
2
+ //
3
+ // Two distinct retryable classes, each with its own classifier so callers opt
4
+ // into exactly the one that is safe for them:
5
+ //
6
+ // 1. TRANSIENT CONNECTION (isTransientDbError, the default):
7
+ // Azure HorizonDB (preview) intermittently drops pooled TLS connections —
8
+ // ECONNRESET / ENOTCONN / "Connection terminated" / "terminating
9
+ // connection", plus the pg connection-class SQLSTATEs (08xxx, 57P0x). These
10
+ // surface either as a rejected query or at connection acquisition; in both
11
+ // cases the statement did not partially commit, so re-running (on a FRESH
12
+ // connection) is safe. Never matches SQL/logic errors — those must surface.
13
+ // Also covers connection-ESTABLISHMENT failures that are transient under
14
+ // load: EADDRNOTAVAIL / ECONNABORTED (local socket exhaustion or aborted
15
+ // connection setup from rapid connection churn) and DNS hiccups (ENOTFOUND /
16
+ // EAI_AGAIN / getaddrinfo) against the cluster host — the query never ran,
17
+ // so a fresh-connection retry is safe.
18
+ //
19
+ // 2. AGE LABEL-CREATION RACE (isLabelCreationRaceError):
20
+ // Apache AGE creates a label's backing table LAZILY on the first
21
+ // CREATE/MERGE that references it. The label set is owned by the layer above
22
+ // us (the harvester / app choose node kinds + edge predicates), so we never
23
+ // enumerate it. When two writers race the first reference to the same label,
24
+ // the loser's internal label-table creation aborts the whole Cypher statement
25
+ // — so the MERGE/CREATE persisted nothing. The label now EXISTS, so re-running
26
+ // the SAME statement succeeds. This is label-agnostic by construction.
27
+ //
28
+ // The race has TWO observed manifestations (HorizonDB AGE 1.6.0), depending
29
+ // on WHERE the duplicate is detected:
30
+ // - 42P07 duplicate_table — `relation "<label>" already exists` (caught at
31
+ // the relation level), and
32
+ // - 23505 unique_violation on a SYSTEM-CATALOG index, e.g.
33
+ // `pg_class_relname_nsp_index` (two concurrent label-table creations race
34
+ // into pg_class before either registered). NOTE the "already exists"
35
+ // text is in err.detail, not err.message, so a message regex misses it —
36
+ // we key off the code + the catalog constraint name instead. We retry
37
+ // 23505 ONLY when the violated constraint is a pg_catalog index, never a
38
+ // user constraint (the graph has no user unique constraints anyway).
39
+ //
40
+ // The optional process-wide onRetry hook lets the eval harness record retries
41
+ // for its perf/error report.
42
+ const TRANSIENT_DB_ERROR = /ECONNRESET|ENOTCONN|EPIPE|ETIMEDOUT|Connection terminated|terminating connection|server closed the connection|connection to server|read ECONN|EADDRNOTAVAIL|ECONNABORTED|ENOTFOUND|EAI_AGAIN|getaddrinfo/i;
43
+ /** pg connection-class SQLSTATEs: 08xxx connection exceptions, 57P0x
44
+ * admin-shutdown / crash-recovery. All mean "connection went away" → safe to
45
+ * re-run on a fresh connection. */
46
+ const TRANSIENT_PG_CODE = new Set(["08006", "08003", "08000", "08001", "08004", "57P01", "57P02", "57P03"]);
47
+ /** System-catalog unique indexes touched when a label-table creation races
48
+ * itself: pg_class (relname,relnamespace), pg_type (typname,typnamespace),
49
+ * pg_namespace. A 23505 on one of these in the graph layer = the AGE
50
+ * label-creation race. */
51
+ const CATALOG_RACE_CONSTRAINT = /pg_(class|type|namespace)_/i;
52
+ export function isTransientDbError(err) {
53
+ if (err?.code && TRANSIENT_PG_CODE.has(String(err.code)))
54
+ return true;
55
+ const s = `${err?.code ?? ""} ${err?.message ?? ""}`;
56
+ return TRANSIENT_DB_ERROR.test(s);
57
+ }
58
+ /** The AGE lazy-label-creation race, in BOTH manifestations (42P07 duplicate_table
59
+ * and 23505 unique_violation on a pg_catalog index). Re-running the same
60
+ * idempotent graph statement once the label exists succeeds. Label-agnostic. */
61
+ export function isLabelCreationRaceError(err) {
62
+ const code = String(err?.code ?? "");
63
+ if (code === "42P07")
64
+ return true; // duplicate_table
65
+ if (code === "23505") { // unique_violation…
66
+ const constraint = String(err?.constraint ?? "");
67
+ const message = String(err?.message ?? "");
68
+ if (CATALOG_RACE_CONSTRAINT.test(constraint) || CATALOG_RACE_CONSTRAINT.test(message))
69
+ return true;
70
+ }
71
+ return /already exists/i.test(String(err?.message ?? ""));
72
+ }
73
+ let DB_RETRY_HOOKS = {};
74
+ /** Install process-wide retry observers (used by the eval perf/error report). */
75
+ export function setDbRetryHooks(hooks) { DB_RETRY_HOOKS = hooks ?? {}; }
76
+ export async function withDbRetry(label, fn, opts = {}) {
77
+ const tries = opts.tries ?? 4;
78
+ const isRetryable = opts.isRetryable ?? isTransientDbError;
79
+ let lastErr;
80
+ for (let attempt = 1; attempt <= tries; attempt++) {
81
+ try {
82
+ return await fn();
83
+ }
84
+ catch (err) {
85
+ lastErr = err;
86
+ if (!isRetryable(err) || attempt === tries)
87
+ throw err;
88
+ DB_RETRY_HOOKS.onRetry?.({ label, attempt, error: err });
89
+ await new Promise((r) => setTimeout(r, Math.min(200 * 2 ** (attempt - 1), 2000)));
90
+ }
91
+ }
92
+ throw lastErr;
93
+ }
94
+ //# sourceMappingURL=db-retry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db-retry.js","sourceRoot":"","sources":["../../src/db-retry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { EmbeddingEndpointConfig } from "./config.js";
2
+ export declare class EmbeddingClient {
3
+ private readonly cfg;
4
+ constructor(cfg: EmbeddingEndpointConfig);
5
+ get dim(): number;
6
+ /** Embed a single string. Returns a vector of length `cfg.dim`. */
7
+ embed(text: string): Promise<number[]>;
8
+ /** Embed a batch. Returns one vector per input, in order. */
9
+ embedBatch(texts: string[]): Promise<number[][]>;
10
+ }
11
+ /** Format a JS number[] as a pgvector literal: "[0.1,0.2,...]". */
12
+ export declare function toVectorLiteral(vec: number[]): string;
13
+ //# sourceMappingURL=embedding-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embedding-client.d.ts","sourceRoot":"","sources":["../../src/embedding-client.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAE3D,qBAAa,eAAe;IACZ,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,uBAAuB;IAKzD,IAAI,GAAG,IAAI,MAAM,CAEhB;IAED,mEAAmE;IAC7D,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAK5C,6DAA6D;IACvD,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;CAyCzD;AAED,mEAAmE;AACnE,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,CAErD"}
@@ -0,0 +1,79 @@
1
+ // pilotswarm-horizon-store — Node-side embedding client.
2
+ //
3
+ // Two roles:
4
+ // 1. QUERY-TIME embedding for semantic/hybrid search (the adapter embeds the
5
+ // query string here, then passes the vector to SQL as `$1::vector`).
6
+ // 2. A reference implementation of the request/response contract that the
7
+ // 2. A reference implementation of the request/response contract that any
8
+ // provider's embedding path must speak. The HorizonDB provider mirrors it
9
+ // in-database (sql/006 df.http); the integration test stub server speaks
10
+ // this exact shape.
11
+ //
12
+ // It calls an OpenAI/Azure-OpenAI-compatible embeddings endpoint over HTTP using
13
+ // the global fetch (Node ≥ 18).
14
+ export class EmbeddingClient {
15
+ cfg;
16
+ constructor(cfg) {
17
+ this.cfg = cfg;
18
+ if (!cfg?.url)
19
+ throw new Error("EmbeddingClient requires an endpoint url.");
20
+ if (!cfg.dim || cfg.dim < 1)
21
+ throw new Error("EmbeddingClient requires a positive dim.");
22
+ }
23
+ get dim() {
24
+ return this.cfg.dim;
25
+ }
26
+ /** Embed a single string. Returns a vector of length `cfg.dim`. */
27
+ async embed(text) {
28
+ const [v] = await this.embedBatch([text]);
29
+ return v;
30
+ }
31
+ /** Embed a batch. Returns one vector per input, in order. */
32
+ async embedBatch(texts) {
33
+ if (texts.length === 0)
34
+ return [];
35
+ const inputField = this.cfg.inputField ?? "input";
36
+ const headers = {
37
+ "content-type": "application/json",
38
+ ...(this.cfg.headers ?? {}),
39
+ };
40
+ if (this.cfg.apiKey) {
41
+ const headerName = this.cfg.apiKeyHeader ?? "api-key";
42
+ headers[headerName] = this.cfg.bearer ? `Bearer ${this.cfg.apiKey}` : this.cfg.apiKey;
43
+ }
44
+ const body = JSON.stringify({ [inputField]: texts.length === 1 ? texts[0] : texts, model: this.cfg.model });
45
+ const controller = new AbortController();
46
+ const timer = setTimeout(() => controller.abort(), this.cfg.timeoutMs ?? 30_000);
47
+ let res;
48
+ try {
49
+ res = await fetch(this.cfg.url, { method: "POST", headers, body, signal: controller.signal });
50
+ }
51
+ finally {
52
+ clearTimeout(timer);
53
+ }
54
+ if (!res.ok) {
55
+ const detail = await res.text().catch(() => "");
56
+ throw new Error(`embedding endpoint ${res.status}: ${detail.slice(0, 200)}`);
57
+ }
58
+ const json = await res.json();
59
+ const data = json?.data;
60
+ if (!Array.isArray(data) || data.length === 0) {
61
+ throw new Error("embedding endpoint returned no data[]");
62
+ }
63
+ const out = data.map((d) => {
64
+ const emb = d?.embedding;
65
+ if (!Array.isArray(emb))
66
+ throw new Error("embedding endpoint item missing embedding[]");
67
+ if (emb.length !== this.cfg.dim) {
68
+ throw new Error(`embedding dim mismatch: got ${emb.length}, expected ${this.cfg.dim}`);
69
+ }
70
+ return emb;
71
+ });
72
+ return out;
73
+ }
74
+ }
75
+ /** Format a JS number[] as a pgvector literal: "[0.1,0.2,...]". */
76
+ export function toVectorLiteral(vec) {
77
+ return `[${vec.join(",")}]`;
78
+ }
79
+ //# sourceMappingURL=embedding-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embedding-client.js","sourceRoot":"","sources":["../../src/embedding-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Normalize a surface name for SURFACE-FORM dedup only (case, whitespace,
3
+ * punctuation, diacritics). This does NOT solve semantic identity — "Tom Lane"
4
+ * vs "tgl" is resolved by the harvester via searchGraphNodes + mergeGraphNodes,
5
+ * which records an alias. Keep this conservative so we never collapse distinct
6
+ * people.
7
+ */
8
+ export declare function normalizeName(raw: string): string;
9
+ /** Canonical dedup key: `<normalized-kind>:<normalized-name>`. */
10
+ export declare function nodeKeyOf(kind: string, name: string): string;
11
+ /** Merge alias lists, de-duplicated by surface form, preserving first-seen order. */
12
+ export declare function mergeAliases(existing: string[], incoming: string[]): string[];
13
+ /**
14
+ * Normalize a free-text predicate into a grouping key. The original predicate
15
+ * is kept verbatim on the edge; this is only for matching/analytics so the open
16
+ * vocabulary stays queryable without being frozen.
17
+ */
18
+ export declare function predicateKey(predicate: string): string;
19
+ /**
20
+ * Combine an existing edge confidence with a new independent observation.
21
+ * Monotonic, saturating, order-independent: c' = 1 - (1-c_old)(1-c_obs).
22
+ */
23
+ export declare function reinforceConfidence(existing: number, observation: number): number;
24
+ export declare function clamp01(x: number): number;
25
+ export interface ExistingEdge {
26
+ confidence: number;
27
+ observations: number;
28
+ evidence: string[];
29
+ }
30
+ export interface EdgeUpsertDecision {
31
+ /** create: no edge existed. reinforce: bump observations + noisy-OR.
32
+ * noop: re-assert carried only already-known evidence — idempotent. */
33
+ action: "create" | "reinforce" | "noop";
34
+ confidence: number;
35
+ observations: number;
36
+ evidence: string[];
37
+ }
38
+ /**
39
+ * Evidence is OPTIONAL (the graph stays permissive). Reinforcement counts only
40
+ * NOVEL observations: an assertion reinforces iff it carries ≥1 evidence
41
+ * scopeKey not already on the edge, or carries no evidence at all. Re-asserting
42
+ * with only already-known evidence is an idempotent no-op — a duplicate/replayed
43
+ * harvest of the same fact cannot inflate observations or confidence.
44
+ */
45
+ export declare function decideEdgeUpsert(input: {
46
+ confidence?: number;
47
+ evidence?: string[];
48
+ }, existing: ExistingEdge | null): EdgeUpsertDecision;
49
+ //# sourceMappingURL=graph-model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-model.d.ts","sourceRoot":"","sources":["../../src/graph-model.ts"],"names":[],"mappings":"AAQA;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CASjD;AAED,kEAAkE;AAClE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAI5D;AAED,qFAAqF;AACrF,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAU7E;AAWD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAOtD;AAID;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAIjF;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAKzC;AAID,MAAM,WAAW,YAAY;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IAC/B;2EACuE;IACvE,MAAM,EAAE,QAAQ,GAAG,WAAW,GAAG,MAAM,CAAC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACtB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC5B,KAAK,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,EACnD,QAAQ,EAAE,YAAY,GAAG,IAAI,GAC9B,kBAAkB,CA0BpB"}
@@ -0,0 +1,112 @@
1
+ // pilotswarm-horizon-store — open-graph quality core (DB-less, unit-tested).
2
+ //
3
+ // An LLM-built, ontology-free graph degenerates into noise without these pure
4
+ // guards. These functions hold the policy; the typed Cypher layer just applies
5
+ // their output. See docs/proposals/enhancedfactstore/01-functional-spec.md §6.
6
+ // ─── Node canonicalization ───────────────────────────────────────────────────
7
+ /**
8
+ * Normalize a surface name for SURFACE-FORM dedup only (case, whitespace,
9
+ * punctuation, diacritics). This does NOT solve semantic identity — "Tom Lane"
10
+ * vs "tgl" is resolved by the harvester via searchGraphNodes + mergeGraphNodes,
11
+ * which records an alias. Keep this conservative so we never collapse distinct
12
+ * people.
13
+ */
14
+ export function normalizeName(raw) {
15
+ return (raw ?? "")
16
+ .normalize("NFKD")
17
+ .replace(/(\p{Script=Latin})\p{M}+/gu, "$1") // strip Latin diacritics
18
+ .normalize("NFC")
19
+ .toLowerCase()
20
+ .replace(/[^\p{L}\p{N}]+/gu, " ") // punctuation → space, keep all scripts
21
+ .trim()
22
+ .replace(/\s+/g, " ");
23
+ }
24
+ /** Canonical dedup key: `<normalized-kind>:<normalized-name>`. */
25
+ export function nodeKeyOf(kind, name) {
26
+ const k = normalizeName(kind).replace(/\s+/g, "_");
27
+ const n = normalizeName(name).replace(/\s+/g, "-");
28
+ return `${k}:${n}`;
29
+ }
30
+ /** Merge alias lists, de-duplicated by surface form, preserving first-seen order. */
31
+ export function mergeAliases(existing, incoming) {
32
+ const seen = new Set();
33
+ const out = [];
34
+ for (const a of [...(existing ?? []), ...(incoming ?? [])]) {
35
+ const norm = normalizeName(a);
36
+ if (!norm || seen.has(norm))
37
+ continue;
38
+ seen.add(norm);
39
+ out.push(a.trim());
40
+ }
41
+ return out;
42
+ }
43
+ // ─── Predicate normalization ─────────────────────────────────────────────────
44
+ // Tiny stopword set so "revives argument from" and "revives the argument from"
45
+ // group together. Deliberately small — we group, we do NOT enforce a vocabulary.
46
+ const PREDICATE_STOPWORDS = new Set([
47
+ "a", "an", "the", "to", "of", "in", "on", "with", "for", "from", "by",
48
+ "that", "this", "it", "its", "their", "his", "her",
49
+ ]);
50
+ /**
51
+ * Normalize a free-text predicate into a grouping key. The original predicate
52
+ * is kept verbatim on the edge; this is only for matching/analytics so the open
53
+ * vocabulary stays queryable without being frozen.
54
+ */
55
+ export function predicateKey(predicate) {
56
+ const words = normalizeName(predicate)
57
+ .split(" ")
58
+ .filter((w) => w && !PREDICATE_STOPWORDS.has(w));
59
+ // crude stem: drop a trailing 's' on each token (comments→comment)
60
+ const stemmed = words.map((w) => (w.length > 3 && w.endsWith("s") ? w.slice(0, -1) : w));
61
+ return stemmed.join("_");
62
+ }
63
+ // ─── Confidence reinforcement (noisy-OR) ─────────────────────────────────────
64
+ /**
65
+ * Combine an existing edge confidence with a new independent observation.
66
+ * Monotonic, saturating, order-independent: c' = 1 - (1-c_old)(1-c_obs).
67
+ */
68
+ export function reinforceConfidence(existing, observation) {
69
+ const a = clamp01(existing);
70
+ const b = clamp01(observation);
71
+ return 1 - (1 - a) * (1 - b);
72
+ }
73
+ export function clamp01(x) {
74
+ if (!Number.isFinite(x))
75
+ return 0;
76
+ if (x < 0)
77
+ return 0;
78
+ if (x > 1)
79
+ return 1;
80
+ return x;
81
+ }
82
+ /**
83
+ * Evidence is OPTIONAL (the graph stays permissive). Reinforcement counts only
84
+ * NOVEL observations: an assertion reinforces iff it carries ≥1 evidence
85
+ * scopeKey not already on the edge, or carries no evidence at all. Re-asserting
86
+ * with only already-known evidence is an idempotent no-op — a duplicate/replayed
87
+ * harvest of the same fact cannot inflate observations or confidence.
88
+ */
89
+ export function decideEdgeUpsert(input, existing) {
90
+ const obsConfidence = clamp01(input.confidence ?? 1.0);
91
+ const incoming = [...new Set(input.evidence ?? [])];
92
+ if (!existing) {
93
+ return { action: "create", confidence: obsConfidence, observations: 1, evidence: incoming };
94
+ }
95
+ const known = new Set(existing.evidence ?? []);
96
+ const novel = incoming.filter((e) => !known.has(e));
97
+ if (incoming.length > 0 && novel.length === 0) {
98
+ return {
99
+ action: "noop",
100
+ confidence: existing.confidence,
101
+ observations: existing.observations,
102
+ evidence: existing.evidence,
103
+ };
104
+ }
105
+ return {
106
+ action: "reinforce",
107
+ confidence: reinforceConfidence(existing.confidence, obsConfidence),
108
+ observations: existing.observations + 1,
109
+ evidence: [...(existing.evidence ?? []), ...novel],
110
+ };
111
+ }
112
+ //# sourceMappingURL=graph-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-model.js","sourceRoot":"","sources":["../../src/graph-model.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,83 @@
1
+ import type { AccessContext, GraphEdgeHit, GraphEdgeInput, GraphEdgeQuery, GraphNodeHit, GraphNodeInput, GraphNodeQuery, GraphNodeRef, GraphEdgeRef, GraphEvidenceRemovalResult, SubGraph } from "./types.js";
2
+ /**
3
+ * Load the AGE shared library for this session. On managed Postgres where age
4
+ * is preloaded via shared_preload_libraries, an explicit LOAD is rejected with
5
+ * `access to library "age" is not allowed` — tolerated, since preloaded means
6
+ * there is nothing to do.
7
+ */
8
+ export declare function prepareAgeSession(client: any): Promise<void>;
9
+ /** Parse a scalar agtype column value into a JS value. */
10
+ export declare function ag(v: any): any;
11
+ /** Parse an agtype array column into string[]. */
12
+ export declare function agArr(v: any): string[];
13
+ /**
14
+ * Typed Cypher layer (AGE). Provides the GraphStore READ/WRITE method bodies;
15
+ * `HorizonDBGraphStore` wraps this with the lifecycle (initialize/close) to form
16
+ * the full `GraphStore` provider (07 D2). This inner class deliberately does NOT
17
+ * implement the lifecycle, so it is not itself a `GraphStore`.
18
+ */
19
+ export declare class GraphQueries {
20
+ private readonly pool;
21
+ private readonly graphName;
22
+ private readonly prepared;
23
+ constructor(pool: any, graphName: string);
24
+ private withAge;
25
+ private cypher;
26
+ searchGraphNodes(q: GraphNodeQuery, access?: AccessContext): Promise<GraphNodeHit[]>;
27
+ /**
28
+ * Idempotent, duplicate-anchor-proof evidence linking. A naive
29
+ * `MERGE (f:Fact {scope_key}) MERGE (e)-[:EVIDENCED_BY]->(f)` is NOT safe
30
+ * here: AGE has no unique constraints, so concurrent upserts can race the
31
+ * anchor MERGE into duplicate :Fact nodes — after which a later MERGE
32
+ * binds EVERY duplicate and mints extra edges. Instead: skip when any
33
+ * edge already exists; bind exactly ONE anchor (WITH … LIMIT 1) or create
34
+ * it when none exists.
35
+ */
36
+ private linkEvidenceAnchor;
37
+ /** EVIDENCED_BY scopeKeys per node, ACL-filtered (01 §6.1a), deduped
38
+ * (duplicate anchors must never surface as duplicate evidence keys). */
39
+ private evidenceFor;
40
+ searchGraphEdges(q: GraphEdgeQuery, access?: AccessContext): Promise<GraphEdgeHit[]>;
41
+ graphNeighbourhood(nodeKey: string, depth: number, _access?: AccessContext, opts?: {
42
+ namespace?: string;
43
+ }): Promise<SubGraph>;
44
+ /**
45
+ * Cheap whole-graph counts for `graph_stats` (enhancedfactstore 07 P5):
46
+ * a single `count()` Cypher per axis instead of a client-side fan-out.
47
+ * AGE has no edge-only catalog, so edges are counted via a directed match.
48
+ */
49
+ graphStats(opts?: {
50
+ namespace?: string;
51
+ }): Promise<{
52
+ nodeCount: number;
53
+ edgeCount: number;
54
+ }>;
55
+ upsertGraphNode(n: GraphNodeInput): Promise<GraphNodeRef>;
56
+ upsertGraphEdge(e: GraphEdgeInput): Promise<GraphEdgeRef>;
57
+ mergeGraphNodes(fromKey: string, intoKey: string, reason: string, opts?: {
58
+ namespace?: string;
59
+ }): Promise<void>;
60
+ deleteGraphNode(nodeKey: string, opts?: {
61
+ namespace?: string;
62
+ }): Promise<boolean>;
63
+ deleteGraphEdge(fromKey: string, toKey: string, predicateKey: string, opts?: {
64
+ namespace?: string;
65
+ }): Promise<boolean>;
66
+ removeGraphEvidence(scopeKey: string, opts?: {
67
+ namespace?: string;
68
+ }): Promise<GraphEvidenceRemovalResult>;
69
+ /**
70
+ * Drop graph data for an EXACT namespace (graph-fact-search enhancements,
71
+ * `deleteGraphNamespace`). NOT subtree: only records whose own namespace
72
+ * equals `namespace` are removed — child namespaces are deleted explicitly.
73
+ * Edges with this namespace are removed first; then nodes with this
74
+ * namespace are DETACH DELETEd (which also clears their remaining edges and
75
+ * EVIDENCED_BY anchors). Idempotent / re-runnable. Refuses the reserved
76
+ * `default` token / empty (the NULL partition is never bulk-deleted).
77
+ */
78
+ deleteNamespaceData(namespace: string): Promise<{
79
+ nodesDeleted: number;
80
+ edgesDeleted: number;
81
+ }>;
82
+ }
83
+ //# sourceMappingURL=graph-queries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-queries.d.ts","sourceRoot":"","sources":["../../src/graph-queries.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EACR,aAAa,EAAE,YAAY,EAAE,cAAc,EAAE,cAAc,EAC3D,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,YAAY,EAAE,YAAY,EAAE,0BAA0B,EAAE,QAAQ,EACjH,MAAM,YAAY,CAAC;AA+EpB;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAQlE;AAED,0DAA0D;AAC1D,wBAAgB,EAAE,CAAC,CAAC,EAAE,GAAG,GAAG,GAAG,CAK9B;AAED,kDAAkD;AAClD,wBAAgB,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,MAAM,EAAE,CAGtC;AAED;;;;;GAKG;AACH,qBAAa,YAAY;IAWT,OAAO,CAAC,QAAQ,CAAC,IAAI;IAAO,OAAO,CAAC,QAAQ,CAAC,SAAS;IAFlE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;gBAErB,IAAI,EAAE,GAAG,EAAmB,SAAS,EAAE,MAAM;YAE5D,OAAO;YAkBP,MAAM;IAiCd,gBAAgB,CAAC,CAAC,EAAE,cAAc,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAqF1F;;;;;;;;OAQG;YACW,kBAAkB;IAkBhC;4EACwE;YAC1D,WAAW;IAkBnB,gBAAgB,CAAC,CAAC,EAAE,cAAc,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IA2BpF,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,EAAE,IAAI,GAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IA4CvI;;;;OAIG;IACG,UAAU,CAAC,IAAI,GAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAiBhG,eAAe,CAAC,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC;IAyCzD,eAAe,CAAC,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC;IAiEzD,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,GAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAuFnH,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAYrF,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,GAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAe1H,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,GAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,OAAO,CAAC,0BAA0B,CAAC;IA6EnH;;;;;;;;OAQG;IACG,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC;CA+BxG"}