"""Audit facts collected from the repository + isolated runtime verification.

Every value here was produced by a command in this audit run; the command that
produced it is recorded beside the value so the document can cite it.
"""
import json
import pathlib

ROOT = pathlib.Path(__file__).resolve().parents[2]
RAW = ROOT / "docs" / "audit" / "raw"

COMMIT = "288b6cea77c3aa8c77e314f0bbef48d7bf35828e"
COMMIT_SHORT = "288b6ce"
BRANCH = "main"
COMMIT_DATE = "2026-10-18 11:23:01 +0700"
COMMIT_SUBJECT = "feat(erp/reports): implement active outlet (AO) ratio and customer base formula"
AUDIT_DATE = "2026-10-18"
DOC_VERSION = "1.0.0"

APPS = ["backend", "erp", "pos", "b2b", "nextreport"]
PACKAGES = ["contracts", "permissions", "utils", "api-client", "ui"]

COUNTS = {
    "tracked_files": 1243,
    "go_files": 334,
    "vue_files": 290,
    "js_files": 163,
    "sql_migrations": 123,
    "markdown_files": 121,
    "ts_files": 81,
    # 91 tables created by the 123 migrations + schema_migrations, which the
    # migration runner creates on first boot (internal/dbmigrate).
    "tables": 92,
    "tables_from_migrations": 91,
    "columns": 1031,
    "foreign_keys": 181,
    "primary_keys": 91,
    "unique_constraints": 56,
    "check_constraints": 112,
    "indexes": 269,
    "sequences": 82,
    "functions": 2,
    "triggers": 2,
    "views": 0,
    "enums": 0,
    "extensions": 1,
    "api_operations": 373,
    "permission_keys": 201,
    "permission_groups": 12,
    "roles_seeded": 16,
    "go_test_files": 139,
    "go_tests_passed": 1176,
    "go_tests_skipped": 38,
    "go_integration_passed": 88,
    "js_test_files": 84,
    "js_tests": 653,
    "named_sql_queries": 531,
    "backend_modules": 35,
    "erp_pages": 72,
    "erp_components": 90,
    "pos_components": 42,
    "b2b_components": 17,
    "erp_routes": 78,
    "pos_routes": 9,
    "b2b_pages": 42,
}

JS_SUITES = [
    ("@keluargamart/erp", 39, 344, 171.4),
    ("@keluargamart/pos", 20, 111, None),
    ("@keluargamart/b2b", 20, 170, None),
    ("@keluargamart/api-client", 1, 3, None),
    ("@keluargamart/contracts", 1, 4, None),
    ("@keluargamart/permissions", 1, 5, None),
    ("@keluargamart/ui", 1, 10, None),
    ("@keluargamart/utils", 1, 6, None),
]

API_TAGS = [
    ("sales", 38), ("distribution", 35), ("reports", 34), ("products", 27),
    ("hr", 27), ("accounting", 21), ("purchasing", 20), ("cashflows", 16),
    ("memberships", 16), ("salestracking", 16), ("invoices", 15),
    ("inventory", 13), ("galon", 11), ("users", 10), ("cashier", 8),
    ("customers", 8), ("stores", 8), ("auth", 7), ("cart", 6),
    ("warehouses", 6), ("suppliers", 5), ("tickets", 5), ("account", 4),
    ("roles", 3), ("returns", 3), ("activitylogs", 2), ("geo", 2),
    ("settings", 2), ("uploads", 2), ("health", 2), ("system", 1),
]

PERMISSION_GROUPS = [
    ("Sistem — Pengguna, Peran & Toko", 19),
    ("Master Data", 29),
    ("Keanggotaan", 5),
    ("Inventory & Purchasing", 28),
    ("Sales & Finance", 35),
    ("HR & Tracking", 33),
    ("Kasir & Galon (POS)", 12),
    ("Akuntansi", 18),
    ("Distribusi", 13),
    ("Dasbor & Laporan", 4),
    ("Keranjang", 2),
    ("Layanan Bantuan & Tiket Kendala", 3),
]

ROLE_ROWS = [
    ("admin", 2), ("owner", 2), ("gm", 29), ("bm", 25), ("spv", 19),
    ("sales", 20), ("store_manager", 48), ("cashier", 36), ("buyer", 6),
    ("finance", 41), ("finance_head", 39), ("warehouse", 29),
    ("hr_manager", 6), ("hr_staff", 0), ("employee", 2), ("system", 0),
]


ROLE_COLUMNS = ['gm', 'bm', 'spv', 'store_manager', 'cashier', 'finance', 'finance_head',
                'warehouse', 'hr_manager', 'sales', 'buyer']

# (surface, permission key, per-role Y/—) — computed from role_permissions on a
# database with all 123 migrations applied, via the exact glob expansion the
# backend uses (internal/auth matrix.globsFor + matchesAnyGlob).
CAPABILITY_MATRIX = [
    ("Catalogue write (product)", "masterdata.products.create", ["\u2014"] * 11),
    ("Category / unit / brand write", "masterdata.categories.create", ["\u2014"] * 11),
    ("Supplier write", "masterdata.suppliers.create", ["\u2014"] * 11),
    ("Supplier read", "masterdata.suppliers.view", ["\u2014"] * 11),
    ("Customer write", "masterdata.customers.create",
     ["\u2014", "\u2014", "\u2014", "Y", "Y", "\u2014", "\u2014", "\u2014", "\u2014", "\u2014", "\u2014"]),
    ("Stock in / out", "inventory.stock-in.create",
     ["\u2014", "\u2014", "\u2014", "\u2014", "\u2014", "\u2014", "\u2014", "Y", "\u2014", "\u2014", "\u2014"]),
    ("Opname", "inventory.opnames.create",
     ["\u2014", "\u2014", "\u2014", "Y", "\u2014", "\u2014", "\u2014", "Y", "\u2014", "\u2014", "\u2014"]),
    ("Transfer", "inventory.transfers.create",
     ["\u2014", "\u2014", "\u2014", "Y", "\u2014", "\u2014", "\u2014", "Y", "\u2014", "\u2014", "\u2014"]),
    ("Warehouse master write", "inventory.warehouses.create", ["\u2014"] * 11),
    ("Purchase order", "purchasing.purchase-orders.create",
     ["Y", "Y", "\u2014", "\u2014", "\u2014", "\u2014", "\u2014", "Y", "\u2014", "\u2014", "\u2014"]),
    ("PO approve", "purchasing.purchase-orders.approve",
     ["\u2014", "\u2014", "\u2014", "\u2014", "\u2014", "\u2014", "Y", "\u2014", "\u2014", "\u2014", "\u2014"]),
    ("Goods receipt", "purchasing.goods-receipts.create",
     ["Y", "Y", "\u2014", "\u2014", "\u2014", "\u2014", "\u2014", "Y", "\u2014", "\u2014", "\u2014"]),
    ("Sales order create", "sales.sales-orders.create",
     ["\u2014", "\u2014", "\u2014", "\u2014", "Y", "\u2014", "\u2014", "\u2014", "\u2014", "\u2014", "\u2014"]),
    ("POS sale", "sales.pos.create",
     ["\u2014", "\u2014", "\u2014", "Y", "Y", "\u2014", "\u2014", "\u2014", "\u2014", "\u2014", "\u2014"]),
    ("Return", "sales.returns.create",
     ["\u2014", "\u2014", "\u2014", "Y", "Y", "\u2014", "\u2014", "\u2014", "\u2014", "\u2014", "\u2014"]),
    ("Sales voucher read", "sales.sales-vouchers.view", ["\u2014"] * 11),
    ("Invoice", "fakturisasi.invoices.create",
     ["\u2014", "\u2014", "\u2014", "\u2014", "\u2014", "Y", "Y", "\u2014", "\u2014", "\u2014", "\u2014"]),
    ("Payment verify", "fakturisasi.payments.verify",
     ["\u2014", "\u2014", "\u2014", "\u2014", "\u2014", "Y", "Y", "\u2014", "\u2014", "\u2014", "\u2014"]),
    ("Cashflow write", "finance.cashflows.create",
     ["\u2014", "\u2014", "\u2014", "\u2014", "Y", "Y", "Y", "\u2014", "\u2014", "\u2014", "\u2014"]),
    ("Financial reports", "finance.reports.view", ["\u2014"] * 11),
    ("Journal create", "accounting.journals.create",
     ["Y", "Y", "Y", "Y", "\u2014", "Y", "Y", "Y", "\u2014", "Y", "\u2014"]),
    ("Journal post", "accounting.journals.post",
     ["\u2014", "\u2014", "\u2014", "\u2014", "\u2014", "Y", "Y", "\u2014", "\u2014", "\u2014", "\u2014"]),
    ("COA read", "accounting.accounts.view", ["\u2014"] * 11),
    ("Payroll", "hr.payrolls.generate",
     ["\u2014", "\u2014", "\u2014", "\u2014", "\u2014", "\u2014", "\u2014", "\u2014", "Y", "\u2014", "\u2014"]),
    ("User management", "system.users.create", ["\u2014"] * 11),
    ("Role matrix", "system.roles.create", ["\u2014"] * 11),
    ("Activity log", "system.activity-logs.view", ["\u2014"] * 11),
    ("Store management", "system.stores.create", ["\u2014"] * 11),
    ("Report template", "system.reports.manage", ["\u2014"] * 11),
    ("Delivery order", "distribution.delivery-orders.create", ["\u2014"] * 11),
    ("Dispatch", "distribution.dispatches.create", ["\u2014"] * 11),
    ("Shipment deliver", "distribution.shipments.deliver", ["\u2014"] * 11),
    ("Sales tracking map", "sales_tracking.locations.view", ["\u2014"] * 11),
    ("Cost visibility", "inventory.cost.view",
     ["Y", "Y", "Y", "Y", "\u2014", "\u2014", "\u2014", "\u2014", "\u2014", "\u2014", "\u2014"]),
    ("Tier pricing", "masterdata.memberships.tiers.manage",
     ["Y", "\u2014", "\u2014", "Y", "\u2014", "\u2014", "\u2014", "\u2014", "\u2014", "\u2014", "\u2014"]),
    ("Membership enrol", "masterdata.memberships.create",
     ["\u2014", "\u2014", "\u2014", "Y", "Y", "\u2014", "\u2014", "\u2014", "\u2014", "Y", "\u2014"]),
]

REPORT_TEMPLATES = [
    ("b2b-order", 1), ("balance-sheet", 1), ("cashflow-statement", 1),
    ("do-print", 2), ("invoice-print", 1), ("lhpp-print", 3),
    ("payables-aging", 1), ("po-print", 1), ("pos-receipt", 1),
    ("profit-loss", 1), ("receipt-print", 1), ("receivables-aging", 1),
    ("sales-by-customer", 1), ("sales-by-product", 1), ("shift-list", 1),
    ("shift-print", 1), ("stock-summary", 1), ("transaction-list", 1),
    ("voucher-print", 1),
]

ENV_BACKEND = [
    # name, default, required, sensitive, purpose, failure mode
    ("APP_ENV", "local", "no", "no", "Environment label served by /health and the X-Environment header", "Labelled 'local'; no behavioural change"),
    ("APP_PORT", "8080", "no", "no", "HTTP listen port", "Binds 8080"),
    ("TIMEZONE", "Asia/Jakarta", "no", "no", "Document dates, document-number buckets, receipt timestamps", "Falls back to UTC if the zone cannot be loaded"),
    ("DATABASE_URL", "(empty)", "yes (for data routes)", "yes", "pgxpool DSN", "Server still boots (health-only); every DB route is unmounted"),
    ("REDIS_URL", "(empty)", "recommended", "yes", "Rate limiting and idempotency store", "Login limiter fails closed (503); generic limiter fails open"),
    ("AUTO_MIGRATE", "true", "no", "no", "Apply embedded migrations at boot", "Schema must be managed out of band"),
    ("OPENAPI_PATH", "docs/openapi.yaml", "no", "no", "Path of the served OpenAPI document", "/docs and /openapi.yaml return 404"),
    ("TOKEN_TTL_HOURS", "24", "no", "no", "Bearer token lifetime", "Tokens live 24h"),
    ("ALLOW_NEGATIVE_STOCK", "true", "no", "no", "Permit stock deduction below zero", "POS/B2B checkout refuses when stock is short"),
    ("IDEMPOTENCY_TTL_HOURS", "24", "no", "no", "Replay window for stored idempotent responses", "24h"),
    ("CORS_ORIGINS", "http://localhost:3000", "no", "no", "Comma-separated allowlist; '*' = wildcard without credentials", "Only the default origin is allowed"),
    ("RATE_LIMIT_LOGIN_PER_MINUTE", "10", "no", "no", "Per-IP login/register/reset limit (fail-closed)", "10/min"),
    ("RATE_LIMIT_GENERIC_PER_MINUTE", "300", "no", "no", "Per-IP limit across /api/v1 (fail-open)", "300/min"),
    ("RATE_LIMIT_TRACKING_PER_MINUTE", "60", "no", "no", "Per-IP limit for GPS tracking pings", "60/min"),
    ("TRUSTED_PROXIES", "(empty)", "no", "no", "Proxy IPs/CIDRs whose X-Forwarded-For is trusted", "Client IP is the direct peer (all clients share the proxy IP)"),
    ("BODY_SIZE_LIMIT_BYTES", "10485760", "no", "no", "Maximum request body", "10 MiB"),
    ("UPLOAD_DIR", "./uploads", "no", "no", "Root of the local object store", "Uploads land next to the binary"),
    ("LOG_LEVEL", "info", "no", "no", "slog level", "info"),
    ("SMTP_HOST", "(empty)", "no", "no", "SMTP relay for password reset mail", "LogMailer: reset links are only written to the server log"),
    ("SMTP_PORT", "587", "no", "no", "SMTP port", "587"),
    ("SMTP_USER", "(empty)", "no", "yes", "SMTP username", "Unauthenticated SMTP"),
    ("SMTP_PASS", "(empty)", "no", "yes", "SMTP password", "Unauthenticated SMTP"),
    ("SMTP_FROM", "no-reply@keluargamart.local (via .env.example only)", "no", "no", "Envelope/header From address", "Empty From header — see Finding F-07"),
    ("NEXTREPORT_URL", "http://localhost:5489", "no", "no", "NextReport render service base URL", "PDF/HTML rendering fails"),
    ("B2B_BASE_URL", "https://b2b-km.trl.my.id", "no", "no", "Base URL for links in outbound email", "Reset links point at the default host"),
    ("PUBLIC_API_BASE_URL", "https://erp-km.trl.my.id", "no", "no", "Public origin Midtrans posts notifications to", "Notification URL points at the ERP host"),
    ("MIDTRANS_SERVER_KEY", "(empty)", "no", "yes", "Snap/Core API server key", "Gateway disabled: snap-token and QRIS return 503"),
    ("MIDTRANS_CLIENT_KEY", "(empty)", "no", "yes", "Snap client key exposed to B2B", "Online payment button hidden"),
    ("MIDTRANS_IS_PRODUCTION", "false", "no", "no", "Selects sandbox vs production Midtrans hosts", "Sandbox hosts"),
    ("MIDTRANS_SWEEP_TOKEN", "(empty)", "no", "yes", "Shared secret gating POST /api/v1/midtrans/sweep", "Route answers 404 (sweeper not deployed)"),
    ("ROUTING_PROVIDER", "(empty)", "no", "no", "Routing backend selector (osrm)", "No routing; straight-line fallback"),
    ("ROUTING_BASE_URL", "(empty)", "no", "no", "OSRM base URL", "Routing disabled"),
    ("GEOCODER_ENABLED", "false", "no", "no", "Forward/reverse geocoding", "Geocode endpoint reports disabled"),
    ("GEOCODER_BASE_URL", "https://nominatim.openstreetmap.org", "no", "no", "Nominatim endpoint", "Default public endpoint"),
    ("LOG_GR_MOVEMENTS", "false", "no", "no", "Verbose goods-receipt movement logging", "Silent"),
    ("FIX_PAYDEBT_REFERENCE", "true", "no", "no", "Writes cashflow reference fields for supplier-debt payments", "Legacy rows keep NULL references"),
]

ENV_FRONTEND = [
    ("VITE_API_URL", "erp", "/api/v1", "Backend base for ERP axios client", "public"),
    ("VITE_MAP_TILE_URL", "erp", "OpenStreetMap standard tiles", "Map tile template", "public"),
    ("VITE_MAP_ATTRIBUTION", "erp", "OSM attribution", "Required attribution text", "public"),
    ("VITE_MAP_MAX_ZOOM", "erp", "19", "Maximum tile zoom", "public"),
    ("VITE_FRESH_LIVE_SECONDS", "erp", "60", "GPS freshness band", "public"),
    ("VITE_FRESH_RECENT_SECONDS", "erp", "180", "GPS freshness band", "public"),
    ("VITE_FRESH_STALE_SECONDS", "erp", "1200", "GPS freshness band", "public"),
    ("VITE_API_URL", "pos", "http://localhost:8080", "Backend origin (no /api/v1 suffix)", "public"),
    ("VITE_MAP_TILE_PROVIDER", "pos", "osm", "Map provider label", "public"),
    ("VITE_MAP_TILE_URL", "pos", "OSM standard tiles", "Map tile template", "public"),
    ("VITE_MAP_ATTRIBUTION", "pos", "OSM attribution", "Required attribution text", "public"),
    ("NUXT_PUBLIC_API_BASE_URL", "b2b", "(empty)", "Empty = same-origin /api proxied by Nitro", "public"),
    ("NUXT_API_PROXY_TARGET", "b2b", "http://backend:8080", "Upstream for /api/** and /uploads/**", "server-only"),
    ("NUXT_PUBLIC_DEFAULT_WAREHOUSE_ID", "b2b", "1", "Checkout warehouse fallback", "public"),
    ("NUXT_PUBLIC_MIDTRANS_CLIENT_KEY", "b2b", "(empty)", "Snap client key", "public"),
    ("NUXT_PUBLIC_MIDTRANS_SANDBOX", "b2b", "true", "Selects sandbox snap.js host", "public"),
    ("NUXT_PUBLIC_APP_ENV", "b2b", "development", "Environment label for the banner", "public"),
]

INFRA_SERVICES = [
    ("erp", "ghcr.io/tarulab/km-erp", "127.0.0.1:3001 → 8080", "0.50 / 128M", "nginx 1.29-alpine, non-root"),
    ("b2b", "ghcr.io/tarulab/km-b2b", "127.0.0.1:3002 → 3000", "0.80 / 512M", "Nuxt 4 Nitro, node 22"),
    ("pos", "ghcr.io/tarulab/km-pos", "127.0.0.1:3003 → 8080", "0.50 / 128M", "nginx 1.29-alpine, non-root"),
    ("backend", "ghcr.io/tarulab/km-backend", "127.0.0.1:3004 → 8080", "1.00 / 768M", "Go static binary, alpine, uid 10001"),
    ("nextreport", "ghcr.io/tarulab/km-nextreport", "internal 5489 only", "0.80 / 512M", "Node 22 + Chromium"),
    ("postgres", "postgres:16-alpine", "internal 5432 only", "1.00 / 1024M", "pgdata volume"),
    ("redis", "redis:7-alpine", "internal 6379 only", "0.20 / 128M", "AOF on, maxmemory 96mb, volatile-lru"),
]

MIGRATIONS = [
    ("000001_init_schema.sql", "Baseline schema ported from the Laravel/MySQL source (43 tables)"),
    ("000002_add_cart.sql", "Cart + cart items for the e-commerce channel"),
    ("000003_payment_methods_qris_kartu.sql", "Adds QRIS and card payment methods"),
    ("000004_add_ecommerce.sql", "E-commerce order type and buyer columns"),
    ("000005_unique_invoice_sales_order.sql", "One invoice per sales order"),
    ("000006_finance_contract_guards.sql", "Cashflow immutability trigger and period lock"),
    ("000007_cashflow_reversal.sql", "Reversal chain columns on cashflows"),
    ("000008_distribution_foundation.sql", "Delivery orders, shipments, dispatches, fleet"),
    ("000009_shipment_tracking.sql", "Shipment tracking events and barcode"),
    ("000010_catalog_brands_units_barcodes.sql", "Brands, product units, product barcodes"),
    ("000011_pricing_snapshots.sql", "Immutable priced line snapshot + guard trigger"),
    ("000012_membership_identity.sql", "Memberships and tier catalogue"),
    ("000013_membership_benefit.sql", "Membership discount on the line snapshot"),
    ("000014_membership_tier_config.sql", "DB-backed tier configuration + history"),
    ("000015_sales_vouchers.sql", "Sales voucher table"),
    ("000016_galon.sql", "Galon (water container) transactions"),
    ("000017_sales_returns.sql", "Sales return header/items"),
    ("000018_cashier_shifts.sql", "Cashier shifts and drawer entries"),
    ("000019_cashflow_reversal_unique.sql", "One reversal per generated cashflow"),
    ("000020_pat_expires_not_null.sql", "Token expiry becomes mandatory"),
    ("000021_add_buyer_role.sql", "buyer role added to the users CHECK"),
    ("000022_return_item_condition.sql", "Per-line condition + restocked flag"),
    ("000023_galon_loan.sql", "Container loan value and return flag"),
    ("000024_cashier_register.sql", "Register/store binding for shifts"),
    ("000025_employee_leave_balance.sql", "Leave entitlement per employee"),
    ("000026_membership_points.sql", "Membership points accrual"),
    ("000027_audit_indexes_and_cashier_guard.sql", "Missing indexes and concurrency guards"),
    ("000028_payment_proof_url.sql", "Payment proof URL"),
    ("000029_add_products_is_featured.sql", "Featured flag for the B2B catalogue"),
    ("000030_multi_tenant_foundation.sql", "stores, store_users, app_settings, cash_accounts, COA"),
    ("000031_sales_settings_view.sql", "sales role may read store settings"),
    ("000032_sales_order_source_type.sql", "NOO / EC / AO source classification"),
    ("000033_galon_store_binding.sql", "store_id on galon transactions"),
    ("000034_rbac_application_scope.sql", "Application-scope permission tightening"),
    ("000035_customer_ecommerce_admin.sql", "is_ecommerce_admin on customers"),
    ("000036_location_foundation.sql", "Coordinates, geo areas, location tables"),
    ("000037_application_access.sql", "users.application_access materialised"),
    ("000038_galon_barcode.sql", "gallon_code on galon transactions"),
    ("000039_owner_role.sql", "owner role added"),
    ("000040_po_approval_audit.sql", "PO approver identity"),
    ("000041_finance_hardening.sql", "Accounting-aware cash management"),
    ("000042_sales_hierarchy_roles.sql", "gm / bm / spv roles"),
    ("000043_sales_order_approval.sql", "pending_approval status + supervisor"),
    ("000044_store_roles.sql", "store_manager and cashier roles"),
    ("000045_store_warehouse_link.sql", "stores.warehouse_id"),
    ("000046_bind_kepalatoko_store.sql", "Bind the demo store manager to an outlet"),
    ("000047_product_images.sql", "Product image backfill"),
    ("000048_stores_view_roles.sql", "Store read grants for GM/BM/Finance/Warehouse"),
    ("000049_sales_order_payment_terms.sql", "payment_terms on sales orders"),
    ("000050_accounting_module.sql", "COA, journals, periods, departments, advances"),
    ("000051_notifications_grant_cleanup.sql", "Drops the dead broadcast grant"),
    ("000052_add_finance_head_role.sql", "finance_head role"),
    ("000053_finance_rbac_fix.sql", "Finance RBAC corrections"),
    ("000054_po_approval_segregation.sql", "Segregation of PO approval"),
    ("000055_voucher_ap_journal_link.sql", "Voucher → PO/supplier link"),
    ("000056_employee_job_level.sql", "job_level and allowance columns"),
    ("000057_role_registry.sql", "Roles become a registry with 16 builtin rows"),
    ("000058_expand_role_permissions_wildcards.sql", "Expands '*' globs into concrete keys"),
    ("000059_warehouse_deposits.sql", "Driver cash deposit table"),
    ("000060_visit_schedule_rbac.sql", "Visit scheduling ownership"),
    ("000061_rbac_launch_reset.sql", "Launch reset of the role matrix"),
    ("000062_purchase_accrual_accounts.sql", "Accrual COA for purchases"),
    ("000063_accounting_rbac_piutang_kasir.sql", "Journal grants for sales/cashier"),
    ("000064_report_templates.sql", "report_templates table + invoice-print seed"),
    ("000065_phase2_report_templates.sql", "po-print, do-print, receipt-print seeds"),
    ("000066_phase3_analytical_templates.sql", "Analytical report seeds"),
    ("000067_phase4_financial_templates.sql", "Financial statement seeds"),
    ("000068_phase5_operational_slips.sql", "Operational slip seeds"),
    ("000069_b2b_pos_templates.sql", "B2B/POS template seeds"),
    ("000070_accounting_departments_plant_depo.sql", "Plant/depo cost centres"),
    ("000071_accounting_rbac_general_ledger_operators.sql", "GL operator grants"),
    ("000072_warehouse_deposit_driver_link.sql", "Deposit → driver link"),
    ("000073_lhpp_print_template_v2.sql", "LHPP print template v2"),
    ("000074_lhpp_print_template_v3.sql", "LHPP print template v3 (TOTAL fix)"),
    ("000075_payments_deposit_link.sql", "Payment → deposit link"),
    ("000076_hr_suite_integrations.sql", "HR fields and permissions"),
    ("000077_membership_tier_requirements.sql", "min_orders / min_spend tier requirements"),
    ("000078_chart_of_accounts_standard_5_groups.sql", "Standard 5-group COA seed"),
    ("000079_warehouse_customer_orders_view.sql", "Warehouse customer-order grants"),
    ("000080_operator_cashflow_journal_update.sql", "RBAC gap closure"),
    ("000081_finance_cashier_grants.sql", "Restores grants wiped by 000061"),
    ("000082_journal_action_grants.sql", "Journal action-verb grants"),
    ("000083_truncate_cashflows.sql", "DESTRUCTIVE: TRUNCATE cashflows RESTART IDENTITY CASCADE"),
    ("000084_document_number_sequences_backfill.sql", "Restores the allocator above existing data"),
    ("000085_purchasing_role_gap_restores.sql", "Purchasing RBAC completeness"),
    ("000086_ppn_masukan_account.sql", "PPN masukan account"),
    ("000087_sales_targets.sql", "Sales targets + activity expansion"),
    ("000088_do_print_template_v2.sql", "Delivery-order print template v2"),
    ("000089_store_manager_pos_grants.sql", "store_manager POS grants"),
    ("000090_reconcile_and_sync_stocks.sql", "DATA MUTATION: clamps negative stock, rebuilds reservations"),
    ("000091_grant_warehouse_transfer_permissions.sql", "Warehouse transfer/opname grants"),
    ("000092_sync_sales_order_delivery_status.sql", "DATA MUTATION: reconciles SO status with DOs"),
    ("000093_sales_targets_and_tracking_permissions.sql", "Targets/tracking grants"),
    ("000094_customer_geo_areas.sql", "Administrative area columns"),
    ("000095_midtrans_payments.sql", "Gateway columns for Snap"),
    ("000096_buyer_addresses.sql", "Buyer address book"),
    ("000097_sales_targets_gm_only.sql", "Targets restricted to GM"),
    ("000098_me_views_indexes.sql", "Composite indexes for buyer /me views"),
    ("000099_cashier_pos_grants.sql", "Cashier POS/membership grants"),
    ("000100_payments_proof_url_idx.sql", "Proof authz index"),
    ("000101_lhpp_manual_groups.sql", "LHPP group tables + backfill"),
    ("000102_snap_reconcile_tracking.sql", "Snap sweep throttle column + index"),
    ("000103_revoke_operator_accounting_and_restore_hr.sql", "Revokes operator accounting grants"),
    ("000104_cashflow_outlet_scope.sql", "cashflows.store_id"),
    ("000105_clear_operator_permission_overrides.sql", "Clears users.permissions overrides"),
    ("000106_employee_number_sequence_anchor.sql", "EMP- counter anchor"),
    ("000107_warehouse_code_sequence_anchor.sql", "GDG- counter anchor"),
    ("000108_payroll_number_sequence_month_buckets.sql", "PAY- monthly buckets"),
    ("000109_sales_orders_sales_id_employee_remap.sql", "sales_id remapped to employees(id)"),
    ("000110_restore_visit_schedule_and_lhpp_view_grants.sql", "Restores visit/LHPP grants"),
    ("000111_sales_membership_enrolment_grants.sql", "Membership enrolment grants"),
    ("000112_gm_and_store_manager_membership_tier_pricing.sql", "Tier pricing grants"),
    ("000113_gm_reads_the_tier_catalogue.sql", "GM reads the tier catalogue"),
    ("000114_buyer_validates_sales_vouchers.sql", "buyer may validate vouchers"),
    ("000115_cashier_reads_store_settings_for_receipts.sql", "Cashier settings read"),
    ("000116_deactivate_duplicate_cash_accounts.sql", "DATA MUTATION: deactivates duplicate cash accounts"),
    ("000117_customer_profile_fields.sql", "Customer profile/geo fields"),
    ("000118_gateway_transaction_ref.sql", "gateway_transaction_id / expires_at"),
    ("000119_warehouse_coordinate_precision.sql", "Warehouse coordinate provenance"),
    ("000120_store_coordinate_precision.sql", "Store coordinate provenance"),
    ("000121_buyer_membership_read.sql", "buyer membership read grant"),
    ("000122_support_tickets.sql", "tickets + ticket_comments"),
    ("000123_seed_walkin_customer.sql", "Seeds the Pelanggan Umum walk-in customer"),
]

FINDINGS = [
    # id, title, severity, area, evidence, impact, recommendation
    ("F-01", "Legacy Laravel framework tables ship in the schema and are never used",
     "Low", "Database",
     "apps/backend/migrations/000001_init_schema.sql creates cache, cache_locks, jobs, job_batches, failed_jobs, sessions and migrations; the Go backend replaces all of them with Redis or bearer tokens (pkg/redis, internal/auth/token.go).",
     "Dead objects consume ~24 kB per database and invite confusion during incident response (someone will look for queued jobs that never exist).",
     "Drop them in a dedicated migration after confirming no external tool reads them."),
    ("F-02", "Documentation points at module paths that do not exist",
     "Medium", "Documentation",
     "AGENTS.md:20-21 and :72 cite internal/database/migrations and internal/modules/rbac/catalog.go; the real paths are apps/backend/migrations and apps/backend/internal/auth/catalog.go. docs/architecture/overview.md repeats both.",
     "A new contributor (human or agent) following the canonical instruction file lands on nonexistent directories and may create a second convention.",
     "Correct the paths in AGENTS.md, docs/architecture/overview.md and apps/backend/AGENTS.md."),
    ("F-03", "CI pins Go 1.24 while go.mod requires 1.26.5",
     "High", "CI/CD",
     ".github/workflows/ci.yml:41 sets go-version 1.24; apps/backend/go.mod:3 declares go 1.26.5.",
     "The backend job cannot build a module that requires a newer toolchain; the quality gate fails before any test runs.",
     "Set go-version-file: apps/backend/go.mod (or '1.26.x') in both CI jobs."),
    ("F-04", "Repository-wide test command in the root manifest mixes incompatible runners",
     "Medium", "Tooling",
     "package.json test script is 'turbo run test && pnpm test:backend'; turbo runs the JS suites, then Go tests run from the root without a database, so internal/integration skips and the remaining Go packages rely on DATABASE_URL being unset.",
     "A single `pnpm test` gives a false sense of full coverage: the integration suite silently skips instead of running.",
     "Document the two-phase requirement (DB up + KG_ALLOW_DESTRUCTIVE_DB_TESTS=1) in CONTRIBUTING.md, or split into test:unit and test:integration."),
    ("F-05", "The integration test package truncates 66 tables when given consent",
     "High", "Testing",
     "internal/integration/helpers_test.go:requireDestructiveConsent + setUp TRUNCATEs users, customers, products and sales_orders with CASCADE. Replaying every migration into a scratch database and running TRUNCATE users CASCADE produced 66 cascade notices, i.e. 67 tables emptied including users itself.",
     "One exported DATABASE_URL plus KG_ALLOW_DESTRUCTIVE_DB_TESTS=1 against a shared database destroys business data. The comment in the file records that this already happened once.",
     "Keep the two-variable consent, and additionally refuse to run when the target database name does not match a throwaway pattern (e.g. *_test)."),
    ("F-06", "Post-login permission map for admin/owner serialises as a wildcard only",
     "Low", "Auth",
     "GET /api/v1/auth/me returned permissions {\"*\":true, \"sales.lhpp.merge\":true, \"sales.lhpp.view\":true} for the seeded admin; EffectivePermissions publishes '*' for admin/owner (internal/auth/model.go).",
     "Clients that test a concrete key rather than the wildcard see an empty grant set for admin. The ERP guard has an explicit fallback, POS has a special case; a new client would regress.",
     "Publish the fully expanded key set alongside the wildcard, or document the wildcard contract in packages/permissions."),
    ("F-07", "SMTP_FROM is documented but never read",
     "Medium", "Configuration",
     ".env.example:SMTP_FROM and pkg/config.Config.SMTPFrom exist; pkg/config/config.go Load() never calls getEnv(\"SMTP_FROM\"), so From is always empty and mailer.SMTPMailer sends an empty From header.",
     "Password-reset mail from a real SMTP relay is rejected or spam-filed, breaking account recovery.",
     "Add SMTPFrom: getEnv(\"SMTP_FROM\", \"no-reply@keluargamart.local\") to Load()."),
    ("F-08", "Permission catalog and role list drift in the generated client package",
     "Medium", "Auth",
     "packages/permissions/src/index.js ROLES lists admin, owner, finance, warehouse, cashier, customer, store_manager, system (8), while internal/auth/model.go AllRoles and migration 000057 seed 16 roles (gm, bm, spv, sales, buyer, finance_head, hr_manager, hr_staff, employee, ...). 'customer' is not a backend role.",
     "Any client-side role check against ROLES is wrong for 9 real roles and offers a nonexistent one.",
     "Generate ROLES from AllRoles in scripts/gen-permissions.py instead of hard-coding the list."),
    ("F-09", "Migration 000058 only applies inside a single transaction",
     "Low", "Database",
     "000058_expand_role_permissions_wildcards.sql creates a TEMP TABLE ... ON COMMIT DROP. Applied statement-by-statement (psql without --single-transaction, or any tool that autocommits), the temp table is dropped before the INSERT and the migration fails with 'relation catalog_spec does not exist' (reproduced).",
     "Out-of-band migration runs (psql -f, GUI clients) fail on this file and leave the schema half-migrated.",
     "Replace the temp table with a CTE/VALUES list, or document --single-transaction as mandatory for manual application."),
    ("F-10", "Two compose stacks and two seed paths disagree on PostgreSQL version and ports",
     "Medium", "Deployment",
     "infra/containers/compose.prod.yml uses postgres:16-alpine; apps/backend/docker-compose.yml uses postgres:17-alpine. deploy/.env.prod.example maps ERP_PORT=3003/POS_PORT=3005 while compose.prod.yml defaults 3001/3003 and the Cloudflare doc fixes 3001/3002/3003/3004.",
     "Following the wrong runbook yields a stack whose ports do not match the tunnel ingress, and a database major version that differs from production.",
     "Make infra/containers/compose.prod.yml the only deployment stack; retire or clearly mark the legacy compose files."),
    ("F-11", "Frontend nginx proxy resolves the backend per request but only when the DNS resolver is reachable at start-up",
     "Low", "Deployment",
     "apps/erp/deploy/nginx-entrypoint.sh reads the first nameserver from /etc/resolv.conf and exits non-zero if it is absent; the config is generated at container start.",
     "In a runtime whose resolv.conf is generated after the entrypoint (or absent), the container exits instead of degrading.",
     "Document the dependency, or fall back to a literal upstream with a reload-on-change watcher."),
    ("F-12", "The B2B proxy forwards the auth cookie but the API also accepts a bearer token from any origin",
     "Medium", "Security",
     "apps/b2b/server/api/v1/[...path].ts copies the km_auth_token cookie into an Authorization header; the backend's CORS default is '*' (pkg/config), which allows any site to call the API with a token it already holds.",
     "Not a direct compromise (the attacker still needs a token), but it removes the origin boundary that would otherwise limit token replay from a compromised page.",
     "Set CORS_ORIGINS explicitly in production to the four public hostnames."),
    ("F-13", "Unknown X-Request-Id values are silently replaced instead of rejected",
     "Informational", "Observability",
     "pkg/middleware/requestid.go:isValidRequestID replaces a client-supplied ID that fails the allowlist.",
     "Client-side correlation IDs disappear without a signal, making cross-system tracing harder during an incident.",
     "Log a debug line when a supplied ID is discarded."),
    ("F-14", "Idempotency is mandatory on money endpoints but the ERP client mints a random key by default",
     "Medium", "Financial correctness",
     "apps/erp/src/services/http.js injects `idem-fallback-<ts>-<rand>` on every POST/PUT/PATCH that carries no key, and its own comment says a key minted there is unique per request so the server's dedup branch can never fire. Money call sites that use services/idempotency.js (keyFor/settle, covered by 12 unit tests) are protected; the fallback exists for the ones that are not migrated yet and logs a development warning naming each.",
     "A double-clicked form on an unmigrated call site still posts twice, and the protection is invisible in production because the warning only prints in development.",
     "Make the fallback fail loudly in development (throw) and enumerate the unmigrated call sites as a checklist."),
    ("F-15", "Report template table is empty on a schema that was created without running the seed migrations in one transaction",
     "Low", "Reports",
     "report_templates rows are seeded by migrations 000064-000069/073/074/088. Any TRUNCATE ... CASCADE that reaches users also truncates report_templates (created_by FK), which was observed on the audit database; a fresh migrate-only database has 22 rows.",
     "PDF/HTML rendering returns 404 'Active template not found' with no hint that the cause is missing seed rows.",
     "Add a startup check (or a health detail) that reports the number of active templates per report code."),
    ("F-16", "Cloudflare ingress and host ports are documented but not verifiable from the repository",
     "Informational", "Deployment",
     "docs/deployment/cloudflare-tunnel.md fixes erp 3001, b2b 3002, pos 3003, api 3004. No cloudflared config.yml is committed (correctly, since it carries the tunnel UUID and credentials).",
     "A port change in compose silently breaks a hostname until someone reads the tunnel dashboard.",
     "Keep the mapping in one place (an env file consumed by compose and referenced by the runbook)."),
    ("F-17", "POS has no offline transaction queue despite 'offline-first' claims in the overview",
     "Medium", "Documentation",
     "docs/architecture/overview.md and apps/pos/AGENTS.md describe an offline queue; no service worker, IndexedDB queue or pending-sync code exists under apps/pos/src (only localStorage caches for settings/session).",
     "Cashiers may assume sales survive a network drop; they do not. A dropped request is simply lost.",
     "Either implement the queue or correct the documentation to state that POS requires connectivity."),
    ("F-18", "Frontend builds are shipped from the repo's local dist/.output directories",
     "Informational", "Build hygiene",
     "apps/erp/dist, apps/pos/dist and apps/b2b/.output exist in the working tree (gitignored) and were used by the local test runs; the Dockerfiles rebuild from source.",
     "A stale local build can be mistaken for the deployed artifact during a demo.",
     "Keep using the GHCR images as the deployment source of truth; never copy dist/ to a server."),
    ("F-19", "sqlc schema snapshot lags the migration set",
     "Low", "Database tooling",
     "apps/backend/db/schema.sql declares 86 tables; the migrated schema has 92. report_templates, sales_targets, ticket_comments, tickets, warehouse_deposits and schema_migrations are missing, so those modules use hand-written SQL.",
     "Running `sqlc generate` cannot type-check queries against the newer tables, which pushes new code toward untyped raw SQL.",
     "Refresh db/schema.sql from the migrated database and add the missing queries to db/query."),
    ("F-21", "OpenAPI RoleName enum lists 8 roles while the backend validates 16",
     "Medium", "API contract",
     "apps/backend/docs/openapi.yaml components.schemas.RoleName enumerates admin, sales, finance, warehouse, hr_manager, hr_staff, employee, system. internal/auth/model.go AllRoles has 16 (adds owner, gm, bm, spv, store_manager, cashier, buyer and finance_head to the eight listed).",
     "Generated clients validate against a role set that rejects every outlet-level role, so typed clients cannot represent a store_manager or cashier user.",
     "Regenerate the enum from AllRoles in the same script that writes packages/permissions."),
    ("F-24", "Two live routes bypass the route/OpenAPI contract entirely",
     "Medium", "API contract",
     "cmd/server/main.go mounts /api/v1/reports/render and /api/v1/reports/preview with rr.HandleFunc, not rr.Get/Post; the handlers branch on r.Method themselves (render accepts GET with query params and POST with a JSON body). The route contract test (parseGatedRoutes) and the drift guard both enumerate only Get/Post/Put/Patch/Delete, so these two paths appear in neither the 373-operation count nor openapi.yaml. They are reachable, permission-checked inside the handler (templatePerms + buyer ownership), and used by every print button in ERP and POS.",
     "The contract says 373 operations and the router serves 375. A client generated from openapi.yaml cannot see the render endpoint that the frontends actually call, and a future gate regression on it would not be caught by the route table.",
     "Document both paths in openapi.yaml, mount them with explicit methods, and extend the contract test to include HandleFunc registrations."),
    ("F-22", "Launch-reset migration wiped the seeded role matrix and nothing restored the bulk of it",
     "High", "Authorization",
     "000061_rbac_launch_reset.sql runs DELETE FROM role_permissions and deliberately re-inserts nothing; seed.sql contains zero role_permissions rows. Measured on a database with all 123 migrations: 304 rows remain, and 76 of the 201 catalogue keys are held by no role at all; 71 of those 76 are gated on a live route. Before 000061 the same database holds 860 rows: sampled at that point, masterdata.products.create had 5 role holders and finance.reports.view had 5.",
     "On a fresh deployment only admin and owner can create or edit a product, maintain suppliers, read any financial report, open the chart of accounts, administer users or roles, read the activity log, manage stores, or use any distribution screen. A GM logging in sees 16 of 66 navigation items and gets 403 on financial reports, stores and distribution; a store_manager gets 403 on users and financial reports. 19 navigation entries are hidden from every non-bypass role. Recoverable (an admin can re-grant via PUT /role-permissions), but not the documented default.",
     "Re-seed the intended baseline matrix in a migration (or in seed.sql) and add a test asserting that every catalogue key is held by at least one role, or that the set of unheld keys is an explicit allowlist."),
    ("F-23", "No automated check that a permission key is actually reachable",
     "Medium", "Authorization",
     "The route\u2192gate table proves each route HAS a gate, and the drift guard proves the catalogue and the route list match, but nothing asserts that a key is granted to some role. Result: 76 keys are dead grants (F-22) and 19 ERP navigation entries are hidden from every non-bypass role.",
     "A permission can be introduced, wired to a route and documented while being unusable by anyone but admin/owner; the gap is invisible until a user reports a 403.",
     "Extend the RBAC test suite with a coverage assertion over role_permissions, and make the unheld-key list an explicit, reviewed allowlist."),
    ("F-20", "The ERD/route inventories and this document are generated, but no CI job regenerates them",
     "Informational", "Documentation",
     "docs/docgen/build_doc.py and docs/diagrams/gen_erd.py reproduce this document; .github/workflows/ci.yml does not run them.",
     "The document can silently drift from the code it describes.",
     "Add a scheduled job that regenerates the inventories and fails on a diff."),
]

FINDINGS.sort(key=lambda f: int(f[0].split("-")[1]))

WORKFLOWS = [
    ("W-01", "Authentication and session", "auth",
     "POST /api/v1/auth/login → GET /api/v1/auth/me → POST /api/v1/auth/logout",
     "personal_access_tokens, users, activity_logs"),
    ("W-02", "Product catalogue maintenance", "catalog",
     "POST/PUT/DELETE /api/v1/products (+ /units, /barcodes), /categories, /brands, /units",
     "products, product_units, product_barcodes, categories, brands, units"),
    ("W-03", "Stock-in / stock-out / adjustment", "inventory",
     "POST /api/v1/inventory/stock-in, /stock-out, GET /inventory/movements",
     "stocks, stock_movements"),
    ("W-04", "Stock opname", "inventory",
     "POST /api/v1/inventory/opnames → finalize",
     "stock_opnames, stock_opname_items, stocks, stock_movements"),
    ("W-05", "Inter-warehouse transfer", "inventory",
     "POST /api/v1/inventory/transfers → approve → send → receive",
     "stock_transfers, stock_transfer_items, stocks, stock_movements"),
    ("W-06", "Purchase order to goods receipt", "purchasing",
     "POST /api/v1/purchase-orders → approve → POST /goods-receipts",
     "purchase_orders, purchase_order_items, goods_receipts, goods_receipt_items, stocks, journal_entries"),
    ("W-07", "Supplier debt payment and voucher", "purchasing",
     "POST /api/v1/purchase-orders/{id}/pay, POST /api/v1/vouchers",
     "purchase_orders, payments, vouchers, cashflows"),
    ("W-08", "Regular sales order", "sales",
     "POST /api/v1/sales-orders → PATCH /{id}/status confirmed → invoice → payment",
     "sales_orders, sales_order_items, stocks (reserved), invoices, payments"),
    ("W-09", "POS checkout", "sales",
     "POST /api/v1/pos (Idempotency-Key mandatory)",
     "sales_orders, sales_order_items, stocks, stock_movements, invoices, payments, cashflows"),
    ("W-10", "POS shift open / close", "cashier",
     "POST /api/v1/cashier/shifts → POST /cashier/shifts/{id}/close",
     "cashier_shifts, cashier_drawer_entries, sales_orders (shift_id)"),
    ("W-11", "Sales return and refund", "sales",
     "POST /api/v1/returns (line-level, pro-rata refund)",
     "sales_returns, sales_return_items, stocks, stock_movements, cashier_drawer_entries"),
    ("W-12", "Membership enrolment and tier change", "membership",
     "POST /api/v1/memberships, POST /memberships/{id}/tier",
     "memberships, membership_tier_history, membership_tiers"),
    ("W-13", "B2B cart checkout", "cart",
     "POST /api/v1/cart/items → POST /api/v1/cart/checkout",
     "carts, cart_items, sales_orders, sales_order_items, stocks"),
    ("W-14", "Midtrans online payment", "finance",
     "POST /api/v1/sales-orders/{id}/snap-token → provider → POST /api/v1/midtrans/notifications",
     "invoices, payments, sales_orders"),
    ("W-15", "Journal entry lifecycle", "accounting",
     "POST /api/v1/accounting/journals → POST /{id}/post → POST /{id}/reverse",
     "journal_entries, journal_lines, accounts"),
    ("W-16", "Cashflow entry and reversal", "finance",
     "POST /api/v1/cashflows → POST /{id}/reverse",
     "cashflows, accounting_periods (trigger)"),
    ("W-17", "Delivery order and shipment", "distribution",
     "POST /api/v1/delivery-orders → dispatch → shipment → tracking events",
     "delivery_orders, delivery_order_items, dispatches, shipments, shipment_items, shipment_tracking_events"),
    ("W-18", "LHPP settlement", "distribution",
     "POST /api/v1/lhpp → add DOs → print",
     "lhpp_groups, lhpp_group_items, lhpp_group_history, warehouse_deposits"),
    ("W-19", "Payroll generation", "hr",
     "POST /api/v1/hr/payrolls → POST /{id}/approve → POST /{id}/pay",
     "payrolls, employees, journal_entries"),
    ("W-20", "Support ticket lifecycle", "tickets",
     "POST /api/v1/tickets → comment → resolve",
     "tickets, ticket_comments"),
    ("W-21", "Report rendering", "reports",
     "GET /api/v1/reports/render?template=… → NextReport /render",
     "report_templates (read-only)"),
]
