Rename legal_entitytenant (and old tenantorganization) Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Atomically rename the DB tables, columns, FK/index/constraint names, trigger-function payload keys, Malli registry keys, Clojure namespaces, routes, and UI strings such that the operational unit (VAT-holding company) is called tenant and the billing/membership grouping is called organization, with no behavior change. Then drop legal_entity.accounting_system and the accounting_system enum (the only real scope cleanup that still applies — see "Spec discrepancies" below).

Architecture: Single atomic migration pair (.up.sql + .down.sql, both wrapped in a transaction) + a coordinated code flip on the same branch. Stage code changes in two phases to keep the branch compilable at each commit: first old-tenantorganization, then legal_entitytenant. ALTER TABLE ... RENAME is metadata-only in Postgres, so migration cost is in ordering (avoid name collisions) and the code diff.

Tech Stack: PostgreSQL (ALTER TABLE/INDEX/CONSTRAINT RENAME, jsonb_set, CREATE OR REPLACE FUNCTION), Clojure/HoneySQL, next.jdbc, Reitit, Malli, Integrant, Hiccup.


Spec discrepancies discovered during plan authoring (2026-04-24)

Investigation of the live schema (psql against local dev DB) revealed that several claims in the spec do not reflect the current DB. These are locked-in decisions for this plan — if any turn out to be wrong, resolve inline rather than blocking.

  1. Scope cleanup on audit/stats tables is a no-op. ap_acquisition_llm_stat, ap_datev_export_audit, and ap_qa_dataset_item each carry only legal_entity_id today — NO redundant organization_id / old-sense tenant_id column exists on any of them. They will be renamed (legal_entity_idtenant_id) with no extra column drops. The spec's "replace acquisition_llm_stat.organization_id with tenant_id FK" item reduces to a plain rename.
  2. accounting_system column is on legal_entity, not on old tenant. Post-rename naming: drop is tenant.accounting_system, NOT organization.accounting_system. The enum type accounting_system is referenced only by this one column, so DROP TYPE accounting_system is safe after the column drop.
  3. Extra tables carrying legal_entity_id not listed in the spec: fpna_data_map, booking_history_item, ap_supplier_verification, notification_routing. All get the same treatment: legal_entity_idtenant_id.
  4. Spec names notification_workflow / notification_workflow_channel; actual tables are notification_routing / notification_routing_channel. No workflow-named tables exist. The child table notification_routing_channel does NOT carry legal_entity_id (only parent routing_id FK).
  5. notification_channel_teams.tenant_id is a TEXT column (Microsoft Teams tenant ID — external identifier). DO NOT rename it. It is unrelated to our tenant table and to this refactor.
  6. Historic pkey/index collisions from prior renames. The current tenant table's pkey is tenant_pkey1 and its slug index/check are tenant_slug_key1 / tenant_slug_format / idx_tenant_slug; legal_entity's pkey is literally named tenant_pkey. The migration must rename these to the target names (organization_pkey, tenant_pkey on the new tenant) in the correct order to avoid collisions.
  7. legal_entity.fpna_data_source (jsonb) is not listed in the spec's JSONB audit candidates. Add it to the audit.

File Structure

DB migration (one pair)

Use the actual timestamp at commit time (UTC YYYYMMDDHHMMSS). These files hold the complete DB change: table renames, column renames, constraint/index renames, trigger-function bodies, JSONB UPDATE statements (derived from the audit), and the accounting_system drop. Both are wrapped in a single transaction (BEGIN; ... COMMIT; — the migration runner does NOT wrap automatically; see existing migration files in the directory for the convention).

Clojure namespace renames (old sense → new sense)

Old path New path
src/com/getorcha/schema/tenant.clj (old sense, grouping) src/com/getorcha/schema/organization.clj
src/com/getorcha/schema/legal_entity.clj src/com/getorcha/schema/tenant.clj
src/com/getorcha/admin/http/tenants.clj src/com/getorcha/admin/http/organizations.clj
src/com/getorcha/admin/http/tenants/file_store.clj src/com/getorcha/admin/http/organizations/file_store.clj
src/com/getorcha/admin/http/tenants/prompt_customizations.clj src/com/getorcha/admin/http/organizations/prompt_customizations.clj
Test namespaces mirroring all of the above same structure under test/

Use git mv so blame is preserved. Any other file whose name embeds legal_entity or old-sense tenant is discovered and renamed during implementation (see enumeration step in Task 6).

Documentation


Commit convention

Frequent commits. One logical unit per commit. Suggested messages:

Follow existing commit style (type(scope): subject) observed in git log --oneline -20.


Task 1: Enumerate current schema state and produce an audit file

Purpose: Freeze the ground-truth list of columns, constraints, and indexes so every later task can be checked against the same reference.

Files:

Run:

psql -h localhost -U postgres -d orcha -c "SELECT table_name, column_name FROM information_schema.columns WHERE table_schema='public' AND column_name IN ('tenant_id','legal_entity_id') ORDER BY table_name;"

