import { table, text, integer, ownableColumns, createSharesTable, } from "@agent-native/core/db/schema"; import { PLAN_AUTHORS, PLAN_COMMENT_KINDS, PLAN_COMMENT_RESOLUTION_TARGETS, PLAN_COMMENT_STATUSES, PLAN_KINDS, PLAN_REPORT_REASONS, PLAN_REPORT_STATUSES, PLAN_SECTION_TYPES, PLAN_SOURCES, PLAN_STATUSES, } from "../../shared/types.js"; export const plans = table("plans", { id: text("id").primaryKey(), title: text("title").notNull(), brief: text("brief").notNull(), kind: text("kind", { enum: PLAN_KINDS }).notNull().default("plan"), status: text("status", { enum: PLAN_STATUSES }).notNull().default("draft"), source: text("source", { enum: PLAN_SOURCES }).notNull().default("manual"), repoPath: text("repo_path"), currentFocus: text("current_focus"), html: text("html"), markdown: text("markdown"), content: text("content"), hostedPlanId: text("hosted_plan_id"), hostedPlanUrl: text("hosted_plan_url"), createdAt: text("created_at").notNull(), updatedAt: text("updated_at").notNull(), approvedAt: text("approved_at"), // LLM usage + derived cost for the run that produced this row. Nullable โ€” // only populated for `kind="recap"` recaps generated by the PR Visual Recap // workflow (Claude Code / Codex). Column names mirror core's token_usage / // UsageRecord; cost is stored in centicents (1/100ยข) to match it exactly. usageAgent: text("usage_agent"), usageModel: text("usage_model"), usageInputTokens: integer("usage_input_tokens"), usageOutputTokens: integer("usage_output_tokens"), usageCacheReadTokens: integer("usage_cache_read_tokens"), usageCacheWriteTokens: integer("usage_cache_write_tokens"), usageCostCentsX100: integer("usage_cost_cents_x100"), usageCostSource: text("usage_cost_source"), usageRecordedAt: text("usage_recorded_at"), // URL of the source PR, issue, or page that triggered this recap (e.g. the // GitHub PR URL). Nullable โ€” only populated when the caller supplies it. sourceUrl: text("source_url"), // Structured source metadata for recap/product-knowledge search. Nullable so // older imported recaps and non-PR recaps keep working unchanged. sourceType: text("source_type"), sourceRepo: text("source_repo"), sourcePrNumber: integer("source_pr_number"), sourcePrState: text("source_pr_state"), sourcePrMergedAt: text("source_pr_merged_at"), sourceAuthorEmail: text("source_author_email"), sourceAuthorName: text("source_author_name"), sourceAuthorLogin: text("source_author_login"), // Stable key used by PR Visual Recap publish retries to replace the recap // created by an earlier attempt instead of creating duplicate recap rows. recapIdempotencyKey: text("recap_idempotency_key"), deletedAt: text("deleted_at"), deletedBy: text("deleted_by"), ...ownableColumns(), }); export const planSections = table("plan_sections", { id: text("id").primaryKey(), planId: text("plan_id") .notNull() .references(() => plans.id), type: text("type", { enum: PLAN_SECTION_TYPES }).notNull().default("custom"), title: text("title").notNull(), body: text("body").notNull().default(""), html: text("html"), order: integer("sort_order").notNull().default(0), createdBy: text("created_by", { enum: PLAN_AUTHORS }) .notNull() .default("agent"), createdAt: text("created_at").notNull(), updatedAt: text("updated_at").notNull(), }); export const planComments = table("plan_comments", { id: text("id").primaryKey(), planId: text("plan_id") .notNull() .references(() => plans.id), parentCommentId: text("parent_comment_id").references(() => planComments.id), sectionId: text("section_id").references(() => planSections.id), kind: text("kind", { enum: PLAN_COMMENT_KINDS }).notNull().default("comment"), status: text("status", { enum: PLAN_COMMENT_STATUSES }) .notNull() .default("open"), anchor: text("anchor"), message: text("message").notNull(), createdBy: text("created_by", { enum: PLAN_AUTHORS }) .notNull() .default("human"), authorEmail: text("author_email"), authorName: text("author_name"), resolutionTarget: text("resolution_target", { enum: PLAN_COMMENT_RESOLUTION_TARGETS, }), mentionsJson: text("mentions_json"), resolvedBy: text("resolved_by"), resolvedAt: text("resolved_at"), consumedAt: text("consumed_at"), deletedAt: text("deleted_at"), deletedBy: text("deleted_by"), createdAt: text("created_at").notNull(), updatedAt: text("updated_at").notNull(), }); export const planEvents = table("plan_events", { id: text("id").primaryKey(), planId: text("plan_id") .notNull() .references(() => plans.id), type: text("type").notNull(), message: text("message").notNull(), payload: text("payload"), createdBy: text("created_by", { enum: PLAN_AUTHORS }) .notNull() .default("agent"), createdAt: text("created_at").notNull(), }); export const planReports = table("plan_reports", { id: text("id").primaryKey(), planId: text("plan_id") .notNull() .references(() => plans.id), reason: text("reason", { enum: PLAN_REPORT_REASONS }).notNull(), details: text("details"), status: text("status", { enum: PLAN_REPORT_STATUSES }) .notNull() .default("open"), reporterEmail: text("reporter_email"), reporterName: text("reporter_name"), pageUrl: text("page_url"), occurrenceCount: integer("occurrence_count").notNull().default(1), createdAt: text("created_at").notNull(), updatedAt: text("updated_at").notNull(), }); export const planVersions = table("plan_versions", { id: text("id").primaryKey(), ownerEmail: text("owner_email").notNull().default("local@localhost"), planId: text("plan_id") .notNull() .references(() => plans.id), title: text("title").notNull(), snapshotJson: text("snapshot_json").notNull(), changeLabel: text("change_label"), createdBy: text("created_by", { enum: PLAN_AUTHORS }) .notNull() .default("agent"), createdAt: text("created_at").notNull(), // Denormalized copies of summarizePlanVersion's derived fields, populated at // snapshot-write time so list-plan-versions can project just these small // columns instead of fetching + JSON.parsing every row's full snapshot_json // blob. Nullable so pre-existing rows (written before this column existed) // fall back to parsing snapshot_json lazily โ€” see summarizePlanVersionRow. status: text("summary_status", { enum: PLAN_STATUSES }), source: text("summary_source", { enum: PLAN_SOURCES }), blockCount: integer("block_count"), sectionCount: integer("section_count"), hasCanvas: integer("has_canvas", { mode: "boolean" }), hasPrototype: integer("has_prototype", { mode: "boolean" }), previewText: text("preview_text"), }); export const planShares = createSharesTable("plan_shares"); export const planGuestMints = table("plan_guest_mints", { id: text("id").primaryKey(), ipHash: text("ip_hash").notNull(), createdAt: text("created_at").notNull(), }); export const planAssets = table("plan_assets", { id: text("id").primaryKey(), planId: text("plan_id") .notNull() .references(() => plans.id), filename: text("filename").notNull(), mimeType: text("mime_type").notNull(), /** Base64-encoded image data. Used as SQL-fallback when no upload provider is configured. */ data: text("data").notNull(), byteSize: integer("byte_size").notNull(), createdAt: text("created_at").notNull(), });