-- =============================================================================
-- 006_doc_pool.sql — which documents are in the retrieval pool.
--
--   psql -h pg-ai -U postgres -d plant -f 006_doc_pool.sql
--
-- Applied at Phase 9, after 005. Additive: `pool_enabled` defaults TRUE, so
-- every document already ingested stays exactly where it was.
--
-- TWO REASONS A CHUNK IS NOT RETRIEVABLE, AND THEY MUST NOT BE CONFLATED:
--
--   superseded = TRUE     A statement ABOUT THE DOCUMENT. It is withdrawn, or
--                         a newer revision replaced it. Document-control state.
--                         Safety-meaningful. Changing it is an operational act
--                         with a reason and an audit row (005).
--
--   pool_enabled = FALSE  A statement ABOUT THE CORPUS. This document is not
--                         part of the set we are running with. It says nothing
--                         about whether the document is valid or current.
--                         Curation, and — see section 16.13 — demonstration.
--
-- Retrieval requires BOTH: superseded = FALSE AND pool_enabled. That is what
-- makes them orthogonal and safe to expose separately. Re-enabling a withdrawn
-- document in the pool does NOT make it citable again; only a restore does.
-- Somebody curating the pool cannot accidentally resurrect a withdrawn
-- procedure, which is the mistake this separation is here to make impossible.
--
-- If these two ever collapse into one flag, a demo that trimmed the corpus
-- becomes indistinguishable from a document that was withdrawn on purpose.
-- =============================================================================

ALTER TABLE doc_chunks
    ADD COLUMN IF NOT EXISTS pool_enabled BOOLEAN NOT NULL DEFAULT TRUE;

-- The retrieval predicate, as an index. Replaces doc_chunks_live_type as the
-- filter that matches what tools/retrieval.py actually asks for.
CREATE INDEX IF NOT EXISTS doc_chunks_retrievable
    ON doc_chunks (doc_type)
    WHERE superseded = FALSE AND pool_enabled;

-- -----------------------------------------------------------------------------
-- READ THIS BEFORE DISABLING MOST OF THE CORPUS.
--
-- The HNSW index is built over EVERY embedding, superseded and disabled rows
-- included. An approximate scan finds the k nearest vectors and the WHERE
-- clause filters afterwards, so if 90% of the pool is disabled — exactly what a
-- coverage demo does — the scan can come back with almost nothing even though
-- relevant enabled documents exist. The failure looks like "retrieval got
-- worse", which in a demo about corpus size is the single most misleading
-- result available.
--
-- With a corpus this small (thousands of chunks, not millions) the fix is
-- cheap. Either raise the candidate list for the query:
--
--     SET LOCAL hnsw.ef_search = 200;
--
-- or, when the enabled fraction is low, drop to an exact scan for that query:
--
--     SET LOCAL enable_indexscan = off;   -- sequential + exact, milliseconds here
--
-- tools/retrieval.py should do the second automatically below a threshold
-- (POOL_EXACT_SCAN_BELOW_PCT). Measure it before the demo, not during.
-- -----------------------------------------------------------------------------