Expected (as of 2026-04-24 dev DB, use this as reference — re-run to confirm on your DB):

ap_acquisition_llm_stat           | legal_entity_id
ap_datev_export_audit             | legal_entity_id
ap_doc_source                     | legal_entity_id
ap_qa_dataset_item                | legal_entity_id
ap_supplier_verification          | legal_entity_id
api_key                           | legal_entity_id
api_request_log                   | legal_entity_id
booking_history_item              | legal_entity_id
booking_history_upload            | legal_entity_id
business_partner_dataset          | legal_entity_id
cost_center_dataset               | legal_entity_id
datev_rewe_link                   | legal_entity_id
document                          | legal_entity_id
fpna_data_map                     | legal_entity_id
gl_accounts_dataset               | legal_entity_id
legal_entity                      | tenant_id         ← old-sense tenant_id (→ organization_id)
legal_entity_datev_integration    | legal_entity_id
legal_entity_oauth_integration    | legal_entity_id
legal_entity_prompt_customization | legal_entity_id
notification_channel              | legal_entity_id
notification_channel_teams        | tenant_id         ← EXTERNAL Teams tenant ID (TEXT), DO NOT RENAME
notification_routing              | legal_entity_id
tenant_membership                 | tenant_id         ← old-sense (→ organization_id)

Write this output verbatim to dump/rename-audit.md under section "Columns to rename".

Run:

psql -h localhost -U postgres -d orcha -c "SELECT conname, conrelid::regclass AS table_name, pg_get_constraintdef(oid) AS def FROM pg_constraint WHERE contype='f' AND (conname LIKE '%tenant%' OR conname LIKE '%legal_entity%') ORDER BY table_name, conname;"

Write output to dump/rename-audit.md under "FK constraints".

Run:

psql -h localhost -U postgres -d orcha -c "SELECT schemaname, tablename, indexname FROM pg_indexes WHERE schemaname='public' AND (indexname LIKE '%tenant%' OR indexname LIKE '%legal_entity%') ORDER BY tablename, indexname;"

Write output to dump/rename-audit.md under "Indexes".

Run:

psql -h localhost -U postgres -d orcha -c "SELECT proname, pg_get_functiondef(p.oid) AS def FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname='public' AND (prosrc LIKE '%legal_entity%' OR prosrc LIKE '%tenant_id%') ORDER BY proname;"

The pg_proc enumeration is authoritative — include every function the query returns, do NOT rely on a hand-maintained list. Known-present functions (migrations confirmed to have added them): notify_datev_export_event (from 20260219091057-squashed-to-v1-963), notify_document_classified (from 20260310200000-add-document-classified-trigger), notify_ingestion_event (from the squash), and a matching-event notifier from 20260302194439-add-matching-event-trigger. There may be more. Copy the full function bodies into dump/rename-audit.md under "Trigger functions" — these are the source material for the CREATE OR REPLACE FUNCTION blocks in the migration.

Run:

psql -h localhost -U postgres -d orcha -c "SELECT table_name, column_name FROM information_schema.columns WHERE data_type IN ('jsonb','json') AND table_schema='public' ORDER BY table_name, column_name;"

Write to dump/rename-audit.md under "JSONB columns (audit targets)".

For each column {table}.{col} from Step 5, run:

psql -h localhost -U postgres -d orcha -c "SELECT COUNT(*) AS rows_with_hit FROM {table} WHERE {col}::text ~ 'legal_entity_id|tenant_id';"

For any column with rows_with_hit > 0, run a follow-up sampling:

psql -h localhost -U postgres -d orcha -c "SELECT {col} FROM {table} WHERE {col}::text ~ 'legal_entity_id|tenant_id' LIMIT 3;"

Record findings in dump/rename-audit.md under "JSONB audit findings". For each hit, note the JSONB path (e.g. $.permissions[*].legal_entity_id) — this is the source material for the jsonb_set / UPDATE statements in the migration.

Expected high-probability hits (confirm or falsify):


Task 2: Write .up.sql migration

Files:

Use the current UTC timestamp for YYYYMMDDHHMMSS. Look at the most recent migration file for reference:

ls -1 resources/migrations/ | sort | tail -5
-- Rename: legal_entity → tenant, old tenant → organization.
-- Also drops legal_entity.accounting_system (and the accounting_system enum type).
--
-- See docs/superpowers/specs/2026-04-24-rename-legal-entity-to-tenant-design.md
-- See docs/superpowers/plans/2026-04-24-rename-legal-entity-to-tenant.md
--
-- Ordering matters. The column rename passes (tenant_id → organization_id, then
-- legal_entity_id → tenant_id) MUST run in that order to avoid collisions.
-- Likewise, the old `tenant` table must be renamed to `organization` BEFORE
-- `legal_entity` is renamed to `tenant`.
--
-- The `.down.sql` path is asymmetric on the `accounting_system` column: values
-- are lost when the column is dropped here, and `.down.sql` recreates the
-- column NULLABLE without them. We accept this.

BEGIN;

-- (statements go here)

