@rubytech/create-maxy 1.0.715 → 1.0.716

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/index.js CHANGED
@@ -1451,6 +1451,39 @@ function setupAccount() {
1451
1451
  if (existsSync(seedScript)) {
1452
1452
  shell("bash", [seedScript], { cwd: INSTALL_DIR });
1453
1453
  }
1454
+ // Task 748 — universal embedding coverage backfill. Run after seed so the
1455
+ // entity_search index is in place and any pre-Task-748 nodes (e.g. the
1456
+ // 5096 LinkedIn-imported Persons on existing Pis that bulk-import skipped
1457
+ // embedding for) get a vector populated. Idempotent — instant no-op when
1458
+ // nothing is pending, so re-running on every install is harmless.
1459
+ //
1460
+ // Failure-mode policy: WARN, do not abort. The fulltext index is already
1461
+ // applied above, so BM25 search works end-to-end without embeddings; the
1462
+ // only gap is vector ranking quality on legacy nodes. Aborting the
1463
+ // installer on an Ollama hiccup would block every install for a
1464
+ // strictly-degradable feature. The script's own loud-failure output
1465
+ // tells the operator how to re-run.
1466
+ const backfillScript = join(INSTALL_DIR, "platform/scripts/embed-backfill.sh");
1467
+ if (existsSync(backfillScript)) {
1468
+ const start = Date.now();
1469
+ logFile(`> bash ${backfillScript} (warn-not-abort)`);
1470
+ const result = spawnSync("bash", [backfillScript], {
1471
+ stdio: "inherit",
1472
+ timeout: 30 * 60_000,
1473
+ cwd: INSTALL_DIR,
1474
+ });
1475
+ const dur = ((Date.now() - start) / 1000).toFixed(1);
1476
+ if (result.status !== 0 || result.signal) {
1477
+ const reason = result.signal ? `signal=${result.signal}` : `exit=${result.status}`;
1478
+ logFile(` WARN: embed-backfill non-zero (${reason}) after ${dur}s`);
1479
+ console.warn(`\n WARNING: embed-backfill did not complete (${reason}) — BM25 search works,\n` +
1480
+ ` but vector ranking on legacy nodes will be sparse until you re-run:\n` +
1481
+ ` bash ${backfillScript}\n`);
1482
+ }
1483
+ else {
1484
+ logFile(` OK embed-backfill in ${dur}s`);
1485
+ }
1486
+ }
1454
1487
  }
1455
1488
  // ---------------------------------------------------------------------------
1456
1489
  // Tunnel script shortcuts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy",
3
- "version": "1.0.715",
3
+ "version": "1.0.716",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy": "./dist/index.js"
@@ -1,12 +1,21 @@
1
1
  /**
2
2
  * Shared hybrid search primitive over the Neo4j knowledge graph.
3
3
  *
4
- * Pre-Task-675 there were two BM25 implementations over the same
5
- * `knowledge_fulltext` index — one in the memory MCP tool, one in the admin
6
- * Hono route — with divergent filter semantics (soft-delete primitive) and
7
- * divergent ranking (agent ran hybrid vector+BM25, UI ran BM25-only). Task
8
- * 675 collapses both into this lib; memory MCP + admin route now share
9
- * a single code path so `/graph` UI ranking matches what the agent sees.
4
+ * Pre-Task-675 there were two BM25 implementations over the same fulltext
5
+ * index — one in the memory MCP tool, one in the admin Hono route — with
6
+ * divergent filter semantics (soft-delete primitive) and divergent ranking
7
+ * (agent ran hybrid vector+BM25, UI ran BM25-only). Task 675 collapses both
8
+ * into this lib; memory MCP + admin route now share a single code path so
9
+ * `/graph` UI ranking matches what the agent sees.
10
+ *
11
+ * Task 748 — fulltext index `knowledge_fulltext` was renamed to `entity_search`
12
+ * and its label union expanded from 3 labels (KnowledgeDocument | Section |
13
+ * Chunk) to the full operator-meaningful label set (~40 labels) on every
14
+ * textual property the schema's writers assign. The lib references the new
15
+ * name via FULLTEXT_INDEX_NAME below; the doctrine test at
16
+ * `__tests__/fulltext-coverage.test.ts` parses `platform/neo4j/schema.cypher`
17
+ * and asserts the index covers the union of `GRAPH_LABEL_COLOURS` and every
18
+ * label declared in the schema.
10
19
  *
11
20
  * QUERY --> EMBED --> VECTOR SEARCH (per index) --> ┐
12
21
  * │ ├--> MERGE --> EXPAND --> RESULTS
@@ -30,6 +39,13 @@
30
39
  * line with the prefix and fields it wants.
31
40
  */
32
41
  import { type Session } from "neo4j-driver";
42
+ /**
43
+ * Name of the universal fulltext index (Task 748). Mirrors the `CREATE FULLTEXT
44
+ * INDEX` declaration in `platform/neo4j/schema.cypher`. The doctrine test
45
+ * (`__tests__/fulltext-coverage.test.ts`) asserts the schema's index name
46
+ * matches this constant — drift between the two breaks BM25 silently.
47
+ */
48
+ export declare const FULLTEXT_INDEX_NAME = "entity_search";
33
49
  export interface SearchHit {
34
50
  nodeId: string;
35
51
  labels: string[];
@@ -118,10 +134,10 @@ export declare function discoverIndexes(session: Session): Promise<Map<string, s
118
134
  /** Clear the index cache — call after schema changes. */
119
135
  export declare function clearIndexCache(): void;
120
136
  /**
121
- * BM25 full-text search against the `knowledge_fulltext` index.
137
+ * BM25 full-text search against the universal `entity_search` index (Task 748).
122
138
  * Returns [] when the index doesn't exist — matches memory-search.ts
123
- * graceful-fallback semantics so a fresh account with no documents
124
- * doesn't 500 the caller.
139
+ * graceful-fallback semantics so a fresh account or a Pi mid-migration
140
+ * (POPULATING window) doesn't 500 the caller.
125
141
  */
126
142
  export declare function bm25Only(session: Session, params: Bm25OnlyParams): Promise<SearchHit[]>;
127
143
  /**
@@ -134,8 +150,8 @@ export declare function bm25Only(session: Session, params: Bm25OnlyParams): Prom
134
150
  * 2. Vector search per label (one query per vector index discovered at
135
151
  * boot). Nodes-by-label filter short-circuits when the requested
136
152
  * labels have no index.
137
- * 3. BM25 search on knowledge_fulltext same filter semantics as
138
- * vector half (scope/agent/keyword/trashed).
153
+ * 3. BM25 search on the universal `entity_search` index (Task 748) —
154
+ * same filter semantics as vector half (scope/agent/keyword/trashed).
139
155
  * 4. Keyword subscriptions (when set): BM25 per keyword + property
140
156
  * lookup against `node.keywords` array. Both bypass the agentSlug
141
157
  * filter — subscriptions are scope-inclusive by design (matches
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAO,KAAK,OAAO,EAAE,MAAM,cAAc,CAAC;AAOjD,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAa,SAAQ,SAAS;IAC7C,OAAO,EAAE,KAAK,CAAC;QACb;;;;WAIG;QACH,MAAM,EAAE,MAAM,CAAC;QACf,YAAY,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,EAAE,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACrC,CAAC,CAAC;CACJ;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE3C,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,YAAY,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IAC7B;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,YAAa,SAAQ,cAAc;IAClD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,UAAU,CAAC;IACjB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,OAAO,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;AAE1D;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAElD;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAO9D;AAMD,wBAAsB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAapF;AAED,yDAAyD;AACzD,wBAAgB,eAAe,IAAI,IAAI,CAEtC;AAmBD;;;;;GAKG;AACH,wBAAsB,QAAQ,CAC5B,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,SAAS,EAAE,CAAC,CAyDtB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,MAAM,CAC1B,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,cAAc,CAAC,CAsPzB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -2,12 +2,21 @@
2
2
  /**
3
3
  * Shared hybrid search primitive over the Neo4j knowledge graph.
4
4
  *
5
- * Pre-Task-675 there were two BM25 implementations over the same
6
- * `knowledge_fulltext` index — one in the memory MCP tool, one in the admin
7
- * Hono route — with divergent filter semantics (soft-delete primitive) and
8
- * divergent ranking (agent ran hybrid vector+BM25, UI ran BM25-only). Task
9
- * 675 collapses both into this lib; memory MCP + admin route now share
10
- * a single code path so `/graph` UI ranking matches what the agent sees.
5
+ * Pre-Task-675 there were two BM25 implementations over the same fulltext
6
+ * index — one in the memory MCP tool, one in the admin Hono route — with
7
+ * divergent filter semantics (soft-delete primitive) and divergent ranking
8
+ * (agent ran hybrid vector+BM25, UI ran BM25-only). Task 675 collapses both
9
+ * into this lib; memory MCP + admin route now share a single code path so
10
+ * `/graph` UI ranking matches what the agent sees.
11
+ *
12
+ * Task 748 — fulltext index `knowledge_fulltext` was renamed to `entity_search`
13
+ * and its label union expanded from 3 labels (KnowledgeDocument | Section |
14
+ * Chunk) to the full operator-meaningful label set (~40 labels) on every
15
+ * textual property the schema's writers assign. The lib references the new
16
+ * name via FULLTEXT_INDEX_NAME below; the doctrine test at
17
+ * `__tests__/fulltext-coverage.test.ts` parses `platform/neo4j/schema.cypher`
18
+ * and asserts the index covers the union of `GRAPH_LABEL_COLOURS` and every
19
+ * label declared in the schema.
11
20
  *
12
21
  * QUERY --> EMBED --> VECTOR SEARCH (per index) --> ┐
13
22
  * │ ├--> MERGE --> EXPAND --> RESULTS
@@ -31,6 +40,7 @@
31
40
  * line with the prefix and fields it wants.
32
41
  */
33
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.FULLTEXT_INDEX_NAME = void 0;
34
44
  exports.escapeLucene = escapeLucene;
35
45
  exports.normaliseBm25Scores = normaliseBm25Scores;
36
46
  exports.discoverIndexes = discoverIndexes;
@@ -41,7 +51,13 @@ const neo4j_driver_1 = require("neo4j-driver");
41
51
  const index_js_1 = require("../../graph-trash/dist/index.js");
42
52
  const VECTOR_WEIGHT = 0.7;
43
53
  const BM25_WEIGHT = 0.3;
44
- const FULLTEXT_INDEX_NAME = "knowledge_fulltext";
54
+ /**
55
+ * Name of the universal fulltext index (Task 748). Mirrors the `CREATE FULLTEXT
56
+ * INDEX` declaration in `platform/neo4j/schema.cypher`. The doctrine test
57
+ * (`__tests__/fulltext-coverage.test.ts`) asserts the schema's index name
58
+ * matches this constant — drift between the two breaks BM25 silently.
59
+ */
60
+ exports.FULLTEXT_INDEX_NAME = "entity_search";
45
61
  /**
46
62
  * Lucene-escape special characters per Neo4j's fulltext query grammar.
47
63
  * `/[+\-&|!(){}[\]^"~*?:\\/]/g` — matches memory-search.ts:127 and
@@ -97,10 +113,10 @@ function buildKeywordFilter(keywords, keywordMatch = "any") {
97
113
  };
98
114
  }
99
115
  /**
100
- * BM25 full-text search against the `knowledge_fulltext` index.
116
+ * BM25 full-text search against the universal `entity_search` index (Task 748).
101
117
  * Returns [] when the index doesn't exist — matches memory-search.ts
102
- * graceful-fallback semantics so a fresh account with no documents
103
- * doesn't 500 the caller.
118
+ * graceful-fallback semantics so a fresh account or a Pi mid-migration
119
+ * (POPULATING window) doesn't 500 the caller.
104
120
  */
105
121
  async function bm25Only(session, params) {
106
122
  const { query, accountId, limit, allowedScopes, agentSlug, keywords, keywordMatch, labels } = params;
@@ -128,7 +144,7 @@ async function bm25Only(session, params) {
128
144
  RETURN node, score, labels(node) AS nodeLabels, elementId(node) AS nodeId
129
145
  ORDER BY score DESC
130
146
  LIMIT $limit`, {
131
- indexName: FULLTEXT_INDEX_NAME,
147
+ indexName: exports.FULLTEXT_INDEX_NAME,
132
148
  query: escaped,
133
149
  accountId,
134
150
  limit: (0, neo4j_driver_1.int)(limit),
@@ -167,8 +183,8 @@ async function bm25Only(session, params) {
167
183
  * 2. Vector search per label (one query per vector index discovered at
168
184
  * boot). Nodes-by-label filter short-circuits when the requested
169
185
  * labels have no index.
170
- * 3. BM25 search on knowledge_fulltext same filter semantics as
171
- * vector half (scope/agent/keyword/trashed).
186
+ * 3. BM25 search on the universal `entity_search` index (Task 748) —
187
+ * same filter semantics as vector half (scope/agent/keyword/trashed).
172
188
  * 4. Keyword subscriptions (when set): BM25 per keyword + property
173
189
  * lookup against `node.keywords` array. Both bypass the agentSlug
174
190
  * filter — subscriptions are scope-inclusive by design (matches
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,267 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { FULLTEXT_INDEX_NAME } from "../index.js";
6
+
7
+ /**
8
+ * Doctrine test (Task 748) — pins the universal-coverage invariant for the
9
+ * fulltext index so future schema edits cannot silently re-narrow.
10
+ *
11
+ * The pre-Task-748 fulltext index `knowledge_fulltext` covered 3 of ~40
12
+ * registered labels (`KnowledgeDocument | Section | Chunk`); BM25 silently
13
+ * returned zero hits for Person/Organization/Task/Conversation/etc. queries
14
+ * regardless of term. The doctrine: search is "find any node in my graph
15
+ * that mentions this term" — there is no allowlist of search-eligible labels.
16
+ *
17
+ * This test reads `platform/neo4j/schema.cypher` (the schema source of truth)
18
+ * and `platform/ui/app/lib/graph-labels.ts` (the canvas label registry) at
19
+ * runtime, parses both, and asserts:
20
+ *
21
+ * 1. The fulltext index name in `schema.cypher` matches the
22
+ * `FULLTEXT_INDEX_NAME` constant in the lib. Drift between the two
23
+ * breaks BM25 silently — the lib queries an index name that doesn't
24
+ * exist, hits the graceful-fallback `[]` branch, and search returns
25
+ * no BM25 hits.
26
+ * 2. The label union in the index ⊇ union of:
27
+ * - `Object.keys(GRAPH_LABEL_COLOURS)` (canvas-registered labels)
28
+ * - every label declared via `CREATE CONSTRAINT|INDEX FOR (n:Label)`
29
+ * in `schema.cypher` (write-targeted labels)
30
+ * minus a small explicit excluded set (`Trashed`, `GraphPreference`)
31
+ * that carry no operator-meaningful textual content.
32
+ * 3. The property union in the index ⊇ the canonical text-property list
33
+ * (the properties that writers across the codebase assign to nodes).
34
+ * New writers extending the canon must add their property here.
35
+ *
36
+ * Failing assertions point the next person at exactly the schema drift to
37
+ * close — not "search is mysteriously broken."
38
+ */
39
+
40
+ const __dirname = dirname(fileURLToPath(import.meta.url));
41
+ const SCHEMA_PATH = resolve(__dirname, "../../../../neo4j/schema.cypher");
42
+ const LABELS_PATH = resolve(__dirname, "../../../../ui/app/lib/graph-labels.ts");
43
+
44
+ /**
45
+ * Labels declared in `schema.cypher` or `GRAPH_LABEL_COLOURS` that legitimately
46
+ * carry no searchable textual content. The doctrine test allows these to be
47
+ * absent from the fulltext index union without failing.
48
+ *
49
+ * - `Trashed` — soft-delete flag label; nodes carrying it are
50
+ * already excluded from every search via `notTrashed()`
51
+ * in the lib.
52
+ * - `GraphPreference`— per-(accountId, userId) UI metadata for /graph;
53
+ * carries no textual content, never operator-visible
54
+ * as a search hit.
55
+ */
56
+ const EXCLUDED_FROM_INDEX = new Set(["Trashed", "GraphPreference"]);
57
+
58
+ /**
59
+ * Canonical text-property list — every property the platform's writers
60
+ * assign that the operator might search for. Extending the canon in a new
61
+ * writer means extending this list AND the schema's `ON EACH [...]` clause.
62
+ * Discovered via codebase sweep during Task 748 across `memory-write`,
63
+ * `memory-archive-write`, `memory-ingest`, `email-store`, `neo4j-store`,
64
+ * `workflow-create`, `tool-call-writer`, `review-detector/writer`.
65
+ */
66
+ const CANONICAL_TEXT_PROPERTIES = [
67
+ // Generic
68
+ "name", "title", "summary", "body", "content", "text", "description",
69
+ "headline", "abstract", "note", "label", "value", "message", "preview",
70
+ "tagline",
71
+ // Person
72
+ "firstName", "lastName", "givenName", "familyName", "email",
73
+ // Email
74
+ "subject", "bodyPreview", "fromName", "fromAddress", "screeningReason",
75
+ // EmailAccount
76
+ "agentAddress",
77
+ // Credential
78
+ "authority",
79
+ // AccessGrant
80
+ "contactValue",
81
+ // ToolCall
82
+ "toolName",
83
+ ];
84
+
85
+ interface IndexDeclaration {
86
+ name: string;
87
+ labels: Set<string>;
88
+ properties: Set<string>;
89
+ }
90
+
91
+ /**
92
+ * Extract the `CREATE FULLTEXT INDEX entity_search ... ;` block from
93
+ * `schema.cypher` and parse its label and property sets.
94
+ *
95
+ * Uses a paren-balanced scan from the matched `CREATE FULLTEXT INDEX <name>`
96
+ * to the next semicolon — survives whitespace/comment/line-break formatting
97
+ * drift that a single monolithic regex would not. Failing here means the
98
+ * declaration's shape diverged from `CREATE FULLTEXT INDEX <name> IF NOT EXISTS
99
+ * FOR (n:A|B|...) ON EACH [n.x, n.y, ...];` in a way the parser does not
100
+ * recognise — extend this function before the test.
101
+ */
102
+ function parseFulltextIndex(rawCypher: string, indexName: string): IndexDeclaration {
103
+ // Strip line comments first so a historical note like
104
+ // `// pre-Task-748: CREATE FULLTEXT INDEX entity_search FOR (n:OldLabel)`
105
+ // does not fool the extractor into parsing the comment instead of the
106
+ // live declaration.
107
+ const cypher = stripLineComments(rawCypher);
108
+ const declStart = cypher.search(
109
+ new RegExp(`CREATE\\s+FULLTEXT\\s+INDEX\\s+${indexName}\\b`, "m"),
110
+ );
111
+ if (declStart < 0) {
112
+ throw new Error(
113
+ `parseFulltextIndex: could not find 'CREATE FULLTEXT INDEX ${indexName}' in schema.cypher. ` +
114
+ `Drift between schema and FULLTEXT_INDEX_NAME constant in graph-search/src/index.ts.`,
115
+ );
116
+ }
117
+ const declEnd = cypher.indexOf(";", declStart);
118
+ if (declEnd < 0) {
119
+ throw new Error(
120
+ `parseFulltextIndex: no terminating ';' after 'CREATE FULLTEXT INDEX ${indexName}' — schema.cypher is malformed.`,
121
+ );
122
+ }
123
+ const block = cypher.slice(declStart, declEnd);
124
+
125
+ const forMatch = block.match(/FOR\s+\(n:([^\)]+)\)/);
126
+ if (!forMatch) {
127
+ throw new Error(
128
+ `parseFulltextIndex: could not extract FOR (n:...) clause from index ${indexName}. Block: ${block.slice(0, 200)}...`,
129
+ );
130
+ }
131
+ const labels = new Set(
132
+ forMatch[1]
133
+ .split("|")
134
+ .map((s) => s.trim())
135
+ .filter((s) => s.length > 0),
136
+ );
137
+
138
+ const onEachMatch = block.match(/ON\s+EACH\s+\[([^\]]+)\]/);
139
+ if (!onEachMatch) {
140
+ throw new Error(
141
+ `parseFulltextIndex: could not extract ON EACH [...] clause from index ${indexName}. Block: ${block.slice(0, 200)}...`,
142
+ );
143
+ }
144
+ const properties = new Set(
145
+ onEachMatch[1]
146
+ .split(",")
147
+ .map((s) => s.trim().replace(/^n\./, ""))
148
+ .filter((s) => s.length > 0),
149
+ );
150
+
151
+ return { name: indexName, labels, properties };
152
+ }
153
+
154
+ /**
155
+ * Strip Cypher `// line comments` before regex-parsing so an example or
156
+ * migration note inside a comment block — e.g. `// previously: CREATE
157
+ * INDEX FOR (g:Goal) ON (g.embedding)` — does not inject a phantom label
158
+ * into the schema-declared set. Block comments `/* ... *​/` are not
159
+ * stripped because schema.cypher does not use them today.
160
+ */
161
+ function stripLineComments(cypher: string): string {
162
+ return cypher.replace(/^\s*\/\/.*$/gm, "");
163
+ }
164
+
165
+ /**
166
+ * Extract every label declared in `schema.cypher` via `FOR (alias:Label)`.
167
+ * Each `CREATE CONSTRAINT|INDEX|VECTOR INDEX|FULLTEXT INDEX` clause that
168
+ * targets a label produces one entry. The fulltext index itself uses
169
+ * `FOR (n:A|B|...)` — we extract the union of all alternatives.
170
+ */
171
+ function parseSchemaDeclaredLabels(cypher: string): Set<string> {
172
+ const labels = new Set<string>();
173
+ const stripped = stripLineComments(cypher);
174
+ const matches = stripped.matchAll(/FOR\s+\(\w+:([^\)]+)\)/g);
175
+ for (const m of matches) {
176
+ for (const label of m[1].split("|")) {
177
+ const trimmed = label.trim();
178
+ // Filter out anything that doesn't look like a Neo4j label (defensive
179
+ // against future schema patterns that might inject non-label tokens).
180
+ if (/^[A-Z][A-Za-z0-9]*$/.test(trimmed)) {
181
+ labels.add(trimmed);
182
+ }
183
+ }
184
+ }
185
+ return labels;
186
+ }
187
+
188
+ /**
189
+ * Extract `Object.keys(GRAPH_LABEL_COLOURS)` from `graph-labels.ts` by
190
+ * matching the export's object-literal keys. Tolerates trailing commas,
191
+ * inline comments between keys, and leading whitespace.
192
+ */
193
+ function parseGraphLabelColours(source: string): Set<string> {
194
+ const exportMatch = source.match(
195
+ /export\s+const\s+GRAPH_LABEL_COLOURS\s*:\s*[^=]+=\s*\{([\s\S]+?)\n\}/m,
196
+ );
197
+ if (!exportMatch) {
198
+ throw new Error(
199
+ "parseGraphLabelColours: could not find 'export const GRAPH_LABEL_COLOURS' object literal in graph-labels.ts.",
200
+ );
201
+ }
202
+ const labels = new Set<string>();
203
+ // Match keys that are bare identifiers (camelCase or PascalCase). Skip
204
+ // commented-out lines and string-quoted keys (none today, but defensive).
205
+ for (const m of exportMatch[1].matchAll(/^\s*([A-Z][A-Za-z0-9]*)\s*:/gm)) {
206
+ labels.add(m[1]);
207
+ }
208
+ return labels;
209
+ }
210
+
211
+ describe("entity_search fulltext index — universal-coverage doctrine (Task 748)", () => {
212
+ const schemaCypher = readFileSync(SCHEMA_PATH, "utf-8");
213
+ const labelsSource = readFileSync(LABELS_PATH, "utf-8");
214
+
215
+ const indexDecl = parseFulltextIndex(schemaCypher, FULLTEXT_INDEX_NAME);
216
+ const schemaDeclaredLabels = parseSchemaDeclaredLabels(schemaCypher);
217
+ const colourLabels = parseGraphLabelColours(labelsSource);
218
+
219
+ const requiredLabels = new Set<string>();
220
+ for (const l of colourLabels) if (!EXCLUDED_FROM_INDEX.has(l)) requiredLabels.add(l);
221
+ for (const l of schemaDeclaredLabels) if (!EXCLUDED_FROM_INDEX.has(l)) requiredLabels.add(l);
222
+
223
+ it("schema declares the index under the FULLTEXT_INDEX_NAME constant", () => {
224
+ // The lib's BM25 path queries `db.index.fulltext.queryNodes($indexName)`.
225
+ // If `schema.cypher` declares a different name, the lib hits its
226
+ // graceful-fallback `[]` branch and BM25 silently returns no hits —
227
+ // the precise drift Task 748's rename was meant to delete.
228
+ expect(indexDecl.name).toBe(FULLTEXT_INDEX_NAME);
229
+ });
230
+
231
+ it("index labels ⊇ Object.keys(GRAPH_LABEL_COLOURS) − {excluded}", () => {
232
+ const missing = [...colourLabels]
233
+ .filter((l) => !EXCLUDED_FROM_INDEX.has(l))
234
+ .filter((l) => !indexDecl.labels.has(l));
235
+ expect(missing).toEqual([]);
236
+ });
237
+
238
+ it("index labels ⊇ every schema-declared label − {excluded}", () => {
239
+ // Position, Credential, etc. — labels written by the platform but not
240
+ // (yet) registered in GRAPH_LABEL_COLOURS. The doctrine: every label
241
+ // the platform writes is searchable on day 1. Registering for canvas
242
+ // colour is a separate concern.
243
+ const missing = [...schemaDeclaredLabels]
244
+ .filter((l) => !EXCLUDED_FROM_INDEX.has(l))
245
+ .filter((l) => !indexDecl.labels.has(l));
246
+ expect(missing).toEqual([]);
247
+ });
248
+
249
+ it("index properties ⊇ canonical text-property list", () => {
250
+ // Adding a new text property in a writer (e.g. `Position.companyName`
251
+ // hypothetically) without extending the schema's `ON EACH [...]` clause
252
+ // means BM25 cannot match on that property. Catch it here.
253
+ const missing = CANONICAL_TEXT_PROPERTIES.filter(
254
+ (p) => !indexDecl.properties.has(p),
255
+ );
256
+ expect(missing).toEqual([]);
257
+ });
258
+
259
+ it("required-label set is non-trivial — sanity check that the parsers ran", () => {
260
+ // Defensive: if the parsers above silently regressed to "no labels
261
+ // found", the supersets above would vacuously pass. Assert a meaningful
262
+ // floor (we expect ~40 labels post-Task-748).
263
+ expect(requiredLabels.size).toBeGreaterThanOrEqual(30);
264
+ expect(indexDecl.labels.size).toBeGreaterThanOrEqual(30);
265
+ expect(indexDecl.properties.size).toBeGreaterThanOrEqual(20);
266
+ });
267
+ });
@@ -1,12 +1,21 @@
1
1
  /**
2
2
  * Shared hybrid search primitive over the Neo4j knowledge graph.
3
3
  *
4
- * Pre-Task-675 there were two BM25 implementations over the same
5
- * `knowledge_fulltext` index — one in the memory MCP tool, one in the admin
6
- * Hono route — with divergent filter semantics (soft-delete primitive) and
7
- * divergent ranking (agent ran hybrid vector+BM25, UI ran BM25-only). Task
8
- * 675 collapses both into this lib; memory MCP + admin route now share
9
- * a single code path so `/graph` UI ranking matches what the agent sees.
4
+ * Pre-Task-675 there were two BM25 implementations over the same fulltext
5
+ * index — one in the memory MCP tool, one in the admin Hono route — with
6
+ * divergent filter semantics (soft-delete primitive) and divergent ranking
7
+ * (agent ran hybrid vector+BM25, UI ran BM25-only). Task 675 collapses both
8
+ * into this lib; memory MCP + admin route now share a single code path so
9
+ * `/graph` UI ranking matches what the agent sees.
10
+ *
11
+ * Task 748 — fulltext index `knowledge_fulltext` was renamed to `entity_search`
12
+ * and its label union expanded from 3 labels (KnowledgeDocument | Section |
13
+ * Chunk) to the full operator-meaningful label set (~40 labels) on every
14
+ * textual property the schema's writers assign. The lib references the new
15
+ * name via FULLTEXT_INDEX_NAME below; the doctrine test at
16
+ * `__tests__/fulltext-coverage.test.ts` parses `platform/neo4j/schema.cypher`
17
+ * and asserts the index covers the union of `GRAPH_LABEL_COLOURS` and every
18
+ * label declared in the schema.
10
19
  *
11
20
  * QUERY --> EMBED --> VECTOR SEARCH (per index) --> ┐
12
21
  * │ ├--> MERGE --> EXPAND --> RESULTS
@@ -35,7 +44,13 @@ import { notTrashed } from "../../graph-trash/dist/index.js";
35
44
 
36
45
  const VECTOR_WEIGHT = 0.7;
37
46
  const BM25_WEIGHT = 0.3;
38
- const FULLTEXT_INDEX_NAME = "knowledge_fulltext";
47
+ /**
48
+ * Name of the universal fulltext index (Task 748). Mirrors the `CREATE FULLTEXT
49
+ * INDEX` declaration in `platform/neo4j/schema.cypher`. The doctrine test
50
+ * (`__tests__/fulltext-coverage.test.ts`) asserts the schema's index name
51
+ * matches this constant — drift between the two breaks BM25 silently.
52
+ */
53
+ export const FULLTEXT_INDEX_NAME = "entity_search";
39
54
 
40
55
  export interface SearchHit {
41
56
  nodeId: string;
@@ -182,10 +197,10 @@ function buildKeywordFilter(
182
197
  }
183
198
 
184
199
  /**
185
- * BM25 full-text search against the `knowledge_fulltext` index.
200
+ * BM25 full-text search against the universal `entity_search` index (Task 748).
186
201
  * Returns [] when the index doesn't exist — matches memory-search.ts
187
- * graceful-fallback semantics so a fresh account with no documents
188
- * doesn't 500 the caller.
202
+ * graceful-fallback semantics so a fresh account or a Pi mid-migration
203
+ * (POPULATING window) doesn't 500 the caller.
189
204
  */
190
205
  export async function bm25Only(
191
206
  session: Session,
@@ -259,8 +274,8 @@ export async function bm25Only(
259
274
  * 2. Vector search per label (one query per vector index discovered at
260
275
  * boot). Nodes-by-label filter short-circuits when the requested
261
276
  * labels have no index.
262
- * 3. BM25 search on knowledge_fulltext same filter semantics as
263
- * vector half (scope/agent/keyword/trashed).
277
+ * 3. BM25 search on the universal `entity_search` index (Task 748) —
278
+ * same filter semantics as vector half (scope/agent/keyword/trashed).
264
279
  * 4. Keyword subscriptions (when set): BM25 per keyword + property
265
280
  * lookup against `node.keywords` array. Both bypass the agentSlug
266
281
  * filter — subscriptions are scope-inclusive by design (matches
@@ -258,13 +258,59 @@ OPTIONS {
258
258
  }
259
259
  };
260
260
 
261
- // Full-text BM25 index for hybrid keyword search across document levels.
262
- // Post-Task 740: sections carry their body inline so the index covers
263
- // KnowledgeDocument.summary, Section.summary, Section.body, and the legacy
264
- // Chunk.content for any Chunks still present from pre-740 ingests.
265
- CREATE FULLTEXT INDEX knowledge_fulltext IF NOT EXISTS
266
- FOR (k:KnowledgeDocument|Section|Chunk)
267
- ON EACH [k.summary, k.content, k.body];
261
+ // Universal full-text BM25 index for hybrid keyword search (Task 748).
262
+ //
263
+ // Every operator-meaningful label written by the platform is in the index union;
264
+ // every textual property a writer assigns is in the property union. Neo4j silently
265
+ // ignores absent properties on a given label, so over-inclusion is harmless.
266
+ //
267
+ // **Doctrine.** Search is "find any node in my graph that mentions this term" —
268
+ // not "find a knowledge document". Pre-Task-748 the index name `knowledge_fulltext`
269
+ // covered only `KnowledgeDocument | Section | Chunk` (3 of ~40 written labels), so
270
+ // BM25 silently returned zero hits for Person/Organization/Task/Conversation/etc.
271
+ // regardless of query. Universal coverage is the doctrine; the doctrine test at
272
+ // `platform/lib/graph-search/src/__tests__/fulltext-coverage.test.ts` parses this
273
+ // declaration and asserts label-set ⊇ union(GRAPH_LABEL_COLOURS, schema-declared)
274
+ // so future label additions cannot silently re-narrow.
275
+ //
276
+ // Label union — every operator-meaningful label:
277
+ // - Business identity: LocalBusiness, Service, PriceSpecification, OpeningHoursSpecification, Organization
278
+ // - People: Person, UserProfile, Preference, AdminUser, AccessGrant
279
+ // - Knowledge: KnowledgeDocument, Section, Chunk (legacy), DigitalDocument, CreativeWork,
280
+ // Question, FAQPage, DefinedTerm, Review, ImageObject
281
+ // - Conversational: Conversation, AdminConversation, PublicConversation, Message,
282
+ // UserMessage, AssistantMessage, ToolCall
283
+ // - Tasks/projects/events: Task, Project, Event
284
+ // - Workflows: Workflow, WorkflowStep, WorkflowRun, StepResult
285
+ // - Onboarding: OnboardingState
286
+ // - Email: Email, EmailAccount
287
+ // - Review signals: ReviewAlert
288
+ // - CV/career sublabels: Position, Credential
289
+ //
290
+ // Property union — every textual property the schema's writers assign:
291
+ // - Generic: name, title, summary, body, content, text, description, headline, abstract,
292
+ // note, label, value, message, preview, tagline
293
+ // - Person: firstName, lastName, givenName, familyName, email
294
+ // - Email: subject, bodyPreview, fromName, fromAddress
295
+ // - EmailAccount: agentAddress
296
+ // - Email: screeningReason
297
+ // - Credential: authority
298
+ // - AccessGrant: contactValue
299
+ // - ToolCall: toolName
300
+ CREATE FULLTEXT INDEX entity_search IF NOT EXISTS
301
+ FOR (n:LocalBusiness|Service|PriceSpecification|OpeningHoursSpecification|Organization
302
+ |Person|UserProfile|Preference|AdminUser|AccessGrant
303
+ |KnowledgeDocument|Section|Chunk|DigitalDocument|CreativeWork|Question|FAQPage|DefinedTerm|Review|ImageObject
304
+ |Conversation|AdminConversation|PublicConversation|Message|UserMessage|AssistantMessage|ToolCall
305
+ |Task|Project|Event
306
+ |Workflow|WorkflowStep|WorkflowRun|StepResult
307
+ |OnboardingState|Email|EmailAccount|ReviewAlert
308
+ |Position|Credential)
309
+ ON EACH [n.name, n.firstName, n.lastName, n.givenName, n.familyName,
310
+ n.title, n.summary, n.body, n.content, n.text, n.description, n.headline, n.abstract,
311
+ n.email, n.note, n.label, n.value, n.message, n.preview, n.tagline,
312
+ n.subject, n.bodyPreview, n.fromName, n.fromAddress, n.agentAddress, n.screeningReason,
313
+ n.authority, n.contactValue, n.toolName];
268
314
 
269
315
  // Project node (Task 740) — a standalone creative-output node distinct from
270
316
  // :Section. Anchored via (:UserProfile)-[:CREATED]->(:Project), with optional
@@ -18,7 +18,7 @@ QUERY
18
18
  │ ├──► MERGE ──► EXPAND ──► RESULTS
19
19
  │ │
20
20
  └── ESCAPE (Lucene special chars) ──────► BM25 FULL-TEXT ──┘
21
- (knowledge_fulltext index)
21
+ (entity_search index — universal coverage)
22
22
 
23
23
  Merge formula: combined = 0.7 × vector_score + 0.3 × normalised_bm25_score
24
24
  Deduplication: by nodeId — when a node appears in both paths, keep the max score from each method independently, then combine.
@@ -29,7 +29,7 @@ Fallback: if the full-text index doesn't exist, vector-only results are returned
29
29
 
30
30
  **Vector path:** The query is embedded via Ollama (model per `EMBED_MODEL` env var, default `nomic-embed-text`). The resulting vector is compared against Neo4j's HNSW cosine indexes — one per indexed label. Dimensions are configured at install time (default 768). The search runs against all discovered indexes (or a subset if the caller specifies label filters). Scores are in [0, 1] (cosine similarity).
31
31
 
32
- **BM25 path:** The raw query text is escaped for Lucene special characters and run against the `knowledge_fulltext` full-text index, which spans `KnowledgeDocument`, `Section`, and `Chunk` labels on their `summary` and `content` properties. Raw BM25 scores are in [0, infinity) — they are normalised to [0, 1] via min-max scaling within the result set before merging. When all scores are equal (or a single result), all normalise to 1.0.
32
+ **BM25 path:** The raw query text is escaped for Lucene special characters and run against the `entity_search` full-text index (Task 748 — universal coverage), which spans every operator-meaningful label written by the platform on the canonical text-property union (~28 properties: `name`, `firstName`, `lastName`, `givenName`, `familyName`, `title`, `summary`, `body`, `content`, `description`, `headline`, `email`, `subject`, `bodyPreview`, etc.). Pre-Task-748 the index was named `knowledge_fulltext` and covered only `KnowledgeDocument | Section | Chunk` — that gap silently hid Person/Organization/Task/Event/etc. from BM25 regardless of query. Raw BM25 scores are in [0, infinity) — they are normalised to [0, 1] via min-max scaling within the result set before merging. When all scores are equal (or a single result), all normalise to 1.0.
33
33
 
34
34
  **Merge:** Results from both paths are collected in a single map keyed by `nodeId`. A node appearing in both paths accumulates the max vector score and max BM25 score independently. The combined score is `0.7 * vectorScore + 0.3 * bm25Score`. Results are sorted descending by combined score, then sliced to the requested limit (default 10).
35
35
 
@@ -59,7 +59,7 @@ Indexed labels: `Question`, `DefinedTerm`, `Review`, `Service`, `Person`, `Local
59
59
 
60
60
  | Index name | Labels | Properties | Purpose |
61
61
  |---|---|---|---|
62
- | `knowledge_fulltext` | KnowledgeDocument, Section, Chunk | `summary`, `content` | BM25 keyword matching for the hybrid pipeline |
62
+ | `entity_search` | All operator-meaningful labels (~40, see [`schema.cypher`](../../../neo4j/schema.cypher)) | Canonical text-property union (~28) | Universal BM25 keyword matching across the whole graph (Task 748) |
63
63
 
64
64
  ### Embedding lifecycle
65
65
 
@@ -282,7 +282,7 @@ Each public agent can subscribe to up to 5 keywords via `knowledgeKeywords` in i
282
282
 
283
283
  For each subscription keyword, two complementary searches run:
284
284
 
285
- 1. **BM25 full-text search** — queries the `knowledge_fulltext` index with the keyword as the search term. Catches content that mentions the keyword in its text.
285
+ 1. **BM25 full-text search** — queries the universal `entity_search` index (Task 748) with the keyword as the search term. Catches content that mentions the keyword in its text across every operator-meaningful label.
286
286
 
287
287
  2. **Property-based search** — finds nodes whose `keywords` array property contains the subscription keyword (case-insensitive). Catches nodes explicitly tagged with that keyword topic. These matches are boosted to maximum BM25 score (1.0) since they are exact tag matches.
288
288
 
@@ -292,11 +292,13 @@ Or use `maxy-graph-get_neo4j_schema` for a richer one-shot structural summary.
292
292
 
293
293
  ### Fulltext
294
294
 
295
- Use the `knowledge_fulltext` index for keyword-style search across
296
- KnowledgeDocument / Section / Chunk content:
295
+ Use the universal `entity_search` index (Task 748) for keyword-style search
296
+ across every operator-meaningful label Person, Organization, Task, Event,
297
+ Conversation, KnowledgeDocument, Email, etc. — on every textual property
298
+ the platform's writers assign:
297
299
 
298
300
  ```cypher
299
- CALL db.index.fulltext.queryNodes('knowledge_fulltext', $query)
301
+ CALL db.index.fulltext.queryNodes('entity_search', $query)
300
302
  YIELD node, score
301
303
  WHERE score > 0.5
302
304
  RETURN labels(node)[0] AS type,
@@ -306,6 +308,10 @@ RETURN labels(node)[0] AS type,
306
308
  LIMIT 20
307
309
  ```
308
310
 
311
+ Pre-Task-748 the index was named `knowledge_fulltext` and covered only
312
+ `KnowledgeDocument | Section | Chunk`. Existing Pis pick up the rename on
313
+ the next install via `seed-neo4j.sh`.
314
+
309
315
  ### Filter by status or category
310
316
 
311
317
  Events that are cancelled:
@@ -0,0 +1,370 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================
3
+ # embed-backfill.sh — populate embeddings on legacy nodes (Task 748)
4
+ #
5
+ # Walks the Neo4j graph for nodes carrying any registered Maxy label that
6
+ # lack `n.embedding` and have at least one populated text property. For
7
+ # each such node the script builds a text representation from the same
8
+ # property union the fulltext index covers (`name`, `title`, `summary`,
9
+ # `headline`, `body`, `content`, `text`), POSTs it to Ollama's `/api/embed`
10
+ # endpoint, and writes the resulting vector back to the node.
11
+ #
12
+ # Why it exists. Pre-Task-748 bulk-import paths (notably `memory-archive-write`
13
+ # for LinkedIn Connections.csv, ~5096 Persons per import) skipped per-row
14
+ # embedding to keep import latency under five minutes. With Task 748's
15
+ # universal fulltext coverage in place, BM25 catches those nodes immediately
16
+ # but vector ranking is sparse until embeddings exist. This script heals
17
+ # both the legacy backlog and any future bulk-imported population.
18
+ #
19
+ # Idempotent. Re-running picks up exactly where a prior run left off because
20
+ # the gating predicate is `n.embedding IS NULL` — nodes embedded by the
21
+ # previous run are excluded from the next batch query.
22
+ #
23
+ # Loud failure (per feedback_loud_failures.md). Any Ollama HTTP failure or
24
+ # cypher-shell error aborts the script with a non-zero exit and prints a
25
+ # precise re-run instruction. Partial-state-on-abort is safe: nodes whose
26
+ # embedding was committed before the abort stay embedded; the rest fall back
27
+ # into the next run's batch.
28
+ #
29
+ # Concurrent-run safety. flock-guarded — a second concurrent invocation
30
+ # exits immediately with a clear message, no work attempted. Protects
31
+ # against operator double-clicks and against the installer running it
32
+ # while a manual run is in flight.
33
+ #
34
+ # Usage. Stand-alone re-run: `bash platform/scripts/embed-backfill.sh`.
35
+ # Installer-driven: invoked automatically post-`seed-neo4j.sh` on every
36
+ # install (the no-op fast path returns in milliseconds when nothing is
37
+ # pending, so re-running on every install is harmless).
38
+ # ============================================================
39
+
40
+ set -euo pipefail
41
+
42
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
43
+ PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
44
+
45
+ NEO4J_URI="${NEO4J_URI:-bolt://localhost:7687}"
46
+ NEO4J_USER="${NEO4J_USER:-neo4j}"
47
+ OLLAMA_URL="${OLLAMA_URL:-http://localhost:11434}"
48
+ EMBED_MODEL="${EMBED_MODEL:-nomic-embed-text}"
49
+ BATCH_SIZE="${EMBED_BACKFILL_BATCH_SIZE:-50}"
50
+
51
+ # Lock file is brand-scoped via the install directory hash so concurrent
52
+ # Maxy + Real Agent installs (or any two brand installs sharing the device)
53
+ # do not block each other unnecessarily — they target separate Neo4j
54
+ # instances under separate INSTALL_DIRs and have zero shared state. The
55
+ # explicit env var override stays for operator-driven workflows.
56
+ INSTALL_DIR_HASH="$(echo -n "$PROJECT_DIR" | shasum | cut -c1-12)"
57
+ LOCK_FILE="${EMBED_BACKFILL_LOCK_FILE:-/tmp/maxy-embed-backfill-${INSTALL_DIR_HASH}.lock}"
58
+
59
+ # Resolve Neo4j password the same way seed-neo4j.sh does. Explicit env var
60
+ # takes precedence so the installer can pass it through without writing the
61
+ # file twice.
62
+ NEO4J_PASSWORD_FILE="$PROJECT_DIR/config/.neo4j-password"
63
+ if [ -z "${NEO4J_PASSWORD:-}" ]; then
64
+ if [ -f "$NEO4J_PASSWORD_FILE" ]; then
65
+ NEO4J_PASSWORD=$(cat "$NEO4J_PASSWORD_FILE")
66
+ else
67
+ echo "[embed-backfill] FAILED: NEO4J_PASSWORD env var unset and $NEO4J_PASSWORD_FILE missing"
68
+ echo "[embed-backfill] re-run after the seed step writes the password file, or set NEO4J_PASSWORD explicitly"
69
+ exit 1
70
+ fi
71
+ fi
72
+ export NEO4J_URI NEO4J_USER NEO4J_PASSWORD OLLAMA_URL EMBED_MODEL BATCH_SIZE
73
+
74
+ if ! command -v cypher-shell >/dev/null 2>&1; then
75
+ echo "[embed-backfill] FAILED: cypher-shell not on PATH; install Neo4j or add cypher-shell to PATH"
76
+ exit 1
77
+ fi
78
+ if ! command -v python3 >/dev/null 2>&1; then
79
+ echo "[embed-backfill] FAILED: python3 not on PATH; the installer requires it"
80
+ exit 1
81
+ fi
82
+
83
+ # flock guard — second concurrent invocation exits cleanly. The exec on
84
+ # fd 200 keeps the lock held for the lifetime of this process; flock -n
85
+ # is non-blocking so a busy lock returns immediately rather than queueing.
86
+ exec 200>"$LOCK_FILE"
87
+ if ! flock -n 200; then
88
+ echo "[embed-backfill] another instance is already running (lock=$LOCK_FILE), skipping"
89
+ exit 0
90
+ fi
91
+
92
+ # The python heredoc owns the per-batch loop. It uses subprocess to call
93
+ # cypher-shell (avoids re-implementing Bolt) and urllib to call Ollama
94
+ # (no extra deps). cypher-shell `--format plain` returns CSV; the csv
95
+ # module handles quoting/escaping reliably so node text containing commas,
96
+ # quotes, or newlines round-trips correctly.
97
+ #
98
+ # Cypher contract:
99
+ # READ: one row per unembedded node — { id: elementId, text: coalesced }
100
+ # gated by `n.embedding IS NULL` AND `any(label IN labels(n)
101
+ # WHERE label IN $registered)` AND a non-empty coalesce of the
102
+ # text property union. Nodes carrying an :Trashed label are
103
+ # excluded explicitly. READ params (`registered` list of strings,
104
+ # `batchSize` int) are passed via cypher-shell `--param` as plain
105
+ # Cypher expressions (string list literals + integer literal).
106
+ # WRITE: one batched UNWIND per chunk — pairs of (id, embedding[])
107
+ # interpolated into the Cypher payload as bare-key map literals
108
+ # (`{id: '...', embedding: [...]}`). Cypher does NOT accept
109
+ # double-quoted-string map keys, so JSON-serialised values cannot
110
+ # be passed via `--param` for the WRITE side; the inline literal
111
+ # path is the apoc-free alternative.
112
+ #
113
+ # The script does NOT shell out to the existing TS embed() helper because
114
+ # that would require booting Node + the platform/lib build. Calling the
115
+ # Ollama HTTP endpoint directly preserves the same behaviour with zero
116
+ # build dependency.
117
+ exec python3 - <<'PYEOF'
118
+ import json
119
+ import os
120
+ import sys
121
+ import time
122
+ import urllib.error
123
+ import urllib.request
124
+ from subprocess import PIPE, Popen
125
+ from io import StringIO
126
+ import csv
127
+
128
+ NEO4J_URI = os.environ["NEO4J_URI"]
129
+ NEO4J_USER = os.environ["NEO4J_USER"]
130
+ NEO4J_PASSWORD = os.environ["NEO4J_PASSWORD"]
131
+ OLLAMA_URL = os.environ["OLLAMA_URL"]
132
+ EMBED_MODEL = os.environ["EMBED_MODEL"]
133
+ BATCH_SIZE = int(os.environ["BATCH_SIZE"])
134
+
135
+ # Mirrors the FOR (n:...) clause of `entity_search` in schema.cypher.
136
+ # Doctrine: every label written by the platform is searchable AND embeddable.
137
+ # Future label additions must extend BOTH this list and schema.cypher; the
138
+ # fulltext-coverage doctrine test catches the schema half but not this list.
139
+ REGISTERED_LABELS = [
140
+ "LocalBusiness", "Service", "PriceSpecification", "OpeningHoursSpecification", "Organization",
141
+ "Person", "UserProfile", "Preference", "AdminUser", "AccessGrant",
142
+ "KnowledgeDocument", "Section", "Chunk", "DigitalDocument", "CreativeWork",
143
+ "Question", "FAQPage", "DefinedTerm", "Review", "ImageObject",
144
+ "Conversation", "AdminConversation", "PublicConversation", "Message",
145
+ "UserMessage", "AssistantMessage", "ToolCall",
146
+ "Task", "Project", "Event",
147
+ "Workflow", "WorkflowStep", "WorkflowRun", "StepResult",
148
+ "OnboardingState", "Email", "EmailAccount", "ReviewAlert",
149
+ "Position", "Credential",
150
+ ]
151
+
152
+ # Properties to coalesce for the embedding text. Ordered: most identifying
153
+ # property first. Matches the canonical text-property list pinned by the
154
+ # fulltext-coverage doctrine test.
155
+ EMBED_TEXT_PROPS = ["name", "title", "summary", "headline", "body", "content", "text"]
156
+
157
+
158
+ def cypher(query: str, params: dict | None = None) -> str:
159
+ """Run a Cypher statement via cypher-shell --format plain.
160
+ Returns stdout as a single string. Aborts the script on non-zero exit
161
+ so a Cypher syntax error or a Neo4j outage surfaces immediately."""
162
+ cmd = [
163
+ "cypher-shell", "-u", NEO4J_USER, "-p", NEO4J_PASSWORD, "-a", NEO4J_URI,
164
+ "--format", "plain",
165
+ ]
166
+ if params:
167
+ for key, value in params.items():
168
+ cmd.extend(["--param", f"{key} => {json.dumps(value)}"])
169
+ proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
170
+ out, err = proc.communicate(query.encode("utf-8"))
171
+ if proc.returncode != 0:
172
+ sys.stderr.write(f"[embed-backfill] FAILED: cypher-shell exited {proc.returncode}\n")
173
+ sys.stderr.write(err.decode("utf-8", errors="replace"))
174
+ sys.exit(1)
175
+ return out.decode("utf-8", errors="replace")
176
+
177
+
178
+ def parse_csv_rows(stdout: str) -> list[dict]:
179
+ """cypher-shell --format plain emits a CSV header + rows. The csv module
180
+ handles quoting reliably even when text contains commas/quotes/newlines."""
181
+ if not stdout.strip():
182
+ return []
183
+ reader = csv.DictReader(StringIO(stdout))
184
+ return list(reader)
185
+
186
+
187
+ def ollama_embed(text: str, *, timeout: int = 30, retry_on_timeout: bool = True) -> list[float]:
188
+ """POST text to Ollama /api/embed.
189
+
190
+ Cold-start tolerance: when nomic-embed-text is not yet loaded into Ollama's
191
+ process memory, the first request for the model after a fresh boot can
192
+ exceed 30s while the model loads. Subsequent requests are fast. We retry
193
+ ONCE on TimeoutError with a longer (180s) timeout so a cold model load
194
+ does not abort the entire backfill at the first node. Retry is OFF by
195
+ default for the warmup probe to avoid recursion.
196
+
197
+ Aborts the script (non-zero exit) on any non-recoverable HTTP failure
198
+ with a precise message + re-run instruction so the operator never thinks
199
+ the backfill silently completed.
200
+ """
201
+ body = json.dumps({"model": EMBED_MODEL, "input": text}).encode("utf-8")
202
+ req = urllib.request.Request(
203
+ f"{OLLAMA_URL}/api/embed",
204
+ data=body,
205
+ headers={"Content-Type": "application/json"},
206
+ method="POST",
207
+ )
208
+ try:
209
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
210
+ payload = json.loads(resp.read().decode("utf-8"))
211
+ except TimeoutError as e:
212
+ if retry_on_timeout:
213
+ sys.stderr.write(
214
+ f"[embed-backfill] WARN: Ollama timeout after {timeout}s — likely cold-start; retrying with 180s timeout\n"
215
+ )
216
+ return ollama_embed(text, timeout=180, retry_on_timeout=False)
217
+ sys.stderr.write(f"[embed-backfill] FAILED: Ollama timeout after {timeout}s ({e})\n")
218
+ sys.stderr.write(
219
+ f"[embed-backfill] re-run via: bash {os.path.dirname(os.path.realpath(__file__))}/embed-backfill.sh\n"
220
+ )
221
+ sys.exit(1)
222
+ except (urllib.error.URLError, urllib.error.HTTPError) as e:
223
+ sys.stderr.write(f"[embed-backfill] FAILED: Ollama unreachable ({e})\n")
224
+ sys.stderr.write(
225
+ f"[embed-backfill] re-run via: bash {os.path.dirname(os.path.realpath(__file__))}/embed-backfill.sh\n"
226
+ )
227
+ sys.exit(1)
228
+ embeddings = payload.get("embeddings", [])
229
+ if not embeddings or not embeddings[0]:
230
+ sys.stderr.write(f"[embed-backfill] FAILED: Ollama returned no embedding for text length={len(text)}\n")
231
+ sys.exit(1)
232
+ return embeddings[0]
233
+
234
+
235
+ def cypher_string_literal(s: str) -> str:
236
+ """Format a Python string as a Cypher single-quoted string literal.
237
+
238
+ Escapes the two characters Cypher requires escaping inside single-quoted
239
+ strings: backslash and single quote. elementId values from Neo4j 5 are
240
+ typically `<dbprefix>:<uuid>:<recordId>` (alphanumeric + colon + dash) and
241
+ will not normally contain either, but escape defensively so a future
242
+ elementId format change cannot break the WRITE batch with a syntax error.
243
+ """
244
+ return "'" + s.replace("\\", "\\\\").replace("'", "\\'") + "'"
245
+
246
+
247
+ def cypher_float_list(values: list[float]) -> str:
248
+ """Format a list of floats as a Cypher list literal `[v1, v2, ...]`.
249
+
250
+ repr() on a Python float emits a decimal that Cypher accepts as a number
251
+ literal — including the negative sign, scientific notation, and infinity
252
+ edge cases. nomic-embed-text returns finite cosine-bounded floats so
253
+ inf/nan are not expected, but Python's repr is stable for any case that
254
+ does occur.
255
+ """
256
+ return "[" + ",".join(repr(v) for v in values) + "]"
257
+
258
+
259
+ # Build the WHERE clause once. The $registered parameter is interpolated
260
+ # into Cypher as a list literal; cypher-shell --param gives us a typed pass.
261
+ COALESCE_TEXT = "coalesce(" + ", ".join(f"n.{p}" for p in EMBED_TEXT_PROPS) + ", '')"
262
+ COUNT_QUERY = f"""
263
+ MATCH (n) WHERE n.embedding IS NULL
264
+ AND NOT n:Trashed
265
+ AND any(label IN labels(n) WHERE label IN $registered)
266
+ AND {COALESCE_TEXT} <> ''
267
+ RETURN count(n) AS remaining;
268
+ """
269
+ BATCH_QUERY = f"""
270
+ MATCH (n) WHERE n.embedding IS NULL
271
+ AND NOT n:Trashed
272
+ AND any(label IN labels(n) WHERE label IN $registered)
273
+ AND {COALESCE_TEXT} <> ''
274
+ RETURN elementId(n) AS id,
275
+ labels(n)[0] AS firstLabel,
276
+ {COALESCE_TEXT} AS text
277
+ LIMIT $batchSize;
278
+ """
279
+
280
+ count_out = cypher(COUNT_QUERY, {"registered": REGISTERED_LABELS})
281
+ total_remaining = 0
282
+ for row in parse_csv_rows(count_out):
283
+ total_remaining = int(row["remaining"])
284
+
285
+ print(f"[embed-backfill] start total={total_remaining} model={EMBED_MODEL}")
286
+
287
+ if total_remaining == 0:
288
+ print("[embed-backfill] done remaining=0 (nothing to backfill)")
289
+ sys.exit(0)
290
+
291
+ # Pre-warm Ollama so the first per-node call doesn't pay the model-load
292
+ # latency. The cold-start window for nomic-embed-text on a Pi 5 can exceed
293
+ # 30s; calling once with a tiny throwaway input loads the weights into
294
+ # memory before the loop begins. Failure here is treated identically to
295
+ # any other Ollama failure — loud abort with re-run instruction.
296
+ print(f"[embed-backfill] pre-warm model={EMBED_MODEL} timeout=180s")
297
+ ollama_embed("warmup", timeout=180, retry_on_timeout=False)
298
+
299
+ processed_total = 0
300
+ batch_index = 0
301
+ while True:
302
+ batch_start = time.time()
303
+ batch_out = cypher(
304
+ BATCH_QUERY,
305
+ {"registered": REGISTERED_LABELS, "batchSize": BATCH_SIZE},
306
+ )
307
+ rows = parse_csv_rows(batch_out)
308
+ if not rows:
309
+ break
310
+
311
+ # Compute embeddings serially. Ollama on a Pi 5 handles ~3-10 embeds
312
+ # per second with nomic-embed-text; concurrent requests just queue
313
+ # behind the GPU/CPU bottleneck so parallelism wouldn't help.
314
+ pairs: list[tuple[str, list[float]]] = []
315
+ label_counts: dict[str, int] = {}
316
+ for row in rows:
317
+ node_id = row["id"]
318
+ text = row["text"]
319
+ first_label = row["firstLabel"]
320
+ if not text:
321
+ continue
322
+ embedding = ollama_embed(text)
323
+ pairs.append((node_id, embedding))
324
+ label_counts[first_label] = label_counts.get(first_label, 0) + 1
325
+
326
+ if not pairs:
327
+ # Defensive: query said rows exist but all text was empty after
328
+ # the python read — means the COALESCE_TEXT predicate is wider
329
+ # than the python check. Stop to avoid an infinite loop.
330
+ sys.stderr.write("[embed-backfill] WARN: batch returned rows with empty text — stopping to avoid infinite loop\n")
331
+ break
332
+
333
+ # Build the WRITE batch as a Cypher literal payload rather than a
334
+ # `--param` map. cypher-shell's `--param` parses the value as a Cypher
335
+ # expression, and Cypher map keys must be bare identifiers (or backtick-
336
+ # quoted) — NOT double-quoted strings as JSON would emit. Interpolating
337
+ # bare-key map literals directly avoids the question entirely:
338
+ #
339
+ # UNWIND [{id: '4:abc:1', embedding: [0.1, 0.2, ...]}, ...] AS pair
340
+ # MATCH (n) WHERE elementId(n) = pair.id
341
+ # SET n.embedding = pair.embedding;
342
+ #
343
+ # cypher_string_literal escapes any backslash/quote in elementIds
344
+ # defensively; cypher_float_list serialises the embedding via repr()
345
+ # which Cypher accepts as a number literal.
346
+ pair_literals = ",".join(
347
+ f"{{id: {cypher_string_literal(node_id)}, embedding: {cypher_float_list(embedding)}}}"
348
+ for node_id, embedding in pairs
349
+ )
350
+ cypher(
351
+ f"""
352
+ UNWIND [{pair_literals}] AS pair
353
+ MATCH (n) WHERE elementId(n) = pair.id
354
+ SET n.embedding = pair.embedding;
355
+ """
356
+ )
357
+ elapsed_ms = int((time.time() - batch_start) * 1000)
358
+ batch_index += 1
359
+ processed_total += len(pairs)
360
+ label_summary = ", ".join(f"{k}={v}" for k, v in sorted(label_counts.items()))
361
+ print(f"[embed-backfill] batch={batch_index} processed={len(pairs)} elapsed-ms={elapsed_ms} labels={label_summary}")
362
+
363
+ # Final remaining check — should be zero or the diff between original
364
+ # total and processed_total (e.g. if new writes landed mid-run).
365
+ final_out = cypher(COUNT_QUERY, {"registered": REGISTERED_LABELS})
366
+ final_remaining = 0
367
+ for row in parse_csv_rows(final_out):
368
+ final_remaining = int(row["remaining"])
369
+ print(f"[embed-backfill] done processed={processed_total} remaining={final_remaining}")
370
+ PYEOF
@@ -411,13 +411,19 @@ fi
411
411
 
412
412
  echo "==> Connecting to Neo4j at $NEO4J_URI as $NEO4J_USER"
413
413
 
414
- # Migration: drop single-key UserProfile constraint (replaced by composite
415
- # (accountId, userId) in Task 249). Also drop the old preference_category
416
- # index replaced by (accountId, userId, category) composite.
417
- echo "==> Migrating schema: dropping single-key UserProfile constraint..."
414
+ # Schema migrations run before the main schema apply so renames don't collide
415
+ # with the new declarations. Each statement is idempotent (`IF EXISTS`):
416
+ # - Task 249: `user_profile_account_unique` replaced by composite (accountId, userId).
417
+ # - Task 249: `preference_category` index replaced by (accountId, userId, category).
418
+ # - Task 748: `knowledge_fulltext` (3 labels) replaced by `entity_search` (~40 labels)
419
+ # with the universal label/property union. The new index is created by the
420
+ # schema apply below; dropping the old name here is what lets cypher-shell
421
+ # run both in one pass without conflict.
422
+ echo "==> Migrating schema: dropping renamed/obsolete constraints + indexes..."
418
423
  "$CYPHER_SHELL" -u "$NEO4J_USER" -p "$NEO4J_PASSWORD" -a "$NEO4J_URI" << 'MIGRATE_EOF'
419
424
  DROP CONSTRAINT user_profile_account_unique IF EXISTS;
420
425
  DROP INDEX preference_category IF EXISTS;
426
+ DROP INDEX knowledge_fulltext IF EXISTS;
421
427
  MIGRATE_EOF
422
428
 
423
429
  # Vector index dimensions — configurable at install time via --embed-model.
@@ -10127,7 +10127,7 @@ var import_dist = __toESM(require_dist());
10127
10127
  import { int } from "neo4j-driver";
10128
10128
  var VECTOR_WEIGHT = 0.7;
10129
10129
  var BM25_WEIGHT = 0.3;
10130
- var FULLTEXT_INDEX_NAME = "knowledge_fulltext";
10130
+ var FULLTEXT_INDEX_NAME = "entity_search";
10131
10131
  function escapeLucene(query) {
10132
10132
  return query.replace(/[+\-&|!(){}[\]^"~*?:\\/]/g, "\\$&");
10133
10133
  }