-- -----------------------------------------------------------------------------
-- doc_pool_profiles — a named set of documents, applied and reverted as one.
--
-- "Show them the answer with three documents, then with all forty-seven" is not
-- forty-four checkbox clicks, and it must be revertible in one action, in front
-- of an audience, without anyone wondering afterwards whether the pool was left
-- trimmed. A profile is a saved selection, not a copy of the documents.
--
-- Profiles are also how a demo avoids touching global state at all: the
-- intended path is a PER-REQUEST override naming a profile (section 16.14),
-- which changes nothing stored and leaks nothing to the operator asking a real
-- question on the same shared host at the same time.
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS doc_pool_profiles (
    profile_id   UUID PRIMARY KEY,
    name         TEXT NOT NULL UNIQUE,   -- 'full', 'procedures-only', 'minimal-3'
    description  TEXT,
    is_builtin   BOOLEAN NOT NULL DEFAULT FALSE,
    created_by   TEXT,
    created_at   TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE IF NOT EXISTS doc_pool_profile_members (
    profile_id   UUID NOT NULL REFERENCES doc_pool_profiles(profile_id) ON DELETE CASCADE,
    source_file  TEXT NOT NULL,
    PRIMARY KEY (profile_id, source_file)
);

CREATE INDEX IF NOT EXISTS doc_pool_members_file
    ON doc_pool_profile_members (source_file);

-- 'full' is the only profile with no members: it means "no restriction", not
-- "no documents". Special-cased in one place in the API and nowhere else.
INSERT INTO doc_pool_profiles (profile_id, name, description, is_builtin)
VALUES ('00000000-0000-0000-0000-000000000001',
        'full',
        'Every enabled, live document. The operational default.',
        TRUE)
ON CONFLICT (name) DO NOTHING;

-- -----------------------------------------------------------------------------
-- Curating the pool is an audited act, like withdrawing.
-- 005 already holds the trail; it gains two more verbs.
-- -----------------------------------------------------------------------------
ALTER TABLE doc_actions DROP CONSTRAINT IF EXISTS doc_actions_action_ck;
ALTER TABLE doc_actions ADD CONSTRAINT doc_actions_action_ck
    CHECK (action IN ('withdraw','restore','purge','pool_disable','pool_enable'));

-- -----------------------------------------------------------------------------
-- pool_status — what the assistant is actually running with right now.
--
-- The number in the corner of the screen. An operator should be able to see
-- that the pool is not whole without asking anybody, and a demo should not be
-- able to hide it.
-- -----------------------------------------------------------------------------
CREATE OR REPLACE VIEW pool_status AS
SELECT count(DISTINCT source_file) FILTER (
           WHERE superseded = FALSE AND pool_enabled)          AS documents_in_pool,
       count(DISTINCT source_file) FILTER (WHERE superseded = FALSE) AS documents_live,
       count(*) FILTER (WHERE superseded = FALSE AND pool_enabled)   AS chunks_in_pool,
       count(*) FILTER (WHERE superseded = FALSE)              AS chunks_live
  FROM doc_chunks;

-- -----------------------------------------------------------------------------
-- pool_documents — the curation screen's list. Every live document, in or out.
-- -----------------------------------------------------------------------------
CREATE OR REPLACE VIEW pool_documents AS
SELECT source_file,
       max(doc_number)       AS doc_number,
       max(revision)         AS revision,
       max(doc_type)         AS doc_type,
       max(effective_date)   AS effective_date,
       count(*)              AS chunk_count,
       bool_and(pool_enabled) AS pool_enabled,
       max(created_at)       AS ingested_at
  FROM doc_chunks
 WHERE superseded = FALSE
 GROUP BY source_file;

-- =============================================================================
-- Grants.
--
-- pool_enabled is NOT a safety flag, so uploads_rw may set it in BOTH
-- directions - unlike `superseded`, where the trigger in 005 allows only
-- withdrawal. Being in the pool is not a claim that a document is current;
-- `superseded` is still the only thing that says that, and it is still the only
-- thing the web-facing role cannot undo.
-- =============================================================================

GRANT UPDATE (pool_enabled) ON doc_chunks TO uploads_rw;
GRANT SELECT, INSERT, UPDATE, DELETE ON doc_pool_profiles TO uploads_rw;
GRANT SELECT, INSERT, UPDATE, DELETE ON doc_pool_profile_members TO uploads_rw;
GRANT SELECT ON doc_pool_profiles, doc_pool_profile_members TO agent_ro, ingest_rw;
GRANT SELECT ON pool_status, pool_documents TO agent_ro, uploads_rw, ingest_rw;

-- A profile is a selection, so deleting one destroys no documents. `full` is
-- protected because a demo that deletes the way back to the operational pool is
-- not a recoverable position in front of an audience.
CREATE OR REPLACE FUNCTION doc_pool_profiles_protect() RETURNS TRIGGER AS $fn$
BEGIN
    IF OLD.is_builtin THEN
        RAISE EXCEPTION 'the % profile is built in and cannot be % ',
            OLD.name, TG_OP;
    END IF;
    RETURN OLD;
END;
$fn$ LANGUAGE plpgsql;

DROP TRIGGER IF EXISTS doc_pool_profiles_protect_tr ON doc_pool_profiles;
CREATE TRIGGER doc_pool_profiles_protect_tr BEFORE DELETE OR UPDATE
    ON doc_pool_profiles
    FOR EACH ROW EXECUTE FUNCTION doc_pool_profiles_protect();

-- =============================================================================
-- Phase 9 gate — the pool half.
--
--   Orthogonality, which is the property the whole file exists for:
--     UPDATE doc_chunks SET pool_enabled = TRUE
--      WHERE superseded = TRUE;              -- allowed, and changes NOTHING:
--     -- the withdrawn document must still not be retrievable. Ask the question
--     -- that used to cite it and confirm it is not cited.
--
--   As uploads_rw:
--     UPDATE doc_chunks SET pool_enabled = FALSE WHERE ...;   -- must work
--     UPDATE doc_chunks SET pool_enabled = TRUE  WHERE ...;   -- must work
--     UPDATE doc_chunks SET superseded  = FALSE WHERE ...;    -- must be REJECTED
--     DELETE FROM doc_pool_profiles WHERE name = 'full';      -- must be REJECTED
--
--   Retrieval, with most of the corpus disabled - the demo case:
--     Disable 90% of documents, then ask a question whose answer is in one of
--     the remaining 10%. It MUST still be found. If it is not, the HNSW note at
--     the top of this file is why, and the fix is there, not in the prompt.
--
--   The banner, which is the part that protects everyone else on this host:
--     With any document out of the pool, EVERY answer must carry the reduced-
--     pool banner, exactly as fixture data does. An answer from a trimmed
--     corpus that looks like an answer from the whole corpus is the failure
--     this feature introduces, and the banner is the whole mitigation.
-- =============================================================================