COMMIT;

Append:

-- Phase 1: rename old-sense `tenant_id` columns to `organization_id`.
-- Only two tables: legal_entity, tenant_membership.
-- (notification_channel_teams.tenant_id is an EXTERNAL Microsoft Teams tenant
-- id — TEXT — and is NOT renamed.)

ALTER TABLE legal_entity RENAME COLUMN tenant_id TO organization_id;
ALTER TABLE tenant_membership RENAME COLUMN tenant_id TO organization_id;

Append:

-- Phase 2: rename old `tenant` table to `organization`.
-- Its pkey is currently `tenant_pkey1` and slug objects carry `tenant_*_key1`
-- / `idx_tenant_slug` / `tenant_slug_format` from the Feb 2026 rename. Normalize
-- them now so `legal_entity_pkey` can become `tenant_pkey` later.

ALTER TABLE tenant RENAME CONSTRAINT tenant_pkey1 TO organization_pkey;
ALTER INDEX idx_tenant_slug RENAME TO idx_organization_slug;
ALTER TABLE tenant RENAME CONSTRAINT tenant_slug_key1 TO organization_slug_key;
ALTER TABLE tenant RENAME CONSTRAINT tenant_slug_format TO organization_slug_format;
ALTER TRIGGER trigger_tenant_updated_at ON tenant RENAME TO trigger_organization_updated_at;

ALTER TABLE tenant RENAME TO organization;

-- tenant_membership → organization_membership (child of organization).
ALTER TABLE tenant_membership RENAME TO organization_membership;
ALTER TABLE organization_membership RENAME CONSTRAINT tenant_membership_pkey TO organization_membership_pkey;
ALTER INDEX idx_tenant_membership_identity_id RENAME TO idx_organization_membership_identity_id;
ALTER INDEX idx_tenant_membership_tenant_id RENAME TO idx_organization_membership_organization_id;
ALTER TABLE organization_membership RENAME CONSTRAINT tenant_membership_unique TO organization_membership_unique;
ALTER TABLE organization_membership RENAME CONSTRAINT tenant_membership_identity_id_fkey TO organization_membership_identity_id_fkey;
ALTER TABLE organization_membership RENAME CONSTRAINT tenant_membership_tenant_id_fkey TO organization_membership_organization_id_fkey;

-- The FK constraint on legal_entity pointing at old `tenant` gets renamed here
-- while legal_entity still has its old name — it'll be renamed again below
-- when legal_entity itself becomes `tenant`.
ALTER TABLE legal_entity RENAME CONSTRAINT legal_entity_tenant_fk TO legal_entity_organization_fk;

Confirm the current pkey/constraint/index names against your dump/rename-audit.md FK + index enumerations (Task 1 Steps 2 & 3) and ADJUST this block if names differ on your DB. The spec's ground truth is the live schema.

Append:

-- Phase 3: rename all `legal_entity_id` columns to `tenant_id`.
-- Includes all 20 tables from dump/rename-audit.md.

ALTER TABLE ap_acquisition_llm_stat     RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE ap_datev_export_audit       RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE ap_doc_source               RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE ap_qa_dataset_item          RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE ap_supplier_verification    RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE api_key                     RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE api_request_log             RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE booking_history_item        RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE booking_history_upload      RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE business_partner_dataset    RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE cost_center_dataset         RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE datev_rewe_link             RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE document                    RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE fpna_data_map               RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE gl_accounts_dataset         RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE legal_entity_datev_integration     RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE legal_entity_oauth_integration     RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE legal_entity_prompt_customization  RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE notification_channel        RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE notification_routing        RENAME COLUMN legal_entity_id TO tenant_id;

Re-check this list against dump/rename-audit.md Step 1 output. If the live DB shows additional tables, add them.

Append:

-- Phase 4: rename `legal_entity` → `tenant`. Its pkey is already named
-- `tenant_pkey` (a historical artifact from when it was `tenant` originally),
-- which is fine because the OLD tenant's pkey has already been renamed to
-- `organization_pkey` in Phase 2.

ALTER TABLE legal_entity RENAME TO tenant;
-- pkey `tenant_pkey` retained (no rename needed).

-- Child tables: rename the tables themselves.
ALTER TABLE legal_entity_datev_integration    RENAME TO tenant_datev_integration;
ALTER TABLE legal_entity_oauth_integration    RENAME TO tenant_oauth_integration;
ALTER TABLE legal_entity_prompt_customization RENAME TO tenant_prompt_customization;

Append a rename for every FK constraint named *_legal_entity_id_fkey and every constraint carrying legal_entity in the name. Pull the full list from dump/rename-audit.md Step 2. Example block (extend to full list):

