@rubytech/create-realagent 1.0.715 → 1.0.717

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 (29) hide show
  1. package/dist/index.js +33 -0
  2. package/package.json +1 -1
  3. package/payload/platform/lib/graph-search/dist/index.d.ts +27 -11
  4. package/payload/platform/lib/graph-search/dist/index.d.ts.map +1 -1
  5. package/payload/platform/lib/graph-search/dist/index.js +29 -13
  6. package/payload/platform/lib/graph-search/dist/index.js.map +1 -1
  7. package/payload/platform/lib/graph-search/src/__tests__/fulltext-coverage.test.ts +272 -0
  8. package/payload/platform/lib/graph-search/src/index.ts +27 -12
  9. package/payload/platform/neo4j/schema.cypher +57 -7
  10. package/payload/platform/plugins/docs/references/graph.md +8 -0
  11. package/payload/platform/plugins/docs/references/internals.md +4 -4
  12. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/connections.md +3 -1
  13. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-archive-write.test.d.ts +2 -0
  14. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-archive-write.test.d.ts.map +1 -0
  15. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-archive-write.test.js +51 -0
  16. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-archive-write.test.js.map +1 -0
  17. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts +2 -0
  18. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts.map +1 -1
  19. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js +17 -5
  20. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js.map +1 -1
  21. package/payload/platform/plugins/memory/mcp/package.json +4 -2
  22. package/payload/platform/plugins/memory/mcp/vitest.config.ts +15 -0
  23. package/payload/platform/plugins/memory/references/graph-primitives.md +9 -3
  24. package/payload/platform/scripts/embed-backfill.sh +370 -0
  25. package/payload/platform/scripts/seed-neo4j.sh +10 -4
  26. package/payload/server/public/assets/graph-qKc9cr2K.js +50 -0
  27. package/payload/server/public/graph.html +1 -1
  28. package/payload/server/server.js +57 -2
  29. package/payload/server/public/assets/graph-CkcuUDtA.js +0 -50
@@ -258,13 +258,63 @@ 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
+ // currentTitle (Task 753 — denormalised from [:WORKS_FOR].title for
295
+ // LinkedIn imports, since edge properties are invisible to a
296
+ // node-only fulltext index. `currentTitle` (not `title`) avoids
297
+ // shadowing :Position.title semantics in cross-label cypher.)
298
+ // - Email: subject, bodyPreview, fromName, fromAddress
299
+ // - EmailAccount: agentAddress
300
+ // - Email: screeningReason
301
+ // - Credential: authority
302
+ // - AccessGrant: contactValue
303
+ // - ToolCall: toolName
304
+ CREATE FULLTEXT INDEX entity_search IF NOT EXISTS
305
+ FOR (n:LocalBusiness|Service|PriceSpecification|OpeningHoursSpecification|Organization
306
+ |Person|UserProfile|Preference|AdminUser|AccessGrant
307
+ |KnowledgeDocument|Section|Chunk|DigitalDocument|CreativeWork|Question|FAQPage|DefinedTerm|Review|ImageObject
308
+ |Conversation|AdminConversation|PublicConversation|Message|UserMessage|AssistantMessage|ToolCall
309
+ |Task|Project|Event
310
+ |Workflow|WorkflowStep|WorkflowRun|StepResult
311
+ |OnboardingState|Email|EmailAccount|ReviewAlert
312
+ |Position|Credential)
313
+ ON EACH [n.name, n.firstName, n.lastName, n.givenName, n.familyName,
314
+ n.title, n.currentTitle, n.summary, n.body, n.content, n.text, n.description, n.headline, n.abstract,
315
+ n.email, n.note, n.label, n.value, n.message, n.preview, n.tagline,
316
+ n.subject, n.bodyPreview, n.fromName, n.fromAddress, n.agentAddress, n.screeningReason,
317
+ n.authority, n.contactValue, n.toolName];
268
318
 
269
319
  // Project node (Task 740) — a standalone creative-output node distinct from
270
320
  // :Section. Anchored via (:UserProfile)-[:CREATED]->(:Project), with optional
