"""folder_summaries: sync-time lite folder summaries (phase 94, task 01) Revision ID: 0017 Revises: 0016 Create Date: 2026-09-10 Phase 94 (``ls`` becomes a filesystem-style drill-down tree — TODO.md L4): the LLM lists the synced projects, then drills into folders; each level's listing shows the folder's **subtree summary** — a 1–3 sentence plain-text description of what the folder's documents cover, generated at sync time by the aipi ``lite`` model (the ``KB_OVERVIEW_MODE`` contract, ``app.rag.overview``: change-gated by the caller, fail-soft, ``FOLDER_SUMMARY_MODE`` marker for the E2E mock). This migration adds the storage — ONE additive, fully reversible table (A13), no other schema change: * ``folder_summaries`` — one row per folder with ≥ 2 documents (recursive count — the same set the ``ls`` count rule counts, ``00_phase.md``; the ≥ 2 rule and the prune rule are generator policy, ``app.rag.folder_summaries``, not schema constraints): * PK ``(source, folder_path)`` — ``source`` is VARCHAR(120) NOT NULL (mirrors ``documents.source``); ``folder_path`` is VARCHAR(1000) NOT NULL (mirrors ``documents.path`` — the source-relative folder prefix). ``folder_path = ""`` is the SOURCE ROOT: the top-level source summary (the whole source's recursive subtree). * ``summary`` — TEXT NOT NULL: the ``lite``-written description (never empty — the generator validates before storing). * ``updated_at`` — TIMESTAMPTZ NOT NULL, server default now() (house style — the ``kb_overview.updated_at`` precedent); the generator always stamps it explicitly on upsert. """ from __future__ import annotations import sqlalchemy as sa from alembic import op revision = "0017" down_revision = "0016" branch_labels = None depends_on = None def upgrade() -> None: op.create_table( "folder_summaries", sa.Column("source", sa.String(length=120), primary_key=True), sa.Column("folder_path", sa.String(length=1000), primary_key=True), sa.Column("summary", sa.Text(), nullable=False), sa.Column( "updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False, ), ) def downgrade() -> None: # The table is the only 0017 artefact — dropping it leaves the 0016 # schema byte-identical (A13, fully reversible). op.drop_table("folder_summaries")