-- Phase 5: FK constraint renames.
ALTER TABLE ap_acquisition_llm_stat     RENAME CONSTRAINT acquisition_llm_stat_legal_entity_id_fkey     TO acquisition_llm_stat_tenant_id_fkey;
ALTER TABLE ap_datev_export_audit       RENAME CONSTRAINT datev_export_audit_legal_entity_id_fkey       TO datev_export_audit_tenant_id_fkey;
ALTER TABLE ap_doc_source               RENAME CONSTRAINT doc_source_legal_entity_id_fkey               TO doc_source_tenant_id_fkey;
ALTER TABLE ap_qa_dataset_item          RENAME CONSTRAINT qa_dataset_item_legal_entity_id_fkey          TO qa_dataset_item_tenant_id_fkey;
ALTER TABLE api_key                     RENAME CONSTRAINT api_key_legal_entity_id_fkey                  TO api_key_tenant_id_fkey;
ALTER TABLE api_request_log             RENAME CONSTRAINT api_request_log_legal_entity_id_fkey          TO api_request_log_tenant_id_fkey;
ALTER TABLE booking_history_item        RENAME CONSTRAINT booking_history_item_legal_entity_id_fkey     TO booking_history_item_tenant_id_fkey;
ALTER TABLE booking_history_upload      RENAME CONSTRAINT booking_history_upload_legal_entity_id_fkey   TO booking_history_upload_tenant_id_fkey;
ALTER TABLE business_partner_dataset    RENAME CONSTRAINT business_partner_dataset_legal_entity_id_fkey TO business_partner_dataset_tenant_id_fkey;
ALTER TABLE cost_center_dataset         RENAME CONSTRAINT cost_center_dataset_legal_entity_id_fkey      TO cost_center_dataset_tenant_id_fkey;
ALTER TABLE datev_rewe_link             RENAME CONSTRAINT datev_rewe_link_legal_entity_id_fkey          TO datev_rewe_link_tenant_id_fkey;
ALTER TABLE document                    RENAME CONSTRAINT document_legal_entity_id_fkey                 TO document_tenant_id_fkey;
ALTER TABLE fpna_data_map               RENAME CONSTRAINT fpna_data_map_legal_entity_id_fkey            TO fpna_data_map_tenant_id_fkey;
ALTER TABLE gl_accounts_dataset         RENAME CONSTRAINT gl_accounts_dataset_legal_entity_id_fkey      TO gl_accounts_dataset_tenant_id_fkey;
ALTER TABLE tenant_datev_integration    RENAME CONSTRAINT legal_entity_datev_integration_legal_entity_id_fkey   TO tenant_datev_integration_tenant_id_fkey;
ALTER TABLE tenant_oauth_integration    RENAME CONSTRAINT legal_entity_oauth_integration_legal_entity_id_fkey   TO tenant_oauth_integration_tenant_id_fkey;
ALTER TABLE tenant_prompt_customization RENAME CONSTRAINT legal_entity_prompt_customization_legal_entity_id_fkey TO tenant_prompt_customization_tenant_id_fkey;
ALTER TABLE notification_channel        RENAME CONSTRAINT notification_channel_legal_entity_id_fkey    TO notification_channel_tenant_id_fkey;
ALTER TABLE notification_routing        RENAME CONSTRAINT notification_routing_legal_entity_id_fkey    TO notification_routing_tenant_id_fkey;
ALTER TABLE ap_supplier_verification    RENAME CONSTRAINT supplier_verification_legal_entity_id_fkey   TO supplier_verification_tenant_id_fkey;

-- Child-table pkeys from the renamed legal_entity_* tables:
ALTER TABLE tenant_datev_integration    RENAME CONSTRAINT legal_entity_datev_integration_pkey    TO tenant_datev_integration_pkey;
ALTER TABLE tenant_oauth_integration    RENAME CONSTRAINT legal_entity_oauth_integration_pkey    TO tenant_oauth_integration_pkey;
ALTER TABLE tenant_prompt_customization RENAME CONSTRAINT legal_entity_prompt_customization_pkey TO tenant_prompt_customization_pkey;

-- Organization FK on new tenant table:
ALTER TABLE tenant RENAME CONSTRAINT legal_entity_organization_fk TO tenant_organization_fk;

The constraint names above are reference-only — CROSS-CHECK each one against dump/rename-audit.md and adjust. Include ANY additional constraints you find.

Append, adjusting each entry against the enumeration from dump/rename-audit.md Step 3:

-- Phase 6: index renames.
ALTER INDEX idx_acquisition_llm_stat_legal_entity RENAME TO idx_acquisition_llm_stat_tenant;
ALTER INDEX idx_datev_export_audit_legal_entity_id RENAME TO idx_datev_export_audit_tenant_id;
ALTER INDEX idx_qa_dataset_item_legal_entity RENAME TO idx_qa_dataset_item_tenant;
ALTER INDEX idx_notification_routing_legal_entity RENAME TO idx_notification_routing_tenant;
-- ... continue for every index in Task 1 Step 3 output ...

For each function enumerated in Task 1 Step 4 whose body references legal_entity_id, emit a CREATE OR REPLACE FUNCTION with:

Example shape (adjust against the actual function body captured in rename-audit.md):