@@ -4,6 +4,14 @@ The **Graph** admin page (`/graph`) renders a force-directed view of your
4
4
  account's Neo4j subgraph. Labels on the canvas follow the zoom level, so you
5
5
  see the most useful identity at every scale.
6
6
 
7
+ ## Search and pivot
8
+
9
+ Type a term in the search box to highlight matching nodes on the canvas. Hits get an amber border so you can pick them out of a busy view. Click any highlighted node to open its side panel and pivot into its neighbourhood — both clicks (hit and non-hit) behave identically.
10
+
11
+ When a search is active and you click a node, the neighbourhood you pivot into is **narrowed to the search-relevant subset**. For example: searching "david" with 175 matches and clicking yourself returns the Davids you're connected to, not your entire LinkedIn graph. The narrowing applies once per pivot — clearing the search and pivoting again returns the full neighbourhood.
12
+
13
+ Searches reach **every textual property** of every operator-meaningful label, including denormalised fields the platform writes specifically so search can reach them — for example, the current job title of each LinkedIn connection (originally stored on the connection edge, copied to the Person node so the fulltext index can match it).
14
+
7
15
  ## Conversation label tiers
8
16
 
9
17
  Conversation nodes carry the most operator-meaningful identity in the
@@ -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
 
@@ -28,9 +28,11 @@ The real column header is **line 4**. Either skip the first three lines before p
28
28
  | URL | `Person.linkedinUrl` (natural key) |
29
29
  | Email Address | `Person.email` (only when non-empty) |
30
30
  | Company | `Organization.name` |
31
- | Position | `[:WORKS_FOR].title` |
31
+ | Position | `[:WORKS_FOR].title` AND `Person.currentTitle` (denormalised — Task 753) |
32
32
  | Connected On | `[:CONNECTED_ON_LINKEDIN].connectedOn` (ISO 8601) |
33
33
 
