/**
 * acceptance gate — `p.currentTitle` denormalisation for LinkedIn
 * connections.
 *
 * Pre-fix: LinkedIn import wrote the connection's current job title to the
 * `[:WORKS_FOR].title` edge property. Neo4j's `entity_search` BM25 index is
 * node-only, so an operator searching "director" against an account with
 * 5,099 LinkedIn connections returned 26 hits — name-field anomalies and
 * CV-driven Position nodes. The data was correctly stored; it was
 * unreachable by the index.
 *
 * Post-fix: the same SET clause that writes `p.givenName` and `p.familyName`
 * also writes `p.currentTitle = row.title`. The schema's
 * `entity_search ON EACH` list carries `n.currentTitle`, so a fresh import
 * (or the one-shot backfill cypher in `.docs/neo4j.md`) closes the gap.
 *
 * the SET clause no longer writes `p.name`. The denormalised
 * `name = trim(givenName + ' ' + familyName)` line was removed; the
 * canonical pair is the source of truth and the rendered display name is
 * composed at read time by `display-helpers.ts`. The schema validator
 * rejects future `Person.name` writes (schema-base.md "Forbidden
 * Properties").
 *
 * This test is a string-content gate against the cypher constant — no live
 * Neo4j needed. The doctrine that matters: the SET clause writes
 * `p.currentTitle = row.title` unconditionally inside the existing SET
 * (not inside a FOREACH-with-non-empty-guard, because the LinkedIn export
 * is a current-job snapshot — re-import without a title is authoritative).
 */
import { describe, it, expect } from "vitest";
import { PERSON_AND_CONNECTED_EDGE_CYPHER, ORG_AND_WORKS_FOR_CYPHER, OBSIDIAN_PAGE_CYPHER, OBSIDIAN_TAG_CYPHER, OBSIDIAN_ATTACHMENT_CYPHER, NOTION_PAGE_CYPHER, } from "../memory-archive-write.js";
describe("memory-archive-write currentTitle denormalisation", () => {
    it("PERSON_AND_CONNECTED_EDGE_CYPHER writes p.currentTitle = row.title in the SET clause", () => {
        expect(PERSON_AND_CONNECTED_EDGE_CYPHER).toMatch(/p\.currentTitle\s*=\s*row\.title/);
    });
    it("p.currentTitle write is unconditional (in the main SET, not inside a FOREACH non-empty guard)", () => {
        // Locate the bounds of the unconditional SET block: from the first
        // `SET\n` after the MERGE-with-ON-CREATE-SET to the next `FOREACH`
        // (which is the email guard, the first conditional block).
        const setBlockMatch = PERSON_AND_CONNECTED_EDGE_CYPHER.match(/SET\n([\s\S]+?)FOREACH/);
        expect(setBlockMatch).not.toBeNull();
        const setBlock = setBlockMatch[1];
        expect(setBlock).toMatch(/p\.currentTitle\s*=\s*row\.title/);
    });
    it("ORG_AND_WORKS_FOR_CYPHER still writes title to the WORKS_FOR edge — denormalisation is additive, not a move", () => {
        // Edge-property write retained: the Person-side denormalisation gives
        // BM25 reach, but the edge stays the source of truth so cypher queries
        // that walk WORKS_FOR keep working.
        expect(ORG_AND_WORKS_FOR_CYPHER).toMatch(/w\.title\s*=\s*row\.title/);
    });
    it("p.currentTitle is named distinctly from generic n.title (avoids :Position.title shadowing)", () => {
        // The brief calls this out: `p.title` would collide with :Position.title
        // semantics in cross-label cypher — separate node types with separate
        // meanings. `currentTitle` makes the intent explicit.
        expect(PERSON_AND_CONNECTED_EDGE_CYPHER).not.toMatch(/p\.title\s*=\s*row\.title/);
    });
    // regression gate: the LinkedIn-import writer must no longer
    // SET p.name. The canonical pair (givenName + familyName) is authoritative
    // and the rendered display name is composed at read time.
    it("(849) does NOT write p.name — the trim(givenName + ' ' + familyName) denormalisation is removed", () => {
        expect(PERSON_AND_CONNECTED_EDGE_CYPHER).not.toMatch(/p\.name\s*=/);
    });
    it("(849) still writes the canonical pair p.givenName and p.familyName", () => {
        // Ensures the eradication didn't take the canonical pair down with it.
        expect(PERSON_AND_CONNECTED_EDGE_CYPHER).toMatch(/p\.givenName\s*=\s*row\.givenName/);
        expect(PERSON_AND_CONNECTED_EDGE_CYPHER).toMatch(/p\.familyName\s*=\s*row\.familyName/);
    });
});
describe("memory-archive-write connectedOn stays edge-only", () => {
    it("does NOT denormalise connectedOn onto the Person (no backfill required for existing installs)", () => {
        // deliberately rejected the Task-753-style denormalisation
        // here: `connectedOn` is a date, not text, so the fulltext-reach
        // problem `currentTitle` solves doesn't apply. Writing a duplicate
        // node-side property would force a backfill cypher on every customer
        // Pi (no-admin-upgrade-path violation). Side-panel visibility is
        // solved in the UI by rendering incident-edge properties.
        expect(PERSON_AND_CONNECTED_EDGE_CYPHER).not.toMatch(/p\.linkedInConnectedOn/);
        expect(PERSON_AND_CONNECTED_EDGE_CYPHER).not.toMatch(/p\.connectedOn\s*=/);
    });
    it("retains the canonical [:CONNECTED_ON_LINKEDIN].connectedOn write — single source of truth, edge-side", () => {
        expect(PERSON_AND_CONNECTED_EDGE_CYPHER).toMatch(/c\.connectedOn\s*=\s*date\(row\.connectedOn\)/);
    });
});
// Task 636 — every vector-indexed label written by this tool stamps an
// embedding. Entities get a props embedding (row/dynamic-map param); real-bodied
// documents (Obsidian/Notion pages) get a parent summary embedding plus a
// returned doc list that drives :Section creation.
describe("memory-archive-write embeds every indexed label (Task 636)", () => {
    it("LinkedIn Person and Organization carry an embedding param", () => {
        expect(PERSON_AND_CONNECTED_EDGE_CYPHER).toMatch(/p\.embedding\s*=\s*row\.personEmbedding/);
        expect(ORG_AND_WORKS_FOR_CYPHER).toMatch(/o\.embedding\s*=\s*row\.orgEmbedding/);
    });
    it("Obsidian DefinedTerm and DigitalDocument carry a dynamic-map embedding", () => {
        expect(OBSIDIAN_TAG_CYPHER).toMatch(/t\.embedding\s*=\s*\$tagEmbeddings\[tagName\]/);
        expect(OBSIDIAN_ATTACHMENT_CYPHER).toMatch(/d\.embedding\s*=\s*\$attachmentEmbeddings\[att\.archivePath\]/);
    });
    it("Obsidian and Notion KnowledgeDocument set summary + parent embedding and return docs for sectioning", () => {
        expect(OBSIDIAN_PAGE_CYPHER).toMatch(/k\.summary\s*=\s*row\.summary/);
        expect(OBSIDIAN_PAGE_CYPHER).toMatch(/k\.embedding\s*=\s*row\.docEmbedding/);
        expect(OBSIDIAN_PAGE_CYPHER).toMatch(/RETURN collect\(\{ key: row\.obsidianPath, docId: elementId\(k\) \}\) AS docs/);
        expect(NOTION_PAGE_CYPHER).toMatch(/k\.summary\s*=\s*row\.summary/);
        expect(NOTION_PAGE_CYPHER).toMatch(/k\.embedding\s*=\s*row\.docEmbedding/);
        expect(NOTION_PAGE_CYPHER).toMatch(/RETURN collect\(\{ key: row\.notionId, docId: elementId\(k\) \}\) AS docs/);
    });
});
//# sourceMappingURL=memory-archive-write.test.js.map