/**
 * Entity validation schemas for Studiograph
 *
 * 20 canonical entity types post-redesign:
 * - CRM (4): person, organization, position, deal
 * - PM (4): project, event, task, deliverable
 * - KM authored (7): document, deck, artifact, publication, case-study, service, skill
 * - Capture (4): note, reference, dataset, material
 * - System (1): comment
 *
 * The `asset` entity type was retired (asset-as-embed / Phase F); its name is
 * reserved (see RESERVED_ENTITY_TYPES) so it can never be re-created.
 *
 * "An object earns first-class entity status only if it earns its own view
 * behavior across the six primary views (List, Gallery, Kanban, Calendar,
 * Timeline, Graph). Objects that are mostly useful because they're attached
 * to something else become categories, fields, or inline blocks — not types."
 *
 * Workspaces can define additional custom types via schema_extensions.
 */
import { z } from 'zod';
import { ARTIFACT_KINDS } from './artifact-bundle.js';
function dateField(role, label, description) {
    return z
        .string()
        .date()
        .optional()
        .describe(description ?? label)
        .meta({ dateRole: role, label });
}
/**
 * Base entity schema - all entities have these fields
 */
export const EntityBaseSchema = z.object({
    entity_type: z.string().min(1, 'Entity type is required'),
    entity_id: z.string()
        .regex(/^[a-z0-9_-]+$/, 'Entity ID must be kebab-case (lowercase letters, numbers, hyphens, underscores only)')
        .min(1, 'Entity ID is required'),
    created_at: z.string().datetime('Invalid datetime format'),
    updated_at: z.string().datetime('Invalid datetime format'),
    created_by: z.string().min(1, 'Created by is required'),
    updated_by: z.string().min(1, 'Updated by is required'),
    date: dateField('primary', 'Date', "The entity's primary date (YYYY-MM-DD)"),
    tags: z.array(z.string())
        .optional()
        .describe('Tags for categorization'),
}).passthrough(); // Allow custom fields beyond the base schema
// ══════════════════════════════════════════════════════════════════════════════
// CRM — people, organizations, positions, deals
// ══════════════════════════════════════════════════════════════════════════════
/**
 * Person — team members, client contacts, any individual.
 *
 * `category` and `status` are split: category is the relationship type
 * (employee/contractor/client/partner) and status is whether that
 * relationship is current or historical. Mirrors organization.
 */
export const PersonSchema = EntityBaseSchema.extend({
    entity_type: z.literal('person'),
    name: z.string().min(1, 'Person name is required'),
    role: z.string().optional().describe('Free-text title (distinct from `position` entity)'),
    category: z.enum(['employee', 'contractor', 'client', 'partner', 'other'])
        .optional()
        .describe('Relationship category'),
    status: z.enum(['current', 'former'])
        .optional()
        .describe('Whether the relationship is current or historical'),
    email: z.string().optional().describe('Email address'),
    location: z.string().optional().describe('Location'),
    organization: z.string().optional().describe('Organization wikilink'),
    // Bridge to the auth.db user account this person represents. Scalar handle
    // string (not a wikilink): users are out-of-graph identities. Workspace-
    // unique — write-path enforcement lives in user-person-bridge.ts. Optional
    // because person entities can exist without a corresponding user
    // (contractors, client contacts, anyone in the org chart who doesn't log
    // in). Empty string is the "cleared" form — graph.create seeds optional
    // string fields to "" on new entities, and unlink writes "" as well, so
    // the regex must accept it explicitly.
    user_handle: z.string()
        // Empty (cleared) OR a kebab-case handle: leading alnum, alnum/hyphen
        // body, alnum tail, no doubled hyphens. Mirrors what auth's
        // slugifyHandle + allocateHandle can actually emit — accepting
        // `foo-` or `foo--bar` here would let a hand-edit drift outside what
        // findUserByHandle can match.
        .regex(/^(|[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9]))*)$/, 'user_handle must match the auth handle format (lowercase, single hyphens between alnums, alnum endpoints)')
        .optional()
        .describe('Auth handle of the user this person represents'),
});
/**
 * Organization — clients, vendors, partners, prospects, professional
 * associations.
 */
