KB-5AB9 rev 9

P3D Pack 1 Phase 4 — Vocab Prep Implementation Prompt DRAFT v7

33 min read Revision 9
p3dpack1phase4vocabdraft-v7g3-exact-contracthash-provenance-restored

P3D Pack 1 Phase 4 — Vocab Prep ONLY — Implementation Prompt DRAFT v7

Date: 2026-05-11 | Status: DRAFT v7 — G3 exact contract + hash provenance restore + agent wording fix Supersedes: DRAFT v6 (NOT APPROVED — three precision issues per GPT review) Species mapping: READ-ONLY candidate generation only (no INSERT) Hash provenance: DOCUMENT-ONLY (no executable code; preserves TAC source hash as provenance)

Philosophy:

If not certainly right, it is wrong.
Not hardcoded does not mean "wrapped in a temp table"; it means every value is
derived from a declared authoritative source at execution time, and the prompt
states why that source is authoritative.

0. Scope and authoritative sources

Category Mode in v7 Authoritative source (declared at runtime) Why this source is authoritative
vocab.section_type.* Executable candidate public.tac_logical_unit.section_type DISTINCT non-null values currently used in production TAC rows These are the section_type values that already exist in TAC content and must be acceptable to the IU vocab registry before any TAC→IU reconciliation. The set is determined by production data, not by any prompt-supplied list.
vocab.publication_type.* Executable candidate public.tac_publication.publication_type DISTINCT non-null values currently used Same reasoning — publication_type values used by live publications must be present in IU vocab before reconciliation.
vocab.unit_kind.law_unit Executable candidate Pack 1 EVOLVE contract: TAC logical units become native IU law_unit rows TAC has no unit_kind column; law_unit is a migration-contract value, not a snapshot. The literal law_unit appears in this prompt only as a contract-required identifier, gated by fn_iu_create_plan resolution.
Species mapping (entity_species, species_collection_map) Read-only candidate only n/a — authoritative pairing is unresolved Fuzzy ILIKE '%law%' matching is not production-safe. Candidates are reported for GPT/User decision. No INSERT.
Hash provenance (unit_version.content_profile.source_hashes.tac_v1) Document-only n/a — preserves original TAC tac_unit_version.content_hash as provenance reference; no re-hashing See §7 for the corrected contract. Re-hashing TAC content in the IU layer would lose the link to the original TAC commit; provenance must preserve the source hash by reference.

0.1 Difference from v6 (top 3)

  1. G3 now performs EXACT function-contract verification (not containment): count(*) = 1 (no overload), pg_get_function_identity_arguments(oid) exact-string match against the 9-arg production identity, and prorettype exactly jsonb. v6 only checked that the 9 arg names were a subset of proargnames — v7 is strict.
  2. Section E hash provenance restored to Phase-3 discovery contractsource_hashes.tac_v1 preserves the original tac_unit_version.content_hash as a provenance reference, not a re-computed hash of normalized content. The v6 wording (sha256(normalized_content_of_corresponding_tac_logical_unit)) was a regression to the pre-Phase-3 formulation and has been removed.
  3. Agent-boundary wording corrected in §10 — v6 said "Do not run any agent", which contradicts the fact that this prompt is itself executed by the assigned Agent. v7 says "Do not dispatch nested/secondary agents from inside this task; this prompt itself may be executed only by the assigned Agent after GPT/User approval."

All other v6 mechanics (G1, G2, G4, G5, G6, G7 jsonb logic, delta computation, INSERT/RETURNING, rollback model, species read-only scope, Section C call, single-transaction model) are preserved byte-for-byte. Only probe address/title strings carry the version bump v6 → v7 for log clarity.


1. Hard boundaries

  • No species INSERT, no species_collection_map write, no target-collection row literal.
  • No migration, DDL, function/trigger patch, Directus change, Qdrant/vector touch, collection promotion, edge materialization, birth-gate change.
  • No hardcoded seed list, no fixed row count used as logic, no fuzzy ILIKE '%law%' in executable paths.
  • No temp table created in one psql invocation and referenced from another.
  • No description-prefix rollback as primary rollback. Exact keys only.
  • Vocab INSERT executes only when G1..G6 pass, after dry-run preview, and only commits if G7 passes.