34
+ The `Position` column is written **twice**: once on the `[:WORKS_FOR]` edge as the source of truth (preserves the per-employer history when a connection later moves), and again as `Person.currentTitle` so Neo4j's `entity_search` BM25 index reaches it. Edge properties are invisible to a node-only fulltext index, so without the denormalisation an operator searching "director" against 5K connections returns near-zero hits regardless of how many directors are in the graph. The SET is unconditional — re-importing a row with an empty Position clears `currentTitle`, matching the LinkedIn export's "current snapshot" semantics. To filter your import to a specific role before writing, use the [Selective-ingest threshold](#selective-ingest-threshold) gate.
35
+
34
36
  LinkedIn only emits email for connections who opted in, so most rows have a blank email. The MCP tool writes `email` only when non-empty — avoids colliding with `person_email_unique` on empty strings.
35
37
 
36
38
  ## Natural keys
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=memory-archive-write.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-archive-write.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/memory-archive-write.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Task 753 acceptance gate — `p.currentTitle` denormalisation for LinkedIn
3
+ * connections.
4
+ *
5
+ * Pre-fix: LinkedIn import wrote the connection's current job title to the
6
+ * `[:WORKS_FOR].title` edge property. Neo4j's `entity_search` BM25 index is
7
+ * node-only, so an operator searching "director" against an account with
8
+ * 5,099 LinkedIn connections returned 26 hits — name-field anomalies and
9
+ * CV-driven Position nodes. The data was correctly stored; it was
10
+ * unreachable by the index.
11
+ *
12
+ * Post-fix: the same SET clause that writes `p.givenName`, `p.familyName`,
13
+ * and `p.name` also writes `p.currentTitle = row.title`. The schema's
14
+ * `entity_search ON EACH` list carries `n.currentTitle`, so a fresh import
15
+ * (or the one-shot backfill cypher in `.docs/neo4j.md`) closes the gap.
16
+ *
17
+ * This test is a string-content gate against the cypher constant — no live
18
+ * Neo4j needed. The doctrine that matters: the SET clause writes
19
+ * `p.currentTitle = row.title` unconditionally inside the existing SET
20
+ * (not inside a FOREACH-with-non-empty-guard, because the LinkedIn export
21
+ * is a current-job snapshot — re-import without a title is authoritative).
22
+ */
23
+ import { describe, it, expect } from "vitest";
24
+ import { PERSON_AND_CONNECTED_EDGE_CYPHER, ORG_AND_WORKS_FOR_CYPHER, } from "../memory-archive-write.js";
25
+ describe("memory-archive-write — Task 753 currentTitle denormalisation", () => {
26
+ it("PERSON_AND_CONNECTED_EDGE_CYPHER writes p.currentTitle = row.title in the SET clause", () => {
27
+ expect(PERSON_AND_CONNECTED_EDGE_CYPHER).toMatch(/p\.currentTitle\s*=\s*row\.title/);
28
+ });
29
+ it("p.currentTitle write is unconditional (in the main SET, not inside a FOREACH non-empty guard)", () => {
30
+ // Locate the bounds of the unconditional SET block: from the first
31
+ // `SET\n` after the MERGE-with-ON-CREATE-SET to the next `FOREACH`
32
+ // (which is the email guard, the first conditional block).
33
+ const setBlockMatch = PERSON_AND_CONNECTED_EDGE_CYPHER.match(/SET\n([\s\S]+?)FOREACH/);
34
+ expect(setBlockMatch).not.toBeNull();
35
+ const setBlock = setBlockMatch[1];
36
+ expect(setBlock).toMatch(/p\.currentTitle\s*=\s*row\.title/);
37
+ });
38
+ it("ORG_AND_WORKS_FOR_CYPHER still writes title to the WORKS_FOR edge — denormalisation is additive, not a move", () => {
39
+ // Edge-property write retained: the Person-side denormalisation gives
40
+ // BM25 reach, but the edge stays the source of truth so cypher queries
41
+ // that walk WORKS_FOR keep working.
42
+ expect(ORG_AND_WORKS_FOR_CYPHER).toMatch(/w\.title\s*=\s*row\.title/);
43
+ });
44
+ it("p.currentTitle is named distinctly from generic n.title (avoids :Position.title shadowing)", () => {
45
+ // The brief calls this out: `p.title` would collide with :Position.title
46
+ // semantics in cross-label cypher — separate node types with separate
47
+ // meanings. `currentTitle` makes the intent explicit.
48
+ expect(PERSON_AND_CONNECTED_EDGE_CYPHER).not.toMatch(/p\.title\s*=\s*row\.title/);
49
+ });
50
+ });
51
+ //# sourceMappingURL=memory-archive-write.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-archive-write.test.js","sourceRoot":"","sources":["../../../src/tools/__tests__/memory-archive-write.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EACL,gCAAgC,EAChC,wBAAwB,GACzB,MAAM,4BAA4B,CAAC;AAEpC,QAAQ,CAAC,8DAA8D,EAAE,GAAG,EAAE;IAC5E,EAAE,CAAC,sFAAsF,EAAE,GAAG,EAAE;QAC9F,MAAM,CAAC,gCAAgC,CAAC,CAAC,OAAO,CAAC,kCAAkC,CAAC,CAAC;IACvF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+FAA+F,EAAE,GAAG,EAAE;QACvG,mEAAmE;QACnE,mEAAmE;QACnE,2DAA2D;QAC3D,MAAM,aAAa,GAAG,gCAAgC,CAAC,KAAK,CAAC,wBAAwB,CAAC,CAAC;QACvF,MAAM,CAAC,aAAa,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;QACrC,MAAM,QAAQ,GAAG,aAAc,CAAC,CAAC,CAAC,CAAC;QACnC,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,kCAAkC,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6GAA6G,EAAE,GAAG,EAAE;QACrH,sEAAsE;QACtE,uEAAuE;QACvE,oCAAoC;QACpC,MAAM,CAAC,wBAAwB,CAAC,CAAC,OAAO,CAAC,2BAA2B,CAAC,CAAC;IACxE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4FAA4F,EAAE,GAAG,EAAE;QACpG,yEAAyE;QACzE,sEAAsE;QACtE,sDAAsD;QACtD,MAAM,CAAC,gCAAgC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,2BAA2B,CAAC,CAAC;IACpF,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -30,4 +30,6 @@ export interface ArchiveWriteResult {
30
30
  }>;
31
31
  }
32
32
  export declare function memoryArchiveWrite(params: ArchiveWriteParams): Promise<ArchiveWriteResult>;
33
+ export declare const PERSON_AND_CONNECTED_EDGE_CYPHER = "\nMATCH (owner) WHERE elementId(owner) = $ownerNodeId\nWITH owner, $rows AS rows\nUNWIND rows AS row\nMERGE (p:Person {linkedinUrl: row.linkedinUrl})\n ON CREATE SET\n p.accountId = $accountId,\n p.source = 'linkedin',\n p.createdByAgent = 'linkedin-import',\n p.createdBySource = 'linkedin-import',\n p.createdBySession = $sessionId,\n p.createdAt = datetime(),\n p.scope = 'admin'\nSET\n p.givenName = row.givenName,\n p.familyName = row.familyName,\n p.name = trim(coalesce(row.givenName,'') + ' ' + coalesce(row.familyName,'')),\n p.currentTitle = row.title\nFOREACH (_ IN CASE WHEN row.email IS NOT NULL AND row.email <> '' THEN [1] ELSE [] END |\n SET p.email = row.email\n)\nMERGE (owner)-[c:CONNECTED_ON_LINKEDIN]->(p)\n ON CREATE SET\n c.connectedOn = date(row.connectedOn),\n c.source = 'linkedin',\n c.createdAt = datetime()\nRETURN count(*) AS processed\n";
34
+ export declare const ORG_AND_WORKS_FOR_CYPHER = "\nUNWIND $rows AS row\nWITH row WHERE row.company IS NOT NULL AND row.company <> ''\nMATCH (p:Person {linkedinUrl: row.linkedinUrl})\nMERGE (o:Organization {accountId: $accountId, name: row.company})\n ON CREATE SET\n o.source = 'linkedin',\n o.createdByAgent = 'linkedin-import',\n o.createdBySource = 'linkedin-import',\n o.createdBySession = $sessionId,\n o.createdAt = datetime(),\n o.scope = 'admin'\nMERGE (p)-[w:WORKS_FOR]->(o)\n ON CREATE SET\n w.title = row.title,\n w.source = 'linkedin',\n w.current = true,\n w.createdAt = datetime()\n ON MATCH SET\n w.title = coalesce(row.title, w.title)\nRETURN count(*) AS processed\n";
33
35
  //# sourceMappingURL=memory-archive-write.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"memory-archive-write.d.ts","sourceRoot":"","sources":["../../src/tools/memory-archive-write.ts"],"names":[],"mappings":"AA6BA,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,2EAA2E;IAC3E,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,UAAU,GAAG,qBAAqB,CAAC;AAE/C,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,UAAU,EAAE,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrD;AAwBD,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,kBAAkB,CAAC,CA0E7B"}
1
+ {"version":3,"file":"memory-archive-write.d.ts","sourceRoot":"","sources":["../../src/tools/memory-archive-write.ts"],"names":[],"mappings":"AA6BA,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,2EAA2E;IAC3E,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,UAAU,GAAG,qBAAqB,CAAC;AAE/C,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,UAAU,EAAE,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrD;AAwBD,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,kBAAkB,CAAC,CA0E7B;AAqCD,eAAO,MAAM,gCAAgC,+8BA2B5C,CAAC;AAEF,eAAO,MAAM,wBAAwB,8sBAqBpC,CAAC"}
@@ -112,8 +112,19 @@ export async function memoryArchiveWrite(params) {
112
112
  //
113
113
  // We split each batch into two tx.run calls inside one executeWrite so each
114
114
  // statement's summary counters give a clean per-label breakdown.
115
+ //
116
+ // Task 753 — `p.currentTitle` denormalises `[:WORKS_FOR].title` onto the
117
+ // Person node so the `entity_search` BM25 fulltext index reaches it.
118
+ // Pre-task an operator searching "director" against an account with 5K
119
+ // LinkedIn connections returned 26 hits — name-field anomalies and
120
+ // CV-driven Position nodes only — because edge properties are invisible
121
+ // to a node-only fulltext index. Naming `currentTitle` (not `title`)
122
+ // avoids shadowing :Position.title semantics in cross-label cypher.
123
+ // Empty `row.title` writes null (the property is removed); the LinkedIn
124
+ // export is a current-job snapshot, so a re-import without a title is
125
+ // authoritative.
115
126
  // ---------------------------------------------------------------------------
116
- const PERSON_AND_CONNECTED_EDGE_CYPHER = `
127
+ export const PERSON_AND_CONNECTED_EDGE_CYPHER = `
117
128
  MATCH (owner) WHERE elementId(owner) = $ownerNodeId
118
129
  WITH owner, $rows AS rows
119
130
  UNWIND rows AS row
@@ -127,9 +138,10 @@ MERGE (p:Person {linkedinUrl: row.linkedinUrl})
127
138
  p.createdAt = datetime(),
128
139
  p.scope = 'admin'
129
140
  SET
130
- p.givenName = row.givenName,
131
- p.familyName= row.familyName,
132
- p.name = trim(coalesce(row.givenName,'') + ' ' + coalesce(row.familyName,''))
141
+ p.givenName = row.givenName,
142
+ p.familyName = row.familyName,
143
+ p.name = trim(coalesce(row.givenName,'') + ' ' + coalesce(row.familyName,'')),
144
+ p.currentTitle = row.title
133
145
  FOREACH (_ IN CASE WHEN row.email IS NOT NULL AND row.email <> '' THEN [1] ELSE [] END |
134
146
  SET p.email = row.email
135
147
  )
@@ -140,7 +152,7 @@ MERGE (owner)-[c:CONNECTED_ON_LINKEDIN]->(p)
140
152
  c.createdAt = datetime()
141
153
  RETURN count(*) AS processed
142
154
  `;
143
- const ORG_AND_WORKS_FOR_CYPHER = `
155
+ export const ORG_AND_WORKS_FOR_CYPHER = `
144
156
  UNWIND $rows AS row
145
157
  WITH row WHERE row.company IS NOT NULL AND row.company <> ''
146
158
  MATCH (p:Person {linkedinUrl: row.linkedinUrl})
@@ -1 +1 @@
1
- {"version":3,"file":"memory-archive-write.js","sourceRoot":"","sources":["../../src/tools/memory-archive-write.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"memory-archive-write.js","sourceRoot":"","sources":["../../src/tools/memory-archive-write.ts"],"names":[],"mappings":"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"}
@@ -8,7 +8,8 @@
8
8
  "build": "tsc",
9
9
  "start": "node dist/index.js",
10
10
  "smoke": "bash ./scripts/boot-smoke.sh",
11
- "prepublish": "bash ./scripts/boot-smoke.sh"
11
+ "prepublish": "bash ./scripts/boot-smoke.sh",
12
+ "test": "vitest run"
12
13
  },
13
14
  "dependencies": {
14
15
  "@anthropic-ai/sdk": "^0.81.0",
@@ -17,6 +18,7 @@
17
18
  },
18
19
  "devDependencies": {
19
20
  "typescript": "^5.7.0",
20
- "@types/node": "^22.0.0"
21
+ "@types/node": "^22.0.0",
22
+ "vitest": "^4.1.2"
21
23
  }
22
24
  }
@@ -0,0 +1,15 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: "node",
6
+ // Task 753 — narrow to `src/tools/__tests__/` so vitest does not pick up
7
+ // pre-existing `src/lib/__tests__/` files that use Node's built-in
8
+ // `node:test` runner (schema-loader / schema-validator / live-schema-source
9
+ // tests, Task 743). Those files run under their own harness; broadening
10
+ // this glob to `src/**/__tests__/**` would surface them as parse failures
11
+ // here without porting them — out of scope for this task.
12
+ include: ["src/tools/__tests__/**/*.test.ts"],
13
+ testTimeout: 10_000,
14
+ },
15
+ });
@@ -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: