import { test, expect } from "vitest";
import { readFileSync, readdirSync } from "node:fs";
import { resolve, join } from "node:path";
import { parseLabelsFromSchemaCypher } from "../../../../../../lib/graph-mcp/dist/schema-cypher-parser.js";
// Task 358 — schema-doc / schema-cypher drift guard.
//
// Every label declared in prose by an ontology doc under
// platform/plugins/memory/references/schema-*.md must also be declared
// in platform/neo4j/schema.cypher so that
// parseLabelsFromSchemaCypher() registers it before the first node of
// that label has landed. Without this guard, a label written in the prose
// model survives review even when its CREATE CONSTRAINT / CREATE INDEX
// line is missing — and the first memory-write referencing it fails
// against a fresh, label-empty graph (the symptom that produced this
// task, and Task 352 before it).
const REFERENCES_DIR = resolve(import.meta.dirname, "../../../../references");
const SCHEMA_CYPHER_PATH = resolve(import.meta.dirname, "../../../../../../neo4j/schema.cypher");
// Labels named in prose that are deliberately NOT first-class graph
// nodes — they are sub-labels stacked on a base node by the writer, or
// schema.org aliases that never reach the graph as their own label.
// Adding to this set is an intentional exception, not a silent omission.
const PROSE_ONLY_LABELS = new Set([
    // schema-estate-agent.md treats Applicant as an additional label on a
    // base :Person node; there is no standalone :Applicant.
    "Applicant",
    // Email is retired: Task 321 folded threads onto :KnowledgeDocument
    // {source:'email'}, Task 426 then moved them onto :ConversationArchive
    // {source:'email'}. The :Email label never reaches the graph; it is
    // referenced in schema-base.md only for historical orientation.
    "Email",
]);
function backtickedLabels(text) {
    const labels = new Set();
    // Match `:Label` inside backticks — the convention used by every
    // schema-*.md prose doc to name a Neo4j label.
    for (const match of text.matchAll(/`:([A-Z][A-Za-z0-9]*)`/g)) {
        labels.add(match[1]);
    }
    return labels;
}
test("schema-*.md prose labels all appear in schema.cypher", () => {
    const declared = new Set(parseLabelsFromSchemaCypher(readFileSync(SCHEMA_CYPHER_PATH, "utf-8")));
    const files = readdirSync(REFERENCES_DIR).filter((name) => name.startsWith("schema-") && name.endsWith(".md"));
    expect(files.length).toBeGreaterThan(0);
    const missing = [];
    for (const file of files) {
        const text = readFileSync(join(REFERENCES_DIR, file), "utf-8");
        for (const label of backtickedLabels(text)) {
            if (PROSE_ONLY_LABELS.has(label))
                continue;
            if (!declared.has(label)) {
                missing.push({ label, file });
            }
        }
    }
    expect(missing, `Labels named in prose but missing from schema.cypher (add a CREATE CONSTRAINT or CREATE INDEX FOR (x:Label) line, or add to PROSE_ONLY_LABELS with a justification):\n${missing
        .map((m) => `  :${m.label} (${m.file})`)
        .join("\n")}`).toEqual([]);
});
test("estate-agent labels are declared in schema.cypher", () => {
    // Task 358 acceptance — explicit assertion against regression.
    const declared = new Set(parseLabelsFromSchemaCypher(readFileSync(SCHEMA_CYPHER_PATH, "utf-8")));
    for (const label of ["Listing", "Property", "Viewing", "Offer", "ImageObject"]) {
        expect(declared.has(label), `expected :${label} in schema.cypher`).toBe(true);
    }
});
//# sourceMappingURL=schema-cypher-drift.test.js.map