/** * Schema loader — parses every `platform/plugins/memory/references/schema-*.md` * file at server startup and returns a structured LoadedSchema that the * validator consults on every write. * * Design decisions: * * - Header-keyed table parsing. The base schema has 5 columns * (Entity | Neo4j Label | Schema.org Type | Interchange Standard | * Required Properties) while vertical schemas have 4 columns (no * Interchange Standard). We resolve "Neo4j Label" and "Required Properties" * by header name, never by index. * * - Parenthetical annotations in the Required Properties column are type / * enum / policy hints, not property names: * customer (id) -> "customer" * listingType (sale/let) -> "listingType" * status (scope must be `admin`) -> "status" * We strip the first `(...)` suffix and its contents from every comma-split * token before emitting the property name. * * - Disjunctions in the Required Properties column use the literal word `or`: * `email` or `telephone` -> ["email", "telephone"] (any-of) * We split on `\s+or\s+` per token (not per cell) so that a row like * "accountId, email or telephone" yields two groups: one simple, one any-of. * * - Inheritance sentinel. estate-agent's Applicant and hospitality's * GuestProfile use the literal phrase "Base Person properties + …" in the * required-properties column. A second pass resolves these by merging the * referenced parent label's groups. * * - Property naming rules (parsed from every file; today only * schema-base.md carries a table). The `| Correct | Wrong |` * table yields a `wrongToCorrect` map. Rows whose wrong-column contains * "(on" are context-dependent (e.g. `status` is wrong on Order but valid * on Task/Lead) and are skipped to avoid false positives. A future * per-label synonym system could reintroduce them. * * Failures throw. A memory server running with a half-parsed schema is * worse than one that fails to start. */ import { type RelationshipPattern } from "./relationship-patterns.js"; /** A single required-property rule for a label. */ export type RequiredGroup = { kind: "simple"; name: string; } | { kind: "any-of"; names: string[]; }; export interface LabelDefinition { /** Neo4j label (the string the agent puts in `labels`) */ label: string; /** Required-property groups. Every group must be satisfied. */ required: RequiredGroup[]; /** Source file that declared this label (for error messages) */ sourceFile: string; /** * Raw inheritance sentinel, if the cell was "Base X properties + extras". * Consumed and cleared during the second pass. */ inheritsFrom?: string; } export interface LoadedSchema { /** * Markdown-documented label -> definition. The map carries the * required-property and source-file metadata used by the validator's * required-property check. It does NOT determine label existence — * existence is established by `LiveSchemaSource` (live `db.labels()` ∪ * `schema.cypher` declarations). A label not present here is "documented * by markdown? no" → required-property check is skipped (with a structured * log line) but the write is still accepted if the live source recognises * the label. (Renamed from `labels` to make the role explicit.) */ markdownLabels: Map; /** * wrong property name -> correct property name. Parsed from the * `| Correct | Wrong |` table of every loaded schema file (today only * schema-base.md carries one); `sourceFile` drives scoping — base-file * rules apply everywhere, vertical-file rules only within their vertical. */ wrongToCorrect: Map; /** * forbidden properties per label. Parsed from any * "Forbidden Properties" table in any loaded schema markdown file. * Validator rejects writes that include any of these property keys for * the matching primary label, after synonyms have been rewritten. * Source file is preserved for the error message so operators can grep * the rule in the canonical reference. */ forbiddenByLabel: Map>; /** * Property names documented as canonical in prose bullet lists * (``- `prop` — description`` or a bare ``- `prop` `` line), attributed to * the first word of the nearest preceding `###` heading (e.g. the bullets * under "### Listing — full property set" carry `label: "Listing"`). * Consumed only by `detectContradictions` — a doc must never present a * property as canonical while the naming rules ban it. */ documentedCanonicals: Array<{ name: string; label: string; sourceFile: string; }>; /** Absolute paths of every schema file that was read */ sourceFiles: string[]; /** * Brand-declared default vertical (the file basename without `.md`, e.g. * `"schema-estate-agent"`) or `null` when the brand has no vertical (Maxy * default). Read from `brand.json#vertical` at boot. * * The validator scopes per-write rules to base + the active vertical. When * a write resolves a LocalBusiness whose `businessType` differs from this * default, the per-LocalBusiness override wins; otherwise this value is * the resolved vertical. */ brandVertical: string | null; /** * Canonical `(source, edgeType, target)` edges parsed from the * `## Relationship Patterns` fenced block of every loaded schema file, each * tagged with its source file so a write can be judged against base + its * active vertical only. Consumed by the warn-only edge check in the write * path (see `relationship-patterns.ts`). */ relationshipPatterns: RelationshipPattern[]; } /** * Split a pipe-delimited markdown row into cells, stripping the optional * leading/trailing pipes. `|` characters inside a backtick code-span (e.g. * `` `Person|Organization` ``) are treated as literal content and do not * split the row — without this, a single union-typed cell would shred into * multiple cells and the `findTables` width check would throw at module load *. Backtick toggling is single-tick only; nested or escaped * backticks are out of scope (no schema doc uses them today). */ export declare function splitPipeRow(row: string): string[]; export interface LoadSchemaOptions { /** * Override the brand.json discovery path. When omitted, the loader reads * `${process.env.MAXY_PLATFORM_ROOT}/config/brand.json` if the env var is * set, otherwise treats the install as dev-default (`brandVertical = null`). * Tests inject an absolute fixture path here. */ brandJsonPath?: string; /** * Override the references directory the loader reads `schema-*.md` files * from. When omitted, resolves to the plugin's shipped references * directory. Tests inject fixture directories here. */ referencesDir?: string; } /** * Load every `schema-*.md` file from the memory plugin's references directory. * * The directory is resolved relative to this source file, matching the * pattern used by neo4j.ts for config paths. Both src/ and dist/ locations * end up four hops above the references directory * (src/lib/schema-loader.ts -> src/ -> mcp/ -> memory/ -> references/). * * Brand vertical resolution: reads `brand.json#vertical` at boot. The path * is resolved per `LoadSchemaOptions.brandJsonPath`, falling back to * `${MAXY_PLATFORM_ROOT}/config/brand.json`. If the named vertical file is * absent from the references directory, the loader throws at boot — a * memory server running against a misconfigured brand is worse than one * that fails to start. Mirrors paths.ts two-mode resolution: env-unset AND * no explicit path = dev-default (`brandVertical = null`). */ export declare function loadSchema(opts?: LoadSchemaOptions): LoadedSchema; /** * A schema doc prescribing a property name that the loaded naming rules ban * in the same scope. Any such label is structurally unwritable: writing the * banned name trips the validator's synonym rejection, writing the canonical * name trips the missing-required check. */ export interface SchemaContradiction { /** Schema file whose prescription conflicts with the ban */ file: string; /** Label (or nearest heading word for prose bullets) carrying the name */ label: string; /** The banned property name the doc prescribes */ property: string; /** The canonical name the naming rules point at */ correct: string; } /** * Check every required-property token and every documented-canonical bullet * against the synonym map, honouring the validator's scoping rule: a ban * from schema-base.md applies everywhere; a ban from a vertical file applies * only to prescriptions in that same file. Any-of groups are checked * member-by-member — a doc must not present a banned name even as one * alternative. */ /** * Per-label ban exemptions. The global synonym map (schema-base.md) bans * `amount` in favour of `totalPaymentDue` — that is invoice-level semantics. * The billing children `InvoiceLine`, `InvoicePayment` and `Credit` each carry * a line-item monetary `amount` that is genuinely distinct from the * invoice-level total, so they keep the JobLogic-exact `amount` field. * `CashEntry` (Task 1771) carries `amount` for the same reason: it is one * movement of money, not a total owed. A (banned-property -> allowed-labels) * entry here suppresses the contradiction for exactly those labels and nowhere * else. See schema-base.md § Property Naming Rules. * * Exported so the write-path validator applies the identical exemption — the * doc gate and the validator must agree on exactly the same label set, which * means one definition, not two. */ export declare const BAN_EXEMPT: Record>; export declare function detectContradictions(schema: LoadedSchema): SchemaContradiction[]; //# sourceMappingURL=schema-loader.d.ts.map