2. Preflight (bash setup)

set -euo pipefail
TS=$(date +%Y%m%d-%H%M%S)
RUN_MARKER="p3d-phase4-vocab-${TS}"
LOG="/tmp/${RUN_MARKER}.log"
exec > >(tee -a "$LOG") 2>&1
PG_CONTAINER="${PG_CONTAINER:-postgres}"
PG_DB="${PG_DB:-directus}"
PG_USER="${PG_USER:-directus}"
PSQL_BASE=(docker exec -i "$PG_CONTAINER" psql -U "$PG_USER" -d "$PG_DB" -v ON_ERROR_STOP=1)

echo "=== PHASE 4 VOCAB v7 — RUN=$RUN_MARKER ==="

ON_ERROR_STOP=1 ensures any server-side RAISE EXCEPTION propagates as a non-zero psql exit code. The bash set -euo pipefail then aborts the script. Combined with the single-transaction model below, this makes any gate failure a clean rollback + script abort.


3. Section A — Read-only preflight inspection (no temp tables, separate psql calls OK because read-only)

These calls only print state for the agent and report. They do not create temp tables, so they may be in independent psql sessions without consequence.

# A1: dot_config schema snapshot
"${PSQL_BASE[@]}" <<'SQL'
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'dot_config'
ORDER BY ordinal_position;
SQL

# A2: dot_config constraints/indexes snapshot
"${PSQL_BASE[@]}" <<'SQL'
SELECT conname, contype, pg_get_constraintdef(oid) AS def
FROM pg_constraint
WHERE conrelid = 'public.dot_config'::regclass
ORDER BY conname;

SELECT indexname, indexdef
FROM pg_indexes
WHERE schemaname = 'public' AND tablename = 'dot_config'
ORDER BY indexname;
SQL

# A3: source tables snapshot
"${PSQL_BASE[@]}" <<'SQL'
SELECT relname
FROM pg_class
WHERE relname IN ('dot_config','tac_logical_unit','tac_publication')
  AND relkind = 'r'
  AND relnamespace = 'public'::regnamespace
ORDER BY relname;
SQL

# A4: current vocab.* state
"${PSQL_BASE[@]}" <<'SQL'
SELECT key, value
FROM public.dot_config
WHERE key LIKE 'vocab.%'
ORDER BY key;
SQL

# A5: read-only delta preview (no temp tables persisted)
"${PSQL_BASE[@]}" <<'SQL'
WITH delta AS (
  SELECT 'section_type'     AS category,
         tlu.section_type   AS code,
         'vocab.section_type.' || tlu.section_type AS target_key
  FROM   public.tac_logical_unit tlu
  WHERE  tlu.section_type IS NOT NULL
    AND  NOT EXISTS (
           SELECT 1 FROM public.dot_config dc
           WHERE dc.key = 'vocab.section_type.' || tlu.section_type
         )
  GROUP BY tlu.section_type
  UNION ALL
  SELECT 'publication_type',
         tp.publication_type,
         'vocab.publication_type.' || tp.publication_type
  FROM   public.tac_publication tp
  WHERE  tp.publication_type IS NOT NULL
    AND  NOT EXISTS (
           SELECT 1 FROM public.dot_config dc
           WHERE dc.key = 'vocab.publication_type.' || tp.publication_type
         )
  GROUP BY tp.publication_type
  UNION ALL
  SELECT 'unit_kind', 'law_unit', 'vocab.unit_kind.law_unit'
  WHERE  NOT EXISTS (
           SELECT 1 FROM public.dot_config dc
           WHERE dc.key = 'vocab.unit_kind.law_unit'
         )
)
SELECT * FROM delta ORDER BY category, code;
SQL

Agent must include A1..A5 output verbatim in the report. If A5 surfaces any value that does not look like a legitimate production identifier (e.g. obvious garbage), STOP and report; do not run Section B.


4. Section B — Executable vocab transaction (SINGLE psql call, SINGLE transaction)

This is the only section that writes. It must run as one psql invocation. Every temp table, every gate, every INSERT, and the planner verification execute inside one BEGIN..COMMIT. Any RAISE EXCEPTION rolls everything back automatically before commit.

"${PSQL_BASE[@]}" -v run_marker="$RUN_MARKER" <<'SQL'
BEGIN;
SET LOCAL lock_timeout      = '5s';
SET LOCAL statement_timeout = '120s';
SET LOCAL p3d.run_marker    = :'run_marker';

-- =========================================================================
-- G1 (executable) — dot_config has exact required columns
--   Authoritative source: information_schema.columns
-- =========================================================================
DO $$
DECLARE
  required text[] := ARRAY['key','value','description','updated_at'];
  col      text;
  missing  text[] := ARRAY[]::text[];
BEGIN
  FOREACH col IN ARRAY required LOOP
    IF NOT EXISTS (
      SELECT 1 FROM information_schema.columns
      WHERE table_schema = 'public'
        AND table_name   = 'dot_config'
        AND column_name  = col
    ) THEN
      missing := missing || col;
    END IF;
  END LOOP;
  IF array_length(missing,1) IS NOT NULL THEN
    RAISE EXCEPTION 'G1_FAIL dot_config missing required columns: %', missing;
  END IF;
  RAISE NOTICE 'G1_PASS dot_config has columns (key,value,description,updated_at)';
END $$;

-- =========================================================================
-- G2 (executable) — single-column UNIQUE/PK on dot_config(key)
--   Authoritative source: pg_constraint + pg_index
--   Required for ON CONFLICT (key) to be deterministic.
-- =========================================================================
DO $$
DECLARE
  has_unique boolean;
BEGIN
  SELECT
    EXISTS (
      SELECT 1
      FROM pg_constraint c
      JOIN pg_attribute  a
        ON a.attrelid = c.conrelid
       AND a.attnum   = ANY (c.conkey)
      WHERE c.conrelid = 'public.dot_config'::regclass
        AND c.contype IN ('u','p')
        AND array_length(c.conkey,1) = 1
        AND a.attname = 'key'
    )
    OR EXISTS (
      SELECT 1
      FROM pg_index ix
      WHERE ix.indrelid    = 'public.dot_config'::regclass
        AND ix.indisunique
        AND ix.indnkeyatts = 1
        AND (
          SELECT a.attname
          FROM pg_attribute a
          WHERE a.attrelid = ix.indrelid
            AND a.attnum   = ix.indkey[0]
        ) = 'key'
    )
  INTO has_unique;

  IF NOT has_unique THEN
    RAISE EXCEPTION 'G2_FAIL no UNIQUE/PK constraint or single-column unique index on dot_config(key); ON CONFLICT (key) is unsafe';
  END IF;
  RAISE NOTICE 'G2_PASS dot_config(key) is uniquely indexed';
END $$;

-- =========================================================================
-- G3 (executable) — source tables + EXACT planner function CONTRACT
--   v7 change: G3b now performs exact identity verification:
--     1. exactly one function public.fn_iu_create_plan (no overload);
--     2. pg_get_function_identity_arguments(oid) matches the expected
--        9-arg production identity string verbatim;
--     3. return type is exactly jsonb.
--   This is strictly stronger than v6's containment check on proargnames.
--
--   Expected identity (production verified post-v5 dispatch):
--     p_canonical_address text, p_title text, p_body text, p_actor text,
--     p_unit_kind text, p_section_type text, p_owner_ref text,
--     p_publication_type text, p_parent_ref uuid
--
--   Expected return type: jsonb
-- =========================================================================
DO $$
DECLARE
  required_tables text[] := ARRAY['dot_config','tac_logical_unit','tac_publication'];
  t       text;
  missing text[] := ARRAY[]::text[];

  v_proc_count           int;
  v_oid                  oid;
  v_identity_args        text;
  v_rettype              text;

  expected_identity_args constant text :=
    'p_canonical_address text, p_title text, p_body text, p_actor text, '
    || 'p_unit_kind text, p_section_type text, p_owner_ref text, '
    || 'p_publication_type text, p_parent_ref uuid';

  expected_rettype       constant text := 'jsonb';
BEGIN
  -- G3a: required source tables exist
  FOREACH t IN ARRAY required_tables LOOP
    IF NOT EXISTS (
      SELECT 1 FROM pg_class
      WHERE relname      = t
        AND relkind      = 'r'
        AND relnamespace = 'public'::regnamespace
    ) THEN
      missing := missing || t;
    END IF;
  END LOOP;
  IF array_length(missing,1) IS NOT NULL THEN
    RAISE EXCEPTION 'G3_FAIL missing source tables: %', missing;
  END IF;

  -- G3b.1: exactly one function named public.fn_iu_create_plan (no overload).
  --        Overload would make G7's named-arg call ambiguous.
  SELECT count(*), max(p.oid)
  INTO   v_proc_count, v_oid
  FROM   pg_proc p
  JOIN   pg_namespace n ON n.oid = p.pronamespace
  WHERE  n.nspname = 'public'
    AND  p.proname = 'fn_iu_create_plan';

  IF v_proc_count = 0 THEN
    RAISE EXCEPTION 'G3_FAIL function public.fn_iu_create_plan not found';
  END IF;

  IF v_proc_count > 1 THEN
    RAISE EXCEPTION
      'G3_FAIL public.fn_iu_create_plan has % overload(s); G7 named-arg call would be ambiguous',
      v_proc_count;
  END IF;

  -- G3b.2: exact identity-arguments string match (positional + types + names)
  v_identity_args := pg_get_function_identity_arguments(v_oid);
  IF v_identity_args <> expected_identity_args THEN
    RAISE EXCEPTION
      'G3_FAIL fn_iu_create_plan identity arguments mismatch (oid=%); expected="%"; actual="%"',
      v_oid, expected_identity_args, v_identity_args;
  END IF;

  -- G3b.3: exact return type
  SELECT t.typname
  INTO   v_rettype
  FROM   pg_proc p
  JOIN   pg_type t ON t.oid = p.prorettype
  WHERE  p.oid = v_oid;

  IF v_rettype <> expected_rettype THEN
    RAISE EXCEPTION
      'G3_FAIL fn_iu_create_plan return type mismatch (oid=%); expected="%"; actual="%"',
      v_oid, expected_rettype, v_rettype;
  END IF;

  RAISE NOTICE
    'G3_PASS source tables present; fn_iu_create_plan oid=% has exact 9-arg identity and returns jsonb',
    v_oid;
END $$;

-- =========================================================================
-- G4 (executable) — required source columns exist on TAC tables
-- =========================================================================
DO $$
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM information_schema.columns
    WHERE table_schema='public' AND table_name='tac_logical_unit' AND column_name='section_type'
  ) THEN
    RAISE EXCEPTION 'G4_FAIL tac_logical_unit.section_type missing';
  END IF;
  IF NOT EXISTS (
    SELECT 1 FROM information_schema.columns
    WHERE table_schema='public' AND table_name='tac_publication' AND column_name='publication_type'
  ) THEN
    RAISE EXCEPTION 'G4_FAIL tac_publication.publication_type missing';
  END IF;
  RAISE NOTICE 'G4_PASS source columns present';
END $$;

-- =========================================================================
-- G6 (executable) — run_marker GUC is set and non-empty
--   (G5 is value-conflict gate, computed after delta tables exist; see below.)
-- =========================================================================
DO $$
DECLARE
  rm text := current_setting('p3d.run_marker', true);
BEGIN
  IF rm IS NULL OR length(btrim(rm)) = 0 THEN
    RAISE EXCEPTION 'G6_FAIL run_marker GUC empty; bash must pass -v run_marker=...';
  END IF;
  RAISE NOTICE 'G6_PASS run_marker=%', rm;
END $$;

-- =========================================================================
-- Delta temp tables (live only inside THIS transaction)
--
-- Authoritative sources:
--   section_type     <- live public.tac_logical_unit.section_type (DISTINCT, NOT NULL)
--   publication_type <- live public.tac_publication.publication_type (DISTINCT, NOT NULL)
--   unit_kind        <- Pack 1 EVOLVE contract: native IU 'law_unit'
--                       (NOT a TAC-discovered value; declared as contract requirement)
-- =========================================================================
CREATE TEMP TABLE _delta_section_type ON COMMIT DROP AS
SELECT DISTINCT tlu.section_type AS code
FROM   public.tac_logical_unit tlu
WHERE  tlu.section_type IS NOT NULL
  AND  NOT EXISTS (
         SELECT 1 FROM public.dot_config dc
         WHERE dc.key = 'vocab.section_type.' || tlu.section_type
       );

CREATE TEMP TABLE _delta_pub_type ON COMMIT DROP AS
SELECT DISTINCT tp.publication_type AS code
FROM   public.tac_publication tp
WHERE  tp.publication_type IS NOT NULL
  AND  NOT EXISTS (
         SELECT 1 FROM public.dot_config dc
         WHERE dc.key = 'vocab.publication_type.' || tp.publication_type
       );

CREATE TEMP TABLE _delta_unit_kind ON COMMIT DROP AS
SELECT 'law_unit'::text AS code
WHERE  NOT EXISTS (
         SELECT 1 FROM public.dot_config dc
         WHERE dc.key = 'vocab.unit_kind.law_unit'
       );

-- =========================================================================
-- G5 (executable) — no delta key already exists with a DIFFERENT value
--   Defence in depth against race conditions between delta and insert.
--   Delta WHERE clauses already exclude existing keys; G5 only fires if
--   a concurrent writer added a conflicting row between delta and gate.
-- =========================================================================
DO $$
DECLARE
  v_conflicts int;
BEGIN
  WITH delta AS (
    SELECT 'vocab.section_type.'     || code AS k, code AS v FROM _delta_section_type
    UNION ALL
    SELECT 'vocab.publication_type.' || code,      code      FROM _delta_pub_type
    UNION ALL
    SELECT 'vocab.unit_kind.'        || code,      code      FROM _delta_unit_kind
  )
  SELECT count(*)
  INTO   v_conflicts
  FROM   delta d
  JOIN   public.dot_config dc ON dc.key = d.k
  WHERE  dc.value IS DISTINCT FROM d.v;

  IF v_conflicts > 0 THEN
    RAISE EXCEPTION 'G5_FAIL % delta key(s) already exist in dot_config with a DIFFERENT value', v_conflicts;
  END IF;
  RAISE NOTICE 'G5_PASS no value conflict between delta and existing dot_config rows';
END $$;

-- =========================================================================
-- Dry-run preview (informational; same transaction)
-- =========================================================================
SELECT 'section_type'    AS category, code, 'vocab.section_type.'    || code AS target_key FROM _delta_section_type
UNION ALL
SELECT 'publication_type',           code, 'vocab.publication_type.' || code               FROM _delta_pub_type
UNION ALL
SELECT 'unit_kind',                  code, 'vocab.unit_kind.'        || code               FROM _delta_unit_kind
ORDER BY category, code;

SELECT
  (SELECT count(*) FROM _delta_section_type) AS delta_section_type,
  (SELECT count(*) FROM _delta_pub_type)     AS delta_publication_type,
  (SELECT count(*) FROM _delta_unit_kind)    AS delta_unit_kind;

-- =========================================================================
-- INSERT from deltas; capture exact inserted keys via RETURNING in same txn
-- =========================================================================
CREATE TEMP TABLE _inserted_keys (
  category text NOT NULL,
  key      text PRIMARY KEY,
  value    text NOT NULL
) ON COMMIT DROP;

WITH ins AS (
  INSERT INTO public.dot_config (key, value, description, updated_at)
  SELECT 'vocab.section_type.' || code,
         code,
         current_setting('p3d.run_marker'),
         now()
  FROM   _delta_section_type
  ON CONFLICT (key) DO NOTHING
  RETURNING key, value
)
INSERT INTO _inserted_keys (category, key, value)
SELECT 'section_type', key, value FROM ins;

WITH ins AS (
  INSERT INTO public.dot_config (key, value, description, updated_at)
  SELECT 'vocab.publication_type.' || code,
         code,
         current_setting('p3d.run_marker'),
         now()
  FROM   _delta_pub_type
  ON CONFLICT (key) DO NOTHING
  RETURNING key, value
)
INSERT INTO _inserted_keys (category, key, value)
SELECT 'publication_type', key, value FROM ins;

WITH ins AS (
  INSERT INTO public.dot_config (key, value, description, updated_at)
  SELECT 'vocab.unit_kind.' || code,
         code,
         current_setting('p3d.run_marker'),
         now()
  FROM   _delta_unit_kind
  ON CONFLICT (key) DO NOTHING
  RETURNING key, value
)
INSERT INTO _inserted_keys (category, key, value)
SELECT 'unit_kind', key, value FROM ins;

-- Print exact inserted keys BEFORE commit.
-- Agent MUST capture this output verbatim into the report; it is the
-- ONLY authoritative input for the rollback procedure (Section F).
SELECT category, key, value FROM _inserted_keys ORDER BY category, key;
SELECT count(*) AS total_inserted FROM _inserted_keys;

-- =========================================================================
-- G7 (executable) — planner must resolve law_unit (no unresolved_vocab)
--   v6/v7 unchanged: calls production 9-arg signature returning jsonb.
--   p_section_type and p_publication_type are derived LIVE from dot_config
--   (post-insert snapshot within this transaction).
--   p_actor uses the run_marker. p_parent_ref is NULL::uuid.
--   Planner call wrapped in BEGIN/EXCEPTION for clear G7_FAIL labelling.
-- =========================================================================
DO $$
DECLARE
  v_plan             jsonb;
  v_section_type     text;
  v_publication_type text;
  v_status           text;
  v_resolved_kind    text;
BEGIN
  -- Derive probe args from live dot_config (no hardcode)
  SELECT value INTO v_section_type
  FROM   public.dot_config
  WHERE  key LIKE 'vocab.section_type.%'
  ORDER  BY key LIMIT 1;

  SELECT value INTO v_publication_type
  FROM   public.dot_config
  WHERE  key LIKE 'vocab.publication_type.%'
  ORDER  BY key LIMIT 1;

  -- Call planner with production 9-arg contract
  BEGIN
    v_plan := public.fn_iu_create_plan(
      p_canonical_address := 'planner-probe/p3d-phase4-v7-' || current_setting('p3d.run_marker'),
      p_title             := 'Phase 4 v7 planner probe',
      p_body              := 'Probe body',
      p_actor             := current_setting('p3d.run_marker'),
      p_unit_kind         := 'law_unit',
      p_section_type      := v_section_type,
      p_owner_ref         := NULL,
      p_publication_type  := v_publication_type,
      p_parent_ref        := NULL::uuid
    );
  EXCEPTION WHEN OTHERS THEN
    RAISE EXCEPTION 'G7_FAIL planner call exception: % [SQLSTATE %]', SQLERRM, SQLSTATE;
  END;

  -- Validate jsonb structure before reading keys
  IF v_plan IS NULL THEN
    RAISE EXCEPTION 'G7_FAIL planner returned NULL';
  END IF;

  IF jsonb_typeof(v_plan) <> 'object' THEN
    RAISE EXCEPTION 'G7_FAIL planner returned non-object jsonb (typeof=%); plan=%',
      jsonb_typeof(v_plan), v_plan;
  END IF;

  IF NOT (v_plan ? 'status') THEN
    RAISE EXCEPTION 'G7_FAIL planner jsonb missing "status" key; plan=%', v_plan;
  END IF;

  IF NOT (v_plan ? 'resolved_unit_kind') THEN
    RAISE EXCEPTION 'G7_FAIL planner jsonb missing "resolved_unit_kind" key; plan=%', v_plan;
  END IF;

  -- Assert vocab resolution succeeded
  v_status := v_plan ->> 'status';
  IF v_status ILIKE '%unresolved_vocab%' THEN
    RAISE EXCEPTION 'G7_FAIL planner reports unresolved_vocab; plan=%', v_plan;
  END IF;

  v_resolved_kind := v_plan ->> 'resolved_unit_kind';
  IF v_resolved_kind IS DISTINCT FROM 'law_unit' THEN
    RAISE EXCEPTION 'G7_FAIL planner resolved_unit_kind=% (expected law_unit); plan=%',
      v_resolved_kind, v_plan;
  END IF;

  RAISE NOTICE 'G7_PASS status=% resolved_unit_kind=% plan=%',
    v_status, v_resolved_kind, v_plan;
END $$;

COMMIT;
SQL

If any gate raises an exception, the entire transaction rolls back and no dot_config row changes. The script exits non-zero (because of ON_ERROR_STOP=1 plus set -e); no further sections run.


5. Section C — Post-commit verification (read-only, separate psql call)

"${PSQL_BASE[@]}" -v run_marker="$RUN_MARKER" <<'SQL'
-- Persisted rows from THIS run
SELECT key, value, updated_at
FROM   public.dot_config
WHERE  description = :'run_marker'
ORDER  BY key;

-- Full vocab.* state after run
SELECT key, value
FROM   public.dot_config
WHERE  key LIKE 'vocab.%'
ORDER  BY key;

-- Standalone planner probe (post-commit sanity, 9-arg jsonb contract)
SELECT public.fn_iu_create_plan(
  p_canonical_address := 'planner-probe/p3d-phase4-v7-postcommit',
  p_title             := 'Phase 4 v7 post-commit probe',
  p_body              := 'Probe body',
  p_actor             := :'run_marker',
  p_unit_kind         := 'law_unit',
  p_section_type      := (SELECT value FROM public.dot_config
                          WHERE key LIKE 'vocab.section_type.%' ORDER BY key LIMIT 1),
  p_owner_ref         := NULL,
  p_publication_type  := (SELECT value FROM public.dot_config
                          WHERE key LIKE 'vocab.publication_type.%' ORDER BY key LIMIT 1),
  p_parent_ref        := NULL::uuid
) AS planner_result;
SQL

Agent appends C1..C3 output to the report.


6. Section D — Species READ-ONLY candidate report (no INSERT)

"${PSQL_BASE[@]}" <<'SQL'
-- D1: active species catalog
SELECT species_code, species_name, composition_level, lifecycle_status
FROM   public.entity_species
WHERE  lifecycle_status = 'active'
ORDER  BY species_code;

-- D2: current species_collection_map state (no filter to specific names)
SELECT scm.collection_name, scm.species_code, scm.composition_level
FROM   public.species_collection_map scm
JOIN   public.collection_registry  cr ON cr.collection_name = scm.collection_name
ORDER  BY scm.collection_name;

-- D3: governed/observed collections without species_code
SELECT collection_name, governance_role, migration_state, species_code, birth_code_strategy
FROM   public.collection_registry
WHERE  species_code IS NULL
  AND  governance_role IN ('observed','governed')
ORDER  BY collection_name;
SQL

Status flags the agent must write into the report verbatim:

species_mapping_executable=false
species_mapping_requires_separate_GPT_User_approved_prompt=true
fuzzy_match_in_executable_path=false

No species_collection_map INSERT. No target-collection row literal. No subordinate literal. No ILIKE '%law%' in executable logic. D1..D3 are candidate reporting only; final mapping is decided by GPT + User in a separate, future prompt.


7. Section E — Hash provenance (DOCUMENT-ONLY)

No executable code in v7. This section documents the Phase-3-discovered contract for a future migration phase.

7.1 Provenance contract (corrected from v5/v6 regression)

The IU layer preserves the original TAC source hash as a provenance reference. It does NOT re-hash normalized content in the IU layer — re-hashing would lose the link to the original TAC commit and destroy the provenance trail.

unit_version.content_profile.source_hashes.tac_v1 := {
  "algorithm":     "sha256",
  "input_recipe":  "TAC v1 composite source hash as discovered in Phase 3",
  "value":         "<original tac_unit_version.content_hash>",
  "source_table":  "tac_unit_version",
  "source_id":     "<source tac_unit_version id>"
}

7.2 Field semantics

Field Meaning
algorithm Hash algorithm used by TAC (sha256). Recorded for forward compatibility if TAC ever rotates algorithms.
input_recipe Human-readable description of WHAT was hashed in TAC v1 (the "composite source hash" recipe discovered in Phase 3).
value The actual hash digest — copied verbatim from tac_unit_version.content_hash of the source row. Not re-computed.
source_table The TAC table the hash came from (tac_unit_version).
source_id The primary key of the source TAC row, enabling round-trip lookup.

7.3 Why preservation, not re-hashing

  • Provenance integrity. The TAC hash is the immutable fingerprint of the TAC commit. Re-hashing in IU would produce a different value that cannot be cross-referenced with TAC history.
  • No content normalization drift. TAC's hash recipe (Phase-3 discovery) may differ from any IU normalization. If IU re-normalizes and re-hashes, the values diverge silently.
  • Forward compatibility. When a future migration adds a tac_v2 hash family, both tac_v1 and tac_v2 provenance entries can coexist without re-computing legacy values.

7.4 Out of scope for this prompt

The target field unit_version.content_profile.source_hashes.tac_v1 is not provisioned in the current schema. No DDL, no backfill, no migration occurs in this prompt. This section records the contract so the next phase can implement it without re-deriving the rule.

Status flag:

hash_provenance_document_only=true
hash_provenance_contract=preserves_original_tac_unit_version_content_hash

8. Section F — Rollback procedure (EXACT KEYS ONLY)

Rollback is allowed only after Section B has committed and the agent has the exact inserted-keys list from the report (printed by the SELECT category, key, value FROM _inserted_keys statement before COMMIT).

"${PSQL_BASE[@]}" -v run_marker="$RUN_MARKER" <<'SQL'
-- PRIMARY rollback: delete by EXACT keys captured in the report.
-- Replace the ARRAY[...] literal with the verbatim key list from the report.
DELETE FROM public.dot_config
WHERE key = ANY(ARRAY[
  -- 'vocab.section_type.<code>',
  -- 'vocab.publication_type.<code>',
  -- 'vocab.unit_kind.law_unit'
]::text[]);

-- Cross-check (NOT a rollback predicate, only verification):
-- after the exact-key DELETE, no row should remain with this run_marker.
SELECT count(*) AS leftover_with_marker
FROM   public.dot_config
WHERE  description = :'run_marker';
-- Expect: 0
SQL

Forbidden rollback patterns in v7:

-- FORBIDDEN: prefix/description pattern as primary rollback
DELETE FROM public.dot_config WHERE description LIKE 'p3d-phase4-vocab-%';
DELETE FROM public.dot_config WHERE description = :'run_marker';  -- not primary

Description-equality is allowed only as the cross-check shown above (must return zero after the exact-key DELETE). It is never the deletion predicate by itself.


9. Report requirements

Save to:

knowledge/dev/laws/dieu44-trien-khai/reports/p3d-pack1-phase4-vocab-species-seed-implementation-report.md

Mandatory contents:

  1. RUN_MARKER value.
  2. Section A output (A1..A5) verbatim.
  3. Section B notices: which gates emitted Gn_PASS. If any gate failed, transcript of the RAISE EXCEPTION line and "rolled back, no dot_config change".
  4. Section B dry-run preview output (categories, codes, target keys, delta counts).
  5. Section B inserted-keys output (_inserted_keys rows, total_inserted).
  6. Section B G7 planner-probe result: the full v_plan jsonb printed in G7_PASS notice.
  7. Section C output (C1 persisted rows, C2 full vocab state, C3 post-commit planner probe jsonb).
  8. Section D output (D1..D3) plus the three status flags from §6.
  9. Status flag block (verbatim):
phase4_draft_v7_executed=true|false
vocab_executable_committed=true|false
single_session_transaction=true
executable_stop_gates=true
temp_table_cross_session_bug_present=false
hardcoded_seed_list_present=false
species_mapping_executable=false
hash_provenance_document_only=true
hash_provenance_contract=preserves_original_tac_unit_version_content_hash
requires_GPT_User_review_before_dispatch=true
  1. Exact-key list for any future rollback (verbatim from _inserted_keys).

10. Out of scope (do not do)

  • Do not dispatch nested/secondary agents from inside this task; this prompt itself may be executed only by the assigned Agent after GPT/User approval.
  • Do not patch any function or trigger.
  • Do not touch Directus, Qdrant, collection_registry write, species_collection_map write, or birth-gate logic.
  • Do not retry a failed gate by editing the prompt; report the failure and stop.
  • Do not pre-compute a vocab list and paste it into the prompt — every value must come from a live SQL source declared in §0.
  • Do not re-compute or normalize TAC content hashes in the IU layer (see §7); preserve the original tac_unit_version.content_hash by reference only.

DRAFT v7 | G3 exact contract + hash provenance preserved + agent wording corrected | Single-session transaction | Executable gates | Exact-key rollback | No hardcode | 2026-05-11

Back to Knowledge Hub knowledge/dev/laws/dieu44-trien-khai/prompts/p3d-pack1-phase4-governance-vocab-species-prep-implementation-prompt-DRAFT.md