-- Phase 7: trigger function bodies.
CREATE OR REPLACE FUNCTION notify_ingestion_event() RETURNS trigger AS $$
BEGIN
  PERFORM pg_notify(
    'ingestion_event',
    json_build_object(
      'ingestion_id', NEW.id,
      'document_id',  NEW.document_id,
      'tenant_id',    NEW.tenant_id,   -- was: legal_entity_id
      'status',       NEW.status
    )::text
  );
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

Include notify_datev_export_event, notify_document_classified, notify_ingestion_event, and any other function captured in Step 4. Use the captured body — do NOT paraphrase. The only changes are identifier + key renames.

For each (table, column, jsonb_path) triple flagged by dump/rename-audit.md Step 6, append an UPDATE statement. Two patterns:

Flat key rename inside an object:

UPDATE api_key
   SET permissions = permissions - 'legal_entity_id' || jsonb_build_object('tenant_id', permissions->'legal_entity_id')
 WHERE permissions ? 'legal_entity_id';

Array-of-objects key rename (e.g. permissions is [{"legal_entity_id": ...}, ...]):

UPDATE api_key
   SET permissions = (
     SELECT jsonb_agg(
       CASE WHEN elem ? 'legal_entity_id'
            THEN (elem - 'legal_entity_id') || jsonb_build_object('tenant_id', elem->'legal_entity_id')
            ELSE elem
       END)
     FROM jsonb_array_elements(permissions) elem
   )
 WHERE permissions::text LIKE '%legal_entity_id%';

Pick the variant matching the actual shape observed during Task 1 Step 6. If audit shows zero rows affected, omit that statement entirely — do NOT ship no-op updates.

Append:

-- Phase 9: drop accounting_system column and enum.
-- No longer needed: DATEV vs other accounting system is derivable from
-- tenant_datev_integration row existence (or .is_active field).
-- Per spec discrepancy note 2: column is on legal_entity (post-rename: tenant),
-- not on the old tenant.

ALTER TABLE tenant DROP COLUMN accounting_system;
DROP TYPE accounting_system;

Confirm accounting_system has no other usages:

psql -h localhost -U postgres -d orcha -c "SELECT table_name, column_name FROM information_schema.columns WHERE udt_name='accounting_system';"

Expected: zero rows (after the column drop would leave it unused; before it, only legal_entity.accounting_system).

Run:

psql -h localhost -U postgres -d orcha -f resources/migrations/YYYYMMDDHHMMSS-rename-legal-entity-to-tenant.up.sql --set ON_ERROR_STOP=on

Expected: commits successfully. If it errors, fix the offending statement and re-run after manually reverting any partial changes (or drop + reinit the local DB — use bb dev:db:init or equivalent).

Run:

psql -h localhost -U postgres -d orcha -c "\dt" | grep -E "tenant|organization|legal"
psql -h localhost -U postgres -d orcha -c "SELECT table_name, column_name FROM information_schema.columns WHERE table_schema='public' AND column_name IN ('tenant_id','legal_entity_id','organization_id') ORDER BY table_name;"

Expected: no legal_entity* tables; organization, organization_membership, tenant, tenant_datev_integration, tenant_oauth_integration, tenant_prompt_customization all present; no legal_entity_id columns remain; notification_channel_teams.tenant_id (TEXT, external) unchanged.

git add resources/migrations/YYYYMMDDHHMMSS-rename-legal-entity-to-tenant.up.sql
git commit -m "chore(db): add legal_entity → tenant rename migration (up)"

Task 3: Write .down.sql migration

Files:

Write a header explaining the known asymmetry (accounting_system column restored as NULL; accounting_system enum restored; JSONB reverts applied; trigger functions restored to pre-rename bodies — use the bodies from rename-audit.md Step 4 verbatim).

Order:

Phase 9 reverse: recreate accounting_system enum + column (NULLABLE; original values lost).
Phase 8 reverse: reverse JSONB key renames.
Phase 7 reverse: restore trigger function bodies from rename-audit.md.
Phase 6 reverse: rename indexes back.
Phase 5 reverse: rename FK constraints back.
Phase 4 reverse: rename tenant → legal_entity, child tables back.
Phase 3 reverse: rename tenant_id columns back to legal_entity_id.
Phase 2 reverse: rename organization → tenant, organization_membership → tenant_membership, all index/constraint normalizations undone.
Phase 1 reverse: rename organization_id columns back to tenant_id (on the now-restored legal_entity and tenant_membership).

Wrap in BEGIN; ... COMMIT;.

-- Phase 9 reverse: recreate enum and column. Enum values lost on drop; they
-- must be re-specified literally. Source: resources/migrations/20260219091057-squashed-to-v1-963.up.sql
-- defined the enum as a single value: `('datev')`.
CREATE TYPE accounting_system AS ENUM ('datev');
ALTER TABLE legal_entity ADD COLUMN accounting_system accounting_system;

Re-confirm the value list before committing by scanning the squash migration:

grep -n "CREATE TYPE accounting_system" resources/migrations/20260219091057-squashed-to-v1-963.up.sql