export const OrganizationSchema = EntityBaseSchema.extend({
    entity_type: z.literal('organization'),
    name: z.string().min(1, 'Organization name is required'),
    category: z.enum(['client', 'vendor', 'partner', 'prospect', 'professional-association', 'other'])
        .optional()
        .describe('Relationship category'),
    status: z.enum(['active', 'dormant', 'former'])
        .default('active')
        .describe('Organization status (`dormant` covers clients you have not worked with in a while but might again)'),
    industry: z.string().optional().describe('Industry sector'),
    website: z.string().url().optional().describe('Website URL'),
});
/**
 * Position — a position spec or open job. Renamed from `role` to avoid
 * collision with `person.role` (the free-text title field). Reframes from
 * "the person's seat" to "the spec for a position."
 */
export const PositionSchema = EntityBaseSchema.extend({
    entity_type: z.literal('position'),
    name: z.string().min(1, 'Position name is required'),
    department: z.string().optional().describe('Department'),
    category: z.enum(['staff', 'contractor', 'intern'])
        .optional()
        .describe('Employment category'),
    status: z.enum(['draft', 'open', 'filled', 'closed'])
        .default('open')
        .describe('Position lifecycle'),
});
/**
 * Deal — pre-project opportunity in the sales pipeline.
 */
export const DealSchema = EntityBaseSchema.extend({
    entity_type: z.literal('deal'),
    name: z.string().min(1, 'Deal name is required'),
    client: z.string().optional().describe('Client organization wikilink'),
    contact: z.string().optional().describe('Primary contact person wikilink'),
    stage: z.enum(['qualification', 'scoping', 'proposal', 'negotiation', 'pending-signature', 'won', 'lost'])
        .default('qualification')
        .describe('Pipeline stage'),
    deal_value: z.number().optional().describe('Estimated deal value'),
    start_date: dateField('start', 'Start date', 'When work on the deal began (enables Timeline rendering of duration)'),
    date: dateField('primary', 'Close date', 'Expected close date'),
});
// ══════════════════════════════════════════════════════════════════════════════
// PM — project, event, task, deliverable
// ══════════════════════════════════════════════════════════════════════════════
/**
 * Project — a client engagement or internal initiative.
 *
 * `services` is a wikilink array to `service` entities the project includes
 * (replaces the previous `practice_area` concept).
 */
export const ProjectSchema = EntityBaseSchema.extend({
    entity_type: z.literal('project'),
    name: z.string().min(1, 'Project name is required'),
    category: z.enum(['client', 'internal'])
        .optional()
        .describe('Project category'),
    project_type: z.string().optional().describe('Kind of project (e.g., website, research, event, rebrand, prototype)'),
    client: z.string().optional().describe('Client organization wikilink'),
    services: z.array(z.string()).optional()
        .describe('Service entities this project includes (wikilinks)'),
    status: z.enum(['planning', 'in-progress', 'paused', 'complete', 'archived'])
        .optional()
        .describe('Current project status'),
    start_date: dateField('start', 'Start date', 'Project start date'),
    date: dateField('primary', 'End date', 'Project end date'),
});
/**
 * Event — any scheduled gathering (meeting, call, workshop, review, etc.).
 * `type` discriminates the kind of gathering for grouping/filtering. The
 * default `meeting` preserves semantics for legacy meeting captures.
 *
 * Note: `deadline` is intentionally NOT a `type` value — deadlines are
 * `task` entries with a `date` (the due date). Events are things you attend.
 */
export const EVENT_TYPES = [
    'meeting', 'call', 'review', 'workshop', 'presentation', 'conference', 'other',
];
export const EventSchema = EntityBaseSchema.extend({
    entity_type: z.literal('event'),
    name: z.string().min(1, 'Event name/subject is required'),
    type: z.enum(EVENT_TYPES).default('meeting').describe('Kind of event'),
    attendees: z.array(z.string()).optional().describe('Attendee wikilinks'),
    location: z.string().optional().describe('Location or meeting link'),
    start_date: dateField('start', 'Start date', 'When a multi-day event begins (omit for single-day events)'),
    date: dateField('primary', 'Date', 'When the event takes place'),
});
/**
 * Task — lightweight action items and to-dos.
 *
 * `phase` is intentionally free-text (not a separate entity type or workspace
 * config list). Phases vary widely across project types; the Timeline view
 * auto-discovers distinct values per-project for grouping.
 */
export const TaskSchema = EntityBaseSchema.extend({
    entity_type: z.literal('task'),
    name: z.string().min(1, 'Task name is required'),
    status: z.enum(['backlog', 'todo', 'in-progress', 'in-review', 'done', 'cancelled'])
        .default('backlog')
        .describe('Task status'),
    priority: z.enum(['low', 'medium', 'high', 'urgent'])
        .optional()
        .describe('Priority level'),
    assignee: z.string().optional().describe('Who the task is assigned to: a workspace account as `[[user:<id>|@handle]]` (preferred — this is what powers "My Tasks"), or a person-entity wikilink `[[person-id]]` for someone without an account')
        // `refType` rides on the field via `.meta()` (same mechanism as the date
        // vocabulary's `dateRole`/`label`). It marks `assignee` as a reference
        // that must point at a `person` entity — surfaced through
        // `describeZodObject` → `/api/schema` + `get_entity_schema` so pickers
        // scope generically, and consumed by the write-path validator in
        // entity-mutations.ts that actually rejects non-person targets. Zod
        // itself can't resolve wikilinks, so this is a hint, not enforcement.
        .meta({ refType: 'person' }),
    phase: z.string().optional().describe('Project phase (free-text — Discover, Design, Prototype, Build, etc.)'),
    start_date: dateField('start', 'Start date', 'Task start date (enables Timeline / Gantt rendering of duration)'),
    date: dateField('primary', 'Due date', 'When the task is due'),
    // Auto-stamped by graph.update when status transitions to/from `done` (see
    // composeUpdatedFrontmatter). Lets surfaces show "completed today" without
    // conflating it with the due date. ISO datetime; cleared when reopened.
    completed_at: z.string().datetime('Invalid datetime format').optional()
        .describe('When the task was marked done (ISO datetime, auto-managed)')
        .meta({ label: 'Completed at' }),
});
/**
 * Deliverable — a scoped output promised to a client.
 */
export const DeliverableSchema = EntityBaseSchema.extend({
    entity_type: z.literal('deliverable'),
    name: z.string().min(1, 'Deliverable name is required'),
    project: z.string().optional().describe('Parent project wikilink'),
    status: z.enum(['planned', 'in-progress', 'delivered', 'accepted', 'rejected'])
        .default('planned')
        .describe('Delivery status'),
    phase: z.string().optional().describe('Project phase (free-text — same semantics as task.phase)'),
    start_date: dateField('start', 'Start date', 'Deliverable start date (enables Timeline rendering of duration)'),
    date: dateField('primary', 'Due date', 'When the deliverable is due'),
});
// ══════════════════════════════════════════════════════════════════════════════
// KM — authored (document, deck, publication, case-study, service, skill)
// ══════════════════════════════════════════════════════════════════════════════
/**
 * Shared description for the `asset_ref` field — now `dataset`-ONLY.
 *
 * Body-embed model (2026-06-01): an import seeds the asset reference INTO THE
 * BODY (an `![](url)` image embed for materials, a `[name](url)` source
 * link for documents) — exactly as a user would. So material/document no longer
 * carry `asset_ref`; the reference is in the markdown, not the frontmatter.
 *
 * `dataset` is the lone exception: a dataset body must stay valid CSV, so it
 * can't hold a body link. `asset_ref` records the STORAGE UUID of the source
 * xlsx (the `assets.db` `asset_id` / immutable-key id, never a URL, never an
 * entity id) as INVISIBLE provenance — survives the entity moving repos and an
 * asset-backend swap. It is one-to-one provenance, NOT an embed graph (the
 * many-to-many image-embed mechanism is still markdown `![](url)` in a body).
 *
 * `.meta({ internal: true })` keeps it out of the field-metadata API, the
 * property panel, and the agent's `get_entity_schema` (same treatment as
 * `source_file`) — it is set by the import path, never hand-edited.
 */
const ASSET_REF_DESC = 'Storage UUID of the source xlsx this dataset was imported from';
/**
 * Document — any authored document. The `guideline` category absorbs the
 * former standalone `guideline` entity type.
 */
export const DocumentSchema = EntityBaseSchema.extend({
    entity_type: z.literal('document'),
    name: z.string().min(1, 'Document name is required'),
    category: z.enum(['proposal', 'contract', 'brief', 'memo', 'report', 'template', 'guideline', 'other'])
        .optional()
        .describe('Document category'),
    status: z.enum(['draft', 'review', 'final', 'archived'])
        .default('draft')
        .describe('Document status'),
    // No asset_ref: under the body-embed model an imported document seeds the
    // extracted text + a `[name](url)` source link directly into its body, so the
    // source-file reference lives in the markdown, not the frontmatter.
});
/**
 * Deck — a slide deck or presentation. Stays its own type alongside
 * `document` because of the dedicated slides renderer + agent skills that
 * pattern-match on `entity_type === 'deck'`.
 */
export const DeckSchema = EntityBaseSchema.extend({
    entity_type: z.literal('deck'),
    name: z.string().min(1, 'Deck name is required'),
    audience: z.enum(['client', 'team', 'board', 'external', 'other'])
        .optional()
        .describe('Intended audience'),
    status: z.enum(['draft', 'review', 'final', 'archived'])
        .default('draft')
        .describe('Deck status'),
});
/**
 * Artifact — a persisted Artifact Canvas bundle. The markdown body stores the
 * serialized ArtifactBundle JSON; frontmatter carries scannable metadata for
 * lists, filters, and the property panel.
 */
export const ArtifactSchema = EntityBaseSchema.extend({
    entity_type: z.literal('artifact'),
    name: z.string().min(1, 'Artifact name is required'),
    kind: z.enum(ARTIFACT_KINDS)
        .default('custom')
        .describe('Artifact kind rendered by the canvas'),
    status: z.enum(['draft', 'review', 'final', 'archived'])
        .default('draft')
        .describe('Artifact status'),
    viewport_width: z.number().int().positive().optional()
        .describe('Canvas viewport width in pixels'),
    viewport_height: z.number().int().positive().optional()
        .describe('Canvas viewport height in pixels'),
    viewport_mode: z.enum(['fixed', 'responsive', 'paged'])
        .optional()
        .describe('Canvas viewport behavior'),
});
/**
 * Publication — authored content published externally.
 */
export const PublicationSchema = EntityBaseSchema.extend({
    entity_type: z.literal('publication'),
    name: z.string().min(1, 'Publication name is required'),
    platform: z.string().optional().describe('Where published (e.g., linkedin, medium, behance, conference)'),
    status: z.enum(['draft', 'review', 'scheduled', 'published'])
        .default('draft')
        .describe('Publication status'),
    url: z.string().optional().describe('URL of published content'),
});
/**
 * Case Study — portfolio narratives for completed projects.
 */
export const CaseStudySchema = EntityBaseSchema.extend({
    entity_type: z.literal('case-study'),
    name: z.string().min(1, 'Case study name is required'),
    client: z.string().optional().describe('Client organization wikilink'),
    services: z.array(z.string()).optional()
        .describe('Service entities this case study highlights (wikilinks)'),
    date_range: z.string().optional().describe('Project timeframe (e.g., 2025-09 / 2026-01)'),
    status: z.enum(['draft', 'review', 'final', 'published'])
        .default('draft')
        .describe('Case study status'),
});
/**
 * Service — the firm's offerings + methodologies. Replaces the old
 * `process` type and absorbs `practice-area` (top-level services replace
 * what practice-area was trying to be).
 *
 * `sprint` links a granular service to its parent service (top-level
 * services have none). `depends_on` / `enables` form a directed graph
 * across the service set — the Graph view's most valuable use case.
 */
export const ServiceSchema = EntityBaseSchema.extend({
    entity_type: z.literal('service'),
    name: z.string().min(1, 'Service name is required'),
    service_id: z.string().optional()
        .describe('Optional human-readable id (e.g. "1.4") for studios that number their services'),
    sprint: z.string().optional()
        .describe('Parent service wikilink for hierarchy (top-level services / sprints have none)'),
    depends_on: z.array(z.string()).optional()
        .describe('Services that must precede this one (wikilinks)'),
    enables: z.array(z.string()).optional()
        .describe('Services that this one unlocks (wikilinks)'),
    deliverables: z.array(z.string()).optional()
        .describe('Named outputs this service produces'),
    status: z.enum(['offered', 'in-development', 'retired'])
        .default('offered')
        .describe('Service lifecycle'),
});
/**
 * Skill — prompt-level instructions the agent can load to teach it how to
 * do a specific job. The entity id is the skill name used by `load_skill`.
 * The body markdown is the instruction content.
 *
 * `applies_to` lets the agent load deck-skills when working on a deck,
 * proposal-skills when on a `document.category=proposal`, etc. Empty /
 * unset = generally applicable.
 *
 * The previous `loading: eager | on-demand` field is gone for graph-authored
 * skills. Filesystem skills (shipped with the product in `src/agent/skills/`)
 * keep their own loading mechanism via skill-loader.ts and orchestrator.ts.
 *
 * `name` and `description` are optional at creation so the sidebar new-entry
 * flow works — the user fills them in afterward via the property panel.
 */
export const SkillSchema = EntityBaseSchema.extend({
    entity_type: z.literal('skill'),
    name: z.string().optional().describe('Human-readable title (falls back to entity id)'),
    description: z.string().optional().describe('Shown in the skill index so the agent knows when to load it'),
    applies_to: z.array(z.string()).optional()
        .describe('Entity types this skill applies to (e.g. ["deck"]). Empty = generally applicable.'),
});
// ══════════════════════════════════════════════════════════════════════════════
// CAPTURE — note, reference
// ══════════════════════════════════════════════════════════════════════════════
/**
 * Note — freeform thinking, research, daily notes.
 *
 * Decisions migrated from the cut `decision` type land here as plain
 * notes; operators may manually convert specific entries to `task` if they
 * read better as work items.
 */
export const NoteSchema = EntityBaseSchema.extend({
    entity_type: z.literal('note'),
    name: z.string().min(1, 'Note name is required'),
    // Entry-primary provenance (A2): when this note was derived from an
    // uploaded asset (e.g. a converted .docx/.pdf), `source_file` holds the
    // SOURCE ASSET'S ENTITY ID — never a URL — so the link survives an asset
    // backend swap and joins the graph (the relationship is also surfaced as a
    // wikilink in the body). Absent for notes created directly.
    //
    // `.meta({ internal: true })` marks it system-managed: the field-metadata
    // API (describeZodObject) omits it, so it never appears in the property
    // panel or the agent's get_entity_schema, and the create/changeType field
    // seeders skip it (so a directly-created note has NO source_file rather
    // than `''`). Set only by the import/convert path.
    source_file: z.string().optional().describe('Entity id of the source asset this note was derived from').meta({ internal: true }),
});
/**
 * Reference — external resources and inspiration.
 */
export const ReferenceSchema = EntityBaseSchema.extend({
    entity_type: z.literal('reference'),
    name: z.string().min(1, 'Reference name is required'),
    source: z.string().optional().describe('Source or author'),
    link: z.string().optional().describe('URL'),
});
/**
 * Dataset — a CSV the workspace owns, edited in a grid.
 *
 * Conventional CSV: the markdown body IS the data, and the header row IS the
 * column list — there's no separate typed column schema. The grid editor
 * (web CsvEditor) and the agent's `query_dataset` / `append_rows` tools read
 * the body through CSVService (src/services/csv-service.ts), deriving columns
 * from the header. Distinct from an uploaded `asset` with
 * `asset_kind: 'spreadsheet'`, which is an opaque binary — a dataset is
 * in-graph, queryable, and row-appendable.
 */
export const DatasetSchema = EntityBaseSchema.extend({
    entity_type: z.literal('dataset'),
    name: z.string().min(1, 'Dataset name is required'),
    // Entry-primary provenance (A2): set when this dataset was derived from an
    // uploaded asset (e.g. one sheet of a converted .xlsx). Holds the SOURCE
    // ASSET'S ENTITY ID, never a URL. See NoteSchema.source_file (incl. the
    // `.meta({ internal: true })` rationale — same system-managed treatment).
    source_file: z.string().optional().describe('Entity id of the source asset this dataset was derived from').meta({ internal: true }),
    // When derived from one sheet of a converted .xlsx, the original worksheet
    // name — the stable key that lets a re-convert (replace) match this dataset
    // back to its sheet and update it IN PLACE, preserving the id + inbound
    // links even if the first conversion's id was collision-suffixed. Internal
    // (system-managed); never user-edited.
    source_sheet: z.string().optional().describe('Original worksheet name this dataset was derived from').meta({ internal: true }),
    // Asset-as-embed provenance: the storage UUID of the source asset (e.g. the
    // .xlsx this sheet was imported from). Frontmatter only — the dataset body
    // stays pure CSV, so the asset link is NEVER a body embed. See ASSET_REF_DESC.
    asset_ref: z.string().optional().describe(ASSET_REF_DESC).meta({ internal: true }),
});
/**
 * Material — a first-class graph entry that wraps a free-standing binary
 * asset (image or opaque file: photo, logo, psd/ai/sketch/fig, video, audio,
 * archive) when no better type fits. The asset-as-embed catch-all: import
 * routes docx/pdf → `document`, xlsx → `dataset`, and everything else (image +
 * opaque) → `material`.
 *
 * Distinct from `asset` (the stored bytes + `assets.db` row) and from
 * `reference` (a pointer to an EXTERNAL source the workspace doesn't own).
 * Under the body-embed model a material's body IS the asset: import seeds an
 * inline `![name](url)` image embed (images) or a `[name](url)` download link
 * (opaque binaries). No `asset_ref` frontmatter — the reference lives in the
 * markdown. A hand-created material (New Entry / agent) is a notes/catalog
 * entry whose body the user writes themselves.
 */
export const MaterialSchema = EntityBaseSchema.extend({
    entity_type: z.literal('material'),
    name: z.string().min(1, 'Material name is required'),
    // Purpose-oriented grouping axis (NOT a re-encoding of the asset's kind,
    // which lives in assets.db). Optional. Values are intentionally minimal —
    // enums are cheap to extend, expensive to remove.
    category: z.enum(['design', 'brand', 'media', 'archive', 'other'])
        .optional()
        .describe('Purpose grouping (design source, brand asset, media, archive, other)'),
});
// ══════════════════════════════════════════════════════════════════════════════
// ASSET KINDS (store-facing — NOT an entity type)
// ══════════════════════════════════════════════════════════════════════════════
/**
 * High-level kinds for stored binaries — assets.db media metadata + per-kind
 * UI (icon/viewer/thumbnail). Drives `detectAssetKind` in
 * src/services/assets/base.ts and the asset STORE, NOT any entity schema.
 *
 * The `asset` ENTITY type was retired (asset-as-embed §6 / Phase F): an
 * imported binary now becomes a typed entry (material/document/dataset) that
 * embeds a free-standing, folder-owned asset. The store (assets.db +
 * immutable-key blobs + serving routes) stays.
 */
export const ASSET_KINDS = [
    'image', 'video', 'audio', 'pdf', 'spreadsheet', 'other',
];
// ══════════════════════════════════════════════════════════════════════════════
// SYSTEM — comment (virtual entity, SQLite-backed)
// ══════════════════════════════════════════════════════════════════════════════
/**
 * Comment — a threaded comment anchored to another entry. Unlike every other
 * entity type, comments are NOT stored as markdown files; they live in
 * `comments.db` (see `src/services/comment-service.ts`) for throughput.
 * Agents see them as first-class typed entities through the virtual-entity
 * adapter (`src/services/comment-virtual-entity.ts`), shared by the MCP and
 * in-app agent surfaces, which is why the schema
 * lives here alongside the file-backed types.
 *
 * The shape documents what fields the MCP envelope exposes; runtime
 * validation happens inside CommentService itself.
 */
export const CommentSchema = EntityBaseSchema.extend({
    entity_type: z.literal('comment'),
    repo: z.string().describe('Folder the anchored entry lives in'),
    entry_type: z.string().describe('Entity type of the anchored entry'),
    entry_id: z.string().describe('Entity id of the anchored entry'),
    thread_id: z.string().describe('Head comment id; replies share the same thread_id'),
    parent_id: z.string().nullable().optional().describe('Parent comment id for replies, null on the head'),
    author_user_id: z.number().int().describe('users.id of the author'),
    author_role: z.enum(['user', 'agent']).describe('Whether the author was a human or the agent'),
    anchor_quote: z.string().nullable().optional()
        .describe('Original highlighted text at creation time, ≤256 chars (used for orphan rendering)'),
    resolved_at: z.string().nullable().optional(),
    resolved_by_user_id: z.number().int().nullable().optional(),
    edited: z.boolean().optional(),
    mentions: z.array(z.string()).optional().describe('Handles of mentioned users'),
});
// ══════════════════════════════════════════════════════════════════════════════
// SCHEMA REGISTRY
// ══════════════════════════════════════════════════════════════════════════════
/**
 * Maps entity types to their Zod schemas. 20 canonical built-in types
 * (19 user-facing + 1 system). Workspaces can register additional custom
 * types via SchemaRegistry (schema_extensions).
 */
export const ENTITY_SCHEMAS = {
    // CRM (4)
    person: PersonSchema,
    organization: OrganizationSchema,
    position: PositionSchema,
    deal: DealSchema,
    // PM (4)
    project: ProjectSchema,
    event: EventSchema,
    task: TaskSchema,
    deliverable: DeliverableSchema,
    // KM — authored (7)
    document: DocumentSchema,
    deck: DeckSchema,
    artifact: ArtifactSchema,
    publication: PublicationSchema,
    'case-study': CaseStudySchema,
    service: ServiceSchema,
    skill: SkillSchema,
    // Capture (4)
    note: NoteSchema,
    reference: ReferenceSchema,
    dataset: DatasetSchema,
    material: MaterialSchema,
    // System (1)
    comment: CommentSchema,
};
/**
 * Retired entity-type names that must never be (re-)created, even if a legacy
 * workspace still carries a `schema_extensions.<name>` for them. `asset` was
 * retired in Phase F (asset-as-embed): binaries are now free-standing in
 * assets.db and embedded by typed entries (material/document/dataset), so a
 * file-backed `asset` entity would be an unvalidatable base-only stub. Enforced
 * at the entity-creation entry points (graph.changeType, the web create route,
 * and getAgentAllowedEntityTypes).
 */
export const RESERVED_ENTITY_TYPES = new Set(['asset']);
/**
 * Plural subfolder name → singular entity_type mapping
 */
export const PLURAL_TO_SINGULAR = {
    'people': 'person',
    'organizations': 'organization',
    'positions': 'position',
    'deals': 'deal',
    'projects': 'project',
    'events': 'event',
    'tasks': 'task',
    'deliverables': 'deliverable',
    'documents': 'document',
    'decks': 'deck',
    'artifacts': 'artifact',
    'publications': 'publication',
    'case-studies': 'case-study',
    'services': 'service',
    'skills': 'skill',
    'notes': 'note',
    'references': 'reference',
    'datasets': 'dataset',
    'materials': 'material',
    'comments': 'comment',
};
/**
 * Singular entity_type → plural subfolder name mapping
 */
export const SINGULAR_TO_PLURAL = Object.fromEntries(Object.entries(PLURAL_TO_SINGULAR).map(([plural, singular]) => [singular, plural]));
/**
 * Cross-field task rule: a task's start date must not fall after its due date.
 *
 * This lives OUTSIDE the Zod schema on purpose. Expressing it as a
 * `TaskSchema.refine(...)` would turn the ZodObject into a ZodEffects, which
 * breaks shape introspection in schema-registry (`describeZodObject` and the
 * schema_extensions `.extend()` path both rely on `instanceof z.ZodObject`) —
 * and that powers the metadata field panel. So both validation entry points
 * (this module's `validateEntity` and `graph.ts`'s private one) call this
 * helper after a successful parse instead.
 *
 * `dateField` stores ISO 'YYYY-MM-DD' strings, so a lexicographic comparison
 * is a correct date comparison.
 */
export function assertValidTaskDates(data) {
    if (data.start_date && data.date && data.start_date > data.date) {
        throw new Error(`Validation failed for task:\n  - start_date: Start date (${data.start_date}) must be on or before the due date (${data.date})`);
    }
}
/**
 * Validation helper with helpful error messages
 */
export function validateEntity(entityType, data) {
    const schema = ENTITY_SCHEMAS[entityType];
    if (!schema) {
        throw new Error(`Unknown entity type: ${entityType}`);
    }
    try {
        const parsed = schema.parse(data);
        if (entityType === 'task')
            assertValidTaskDates(parsed);
        return parsed;
    }
    catch (error) {
        if (error instanceof z.ZodError) {
            const messages = error.issues.map(e => {
                const path = e.path.join('.');
                return `  - ${path}: ${e.message}`;
            });
            throw new Error(`Validation failed for ${entityType}:\n${messages.join('\n')}`);
        }
        throw error;
    }
}
/**
 * Validate custom entity type (no schema, just base fields)
 */
export function validateCustomEntity(data) {
    try {
        return EntityBaseSchema.parse(data);
    }
    catch (error) {
        if (error instanceof z.ZodError) {
            const messages = error.issues.map(e => {
                const path = e.path.join('.');
                return `  - ${path}: ${e.message}`;
            });
            throw new Error(`Validation failed for custom entity:\n${messages.join('\n')}`);
        }
        throw error;
    }
}
/**
 * Check if a type is a known built-in entity type
 */
export function isKnownEntityType(type) {
    return type in ENTITY_SCHEMAS;
}
//# sourceMappingURL=validation.js.map