/**
 * Entity validation schemas for Studiograph
 *
 * 17 canonical entity types post-redesign:
 * - CRM (4): person, organization, position, deal
 * - PM (4): project, event, task, deliverable
 * - KM authored (6): document, deck, publication, case-study, service, skill
 * - Capture (3): note, reference, dataset
 * - System (1): asset
 *
 * "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';
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('Person assigned (wikilink)'),
    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'),
});
/**
 * 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)
// ══════════════════════════════════════════════════════════════════════════════
/**
 * 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'),
});
/**
 * 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'),
});
/**
 * 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'),
});
/**
 * 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'),
});
// ══════════════════════════════════════════════════════════════════════════════
// SYSTEM — asset
// ══════════════════════════════════════════════════════════════════════════════
/**
 * Asset — a hosted binary the workspace owns. The frontmatter is the
 * entity; the body is optional user notes. The binary itself lives in
 * the asset backend (R2 or local volume) at `asset/{entity_id}/{file}`.
 *
 * Distinct from `reference`, which points at an external URL the
 * workspace doesn't own. Per-kind UI (icon, viewer, thumbnail) is
 * driven by `asset_kind`; see `detectAssetKind` in
 * src/services/assets/base.ts.
 *
 * Asset is graph-visible (search, history, wikilinks) but hidden from the
 * New Entry picker — it's created via the upload/drag-drop flow.
 */
export const ASSET_KINDS = [
    'image', 'video', 'audio', 'pdf', 'spreadsheet', 'other',
];
export const AssetSchema = EntityBaseSchema.extend({
    entity_type: z.literal('asset'),
    name: z.string().min(1, 'Asset name is required'),
    asset_url: z.string().min(1, 'asset_url is required').describe('Public URL or /api/assets/... path'),
    asset_kind: z.enum(ASSET_KINDS).describe('High-level kind that drives icon and viewer'),
    asset_extension: z.string().min(1).describe('Original file extension including the dot, e.g. ".csv"'),
    asset_size: z.number().int().nonnegative().describe('Size in bytes'),
    asset_mime: z.string().optional().describe('Detected MIME type'),
    pages: z.number().int().positive().optional().describe('Page count (PDF only)'),
    width: z.number().int().positive().optional().describe('Pixel width (image/video)'),
    height: z.number().int().positive().optional().describe('Pixel height (image/video)'),
    duration: z.number().nonnegative().optional().describe('Length in seconds (audio/video)'),
    checksum: z.string().optional().describe('Content hash, e.g. sha256:...'),
});
// ══════════════════════════════════════════════════════════════════════════════
// 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/mcp/comment-virtual-entity.ts`), 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. 17 canonical built-in types
 * (16 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 (6)
    document: DocumentSchema,
    deck: DeckSchema,
    publication: PublicationSchema,
    'case-study': CaseStudySchema,
    service: ServiceSchema,
    skill: SkillSchema,
    // Capture (3)
    note: NoteSchema,
    reference: ReferenceSchema,
    dataset: DatasetSchema,
    // System (2)
    asset: AssetSchema,
    comment: CommentSchema,
};
/**
 * 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',
    'publications': 'publication',
    'case-studies': 'case-study',
    'services': 'service',
    'skills': 'skill',
    'notes': 'note',
    'references': 'reference',
    'datasets': 'dataset',
    'assets': 'asset',
    '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]));
/**
 * 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 {
        return schema.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 ${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