Expected: CREATE TYPE accounting_system AS ENUM ('datev');. If any subsequent migration ever added values via ALTER TYPE ... ADD VALUE, include those too (unlikely — no such migration exists at time of writing).

# Assuming up has already been applied:
psql -h localhost -U postgres -d orcha -f resources/migrations/YYYYMMDDHHMMSS-rename-legal-entity-to-tenant.down.sql --set ON_ERROR_STOP=on
psql -h localhost -U postgres -d orcha -c "\dt" | grep -E "tenant|organization|legal"

Expected: tenant, tenant_membership, legal_entity, legal_entity_datev_integration, legal_entity_oauth_integration, legal_entity_prompt_customization. No organization* tables.

Run up again to leave the DB in the migrated state for the subsequent code work:

psql -h localhost -U postgres -d orcha -f resources/migrations/YYYYMMDDHHMMSS-rename-legal-entity-to-tenant.up.sql --set ON_ERROR_STOP=on
git add resources/migrations/YYYYMMDDHHMMSS-rename-legal-entity-to-tenant.down.sql
git commit -m "chore(db): add legal_entity → tenant rename migration (down)"

Task 4: Code rename — Stage 1: old-sense tenantorganization

The branch must stay compilable between Task 4 and Task 5. Stage 1 keeps legal-entity meaning tenant, so the codebase is internally coherent: organization is the new name for the grouping, legal-entity is still the operational unit.

Files: every file under src/, test/, dev/, plus resources/com/getorcha/config.edn. Exact list enumerated inside the steps below.

grep -rln ':tenant/\|:tenant-id\|tenant-id\|"tenants"\|"Tenant"\|"Tenants"\|/tenants\|*tenant\*\|defn.*tenant\|ns.*tenant\|tenant-membership\|tenant_membership' src/ test/ dev/ resources/ 2>/dev/null | grep -v legal_entity > /tmp/stage1-candidates.txt
wc -l /tmp/stage1-candidates.txt

Review the list manually — some matches will be false positives (e.g. tokens in strings, unrelated comments). Narrow it against the naming-pattern table from the spec.

git mv src/com/getorcha/schema/tenant.clj src/com/getorcha/schema/organization.clj

Inside src/com/getorcha/schema/organization.clj:

Commit:

git add src/com/getorcha/schema/organization.clj
git commit -m "refactor(schema): rename old-sense tenant namespace to organization"
git mv src/com/getorcha/admin/http/tenants.clj src/com/getorcha/admin/http/organizations.clj

Do NOT move the admin/http/tenants/ directory. Its contents (file_store.clj, prompt_customizations.clj) are per-legal-entity pages, which after Stage 2 become per-new-tenant pages — so the directory name tenants/ is semantically correct post-rename and needs no further move. This avoids a Stage-1-then-undo dance in Stage 2. Final SP1 layout:

admin/http/organizations.clj              ← old tenants.clj (org + tenant list page)
admin/http/tenants/
  file_store.clj                          ← per-tenant, unchanged location
  prompt_customizations.clj               ← per-tenant, unchanged location

SP2 will later add admin/http/tenants.clj (the tenant detail page handler) alongside the directory; that's not SP1's concern.

Inside admin/http/organizations.clj:

Find the route registrations:

grep -rn '"/tenants' src/com/getorcha/admin/

In each hit, replace /tenants/organizations. Example:

;; before
["/tenants" {:get ... :post ...}]
["/tenants/-/generate-slug" ...]
["/tenants/:id" ...]

;; after
["/organizations" {:get ... :post ...}]
["/organizations/-/generate-slug" ...]
["/organizations/:id" ...]

The nested legal-entity sub-routes go through two path segments of rename. Before Stage 1 they look like /tenants/-/legal-entities/:id/... (where the leading /tenants/ is old-sense = organization). Stage 1 flips only the leading segment: /organizations/-/legal-entities/:id/.... Stage 2 (Task 5 Step 4) then flips the nested segment: /organizations/-/tenants/:id/....

Find:

grep -rn ':com.getorcha.schema.tenant/' src/ test/ dev/  # old-sense registry key
grep -rn '\*tenant\*\|\*tenant-id\*\|\*current-tenant' src/ test/ dev/

Each usage: replace tenantorganization in the keyword / symbol. Tenant-id middleware (src/com/getorcha/logging/mdc.clj, src/com/getorcha/app/http/middleware/auth.clj, src/com/getorcha/http/middleware/xray.clj) typically carries these vars — confirm.

grep -rn '::tenant\b\|:com.getorcha.tenant/\|:com.getorcha/tenant\b' src/ resources/ dev/

For every match in old sense (grouping), rewrite tenantorganization. Update resources/com/getorcha/config.edn correspondingly.

grep -rn '"Tenants"\|"Tenant "\|"tenant "\|"tenant list"\|"tenant page"' src/com/getorcha/admin/

Rewrite to "Organizations" / "Organization" where the context is the grouping (today's admin tenant list shows the old-sense concept).

clj-kondo --lint src test dev
clj -X:test:silent 2>&1 | grep -A 5 -E "(FAIL in|ERROR in|Execution error|failed because|Ran .* tests)"

Fix every kondo problem. Every test must pass. No ^:private shortcuts to silence unused-var warnings — if a rename created dead code, remove the dead code.

git add -A
git commit -m "refactor(schema): rename old-sense tenant to organization (code side)"

Now the codebase is internally consistent: organization means grouping, legal-entity means operational unit. This stage flips legal-entity to tenant.

grep -rln "legal[-_]entity\|:legal-entity" src/ test/ dev/ resources/ 2>/dev/null > /tmp/stage2-candidates.txt
wc -l /tmp/stage2-candidates.txt

Expect ~146 files.

git mv src/com/getorcha/schema/legal_entity.clj src/com/getorcha/schema/tenant.clj

Inside:

The admin/http/tenants/ directory was left in place during Stage 1. Its contents (file_store.clj, prompt_customizations.clj) were already per-legal-entity and are now per-new-tenant — the directory name is correct post-rename. No git mv in this step.

What does change inside those files is covered by Step 5 (global legal-entitytenant code rename): (ns com.getorcha.admin.http.tenants.file-store ...) retains its namespace form, but every legal-entity identifier inside the body becomes tenant.

grep -rn 'legal-entities\|"/legal-entit' src/com/getorcha/admin/

Rewrite each route segment:

Prefer IDE-assisted rename. As a fallback, per-file sed is acceptable but risky — always pair with a compile + test run. The pattern order matters:

  1. legal_entity_idtenant_id (the SQL column token — appears in HoneySQL strings, log messages, JSON parsing).
  2. :legal-entity-id:tenant-id.
  3. legal-entity-idtenant-id (symbol).
  4. legal-entity (kebab) → tenant.
  5. legal_entity (snake — any remaining) → tenant.
  6. legalEntity (camel) → tenant.
  7. "Legal Entity" UI string → "Tenant".
  8. "Legal Entities" UI string → "Tenants".
  9. :com.getorcha.schema.legal-entity/ (keyword) → :com.getorcha.schema.tenant/.

Example sed invocation for one file:

sed -i \
  -e 's/legal_entity_id/tenant_id/g' \
  -e 's/:legal-entity-id/:tenant-id/g' \
  -e 's/legal-entity-id/tenant-id/g' \
  -e 's/legal-entity/tenant/g' \
  -e 's/legal_entity/tenant/g' \
  -e 's/legalEntity/tenant/g' \
  -e 's/"Legal Entity"/"Tenant"/g' \
  -e 's/"Legal Entities"/"Tenants"/g' \
  -e 's/:com\.getorcha\.schema\.legal-entity\//:com.getorcha.schema.tenant\//g' \
  src/com/getorcha/path/to/file.clj

Process files in batches, compile after each batch:

clj -M:dev -e "(require 'com.getorcha.main :reload-all)"

The codebase uses le-id / le-ids / primary-le-id / all-le-ids / le-filter-ids / le-name / le-param / le-id-set / le-id-str / other-le-id / new-le-ids / authorized-le-ids / effective-le-ids / active-le-ids as short forms of legal-entity-id. These are NOT caught by the legal-entitytenant rename above. Rename them explicitly to their tenant- equivalents (tenant-id, primary-tenant-id, all-tenant-ids, etc.).

Enumerate current usages (re-run before starting so the list reflects the live branch — origin/master merges may have added new ones):

grep -rnE '\ble-(id|ids|name|param|id-set|id-str|filter|filter-ids)\b|\b([a-z-]+)-le-(id|ids)\b' src/ test/ dev/ 2>/dev/null

Prefer IDE-assisted rename per symbol. As a sed fallback, the regex needs a word boundary (to avoid accidentally matching table-id, simple-id, etc.):

# Match `le-` at start-of-line OR after a non-letter character.
sed -i -E 's/(^|[^a-zA-Z])le-([a-z])/\1tenant-\2/g' src/com/getorcha/path/to/file.clj

Apply to one file at a time, compile after each. DO NOT run as a bulk one-liner over the whole tree without reviewing the diff per file — le- is short and the false-negative risk (from word-boundary edge cases) is real.

After the pass, re-run the enumeration command; it should return zero lines.

Find code that reads "legal_entity_id" as a string key (JSONB path) — this is separate from identifier references:

grep -rn '"legal_entity_id"' src/ test/ dev/

Each match is either:

Update to "tenant_id".

Workers consume pg_notify events emitted by the trigger functions. Phase 7 of the migration changed the JSON keys. Find parsers:

grep -rn 'pg-notify\|listen\|:payload\|:legal_entity_id\|"legal_entity_id"' src/com/getorcha/workers/ src/com/getorcha/notifications/

For each event-parsing callsite, update:

clj-kondo --lint src test dev
clj -X:test:silent 2>&1 | grep -A 5 -E "(FAIL in|ERROR in|Execution error|failed because|Ran .* tests)"

Iterate:

clj -M:dev
;; in the REPL:
(integrant.repl/reset)
;; then open admin UI in browser:
;;   http://localhost:8080/admin/organizations
;;   create an organization
;;   create a tenant under it
;;   edit both
;;   verify stats pages load

Golden path: ingest a test document end-to-end (use /reingest-doc skill or the admin ingestion form). Verify the document routes to the correct tenant. Verify notification send (Slack/Teams if configured locally).

If Slack/Teams/DATEV credentials aren't available locally, note as "skipped — unverified" in the commit message, and verify in staging.

git add -A
git commit -m "refactor(schema): rename legal_entity to tenant (code side)"

find src test dev resources -type f \( -name '*legal_entity*' -o -name '*legal-entity*' \)
find src test dev resources -type f -name '*tenant*' | xargs grep -l "old-sense tenant means" 2>/dev/null
# Second line is a no-op heuristic; inspect each tenant-named file manually to
# decide whether it refers to old-sense tenant (→ rename to organization) or
# new-sense tenant (→ keep).

One commit per git mv (or one commit per batch of moves). Update :require clauses in consumers after each move.

clj-kondo --lint src test dev
clj -X:test:silent 2>&1 | grep -A 5 -E "(FAIL in|ERROR in|Execution error|failed because|Ran .* tests)"
git add -A
git commit -m "refactor: rename remaining legacy-named files to new terminology"

Task 7: Update docs/architecture/*.md

grep -rln 'legal[-_ ]entity\|\bTenant\b' docs/architecture/ 2>/dev/null

Don't rename the files themselves unless the filename embeds the old vocabulary AND a rename improves discoverability. Document content is the priority.

git add docs/architecture/
git commit -m "docs(architecture): update vocabulary after legal_entity → tenant rename"

Task 8: Add rename notes to affected plans/specs

Filter: only docs whose meaning would be confusing to read today without context. Do NOT mass-rename historical text — add the header and move on.

grep -rln 'legal[-_ ]entity' docs/plans/ docs/superpowers/plans/ docs/superpowers/specs/ 2>/dev/null

For each: open it. If it's a completed historical plan that reads clearly as-written (e.g. date of the doc predates today and it uses legal_entity as an internal implementation detail), add the note. If it's a future/active plan that references the old names as if still valid, add the note AND decide whether to update content (only if the content is still actionable).

> **Note (2026-04-24):** After this document was written, `legal_entity` was renamed to `tenant` and the old `tenant` was renamed to `organization`. Read references to these terms with the pre-rename meaning.

Leave a blank line after the blockquote.

git add docs/
git commit -m "docs: add legal_entity → tenant rename note to affected plans/specs"

Task 9: Verify external-consumer assumption

The spec says external API consumers are "declared none". Confirm empirically before merge.

psql -h localhost -U postgres -d orcha -c "SELECT api_key_id, COUNT(*) FROM api_request_log WHERE requested_at > now() - interval '90 days' GROUP BY api_key_id ORDER BY 2 DESC LIMIT 20;"

(Do this against prod when shipping — local dev DB won't have traffic.)

External consumers read API responses; the JSON output of handlers uses new names after the rename. If any client is observed relying on legal_entity_id response keys, stop and discuss compatibility with the user — this would be a spec revision, not an implementation detail.

No commit here — the result of this task either confirms the assumption (PR proceeds) or blocks it (stop-and-discuss).


Task 10: Final verification

clj-kondo --lint src test dev

Expected: 0 problems, 0 warnings, 0 errors.

clj -X:test:silent 2>&1 | tee /tmp/test-out.log | grep -E "(FAIL in|ERROR in|Execution error|Ran .* tests)"

Expected: Ran N tests containing M assertions. 0 failures, 0 errors. and no ERROR lines earlier.

# Drop + recreate DB, run ALL migrations including the new one, then boot.
bb dev:db:init          # or equivalent reset command
clj -M:dev
;; in REPL:
(integrant.repl/go)

Expected: clean boot, no errors.

Obtain a recent prod dump (coordinate with the user; do NOT pull prod data without explicit approval — ask before doing this). Restore into a scratch local DB, apply .up.sql, then .down.sql, then .up.sql again. All three runs must succeed. Note the asymmetry in the accounting_system column (values will be NULL after down+up cycle — acceptable).

This step MAY be skipped if the user confirms staging verification is sufficient, but default is to run it.

Cover:

git add -A
git commit -m "refactor: final polish for legal_entity → tenant rename"

(Only if there are pending changes. If clean, skip.)


Task 11: PR prep

Include:

The plan stops here. Sub-project 2 (admin UI restructure) is a separate plan.


Self-review (performed during authoring)

Spec coverage:

Placeholder scan: No TBD / "implement later" / naked "add error handling" / unstated code. Every step has either an exact command, exact SQL, or a concrete file-level instruction.

Type consistency: Malli registry keys, namespace paths, and route strings are consistent across tasks. Column names (tenant_id, organization_id) match the phase ordering in the migration.

Known remaining risks surfaced in the plan: