From b2bd7ccf725a07cd97f6a3cf784ff022d97cb9e0 Mon Sep 17 00:00:00 2001 From: fred Date: Thu, 27 Aug 2026 15:35:46 -0500 Subject: [PATCH 01/10] feat(db): hierarchy record class schema + witnesses (contract 1, M4-1a) Implements docs/requirements/hierarchy-schema.md sections 2-4 and the schema-layer half of section 6: - Five class tables (companies, estates, platform_projects, workspaces, hierarchy_grants) with the section 2.7 exhaustive column sets: child node tables carry no timestamps (renames are audited via events), no owner_id anywhere (section 4.4 - ownership is computed from grants). - Grant constraints per section 3: exactly-one-subject and exactly-one-target num_nonnulls CHECKs, six-column UNIQUE NULLS NOT DISTINCT, target FKs CASCADE / principal FKs RESTRICT, six btree indexes. - Migration 0018 generated by drizzle-kit; SQL verified against the contract text and applied on PGlite. - hierarchy-schema.witness.test.ts: dual-leg witness suite (PGlite always; real PostgreSQL under DATABASE_URL, the section 6.8 binding leg in CI). Covers parent-FK integrity + catalog assertion, slug scoping, column allowlist (6.2), all six grant subject/target forms, CHECK refusals, NULLS NOT DISTINCT duplicates, NOT NULL refusals, and deletion semantics (6.6): fail-closed parent delete, leaf cascade of exactly its grants, principal RESTRICT. - hierarchy-writer-coverage.test.ts: section 6.3(b) three-prong static assertion (alias-aware symbol writes, class-table names in SQL literals, raw-execution primitives) with empty writer allowlist, closed infrastructure register, and closed importer enumerations for the migration runner and migrate-tier. All prongs proven able to fire via a planted-violation control. Command family, audit events, and route inventory land in M4-1b. --- packages/db/drizzle/0018_clean_cobalt_man.sql | 63 + packages/db/drizzle/meta/0018_snapshot.json | 5034 +++++++++++++++++ packages/db/drizzle/meta/_journal.json | 9 +- .../db/src/hierarchy-schema.witness.test.ts | 471 ++ .../db/src/hierarchy-writer-coverage.test.ts | 349 ++ packages/db/src/schema.ts | 104 + 6 files changed, 6029 insertions(+), 1 deletion(-) create mode 100644 packages/db/drizzle/0018_clean_cobalt_man.sql create mode 100644 packages/db/drizzle/meta/0018_snapshot.json create mode 100644 packages/db/src/hierarchy-schema.witness.test.ts create mode 100644 packages/db/src/hierarchy-writer-coverage.test.ts diff --git a/packages/db/drizzle/0018_clean_cobalt_man.sql b/packages/db/drizzle/0018_clean_cobalt_man.sql new file mode 100644 index 00000000..3b1221d6 --- /dev/null +++ b/packages/db/drizzle/0018_clean_cobalt_man.sql @@ -0,0 +1,63 @@ +CREATE TABLE "companies" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + "slug" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "companies_slug_unique" UNIQUE("slug") +); +--> statement-breakpoint +CREATE TABLE "estates" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + "slug" text NOT NULL, + "company_id" uuid NOT NULL, + CONSTRAINT "estates_company_slug_uniq" UNIQUE("company_id","slug") +); +--> statement-breakpoint +CREATE TABLE "hierarchy_grants" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" text, + "team_id" uuid, + "company_id" uuid, + "estate_id" uuid, + "platform_project_id" uuid, + "role" text NOT NULL, + "granted_by" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "hierarchy_grants_subject_target_role_uniq" UNIQUE NULLS NOT DISTINCT("user_id","team_id","company_id","estate_id","platform_project_id","role"), + CONSTRAINT "hierarchy_grants_subject_check" CHECK (num_nonnulls(user_id, team_id) = 1), + CONSTRAINT "hierarchy_grants_target_check" CHECK (num_nonnulls(company_id, estate_id, platform_project_id) = 1) +); +--> statement-breakpoint +CREATE TABLE "platform_projects" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + "slug" text NOT NULL, + "estate_id" uuid NOT NULL, + CONSTRAINT "platform_projects_estate_slug_uniq" UNIQUE("estate_id","slug") +); +--> statement-breakpoint +CREATE TABLE "workspaces" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + "slug" text NOT NULL, + "platform_project_id" uuid NOT NULL, + CONSTRAINT "workspaces_platform_project_slug_uniq" UNIQUE("platform_project_id","slug") +); +--> statement-breakpoint +ALTER TABLE "estates" ADD CONSTRAINT "estates_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "hierarchy_grants" ADD CONSTRAINT "hierarchy_grants_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "hierarchy_grants" ADD CONSTRAINT "hierarchy_grants_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."teams"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "hierarchy_grants" ADD CONSTRAINT "hierarchy_grants_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "hierarchy_grants" ADD CONSTRAINT "hierarchy_grants_estate_id_estates_id_fk" FOREIGN KEY ("estate_id") REFERENCES "public"."estates"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "hierarchy_grants" ADD CONSTRAINT "hierarchy_grants_platform_project_id_platform_projects_id_fk" FOREIGN KEY ("platform_project_id") REFERENCES "public"."platform_projects"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "hierarchy_grants" ADD CONSTRAINT "hierarchy_grants_granted_by_users_id_fk" FOREIGN KEY ("granted_by") REFERENCES "public"."users"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "platform_projects" ADD CONSTRAINT "platform_projects_estate_id_estates_id_fk" FOREIGN KEY ("estate_id") REFERENCES "public"."estates"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "workspaces" ADD CONSTRAINT "workspaces_platform_project_id_platform_projects_id_fk" FOREIGN KEY ("platform_project_id") REFERENCES "public"."platform_projects"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "hierarchy_grants_company_id_idx" ON "hierarchy_grants" USING btree ("company_id");--> statement-breakpoint +CREATE INDEX "hierarchy_grants_estate_id_idx" ON "hierarchy_grants" USING btree ("estate_id");--> statement-breakpoint +CREATE INDEX "hierarchy_grants_platform_project_id_idx" ON "hierarchy_grants" USING btree ("platform_project_id");--> statement-breakpoint +CREATE INDEX "hierarchy_grants_user_id_idx" ON "hierarchy_grants" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "hierarchy_grants_team_id_idx" ON "hierarchy_grants" USING btree ("team_id");--> statement-breakpoint +CREATE INDEX "hierarchy_grants_granted_by_idx" ON "hierarchy_grants" USING btree ("granted_by"); \ No newline at end of file diff --git a/packages/db/drizzle/meta/0018_snapshot.json b/packages/db/drizzle/meta/0018_snapshot.json new file mode 100644 index 00000000..25c62eac --- /dev/null +++ b/packages/db/drizzle/meta/0018_snapshot.json @@ -0,0 +1,5034 @@ +{ + "id": "12c270b3-4f57-4f30-bcfd-6adddca4263a", + "prevId": "845a2f87-ad97-49a1-a0b0-0a9c39f732b1", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "accounts_provider_account_idx": { + "name": "accounts_provider_account_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "accounts_user_id_idx": { + "name": "accounts_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.admin_tokens": { + "name": "admin_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "admin_tokens_user_id_idx": { + "name": "admin_tokens_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "admin_tokens_hash_idx": { + "name": "admin_tokens_hash_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "admin_tokens_user_id_users_id_fk": { + "name": "admin_tokens_user_id_users_id_fk", + "tableFrom": "admin_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_logs": { + "name": "agent_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'hot'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "summarized_at": { + "name": "summarized_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "agent_logs_session_tier_idx": { + "name": "agent_logs_session_tier_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_logs_user_id_idx": { + "name": "agent_logs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_logs_tier_created_at_idx": { + "name": "agent_logs_tier_created_at_idx", + "columns": [ + { + "expression": "tier", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_logs_user_id_users_id_fk": { + "name": "agent_logs_user_id_users_id_fk", + "tableFrom": "agent_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_prompt": { + "name": "system_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_tools": { + "name": "allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agents_project_id_idx": { + "name": "agents_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_owner_id_idx": { + "name": "agents_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_is_system_idx": { + "name": "agents_is_system_idx", + "columns": [ + { + "expression": "is_system", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_project_id_projects_id_fk": { + "name": "agents_project_id_projects_id_fk", + "tableFrom": "agents", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "agents_owner_id_users_id_fk": { + "name": "agents_owner_id_users_id_fk", + "tableFrom": "agents", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.appreciations": { + "name": "appreciations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "from_user": { + "name": "from_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "to_user": { + "name": "to_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backlog": { + "name": "backlog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "backlog_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ready'" + }, + "depends_on": { + "name": "depends_on", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "claim_owner": { + "name": "claim_owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claim_ttl_seconds": { + "name": "claim_ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acceptance": { + "name": "acceptance", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "backlog_status_priority_idx": { + "name": "backlog_status_priority_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "backlog_status_claimed_at_idx": { + "name": "backlog_status_claimed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "backlog_idempotency_key_idx": { + "name": "backlog_idempotency_key_idx", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.companies": { + "name": "companies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "companies_slug_unique": { + "name": "companies_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector_lease_audit_log": { + "name": "connector_lease_audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logical_agent_id": { + "name": "logical_agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "binding_id": { + "name": "binding_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lease_id": { + "name": "lease_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "lease_epoch": { + "name": "lease_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "connector_lease_audit_binding_occurred_idx": { + "name": "connector_lease_audit_binding_occurred_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "logical_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "binding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connector_lease_audit_correlation_idx": { + "name": "connector_lease_audit_correlation_idx", + "columns": [ + { + "expression": "correlation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conversations": { + "name": "conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "conversations_user_archived_idx": { + "name": "conversations_user_archived_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_project_id_idx": { + "name": "conversations_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_agent_id_idx": { + "name": "conversations_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "conversations_user_id_users_id_fk": { + "name": "conversations_user_id_users_id_fk", + "tableFrom": "conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversations_project_id_projects_id_fk": { + "name": "conversations_project_id_projects_id_fk", + "tableFrom": "conversations", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "conversations_agent_id_agents_id_fk": { + "name": "conversations_agent_id_agents_id_fk", + "tableFrom": "conversations", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.estates": { + "name": "estates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "estates_company_id_companies_id_fk": { + "name": "estates_company_id_companies_id_fk", + "tableFrom": "estates", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "estates_company_slug_uniq": { + "name": "estates_company_slug_uniq", + "nullsNotDistinct": false, + "columns": [ + "company_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "events_type_idx": { + "name": "events_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_date_idx": { + "name": "events_date_idx", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_audit_log": { + "name": "federation_audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "peer_id": { + "name": "peer_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subject_user_id": { + "name": "subject_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_id": { + "name": "grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "verb": { + "name": "verb", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "result_count": { + "name": "result_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "denied_reason": { + "name": "denied_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "query_hash": { + "name": "query_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bytes_out": { + "name": "bytes_out", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "federation_audit_log_peer_created_at_idx": { + "name": "federation_audit_log_peer_created_at_idx", + "columns": [ + { + "expression": "peer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "federation_audit_log_subject_created_at_idx": { + "name": "federation_audit_log_subject_created_at_idx", + "columns": [ + { + "expression": "subject_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "federation_audit_log_created_at_idx": { + "name": "federation_audit_log_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "federation_audit_log_peer_id_federation_peers_id_fk": { + "name": "federation_audit_log_peer_id_federation_peers_id_fk", + "tableFrom": "federation_audit_log", + "tableTo": "federation_peers", + "columnsFrom": [ + "peer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "federation_audit_log_subject_user_id_users_id_fk": { + "name": "federation_audit_log_subject_user_id_users_id_fk", + "tableFrom": "federation_audit_log", + "tableTo": "users", + "columnsFrom": [ + "subject_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "federation_audit_log_grant_id_federation_grants_id_fk": { + "name": "federation_audit_log_grant_id_federation_grants_id_fk", + "tableFrom": "federation_audit_log", + "tableTo": "federation_grants", + "columnsFrom": [ + "grant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_enrollment_tokens": { + "name": "federation_enrollment_tokens", + "schema": "", + "columns": { + "token": { + "name": "token", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "grant_id": { + "name": "grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "peer_id": { + "name": "peer_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "federation_enrollment_tokens_grant_id_federation_grants_id_fk": { + "name": "federation_enrollment_tokens_grant_id_federation_grants_id_fk", + "tableFrom": "federation_enrollment_tokens", + "tableTo": "federation_grants", + "columnsFrom": [ + "grant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "federation_enrollment_tokens_peer_id_federation_peers_id_fk": { + "name": "federation_enrollment_tokens_peer_id_federation_peers_id_fk", + "tableFrom": "federation_enrollment_tokens", + "tableTo": "federation_peers", + "columnsFrom": [ + "peer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_grants": { + "name": "federation_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subject_user_id": { + "name": "subject_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "peer_id": { + "name": "peer_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "grant_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_reason": { + "name": "revoked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "federation_grants_subject_status_idx": { + "name": "federation_grants_subject_status_idx", + "columns": [ + { + "expression": "subject_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "federation_grants_peer_status_idx": { + "name": "federation_grants_peer_status_idx", + "columns": [ + { + "expression": "peer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "federation_grants_subject_user_id_users_id_fk": { + "name": "federation_grants_subject_user_id_users_id_fk", + "tableFrom": "federation_grants", + "tableTo": "users", + "columnsFrom": [ + "subject_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "federation_grants_peer_id_federation_peers_id_fk": { + "name": "federation_grants_peer_id_federation_peers_id_fk", + "tableFrom": "federation_grants", + "tableTo": "federation_peers", + "columnsFrom": [ + "peer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_peers": { + "name": "federation_peers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "common_name": { + "name": "common_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cert_pem": { + "name": "cert_pem", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cert_serial": { + "name": "cert_serial", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cert_not_after": { + "name": "cert_not_after", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "client_key_pem": { + "name": "client_key_pem", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "peer_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "endpoint_url": { + "name": "endpoint_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "federation_peers_cert_serial_idx": { + "name": "federation_peers_cert_serial_idx", + "columns": [ + { + "expression": "cert_serial", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "federation_peers_state_idx": { + "name": "federation_peers_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "federation_peers_common_name_unique": { + "name": "federation_peers_common_name_unique", + "nullsNotDistinct": false, + "columns": [ + "common_name" + ] + }, + "federation_peers_cert_serial_unique": { + "name": "federation_peers_cert_serial_unique", + "nullsNotDistinct": false, + "columns": [ + "cert_serial" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hierarchy_grants": { + "name": "hierarchy_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "estate_id": { + "name": "estate_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "platform_project_id": { + "name": "platform_project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "hierarchy_grants_company_id_idx": { + "name": "hierarchy_grants_company_id_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hierarchy_grants_estate_id_idx": { + "name": "hierarchy_grants_estate_id_idx", + "columns": [ + { + "expression": "estate_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hierarchy_grants_platform_project_id_idx": { + "name": "hierarchy_grants_platform_project_id_idx", + "columns": [ + { + "expression": "platform_project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hierarchy_grants_user_id_idx": { + "name": "hierarchy_grants_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hierarchy_grants_team_id_idx": { + "name": "hierarchy_grants_team_id_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hierarchy_grants_granted_by_idx": { + "name": "hierarchy_grants_granted_by_idx", + "columns": [ + { + "expression": "granted_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "hierarchy_grants_user_id_users_id_fk": { + "name": "hierarchy_grants_user_id_users_id_fk", + "tableFrom": "hierarchy_grants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "hierarchy_grants_team_id_teams_id_fk": { + "name": "hierarchy_grants_team_id_teams_id_fk", + "tableFrom": "hierarchy_grants", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "hierarchy_grants_company_id_companies_id_fk": { + "name": "hierarchy_grants_company_id_companies_id_fk", + "tableFrom": "hierarchy_grants", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "hierarchy_grants_estate_id_estates_id_fk": { + "name": "hierarchy_grants_estate_id_estates_id_fk", + "tableFrom": "hierarchy_grants", + "tableTo": "estates", + "columnsFrom": [ + "estate_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "hierarchy_grants_platform_project_id_platform_projects_id_fk": { + "name": "hierarchy_grants_platform_project_id_platform_projects_id_fk", + "tableFrom": "hierarchy_grants", + "tableTo": "platform_projects", + "columnsFrom": [ + "platform_project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "hierarchy_grants_granted_by_users_id_fk": { + "name": "hierarchy_grants_granted_by_users_id_fk", + "tableFrom": "hierarchy_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "hierarchy_grants_subject_target_role_uniq": { + "name": "hierarchy_grants_subject_target_role_uniq", + "nullsNotDistinct": true, + "columns": [ + "user_id", + "team_id", + "company_id", + "estate_id", + "platform_project_id", + "role" + ] + } + }, + "policies": {}, + "checkConstraints": { + "hierarchy_grants_subject_check": { + "name": "hierarchy_grants_subject_check", + "value": "num_nonnulls(user_id, team_id) = 1" + }, + "hierarchy_grants_target_check": { + "name": "hierarchy_grants_target_check", + "value": "num_nonnulls(company_id, estate_id, platform_project_id) = 1" + } + }, + "isRLSEnabled": false + }, + "public.insights": { + "name": "insights", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'agent'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "relevance_score": { + "name": "relevance_score", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "decayed_at": { + "name": "decayed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "insights_user_id_idx": { + "name": "insights_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "insights_category_idx": { + "name": "insights_category_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "insights_relevance_idx": { + "name": "insights_relevance_idx", + "columns": [ + { + "expression": "relevance_score", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "insights_user_id_users_id_fk": { + "name": "insights_user_id_users_id_fk", + "tableFrom": "insights", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_checkpoints": { + "name": "interaction_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_digest": { + "name": "content_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "compaction_epoch": { + "name": "compaction_epoch", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "interaction_checkpoints_session_idempotency_idx": { + "name": "interaction_checkpoints_session_idempotency_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "interaction_checkpoints_session_epoch_idx": { + "name": "interaction_checkpoints_session_epoch_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "compaction_epoch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "interaction_checkpoints_session_id_interaction_sessions_id_fk": { + "name": "interaction_checkpoints_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_checkpoints", + "tableTo": "interaction_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_handoffs": { + "name": "interaction_handoffs", + "schema": "", + "columns": { + "handoff_id": { + "name": "handoff_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "destination": { + "name": "destination", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "interaction_handoff_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "interaction_handoffs_session_status_idx": { + "name": "interaction_handoffs_session_status_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "interaction_handoffs_session_id_interaction_sessions_id_fk": { + "name": "interaction_handoffs_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_handoffs", + "tableTo": "interaction_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_inbox": { + "name": "interaction_inbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_digest": { + "name": "content_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "interaction_inbox_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "interaction_inbox_session_idempotency_idx": { + "name": "interaction_inbox_session_idempotency_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "interaction_inbox_session_status_created_idx": { + "name": "interaction_inbox_session_status_created_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "interaction_inbox_session_id_interaction_sessions_id_fk": { + "name": "interaction_inbox_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_inbox", + "tableTo": "interaction_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_outbox": { + "name": "interaction_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_digest": { + "name": "content_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "interaction_outbox_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "interaction_outbox_session_idempotency_idx": { + "name": "interaction_outbox_session_idempotency_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "interaction_outbox_session_status_created_idx": { + "name": "interaction_outbox_session_status_created_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "interaction_outbox_session_id_interaction_sessions_id_fk": { + "name": "interaction_outbox_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_outbox", + "tableTo": "interaction_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_sessions": { + "name": "interaction_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "runtime_session_id": { + "name": "runtime_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "interaction_sessions_owner_id_users_id_fk": { + "name": "interaction_sessions_owner_id_users_id_fk", + "tableFrom": "interaction_sessions", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.logical_agent_connector_leases": { + "name": "logical_agent_connector_leases", + "schema": "", + "columns": { + "lease_id": { + "name": "lease_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logical_agent_id": { + "name": "logical_agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "binding_id": { + "name": "binding_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "lease_epoch": { + "name": "lease_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "acquired_at": { + "name": "acquired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "heartbeat_at": { + "name": "heartbeat_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "released_at": { + "name": "released_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "logical_agent_connector_lease_binding_idx": { + "name": "logical_agent_connector_lease_binding_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "logical_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "binding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "logical_agent_connector_lease_expiry_idx": { + "name": "logical_agent_connector_lease_expiry_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "logical_agent_connector_lease_connector_idx": { + "name": "logical_agent_connector_lease_connector_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages": { + "name": "messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "messages_conversation_id_idx": { + "name": "messages_conversation_id_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "messages_conversation_id_conversations_id_fk": { + "name": "messages_conversation_id_conversations_id_fk", + "tableFrom": "messages", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mission_tasks": { + "name": "mission_tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "mission_id": { + "name": "mission_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not-started'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr": { + "name": "pr", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mission_tasks_mission_id_idx": { + "name": "mission_tasks_mission_id_idx", + "columns": [ + { + "expression": "mission_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mission_tasks_task_id_idx": { + "name": "mission_tasks_task_id_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mission_tasks_user_id_idx": { + "name": "mission_tasks_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mission_tasks_status_idx": { + "name": "mission_tasks_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mission_tasks_mission_id_missions_id_fk": { + "name": "mission_tasks_mission_id_missions_id_fk", + "tableFrom": "mission_tasks", + "tableTo": "missions", + "columnsFrom": [ + "mission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mission_tasks_task_id_tasks_id_fk": { + "name": "mission_tasks_task_id_tasks_id_fk", + "tableFrom": "mission_tasks", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mission_tasks_user_id_users_id_fk": { + "name": "mission_tasks_user_id_users_id_fk", + "tableFrom": "mission_tasks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.missions": { + "name": "missions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'planning'" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "milestones": { + "name": "milestones", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "missions_project_id_idx": { + "name": "missions_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_user_id_idx": { + "name": "missions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "missions_project_id_projects_id_fk": { + "name": "missions_project_id_projects_id_fk", + "tableFrom": "missions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "missions_user_id_users_id_fk": { + "name": "missions_user_id_users_id_fk", + "tableFrom": "missions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.platform_projects": { + "name": "platform_projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "estate_id": { + "name": "estate_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "platform_projects_estate_id_estates_id_fk": { + "name": "platform_projects_estate_id_estates_id_fk", + "tableFrom": "platform_projects", + "tableTo": "estates", + "columnsFrom": [ + "estate_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "platform_projects_estate_slug_uniq": { + "name": "platform_projects_estate_slug_uniq", + "nullsNotDistinct": false, + "columns": [ + "estate_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.preferences": { + "name": "preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mutable": { + "name": "mutable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "preferences_user_id_idx": { + "name": "preferences_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "preferences_user_key_idx": { + "name": "preferences_user_key_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "preferences_user_id_users_id_fk": { + "name": "preferences_user_id_users_id_fk", + "tableFrom": "preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_type": { + "name": "owner_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "projects_owner_id_users_id_fk": { + "name": "projects_owner_id_users_id_fk", + "tableFrom": "projects", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "projects_team_id_teams_id_fk": { + "name": "projects_team_id_teams_id_fk", + "tableFrom": "projects", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_credentials": { + "name": "provider_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_type": { + "name": "credential_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_value": { + "name": "encrypted_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "provider_credentials_user_provider_idx": { + "name": "provider_credentials_user_provider_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "provider_credentials_user_id_idx": { + "name": "provider_credentials_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "provider_credentials_user_id_users_id_fk": { + "name": "provider_credentials_user_id_users_id_fk", + "tableFrom": "provider_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routing_rules": { + "name": "routing_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conditions": { + "name": "conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routing_rules_scope_priority_idx": { + "name": "routing_rules_scope_priority_idx", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routing_rules_user_id_idx": { + "name": "routing_rules_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routing_rules_enabled_idx": { + "name": "routing_rules_enabled_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routing_rules_user_id_users_id_fk": { + "name": "routing_rules_user_id_users_id_fk", + "tableFrom": "routing_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_expires_at_idx": { + "name": "sessions_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skills": { + "name": "skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'custom'" + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "installed_by": { + "name": "installed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skills_enabled_idx": { + "name": "skills_enabled_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skills_installed_by_users_id_fk": { + "name": "skills_installed_by_users_id_fk", + "tableFrom": "skills", + "tableTo": "users", + "columnsFrom": [ + "installed_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "skills_name_unique": { + "name": "skills_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.summarization_jobs": { + "name": "summarization_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "logs_processed": { + "name": "logs_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "insights_created": { + "name": "insights_created", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "summarization_jobs_status_idx": { + "name": "summarization_jobs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not-started'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mission_id": { + "name": "mission_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assignee": { + "name": "assignee", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tasks_project_id_idx": { + "name": "tasks_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_mission_id_idx": { + "name": "tasks_mission_id_idx", + "columns": [ + { + "expression": "mission_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_status_idx": { + "name": "tasks_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tasks_project_id_projects_id_fk": { + "name": "tasks_project_id_projects_id_fk", + "tableFrom": "tasks", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tasks_mission_id_missions_id_fk": { + "name": "tasks_mission_id_missions_id_fk", + "tableFrom": "tasks", + "tableTo": "missions", + "columnsFrom": [ + "mission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_members": { + "name": "team_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "team_members_team_user_idx": { + "name": "team_members_team_user_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_members_team_id_teams_id_fk": { + "name": "team_members_team_id_teams_id_fk", + "tableFrom": "team_members", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_user_id_users_id_fk": { + "name": "team_members_user_id_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_invited_by_users_id_fk": { + "name": "team_members_invited_by_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "columnsFrom": [ + "invited_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "manager_id": { + "name": "manager_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "teams_owner_id_users_id_fk": { + "name": "teams_owner_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "teams_manager_id_users_id_fk": { + "name": "teams_manager_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "manager_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "teams_slug_unique": { + "name": "teams_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tickets": { + "name": "tickets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tickets_status_idx": { + "name": "tickets_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verifications": { + "name": "verifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspaces": { + "name": "workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform_project_id": { + "name": "platform_project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "workspaces_platform_project_id_platform_projects_id_fk": { + "name": "workspaces_platform_project_id_platform_projects_id_fk", + "tableFrom": "workspaces", + "tableTo": "platform_projects", + "columnsFrom": [ + "platform_project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspaces_platform_project_slug_uniq": { + "name": "workspaces_platform_project_slug_uniq", + "nullsNotDistinct": false, + "columns": [ + "platform_project_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.backlog_status": { + "name": "backlog_status", + "schema": "public", + "values": [ + "ready", + "claimed", + "blocked", + "done" + ] + }, + "public.grant_status": { + "name": "grant_status", + "schema": "public", + "values": [ + "pending", + "active", + "revoked", + "expired" + ] + }, + "public.interaction_handoff_status": { + "name": "interaction_handoff_status", + "schema": "public", + "values": [ + "pending", + "accepted" + ] + }, + "public.interaction_inbox_status": { + "name": "interaction_inbox_status", + "schema": "public", + "values": [ + "pending", + "processing", + "processed" + ] + }, + "public.interaction_outbox_status": { + "name": "interaction_outbox_status", + "schema": "public", + "values": [ + "pending", + "processing", + "delivered" + ] + }, + "public.peer_state": { + "name": "peer_state", + "schema": "public", + "values": [ + "pending", + "active", + "suspended", + "revoked" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 711a6337..b47130a1 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -127,6 +127,13 @@ "when": 1787609223282, "tag": "0017_accounts_issuer", "breakpoints": true + }, + { + "idx": 18, + "version": "7", + "when": 1787862158838, + "tag": "0018_clean_cobalt_man", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/packages/db/src/hierarchy-schema.witness.test.ts b/packages/db/src/hierarchy-schema.witness.test.ts new file mode 100644 index 00000000..f4e40b93 --- /dev/null +++ b/packages/db/src/hierarchy-schema.witness.test.ts @@ -0,0 +1,471 @@ +/** + * Hierarchy schema witnesses — contract 1 (docs/requirements/hierarchy-schema.md) §6. + * + * Witnesses §6.1 (chain construction, slug scoping, grant CHECKs, grant + * uniqueness, NOT NULLs), §6.2 (column allowlist), the database-level parts of + * §6.6 (RESTRICT/cascade deletion behavior), and §6.7's catalog half (no + * foreign keys from outside the class into class tables). + * + * Two legs run the same witness body: + * - PGlite (WASM Postgres): always runs, so the witnesses execute locally + * with no database configured. + * - Real PostgreSQL (§6.8): runs when DATABASE_URL is set — in CI that is + * the ci-postgres service, migrated by the pipeline before `pnpm test`. + * This leg is the contract's binding witness; the PGlite leg is the local + * development signal. + */ +import { randomUUID } from 'node:crypto'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { sql } from 'drizzle-orm'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { createDb } from './client.js'; +import { createPgliteDb } from './client-pglite.js'; +import { runPgliteMigrations } from './migrate.js'; +import { + companies, + estates, + hierarchyGrants, + platformProjects, + workspaces, + teams, + users, +} from './schema.js'; + +type AnyDb = { + db: { + insert: (t: unknown) => { values: (v: unknown) => Promise }; + delete: (t: unknown) => { where?: unknown } & PromiseLike; + execute: (q: unknown) => Promise<{ rows?: unknown[] } | unknown[]>; + }; + close: () => Promise; +}; + +/** Column allowlist — the exact declared sets of §2/§3. Nothing else. */ +const COLUMN_ALLOWLIST: Record = { + companies: ['id', 'name', 'slug', 'created_at', 'updated_at'], + estates: ['id', 'name', 'slug', 'company_id'], + platform_projects: ['id', 'name', 'slug', 'estate_id'], + workspaces: ['id', 'name', 'slug', 'platform_project_id'], + hierarchy_grants: [ + 'id', + 'user_id', + 'team_id', + 'company_id', + 'estate_id', + 'platform_project_id', + 'role', + 'granted_by', + 'created_at', + ], +}; + +const NODE_TABLES = ['companies', 'estates', 'platform_projects', 'workspaces']; +const CLASS_TABLES = [...NODE_TABLES, 'hierarchy_grants']; + +/** + * Drizzle wraps constraint failures ("Failed query: ...") with the driver + * error attached as `cause`. Match the pattern anywhere along the cause chain. + */ +async function expectViolation(p: Promise, re: RegExp, label = ''): Promise { + let err: unknown; + try { + await p; + } catch (e) { + err = e; + } + expect(err, label || 'expected the statement to be refused').toBeDefined(); + const messages: string[] = []; + let cur: unknown = err; + while (cur instanceof Error) { + messages.push(cur.message); + cur = (cur as { cause?: unknown }).cause; + } + expect(messages.join(' | '), label).toMatch(re); +} + +function rows(res: { rows?: unknown[] } | unknown[]): Record[] { + return (Array.isArray(res) ? res : (res.rows ?? [])) as Record[]; +} + +/** Unique per-run prefix so real-PG runs never collide and clean up safely. */ +const T = `hier-w-${randomUUID().slice(0, 8)}`; + +function witnessSuite(getHandle: () => AnyDb): void { + const db = () => getHandle().db as unknown as ReturnType['db']; + + const userA = `${T}-user-a`; + const userB = `${T}-user-b`; + let teamId: string; + let companyId: string; + let company2Id: string; + let estateId: string; + let estate2Id: string; + let ppId: string; + let workspaceId: string; + + beforeAll(async () => { + await db() + .insert(users) + .values([ + { id: userA, name: 'Witness A', email: `${userA}@example.com` }, + { id: userB, name: 'Witness B', email: `${userB}@example.com` }, + ]); + teamId = randomUUID(); + await db() + .insert(teams) + .values({ + id: teamId, + name: `${T}-team`, + slug: `${T}-team`, + ownerId: userA, + managerId: userA, + }); + }); + + afterAll(async () => { + // Bottom-up, fail-closed order; grants cascade with their targets. + const d = db(); + await d.execute(sql`DELETE FROM hierarchy_grants WHERE granted_by LIKE ${T + '%'}`); + await d.execute(sql`DELETE FROM workspaces WHERE slug LIKE ${T + '%'}`); + await d.execute(sql`DELETE FROM platform_projects WHERE slug LIKE ${T + '%'}`); + await d.execute(sql`DELETE FROM estates WHERE slug LIKE ${T + '%'}`); + await d.execute(sql`DELETE FROM companies WHERE slug LIKE ${T + '%'}`); + await d.execute(sql`DELETE FROM teams WHERE slug LIKE ${T + '%'}`); + await d.execute(sql`DELETE FROM users WHERE id LIKE ${T + '%'}`); + }); + + // ── §6.1 chain construction ──────────────────────────────────────────────── + + it('accepts a full valid chain: company → estate → platform-project → workspace', async () => { + companyId = randomUUID(); + estateId = randomUUID(); + ppId = randomUUID(); + workspaceId = randomUUID(); + await db() + .insert(companies) + .values({ id: companyId, name: 'Acme', slug: `${T}-acme` }); + await db() + .insert(estates) + .values({ id: estateId, name: 'Estate 1', slug: `${T}-e1`, companyId }); + await db() + .insert(platformProjects) + .values({ id: ppId, name: 'PP 1', slug: `${T}-pp1`, estateId }); + await db() + .insert(workspaces) + .values({ id: workspaceId, name: 'WS 1', slug: `${T}-ws1`, platformProjectId: ppId }); + }); + + it('accepts two siblings under one parent (the §2.5 control)', async () => { + estate2Id = randomUUID(); + await db() + .insert(estates) + .values({ id: estate2Id, name: 'Estate 2', slug: `${T}-e2`, companyId }); + }); + + it('refuses inserts with a null parent FK', async () => { + await expectViolation( + db().execute( + sql`INSERT INTO estates (id, name, slug, company_id) VALUES (${randomUUID()}, 'x', ${T + '-null-e'}, NULL)`, + ), + /null value|not-null/i, + ); + await expectViolation( + db().execute( + sql`INSERT INTO platform_projects (id, name, slug, estate_id) VALUES (${randomUUID()}, 'x', ${T + '-null-p'}, NULL)`, + ), + /null value|not-null/i, + ); + await expectViolation( + db().execute( + sql`INSERT INTO workspaces (id, name, slug, platform_project_id) VALUES (${randomUUID()}, 'x', ${T + '-null-w'}, NULL)`, + ), + /null value|not-null/i, + ); + }); + + it('refuses inserts with a dangling parent FK', async () => { + await expectViolation( + db() + .insert(estates) + .values({ id: randomUUID(), name: 'x', slug: `${T}-dangle`, companyId: randomUUID() }), + /foreign key/i, + ); + }); + + it('catalog: each child table has exactly one parent-FK column and no parentage edge table exists', async () => { + const res = rows( + await db().execute(sql` + SELECT tc.table_name, kcu.column_name, ccu.table_name AS ref_table + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema + JOIN information_schema.constraint_column_usage ccu + ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema + WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = 'public' + `), + ); + const nodeSet = new Set(NODE_TABLES); + // Exactly one parent FK per child node table. + for (const [child, parent] of [ + ['estates', 'companies'], + ['platform_projects', 'estates'], + ['workspaces', 'platform_projects'], + ] as const) { + const parentFks = res.filter( + (r) => r['table_name'] === child && nodeSet.has(String(r['ref_table'])), + ); + expect(parentFks.map((r) => `${r['column_name']}->${r['ref_table']}`)).toEqual([ + `${{ estates: 'company_id', platform_projects: 'estate_id', workspaces: 'platform_project_id' }[child]}->${parent}`, + ]); + } + // No table outside the class references a node table (also §6.7's catalog + // half for companies/estates/platform_projects/workspaces), and the only + // multi-FK referencer is hierarchy_grants (grant attachment, not + // parentage). + const referencers = new Map(); + for (const r of res) { + if (nodeSet.has(String(r['ref_table']))) { + const t = String(r['table_name']); + referencers.set(t, (referencers.get(t) ?? 0) + 1); + } + } + for (const [table, count] of referencers) { + expect(CLASS_TABLES, `unexpected referencer of a node table: ${table}`).toContain(table); + if (count > 1) expect(table).toBe('hierarchy_grants'); + } + // No FK anywhere references hierarchy_grants. + expect(res.filter((r) => r['ref_table'] === 'hierarchy_grants')).toEqual([]); + }); + + // ── §6.1 slug scoping ────────────────────────────────────────────────────── + + it('refuses a duplicate slug under the same parent, accepts it under another parent', async () => { + company2Id = randomUUID(); + await db() + .insert(companies) + .values({ id: company2Id, name: 'Beta', slug: `${T}-beta` }); + await expectViolation( + db() + .insert(estates) + .values({ id: randomUUID(), name: 'dup', slug: `${T}-e1`, companyId }), + /duplicate key|unique/i, + ); + // Same slug, different company — accepted. + await db() + .insert(estates) + .values({ id: randomUUID(), name: 'ok', slug: `${T}-e1`, companyId: company2Id }); + // companies.slug is unique per deployment. + await expectViolation( + db() + .insert(companies) + .values({ id: randomUUID(), name: 'dup', slug: `${T}-acme` }), + /duplicate key|unique/i, + ); + }); + + // ── §6.2 column allowlist ────────────────────────────────────────────────── + + it('column allowlist: each class table has exactly its declared columns (no payload, no owner_id)', async () => { + for (const [table, allow] of Object.entries(COLUMN_ALLOWLIST)) { + const res = rows( + await db().execute( + sql`SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = ${table}`, + ), + ); + const actual = res.map((r) => String(r['column_name'])).sort(); + expect(actual, `column set of ${table}`).toEqual([...allow].sort()); + } + }); + + // ── §6.1 grant CHECKs ────────────────────────────────────────────────────── + + it('accepts one valid grant per subject×target form', async () => { + // All six forms; also the base rows for the §6.1 uniqueness witness below. + const forms = [ + { userId: userA, companyId }, + { userId: userA, estateId }, + { userId: userA, platformProjectId: ppId }, + { teamId, companyId }, + { teamId, estateId }, + { teamId, platformProjectId: ppId }, + ]; + for (const form of forms) { + await db() + .insert(hierarchyGrants) + .values({ ...form, role: 'owner', grantedBy: userA }); + } + }); + + it('refuses a grant with zero or two subjects (exactly-one-of CHECK)', async () => { + await expectViolation( + db().insert(hierarchyGrants).values({ companyId, role: 'viewer', grantedBy: userA }), + /check constraint/i, + ); + await expectViolation( + db() + .insert(hierarchyGrants) + .values({ userId: userA, teamId, companyId, role: 'viewer', grantedBy: userA }), + /check constraint/i, + ); + }); + + it('refuses a grant with zero or two targets (exactly-one-of CHECK)', async () => { + await expectViolation( + db().insert(hierarchyGrants).values({ userId: userA, role: 'viewer', grantedBy: userA }), + /check constraint/i, + ); + await expectViolation( + db() + .insert(hierarchyGrants) + .values({ userId: userA, companyId, estateId, role: 'viewer', grantedBy: userA }), + /check constraint/i, + ); + }); + + // ── §6.1 grant uniqueness (NULLS NOT DISTINCT) ───────────────────────────── + + it('refuses a duplicate (subject, target, role) for each of the six forms', async () => { + const forms = [ + { userId: userA, companyId }, + { userId: userA, estateId }, + { userId: userA, platformProjectId: ppId }, + { teamId, companyId }, + { teamId, estateId }, + { teamId, platformProjectId: ppId }, + ]; + for (const form of forms) { + await expectViolation( + db() + .insert(hierarchyGrants) + .values({ ...form, role: 'owner', grantedBy: userB }), + /duplicate key|unique/i, + `duplicate form ${JSON.stringify(form)} must be refused`, + ); + } + // Control: same subject and target with a different role is a new grant. + await db() + .insert(hierarchyGrants) + .values({ userId: userA, companyId, role: `${T}-other-role`, grantedBy: userA }); + }); + + // ── §6.1 NOT NULLs ───────────────────────────────────────────────────────── + + it('refuses null role, granted_by, and null name/slug columns', async () => { + await expectViolation( + db().execute( + sql`INSERT INTO hierarchy_grants (user_id, company_id, role, granted_by) VALUES (${userA}, ${companyId}, NULL, ${userA})`, + ), + /null value|not-null/i, + ); + await expectViolation( + db().execute( + sql`INSERT INTO hierarchy_grants (user_id, company_id, role, granted_by) VALUES (${userA}, ${companyId}, 'x', NULL)`, + ), + /null value|not-null/i, + ); + await expectViolation( + db().execute(sql`INSERT INTO companies (name, slug) VALUES (NULL, ${T + '-nn'})`), + /null value|not-null/i, + ); + await expectViolation( + db().execute(sql`INSERT INTO companies (name, slug) VALUES ('x', NULL)`), + /null value|not-null/i, + ); + await expectViolation( + db().execute( + sql`INSERT INTO estates (name, slug, company_id) VALUES ('x', NULL, ${companyId})`, + ), + /null value|not-null/i, + ); + }); + + // ── §6.6 deletion (database-level witnesses) ─────────────────────────────── + + it('refuses deleting a node with children (fail-closed bottom-up)', async () => { + await expectViolation( + db().execute(sql`DELETE FROM companies WHERE id = ${companyId}`), + /foreign key/i, + ); + await expectViolation( + db().execute(sql`DELETE FROM estates WHERE id = ${estateId}`), + /foreign key/i, + ); + await expectViolation( + db().execute(sql`DELETE FROM platform_projects WHERE id = ${ppId}`), + /foreign key/i, + ); + }); + + it('cascades a deleted leaf node’s grants and nothing else', async () => { + // estate2 is a leaf (no platform-projects). Attach one grant to it. + await db() + .insert(hierarchyGrants) + .values({ userId: userB, estateId: estate2Id, role: 'viewer', grantedBy: userA }); + const grantCount = async () => + Number( + rows( + await db().execute( + sql`SELECT count(*)::int AS n FROM hierarchy_grants WHERE granted_by LIKE ${T + '%'}`, + ), + )[0]!['n'], + ); + const before = await grantCount(); + await db().execute(sql`DELETE FROM estates WHERE id = ${estate2Id}`); + // Exactly the one grant on the deleted estate is gone. + expect(await grantCount()).toBe(before - 1); + }); + + it('refuses deleting a user or team that is a grant subject or granted_by referent (RESTRICT)', async () => { + await expectViolation(db().execute(sql`DELETE FROM users WHERE id = ${userA}`), /foreign key/i); + // userB is only a subject (its estate2 grant cascaded away above, but it + // still holds no grants — re-create one to witness subject RESTRICT). + await db() + .insert(hierarchyGrants) + .values({ userId: userB, companyId: company2Id, role: 'viewer', grantedBy: userA }); + await expectViolation(db().execute(sql`DELETE FROM users WHERE id = ${userB}`), /foreign key/i); + await expectViolation( + db().execute(sql`DELETE FROM teams WHERE id = ${teamId}`), + /foreign key/i, + ); + }); +} + +// ── Leg 1: PGlite (always runs — local witness signal) ─────────────────────── + +describe('hierarchy schema witnesses — PGlite', () => { + let dir: string; + let handle: ReturnType; + + beforeAll(async () => { + dir = mkdtempSync(join(tmpdir(), 'hier-witness-')); + handle = createPgliteDb(dir); + await runPgliteMigrations(handle); + }); + + afterAll(async () => { + await handle.close(); + rmSync(dir, { recursive: true, force: true }); + }); + + witnessSuite(() => handle as unknown as AnyDb); +}); + +// ── Leg 2: real PostgreSQL (§6.8 — binding witness, ci-postgres in CI) ─────── + +const hasPostgres = Boolean(process.env['DATABASE_URL']); + +describe.skipIf(!hasPostgres)('hierarchy schema witnesses — real PostgreSQL', () => { + let handle: ReturnType; + + beforeAll(() => { + handle = createDb(process.env['DATABASE_URL']!); + }); + + afterAll(async () => { + await handle.close(); + }); + + witnessSuite(() => handle as unknown as AnyDb); +}); diff --git a/packages/db/src/hierarchy-writer-coverage.test.ts b/packages/db/src/hierarchy-writer-coverage.test.ts new file mode 100644 index 00000000..e9853966 --- /dev/null +++ b/packages/db/src/hierarchy-writer-coverage.test.ts @@ -0,0 +1,349 @@ +/** + * Hierarchy writer-coverage assertion — contract 1 + * (docs/requirements/hierarchy-schema.md) §6.3(b). + * + * A static CI assertion over the Gateway and package production sources with + * three prongs, each bound to a closed, explicitly enumerated allowlist: + * + * (i) Symbol prong — write references (insert/update/delete) to the + * class-table schema symbols occur only in allowlisted modules. + * Import aliasing is followed: `import { companies as c }` makes `c` + * a class-table symbol in that file. + * (ii) Literal prong — a class-table name inside a SQL string or tagged + * SQL template outside the allowlist fails. Schema definitions and + * generated migrations are excluded from this prong (per contract). + * (iii) Raw-execution prong — raw-SQL execution primitives (the ORM's + * raw/unsafe constructors, driver-level clients) outside the writer + * allowlist and the infrastructure register fail, regardless of SQL + * content. Direct database-driver imports count as raw-execution + * capability: they are what makes dynamically assembled SQL + * executable, and the import is statically detectable even when the + * SQL string is not. + * + * Runtime code-construction primitives (eval, new Function) and non-literal + * dynamic imports fail anywhere — allowlist and register included. + * + * The writer allowlist names hierarchy command/repository modules ONLY. It is + * empty today: the hierarchy command family (M4-1b) has not landed, so no + * production module may write the class tables. The infrastructure register + * holds legitimate non-hierarchy raw execution (migration runner, storage + * adapters, health probes); registered modules are exempt from prong (iii) + * only — prongs (i) and (ii) apply to them with no exemption, and no + * registered module may appear on the writer allowlist. Neither list may take + * a generic raw-SQL helper, and an allowlisted module must not export a + * function that executes caller-supplied SQL (review-enforced, §5.1). + * + * A false positive is resolved in the same PR by adding the module to the one + * enumerated list its role permits — never by weakening the assertion. + * + * Test files (*.spec.*, *.test.*, __tests__/) are not scanned: they are not + * production mutation paths, and the contract's own §6 witnesses must write + * class tables directly to witness database constraints. + */ +import { readdirSync, readFileSync, statSync, existsSync } from 'node:fs'; +import { join, relative, resolve, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +const REPO_ROOT = resolve(fileURLToPath(new URL('.', import.meta.url)), '..', '..', '..'); + +/** Drizzle schema symbols of the five class tables (packages/db/src/schema.ts). */ +const CLASS_SYMBOLS = ['companies', 'estates', 'platformProjects', 'workspaces', 'hierarchyGrants']; + +/** SQL table names of the five class tables. */ +const CLASS_TABLES = [ + 'companies', + 'estates', + 'platform_projects', + 'workspaces', + 'hierarchy_grants', +]; + +/** + * Writer allowlist (§6.3b): hierarchy command/repository modules only. + * EMPTY until the hierarchy command family lands (M4-1b). Adding a module + * here is a contract-conformance decision reviewed under §5.1 — the module + * must be part of the Gateway hierarchy command path. + */ +const WRITER_ALLOWLIST: string[] = []; + +/** + * Infrastructure register: closed enumeration of legitimate non-hierarchy raw + * execution. Exempt from prong (iii) ONLY; prongs (i)/(ii) still apply, and + * none of these may ever join the writer allowlist. + */ +const INFRA_REGISTER: string[] = [ + 'packages/db/src/client.ts', // connection factory (imports postgres driver) + 'packages/db/src/client-pglite.ts', // PGlite factory (imports the pglite driver) + 'packages/db/src/migrate.ts', // migration runner (hash-ledger DDL execution) + 'packages/memory/src/insights.ts', // analytics raw query over memory tables + 'packages/storage/src/tier-detection.ts', // driver import for tier probing + 'packages/storage/src/adapters/pglite.ts', // storage adapter (driver-level query) + 'packages/storage/src/adapters/postgres.ts', // storage adapter (extension bootstrap) + 'packages/storage/src/migrate-tier.ts', // storage tier migration + 'packages/storage/src/cli.ts', // storage CLI health probe + 'apps/gateway/src/admin/admin-health.controller.ts', // SELECT 1 health probe +]; + +/** + * Closed importer enumerations for registered modules that EXPORT + * SQL-executing functions (the laundering path §6.3b closes). Import edges + * are checked re-export-aware — the db package barrel and literal dynamic + * `import('@mosaicstack/db')` are edges like any static import. The measured + * production importer set of the migration runner (contract 1 revision 9): + * the Gateway database module, the storage Postgres adapter, and two mosaic + * CLI commands routed through literal dynamic imports. The gateway + * schema-check module receives the runner's functions by parameter injection + * and has no import edge, so it is not enumerated. Being enumerated confers + * nothing else: importers stay subject to prongs (i)/(ii) and gain no writer + * standing. + */ +const MIGRATION_RUNNER_SYMBOLS = ['runMigrations', 'runPgliteMigrations', 'getMigrationStatus']; +const MIGRATION_RUNNER_IMPORTERS: string[] = [ + 'apps/gateway/src/database/database.module.ts', + 'packages/storage/src/adapters/postgres.ts', + 'packages/mosaic/src/commands/fleet-backlog.ts', + 'packages/mosaic/src/commands/gateway/verify.ts', +]; +const MIGRATE_TIER_SYMBOLS = [ + 'runMigrateTier', + 'checkTargetPreconditions', + 'PostgresMigrationTarget', + 'DrizzleMigrationSource', +]; +const MIGRATE_TIER_IMPORTERS: string[] = [ + 'packages/storage/src/cli.ts', // storage CLI command surface + 'packages/storage/src/index.ts', // package barrel re-export (public API) +]; + +const SCAN_ROOTS = ['apps', 'packages']; +const EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts']); + +function isTestPath(rel: string): boolean { + return ( + /\.(spec|test)\.[cm]?tsx?$/.test(rel) || + rel.split(sep).includes('__tests__') || + rel.endsWith('.d.ts') + ); +} + +function collectSources(): string[] { + const files: string[] = []; + for (const root of SCAN_ROOTS) { + const rootDir = join(REPO_ROOT, root); + if (!existsSync(rootDir)) continue; + for (const pkg of readdirSync(rootDir)) { + const srcDir = join(rootDir, pkg, 'src'); + if (!existsSync(srcDir) || !statSync(srcDir).isDirectory()) continue; + const walk = (dir: string): void => { + for (const entry of readdirSync(dir)) { + const full = join(dir, entry); + const st = statSync(full); + if (st.isDirectory()) { + if (entry === 'node_modules' || entry === 'dist') continue; + walk(full); + } else if (EXTENSIONS.has(full.slice(full.lastIndexOf('.')))) { + const rel = relative(REPO_ROOT, full); + if (!isTestPath(rel)) files.push(rel); + } + } + }; + walk(srcDir); + } + } + return files.sort(); +} + +/** Strip line and block comments so commented-out code cannot trip prongs. */ +function stripComments(src: string): string { + return src.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/(^|[^:])\/\/[^\n]*/g, '$1'); +} + +/** Extract string and template literal spans (approximation, multi-line for templates). */ +function stringSpans(src: string): string[] { + const spans: string[] = []; + const re = /`[^`]*`|'(?:[^'\\\n]|\\.)*'|"(?:[^"\\\n]|\\.)*"/gs; + for (const m of src.matchAll(re)) spans.push(m[0]); + return spans; +} + +/** Local names (including aliases) under which class-table symbols are imported. */ +function classSymbolAliases(src: string): string[] { + const names = new Set(); + const importRe = /import\s*(?:type\s*)?\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g; + for (const m of src.matchAll(importRe)) { + const specifier = m[2]!; + if (!/@mosaicstack\/db|\.\.?\/(?:.*\/)?(?:schema|index)(?:\.js)?$/.test(specifier)) continue; + for (const part of m[1]!.split(',')) { + const seg = part.trim().replace(/^type\s+/, ''); + if (!seg) continue; + const asMatch = /^(\w+)\s+as\s+(\w+)$/.exec(seg); + const original = asMatch ? asMatch[1]! : seg; + const local = asMatch ? asMatch[2]! : seg; + if (CLASS_SYMBOLS.includes(original)) names.add(local); + } + } + return [...names]; +} + +interface Violation { + file: string; + prong: string; + detail: string; +} + +describe('hierarchy writer coverage (contract 1 §6.3b)', () => { + const sources = collectSources(); + + it('scans a non-empty production source set', () => { + expect(sources.length).toBeGreaterThan(100); + }); + + it('enumerated modules exist on disk (no stale allowlist/register entries)', () => { + for (const p of [ + ...WRITER_ALLOWLIST, + ...INFRA_REGISTER, + ...MIGRATION_RUNNER_IMPORTERS, + ...MIGRATE_TIER_IMPORTERS, + ]) { + expect(existsSync(join(REPO_ROOT, p)), `enumerated module missing: ${p}`).toBe(true); + } + }); + + it('no registered module appears on the writer allowlist', () => { + for (const p of INFRA_REGISTER) { + expect(WRITER_ALLOWLIST, `register/allowlist overlap: ${p}`).not.toContain(p); + } + }); + + it('three-prong writer coverage holds', () => { + const violations: Violation[] = []; + const allow = new Set(WRITER_ALLOWLIST); + const register = new Set(INFRA_REGISTER); + + for (const rel of sources) { + const raw = readFileSync(join(REPO_ROOT, rel), 'utf8'); + const src = stripComments(raw); + const inAllowlist = allow.has(rel); + const inRegister = register.has(rel); + const isSchemaDefinition = rel === 'packages/db/src/schema.ts'; + + // Runtime code construction: fails anywhere. + if (/\beval\s*\(|\bnew\s+Function\s*\(/.test(src)) { + violations.push({ file: rel, prong: 'code-construction', detail: 'eval/new Function' }); + } + // Non-literal dynamic import: makes the import graph unanalyzable. + if (/\bimport\s*\(\s*(?!['"`])/.test(src)) { + violations.push({ file: rel, prong: 'dynamic-import', detail: 'non-literal import()' }); + } + + // Prong (i): schema-symbol writes — alias-aware. + if (!inAllowlist) { + const aliases = classSymbolAliases(src); + if (aliases.length > 0) { + const writeRe = new RegExp( + `\\.(insert|update|delete)\\s*\\(\\s*(${aliases.join('|')})\\b`, + 'g', + ); + for (const m of src.matchAll(writeRe)) { + violations.push({ + file: rel, + prong: 'i-symbol', + detail: `.${m[1]}(${m[2]}) outside the writer allowlist`, + }); + } + } + } + + // Prong (ii): class-table names in SQL strings/templates. + // Schema definitions and generated migrations are excluded (contract); + // migrations live outside the scanned source roots already. + if (!inAllowlist && !isSchemaDefinition) { + const tableRe = new RegExp(`\\b(${CLASS_TABLES.join('|')})\\b`); + const sqlContextRe = + /\b(select|insert\s+into|update|delete\s+from|join|truncate|alter\s+table|drop\s+table|references)\b/i; + for (const span of stringSpans(src)) { + if (tableRe.test(span) && sqlContextRe.test(span)) { + violations.push({ + file: rel, + prong: 'ii-literal', + detail: `class-table name in SQL context: ${span.slice(0, 80)}`, + }); + } + } + } + + // Prong (iii): raw-execution primitives outside allowlist ∪ register. + if (!inAllowlist && !inRegister) { + const rawPatterns: Array<[RegExp, string]> = [ + [/\bsql\.raw\s*\(/, 'sql.raw()'], + [/\.unsafe\s*\(/, '.unsafe()'], + // `.execute(sql\`...\`)` is exempt: the tagged template keeps the + // SQL literal in source, where prong (ii) scans it. The flagged + // forms are the ones whose SQL content is not statically visible + // at the call site: `.execute(variable)` and `.execute("string")`. + [/\b(?:db|database|tx|trx)\.execute\s*\((?!\s*sql`)/, 'raw db.execute()'], + [/from\s*['"](?:postgres|pg|@electric-sql\/pglite)['"]/, 'direct driver import'], + [ + /\bimport\s*\(\s*['"](?:postgres|pg|@electric-sql\/pglite)['"]\s*\)/, + 'dynamic driver import', + ], + ]; + for (const [re, label] of rawPatterns) { + if (re.test(src)) { + violations.push({ file: rel, prong: 'iii-raw-execution', detail: label }); + } + } + } + } + + expect( + violations, + violations.map((v) => `[prong ${v.prong}] ${v.file}: ${v.detail}`).join('\n'), + ).toEqual([]); + }); + + it('migration-runner import edges are exactly the closed importer enumeration', () => { + const offenders: string[] = []; + for (const rel of sources) { + if (rel.startsWith('packages/db/src/')) continue; // the runner's own package + const src = stripComments(readFileSync(join(REPO_ROOT, rel), 'utf8')); + const symbolRe = new RegExp(`\\b(${MIGRATION_RUNNER_SYMBOLS.join('|')})\\b`); + if (!symbolRe.test(src)) continue; + // An import edge is a static value import from the db package (or its + // migrate module) naming a runner symbol, or a literal dynamic + // import('@mosaicstack/db') in a file that uses a runner symbol. + // `import type` is erased at runtime and is not an edge; parameter + // injection (the gateway schema-check module) has no edge. + const staticEdge = new RegExp( + `import\\s*(?!type\\b)\\{[^}]*\\b(${MIGRATION_RUNNER_SYMBOLS.join('|')})\\b[^}]*\\}\\s*from\\s*['"](@mosaicstack/db|[^'"]*migrate(\\.js)?)['"]`, + ).test(src); + const dynamicEdge = /import\s*\(\s*['"]@mosaicstack\/db['"]\s*\)/.test(src); + if ((staticEdge || dynamicEdge) && !MIGRATION_RUNNER_IMPORTERS.includes(rel)) { + offenders.push(rel); + } + } + expect(offenders, `unenumerated migration-runner importers:\n${offenders.join('\n')}`).toEqual( + [], + ); + }); + + it('migrate-tier import edges are exactly the closed importer enumeration', () => { + const offenders: string[] = []; + for (const rel of sources) { + if (rel === 'packages/storage/src/migrate-tier.ts') continue; + const src = stripComments(readFileSync(join(REPO_ROOT, rel), 'utf8')); + // Edge = direct module-path import, or a value import of a migrate-tier + // symbol from the storage package barrel (the barrel is itself + // enumerated as a re-exporter, so barrel consumers must not escape). + const pathEdge = + /from\s*['"][^'"]*migrate-tier(\.js)?['"]/.test(src) || + /import\s*\(\s*['"][^'"]*migrate-tier(\.js)?['"]\s*\)/.test(src); + const barrelEdge = new RegExp( + `import\\s*(?!type\\b)\\{[^}]*\\b(${MIGRATE_TIER_SYMBOLS.join('|')})\\b[^}]*\\}\\s*from\\s*['"]@mosaicstack/storage['"]`, + ).test(src); + if ((pathEdge || barrelEdge) && !MIGRATE_TIER_IMPORTERS.includes(rel)) offenders.push(rel); + } + expect(offenders, `unenumerated migrate-tier importers:\n${offenders.join('\n')}`).toEqual([]); + }); +}); diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index aa220616..d2358583 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -3,6 +3,7 @@ * drizzle-kit reads this file directly (avoids CJS/ESM extension issues). */ +import { sql } from 'drizzle-orm'; import { pgTable, pgEnum, @@ -13,6 +14,8 @@ import { jsonb, index, uniqueIndex, + unique, + check, real, integer, bigint, @@ -1048,3 +1051,104 @@ export const federationEnrollmentTokens = pgTable('federation_enrollment_tokens' createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), }); + +// ─── Hierarchy (tenancy/authorization structure record class) ──────────────── +// Contract: docs/requirements/hierarchy-schema.md (D2, ratified 2026-08-27). +// Five tables: companies → estates → platform_projects → workspaces, plus +// hierarchy_grants. Class rows carry parentage, naming, grant, and +// audit-linkage data only — the column sets below are exhaustive (§2.7) and +// witnessed against information_schema (§6.2). No owner_id: ownership is the +// grant structure (§4.4). All writes flow through the Gateway hierarchy +// command family only (§5.1), enforced by the writer-coverage assertion +// (§6.3b) — do not add writers outside that allowlist. + +export const companies = pgTable('companies', { + id: uuid('id').primaryKey().defaultRandom(), + name: text('name').notNull(), + slug: text('slug').notNull().unique(), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), +}); + +export const estates = pgTable( + 'estates', + { + id: uuid('id').primaryKey().defaultRandom(), + name: text('name').notNull(), + slug: text('slug').notNull(), + companyId: uuid('company_id') + .notNull() + .references(() => companies.id, { onDelete: 'restrict' }), + }, + (t) => [unique('estates_company_slug_uniq').on(t.companyId, t.slug)], +); + +export const platformProjects = pgTable( + 'platform_projects', + { + id: uuid('id').primaryKey().defaultRandom(), + name: text('name').notNull(), + slug: text('slug').notNull(), + estateId: uuid('estate_id') + .notNull() + .references(() => estates.id, { onDelete: 'restrict' }), + }, + (t) => [unique('platform_projects_estate_slug_uniq').on(t.estateId, t.slug)], +); + +export const workspaces = pgTable( + 'workspaces', + { + id: uuid('id').primaryKey().defaultRandom(), + name: text('name').notNull(), + slug: text('slug').notNull(), + platformProjectId: uuid('platform_project_id') + .notNull() + .references(() => platformProjects.id, { onDelete: 'restrict' }), + }, + (t) => [unique('workspaces_platform_project_slug_uniq').on(t.platformProjectId, t.slug)], +); + +export const hierarchyGrants = pgTable( + 'hierarchy_grants', + { + id: uuid('id').primaryKey().defaultRandom(), + // Subject: exactly one of user/team (CHECK below). Principal FKs are + // RESTRICT until a deletion-and-retention contract rules otherwise (§3.3). + userId: text('user_id').references(() => users.id, { onDelete: 'restrict' }), + teamId: uuid('team_id').references(() => teams.id, { onDelete: 'restrict' }), + // Target: exactly one of the three grantable levels (CHECK below). + // Target FKs CASCADE — the one permitted cascade in the class (§3.3); + // cascaded grant deletions are audited by the command family (§5.2). + companyId: uuid('company_id').references(() => companies.id, { onDelete: 'cascade' }), + estateId: uuid('estate_id').references(() => estates.id, { onDelete: 'cascade' }), + platformProjectId: uuid('platform_project_id').references(() => platformProjects.id, { + onDelete: 'cascade', + }), + // Role vocabulary and its CHECK constraint are contract 2 §2 (M4-2). + role: text('role').notNull(), + grantedBy: text('granted_by') + .notNull() + .references(() => users.id, { onDelete: 'restrict' }), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [ + check('hierarchy_grants_subject_check', sql`num_nonnulls(user_id, team_id) = 1`), + check( + 'hierarchy_grants_target_check', + sql`num_nonnulls(company_id, estate_id, platform_project_id) = 1`, + ), + // At most one grant per (subject, target, role) across all six + // subject×target forms — NULLS NOT DISTINCT so nullable columns + // participate (§3.2). + unique('hierarchy_grants_subject_target_role_uniq') + .on(t.userId, t.teamId, t.companyId, t.estateId, t.platformProjectId, t.role) + .nullsNotDistinct(), + index('hierarchy_grants_company_id_idx').on(t.companyId), + index('hierarchy_grants_estate_id_idx').on(t.estateId), + index('hierarchy_grants_platform_project_id_idx').on(t.platformProjectId), + index('hierarchy_grants_user_id_idx').on(t.userId), + index('hierarchy_grants_team_id_idx').on(t.teamId), + index('hierarchy_grants_granted_by_idx').on(t.grantedBy), + ], +); -- 2.54.0 From 8305d129a2249c128f982638fa94bc1cb65ce474 Mon Sep 17 00:00:00 2001 From: fred Date: Thu, 27 Aug 2026 16:15:53 -0500 Subject: [PATCH 02/10] fix(db): harden writer-coverage assertion per M4-1a review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review findings F1-F8 (REQUEST_CHANGES) addressed: - F5: replace regex comment stripping with a string-aware lexer producing comment-free code plus accurate string/template spans (handles nesting, regex literals, escapes) - F3: prong (i) now tracks namespace imports and re-export conduits via a fixpoint over the scanned import graph - F1/F2: tagged-template exemption dropped; backlog.ts joins the infra register; capability-gated prong (iii) — driver import flags any-receiver execute/query/unsafe, factory import flags any-receiver execute/unsafe, db-shaped receiver backstop covers DI'd handles; sql.raw tracked through aliases and namespaces - F4: closed importer enumeration added for createDb/createPgliteDb; composition property of remaining register modules documented - F6: plugins/ added to scan roots - F7: per-parent slug scoping witnessed at platform_projects and workspaces - F8: dynamic-import disposition register added; stricter-than-contract stances documented in the header The review's 8 evasion samples are embedded as permanent controls run through the production analyzer, plus clean controls guarding against false positives. --- .../db/src/hierarchy-schema.witness.test.ts | 30 + .../db/src/hierarchy-writer-coverage.test.ts | 799 ++++++++++++++---- 2 files changed, 653 insertions(+), 176 deletions(-) diff --git a/packages/db/src/hierarchy-schema.witness.test.ts b/packages/db/src/hierarchy-schema.witness.test.ts index f4e40b93..ff226e84 100644 --- a/packages/db/src/hierarchy-schema.witness.test.ts +++ b/packages/db/src/hierarchy-schema.witness.test.ts @@ -265,6 +265,36 @@ function witnessSuite(getHandle: () => AnyDb): void { ); }); + it('scopes platform_projects and workspaces slugs per parent (refuse same-parent duplicate, accept cross-parent)', async () => { + // Dedicated parent estate so this test leaves estate2 a leaf (the §3.4 + // cascade witness depends on that). + const estate3Id = randomUUID(); + await db() + .insert(estates) + .values({ id: estate3Id, name: 'Estate 3', slug: `${T}-e3`, companyId }); + // platform_projects: (estate_id, slug) unique. + await expectViolation( + db() + .insert(platformProjects) + .values({ id: randomUUID(), name: 'dup', slug: `${T}-pp1`, estateId }), + /duplicate key|unique/i, + ); + const pp2Id = randomUUID(); + await db() + .insert(platformProjects) + .values({ id: pp2Id, name: 'ok', slug: `${T}-pp1`, estateId: estate3Id }); + // workspaces: (platform_project_id, slug) unique. + await expectViolation( + db() + .insert(workspaces) + .values({ id: randomUUID(), name: 'dup', slug: `${T}-ws1`, platformProjectId: ppId }), + /duplicate key|unique/i, + ); + await db() + .insert(workspaces) + .values({ id: randomUUID(), name: 'ok', slug: `${T}-ws1`, platformProjectId: pp2Id }); + }); + // ── §6.2 column allowlist ────────────────────────────────────────────────── it('column allowlist: each class table has exactly its declared columns (no payload, no owner_id)', async () => { diff --git a/packages/db/src/hierarchy-writer-coverage.test.ts b/packages/db/src/hierarchy-writer-coverage.test.ts index e9853966..af973c62 100644 --- a/packages/db/src/hierarchy-writer-coverage.test.ts +++ b/packages/db/src/hierarchy-writer-coverage.test.ts @@ -2,46 +2,65 @@ * Hierarchy writer-coverage assertion — contract 1 * (docs/requirements/hierarchy-schema.md) §6.3(b). * - * A static CI assertion over the Gateway and package production sources with - * three prongs, each bound to a closed, explicitly enumerated allowlist: + * A static CI assertion over all production sources (apps/, packages/, + * plugins/) with three prongs, each bound to a closed, explicitly enumerated + * allowlist: * * (i) Symbol prong — write references (insert/update/delete) to the * class-table schema symbols occur only in allowlisted modules. - * Import aliasing is followed: `import { companies as c }` makes `c` - * a class-table symbol in that file. - * (ii) Literal prong — a class-table name inside a SQL string or tagged - * SQL template outside the allowlist fails. Schema definitions and - * generated migrations are excluded from this prong (per contract). - * (iii) Raw-execution prong — raw-SQL execution primitives (the ORM's - * raw/unsafe constructors, driver-level clients) outside the writer - * allowlist and the infrastructure register fail, regardless of SQL - * content. Direct database-driver imports count as raw-execution - * capability: they are what makes dynamically assembled SQL - * executable, and the import is statically detectable even when the - * SQL string is not. + * Schema symbols are tracked through named imports (aliased or not), + * namespace imports, and re-export conduits: any scanned module that + * re-exports the schema (or another conduit) is itself treated as a + * schema source, computed to a fixpoint. + * (ii) Literal prong — a class-table name inside a string or template + * span that also carries SQL context fails outside the allowlist. + * Spans are produced by a real lexer, so comments cannot hide code + * and strings cannot hide comments. Schema definitions and generated + * migrations are excluded from this prong only (per contract). + * (iii) Raw-execution prong — content-independent. A file is RAW-CAPABLE + * when it imports a database driver or the createDb/createPgliteDb + * factories (directly or via namespace). In a raw-capable file + * outside the allowlist and register, EVERY `.execute(`, `.query(`, + * and `.unsafe(` call fails, on any receiver, with any argument — + * there is no tagged-template exemption (§6.3(b): "regardless of what + * the SQL string contains or how it is constructed"). In files + * without detected capability (the DI residual), a conventional + * receiver backstop still fires on db/client-shaped receivers. + * `sql.raw` is tracked through import aliasing and namespaces. * * Runtime code-construction primitives (eval, new Function) and non-literal - * dynamic imports fail anywhere — allowlist and register included. + * dynamic imports fail anywhere — allowlist and register included. This is + * deliberately stricter than the contract's minimum: an unanalyzable import + * or constructed code defeats every static prong, so there is no enumerated + * disposition path for them. * * The writer allowlist names hierarchy command/repository modules ONLY. It is * empty today: the hierarchy command family (M4-1b) has not landed, so no * production module may write the class tables. The infrastructure register - * holds legitimate non-hierarchy raw execution (migration runner, storage - * adapters, health probes); registered modules are exempt from prong (iii) - * only — prongs (i) and (ii) apply to them with no exemption, and no - * registered module may appear on the writer allowlist. Neither list may take - * a generic raw-SQL helper, and an allowlisted module must not export a - * function that executes caller-supplied SQL (review-enforced, §5.1). + * holds legitimate non-hierarchy raw execution; registered modules are exempt + * from prong (iii) only — prongs (i) and (ii) apply to them with no + * exemption, and no registered module may appear on the writer allowlist. + * + * Register modules whose exports let a CALLER reach SQL execution carry their + * own closed importer enumerations (the laundering path §6.3(b) closes): + * the migration runner, migrate-tier, and the createDb/createPgliteDb + * factories. The remaining registered modules execute only fixed statements + * or collection-CRUD over the storage `(id, data)` shape — which cannot + * address class-table columns — and export no caller-supplied-SQL surface; + * that composition property is review-enforced (§5.1) on any change to them. * * A false positive is resolved in the same PR by adding the module to the one * enumerated list its role permits — never by weakening the assertion. * * Test files (*.spec.*, *.test.*, __tests__/) are not scanned: they are not * production mutation paths, and the contract's own §6 witnesses must write - * class tables directly to witness database constraints. + * class tables directly to witness database constraints. The known evasion + * forms from the M4-1a review are kept below as permanent controls: the + * analyzer must flag every one of them, so a regression that reopens an + * escape fails this suite. */ import { readdirSync, readFileSync, statSync, existsSync } from 'node:fs'; -import { join, relative, resolve, sep } from 'node:path'; +import { dirname, join, relative, resolve, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; @@ -63,7 +82,8 @@ const CLASS_TABLES = [ * Writer allowlist (§6.3b): hierarchy command/repository modules only. * EMPTY until the hierarchy command family lands (M4-1b). Adding a module * here is a contract-conformance decision reviewed under §5.1 — the module - * must be part of the Gateway hierarchy command path. + * must be part of the Gateway hierarchy command path, and it must not export + * a function that executes caller-supplied SQL. */ const WRITER_ALLOWLIST: string[] = []; @@ -76,9 +96,10 @@ const INFRA_REGISTER: string[] = [ 'packages/db/src/client.ts', // connection factory (imports postgres driver) 'packages/db/src/client-pglite.ts', // PGlite factory (imports the pglite driver) 'packages/db/src/migrate.ts', // migration runner (hash-ledger DDL execution) + 'packages/db/src/backlog.ts', // backlog domain module: parameterized sql`` over backlog tables only 'packages/memory/src/insights.ts', // analytics raw query over memory tables - 'packages/storage/src/tier-detection.ts', // driver import for tier probing - 'packages/storage/src/adapters/pglite.ts', // storage adapter (driver-level query) + 'packages/storage/src/tier-detection.ts', // driver import for tier probing (fixed statements) + 'packages/storage/src/adapters/pglite.ts', // storage adapter (collection CRUD over (id, data)) 'packages/storage/src/adapters/postgres.ts', // storage adapter (extension bootstrap) 'packages/storage/src/migrate-tier.ts', // storage tier migration 'packages/storage/src/cli.ts', // storage CLI health probe @@ -86,17 +107,13 @@ const INFRA_REGISTER: string[] = [ ]; /** - * Closed importer enumerations for registered modules that EXPORT - * SQL-executing functions (the laundering path §6.3b closes). Import edges - * are checked re-export-aware — the db package barrel and literal dynamic - * `import('@mosaicstack/db')` are edges like any static import. The measured - * production importer set of the migration runner (contract 1 revision 9): - * the Gateway database module, the storage Postgres adapter, and two mosaic - * CLI commands routed through literal dynamic imports. The gateway - * schema-check module receives the runner's functions by parameter injection - * and has no import edge, so it is not enumerated. Being enumerated confers - * nothing else: importers stay subject to prongs (i)/(ii) and gain no writer - * standing. + * Closed importer enumerations for registered modules whose exports execute + * SQL or hand out an executing handle. An import edge is a static value + * import naming the symbol (from the module path or the package barrel), or + * a literal dynamic import of the package in a file using the symbol. + * `import type` is erased and is not an edge; parameter injection (the + * gateway schema-check module) has no edge. Being enumerated confers nothing + * else: importers stay subject to every prong and gain no writer standing. */ const MIGRATION_RUNNER_SYMBOLS = ['runMigrations', 'runPgliteMigrations', 'getMigrationStatus']; const MIGRATION_RUNNER_IMPORTERS: string[] = [ @@ -115,9 +132,31 @@ const MIGRATE_TIER_IMPORTERS: string[] = [ 'packages/storage/src/cli.ts', // storage CLI command surface 'packages/storage/src/index.ts', // package barrel re-export (public API) ]; +/** + * Enumerated disposition for non-literal dynamic imports (§6.3b review F8): + * files here may use a computed import specifier; every other prong still + * applies to them in full. Each entry needs a justification. + */ +const DYNAMIC_IMPORT_REGISTER: string[] = [ + 'plugins/macp/src/index.ts', // loads the ACP runtime SDK from a configured sdkRoot; no db access +]; +/** Importers of the db-handle factories (measured set; a new importer must be reviewed in). */ +const DB_FACTORY_SYMBOLS = ['createDb', 'createPgliteDb']; +const DB_FACTORY_IMPORTERS: string[] = [ + 'apps/gateway/src/database/database.module.ts', + 'packages/brain/src/cli.ts', + 'packages/log/src/cli.ts', + 'packages/memory/src/adapters/pgvector.ts', + 'packages/memory/src/cli.ts', + 'packages/mosaic/src/commands/fleet-backlog.ts', + 'packages/storage/src/adapters/postgres.ts', + 'packages/storage/src/cli.ts', + 'packages/storage/src/migrate-tier.ts', +]; -const SCAN_ROOTS = ['apps', 'packages']; +const SCAN_ROOTS = ['apps', 'packages', 'plugins']; const EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts']); +const DRIVER_SPECIFIERS = ['postgres', 'pg', '@electric-sql/pglite']; function isTestPath(rel: string): boolean { return ( @@ -143,7 +182,7 @@ function collectSources(): string[] { if (entry === 'node_modules' || entry === 'dist') continue; walk(full); } else if (EXTENSIONS.has(full.slice(full.lastIndexOf('.')))) { - const rel = relative(REPO_ROOT, full); + const rel = relative(REPO_ROOT, full).split(sep).join('/'); if (!isTestPath(rel)) files.push(rel); } } @@ -154,36 +193,187 @@ function collectSources(): string[] { return files.sort(); } -/** Strip line and block comments so commented-out code cannot trip prongs. */ -function stripComments(src: string): string { - return src.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/(^|[^:])\/\/[^\n]*/g, '$1'); +// --------------------------------------------------------------------------- +// Lexer: single pass producing (a) comment-free source with strings intact +// (for import/export and call-site regexes) and (b) the string/template text +// spans (for the literal prong). String-aware, so `//` inside a string is not +// a comment and a quote inside a comment does not open a string. Template +// expressions re-enter code mode (nesting supported); regex literals are +// recognized with the standard prev-token heuristic so their contents cannot +// open a phantom string. +// --------------------------------------------------------------------------- +interface Lexed { + code: string; + spans: string[]; } -/** Extract string and template literal spans (approximation, multi-line for templates). */ -function stringSpans(src: string): string[] { +function lexSource(src: string): Lexed { + let code = ''; const spans: string[] = []; - const re = /`[^`]*`|'(?:[^'\\\n]|\\.)*'|"(?:[^"\\\n]|\\.)*"/gs; - for (const m of src.matchAll(re)) spans.push(m[0]); - return spans; + let i = 0; + const n = src.length; + // Template nesting: each entry is the accumulated text of one template. + const tplStack: string[] = []; + // Brace depth inside the current ${ } expression, one entry per nesting level. + const exprDepth: number[] = []; + let lastSig = ''; // last significant code char (regex-vs-division heuristic) + let lastWord = ''; // last identifier/keyword emitted to code + + const emit = (ch: string): void => { + code += ch; + if (!/\s/.test(ch)) { + lastSig = ch; + if (/[A-Za-z0-9_$]/.test(ch)) lastWord += ch; + else lastWord = ''; + } + }; + + const regexCanStart = (): boolean => { + if (lastSig === '' || '([{,;=:!&|?+-*%^<>~'.includes(lastSig)) return true; + return ['return', 'typeof', 'case', 'in', 'of', 'new', 'delete', 'void', 'do', 'else'].includes( + lastWord, + ); + }; + + while (i < n) { + const ch = src[i]!; + const next = i + 1 < n ? src[i + 1]! : ''; + + if (tplStack.length > 0 && exprDepth.length < tplStack.length) { + // Inside template literal text. + const top = tplStack.length - 1; + if (ch === '\\') { + tplStack[top] += src.slice(i, i + 2); + i += 2; + continue; + } + if (ch === '`') { + spans.push(tplStack.pop()!); + emit('`'); + i += 1; + continue; + } + if (ch === '$' && next === '{') { + exprDepth.push(0); + emit('$'); + emit('{'); + i += 2; + continue; + } + tplStack[top] += ch; + i += 1; + continue; + } + + // Code mode (possibly inside a ${ } expression). + if (ch === '/' && next === '/') { + while (i < n && src[i] !== '\n') i += 1; + continue; + } + if (ch === '/' && next === '*') { + i += 2; + while (i < n && !(src[i] === '*' && src[i + 1] === '/')) i += 1; + i += 2; + emit(' '); + continue; + } + if (ch === "'" || ch === '"') { + let span = ''; + i += 1; + while (i < n && src[i] !== ch) { + if (src[i] === '\\') { + span += src.slice(i, i + 2); + i += 2; + } else { + span += src[i]; + i += 1; + } + } + i += 1; + spans.push(span); + // Keep quoted strings in code output so import specifiers stay parseable. + emit(ch); + code += span; + emit(ch); + continue; + } + if (ch === '`') { + tplStack.push(''); + emit('`'); + i += 1; + continue; + } + if (ch === '/' && regexCanStart()) { + // Regex literal: consume without interpreting quotes/backticks inside. + i += 1; + let inClass = false; + while (i < n) { + const rc = src[i]!; + if (rc === '\\') { + i += 2; + continue; + } + if (rc === '[') inClass = true; + else if (rc === ']') inClass = false; + else if (rc === '/' && !inClass) break; + else if (rc === '\n') break; // not a regex after all; bail safely + i += 1; + } + i += 1; + while (i < n && /[a-z]/.test(src[i]!)) i += 1; // flags + emit('/'); + continue; + } + if (exprDepth.length > 0) { + const top = exprDepth.length - 1; + if (ch === '{') exprDepth[top] = exprDepth[top]! + 1; + if (ch === '}') { + if (exprDepth[top] === 0) { + exprDepth.pop(); + emit('}'); + i += 1; + continue; + } + exprDepth[top] = exprDepth[top]! - 1; + } + } + emit(ch); + i += 1; + } + while (tplStack.length > 0) spans.push(tplStack.pop()!); + return { code, spans }; } -/** Local names (including aliases) under which class-table symbols are imported. */ -function classSymbolAliases(src: string): string[] { - const names = new Set(); - const importRe = /import\s*(?:type\s*)?\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g; - for (const m of src.matchAll(importRe)) { - const specifier = m[2]!; - if (!/@mosaicstack\/db|\.\.?\/(?:.*\/)?(?:schema|index)(?:\.js)?$/.test(specifier)) continue; - for (const part of m[1]!.split(',')) { - const seg = part.trim().replace(/^type\s+/, ''); - if (!seg) continue; - const asMatch = /^(\w+)\s+as\s+(\w+)$/.exec(seg); - const original = asMatch ? asMatch[1]! : seg; - const local = asMatch ? asMatch[2]! : seg; - if (CLASS_SYMBOLS.includes(original)) names.add(local); - } +// --------------------------------------------------------------------------- +// Import graph helpers +// --------------------------------------------------------------------------- +function resolveSpecifier(fromRel: string, spec: string, fileSet: Set): string | null { + if (!spec.startsWith('.')) return null; + const base = join(dirname(fromRel), spec).split(sep).join('/'); + const noJs = base.replace(/\.(js|mjs|cjs)$/, ''); + for (const cand of [ + base, + noJs, + `${noJs}.ts`, + `${noJs}.tsx`, + `${noJs}.mts`, + `${noJs}.cts`, + `${noJs}/index.ts`, + ]) { + if (fileSet.has(cand)) return cand; } - return [...names]; + return null; +} + +const IMPORT_RE = + /import\s*(type\s+)?(?:(\w+)\s*,\s*)?(?:\{([^}]*)\}|\*\s*as\s+(\w+)|(\w+))?\s*from\s*['"]([^'"]+)['"]/g; +const EXPORT_FROM_RE = + /export\s*(type\s+)?(?:\{([^}]*)\}|\*(?:\s*as\s+\w+)?)\s*from\s*['"]([^'"]+)['"]/g; + +interface FileFacts { + rel: string; + code: string; + spans: string[]; } interface Violation { @@ -192,11 +382,252 @@ interface Violation { detail: string; } -describe('hierarchy writer coverage (contract 1 §6.3b)', () => { - const sources = collectSources(); +/** + * Compute the fixpoint set of "schema sources": module paths from which a + * class-table symbol can be imported. Seeds: the schema module and the db + * package barrel (plus the bare '@mosaicstack/db' specifier, handled + * separately). Any scanned module that re-exports from a schema source + * (star, or a named list carrying a class symbol) — or imports class symbols + * and re-exports those local names — joins the set. + */ +function computeSchemaConduits(files: FileFacts[], fileSet: Set): Set { + const conduits = new Set(['packages/db/src/schema.ts', 'packages/db/src/index.ts']); + const isSchemaSpec = (rel: string, spec: string): boolean => { + if (spec === '@mosaicstack/db') return true; + const resolved = resolveSpecifier(rel, spec, fileSet); + return resolved !== null && conduits.has(resolved); + }; + let changed = true; + while (changed) { + changed = false; + for (const f of files) { + if (conduits.has(f.rel)) continue; + let isConduit = false; + for (const m of f.code.matchAll(EXPORT_FROM_RE)) { + if (m[1]) continue; // export type — erased + if (!isSchemaSpec(f.rel, m[3]!)) continue; + if (m[2] === undefined) { + isConduit = true; // export * from schema source + } else if (CLASS_SYMBOLS.some((s) => new RegExp(`\\b${s}\\b`).test(m[2]!))) { + isConduit = true; + } + } + if (!isConduit) { + const aliases = classAliases(f, conduits, fileSet); + if (aliases.named.length > 0) { + const exported = [...f.code.matchAll(/export\s*\{([^}]*)\}(?!\s*from)/g)] + .map((m) => m[1]!) + .join(','); + if (aliases.named.some((a) => new RegExp(`\\b${a}\\b`).test(exported))) isConduit = true; + } + } + if (isConduit) { + conduits.add(f.rel); + changed = true; + } + } + } + return conduits; +} - it('scans a non-empty production source set', () => { - expect(sources.length).toBeGreaterThan(100); +interface ClassAliases { + named: string[]; // local identifiers bound to class-table symbols + namespaces: string[]; // namespace identifiers over a schema source +} + +function classAliases(f: FileFacts, conduits: Set, fileSet: Set): ClassAliases { + const named = new Set(); + const namespaces = new Set(); + for (const m of f.code.matchAll(IMPORT_RE)) { + const [, typeOnly, , namedList, nsName, , spec] = m; + if (typeOnly) continue; + const fromSchema = + spec === '@mosaicstack/db' || + (() => { + const r = resolveSpecifier(f.rel, spec!, fileSet); + return r !== null && conduits.has(r); + })(); + if (!fromSchema) continue; + if (nsName) namespaces.add(nsName); + if (namedList) { + for (const part of namedList.split(',')) { + const seg = part.trim(); + if (!seg || seg.startsWith('type ')) continue; + const asMatch = /^(\w+)\s+as\s+(\w+)$/.exec(seg); + const original = asMatch ? asMatch[1]! : seg; + const local = asMatch ? asMatch[2]! : seg; + if (CLASS_SYMBOLS.includes(original)) named.add(local); + } + } + } + return { named: [...named], namespaces: [...namespaces] }; +} + +/** Value-import edge naming one of `symbols` (named import from anywhere, or literal dynamic package import while using the symbol). */ +function hasSymbolImportEdge(code: string, symbols: string[], packageName: string): boolean { + const staticEdge = new RegExp( + `import\\s*(?!type\\b)(?:\\w+\\s*,\\s*)?\\{[^}]*\\b(${symbols.join('|')})\\b[^}]*\\}\\s*from\\s*['"][^'"]+['"]`, + ).test(code); + const usesSymbol = new RegExp(`\\b(${symbols.join('|')})\\b`).test(code); + const dynamicEdge = + usesSymbol && + new RegExp(`import\\s*\\(\\s*['"]${packageName.replace('/', '\\/')}['"]\\s*\\)`).test(code); + return staticEdge || dynamicEdge; +} + +// --------------------------------------------------------------------------- +// The analyzer — pure over (rel, source) so the evasion controls below can +// feed synthetic files through the exact production logic. +// --------------------------------------------------------------------------- +function analyzeFile(f: FileFacts, conduits: Set, fileSet: Set): Violation[] { + const violations: Violation[] = []; + const rel = f.rel; + const code = f.code; + const inAllowlist = WRITER_ALLOWLIST.includes(rel); + const inRegister = INFRA_REGISTER.includes(rel); + const isSchemaDefinition = rel === 'packages/db/src/schema.ts'; + + // Runtime code construction: fails anywhere. + if (/\beval\s*\(|\bnew\s+Function\s*\(/.test(code)) { + violations.push({ file: rel, prong: 'code-construction', detail: 'eval/new Function' }); + } + // Non-literal dynamic import: makes the import graph unanalyzable. + if (!DYNAMIC_IMPORT_REGISTER.includes(rel) && /\bimport\s*\(\s*(?!['"])/.test(code)) { + violations.push({ file: rel, prong: 'dynamic-import', detail: 'non-literal import()' }); + } + + const aliases = classAliases(f, conduits, fileSet); + + // Prong (i): schema-symbol writes — alias-, namespace-, and conduit-aware. + if (!inAllowlist) { + const targets: string[] = [...aliases.named]; + for (const ns of aliases.namespaces) { + for (const s of CLASS_SYMBOLS) targets.push(`${ns}\\.${s}`); + } + if (targets.length > 0) { + const writeRe = new RegExp( + `\\.(insert|update|delete)\\s*\\(\\s*(${targets.join('|')})\\b`, + 'g', + ); + for (const m of code.matchAll(writeRe)) { + violations.push({ + file: rel, + prong: 'i-symbol', + detail: `.${m[1]}(${m[2]}) outside the writer allowlist`, + }); + } + } + } + + // Prong (ii): class-table names in string/template spans with SQL context. + // The SQL keyword must be ADJACENT to the table name (optionally quoted): + // co-residence anywhere in one span over-matches prose (English "from" plus + // "pnpm workspaces" in an embedded doc string is not SQL). + if (!inAllowlist && !isSchemaDefinition) { + const sqlAdjacentRe = new RegExp( + `\\b(insert\\s+into|update|delete\\s+from|from|join|truncate(\\s+table)?|alter\\s+table|drop\\s+table|references|into)\\s+["'\`]?(${CLASS_TABLES.join('|')})\\b`, + 'i', + ); + for (const span of f.spans) { + if (sqlAdjacentRe.test(span)) { + violations.push({ + file: rel, + prong: 'ii-literal', + detail: `class-table name in SQL context: ${span.slice(0, 80)}`, + }); + } + } + } + + // Prong (iii): content-independent raw execution. + if (!inAllowlist && !inRegister) { + const driverImport = new RegExp( + `(from\\s*|import\\s*\\(\\s*)['"](${DRIVER_SPECIFIERS.map((s) => s.replace('/', '\\/')).join('|')})['"]`, + ).test(code); + const factoryImport = + hasSymbolImportEdge(code, DB_FACTORY_SYMBOLS, '@mosaicstack/db') || + (aliases.namespaces.length > 0 && + new RegExp(`\\b(${aliases.namespaces.join('|')})\\.(createDb|createPgliteDb)\\b`).test( + code, + )); + if (driverImport) { + violations.push({ file: rel, prong: 'iii-raw-execution', detail: 'direct driver import' }); + // A driver client exposes query/execute/unsafe as raw primitives: + // flag them all on any receiver, any argument. + for (const m of code.matchAll(/\.(execute|query|unsafe)\s*\(/g)) { + violations.push({ + file: rel, + prong: 'iii-raw-execution', + detail: `.${m[1]}() in a driver-importing module`, + }); + } + } else if (factoryImport) { + // A drizzle handle's raw primitive is .execute (its .query namespace is + // the relational builder, and unrelated .query() methods are common), + // so factory capability flags execute/unsafe on any receiver and query + // only on the db/client-shaped receiver backstop below. + for (const m of code.matchAll(/\.(execute|unsafe)\s*\(/g)) { + violations.push({ + file: rel, + prong: 'iii-raw-execution', + detail: `.${m[1]}() in a factory-importing module`, + }); + } + } + if (!driverImport) { + // DI residual backstop: db/client-shaped receivers fire regardless of + // detected capability (a handle can arrive by injection). + for (const m of code.matchAll( + /\b(?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)\.(execute|query|unsafe)\s*\(/g, + )) { + violations.push({ + file: rel, + prong: 'iii-raw-execution', + detail: `.${m[1]}() on a db-shaped receiver`, + }); + } + } + // sql.raw through aliases and namespaces (drizzle-orm and the db barrel). + const sqlAliases = new Set(); + for (const m of code.matchAll(IMPORT_RE)) { + const [, typeOnly, , namedList, nsName, , spec] = m; + if (typeOnly) continue; + if (!/^drizzle-orm|^@mosaicstack\/db$/.test(spec!)) continue; + if (namedList) { + for (const part of namedList.split(',')) { + const seg = part.trim(); + const asMatch = /^sql\s+as\s+(\w+)$/.exec(seg); + if (seg === 'sql') sqlAliases.add('sql'); + else if (asMatch) sqlAliases.add(asMatch[1]!); + } + } + if (nsName) sqlAliases.add(`${nsName}\\.sql`); + } + if (sqlAliases.size > 0) { + const rawRe = new RegExp(`\\b(${[...sqlAliases].join('|')})\\.raw\\s*\\(`); + if (rawRe.test(code)) { + violations.push({ file: rel, prong: 'iii-raw-execution', detail: 'sql.raw()' }); + } + } + } + return violations; +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- +describe('hierarchy writer coverage (contract 1 §6.3b)', () => { + const sourceRels = collectSources(); + const fileSet = new Set(sourceRels); + const files: FileFacts[] = sourceRels.map((rel) => { + const { code, spans } = lexSource(readFileSync(join(REPO_ROOT, rel), 'utf8')); + return { rel, code, spans }; + }); + const conduits = computeSchemaConduits(files, fileSet); + + it('scans a non-empty production source set including plugins', () => { + expect(files.length).toBeGreaterThan(100); + expect(sourceRels.some((r) => r.startsWith('plugins/'))).toBe(true); }); it('enumerated modules exist on disk (no stale allowlist/register entries)', () => { @@ -205,6 +636,8 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { ...INFRA_REGISTER, ...MIGRATION_RUNNER_IMPORTERS, ...MIGRATE_TIER_IMPORTERS, + ...DB_FACTORY_IMPORTERS, + ...DYNAMIC_IMPORT_REGISTER, ]) { expect(existsSync(join(REPO_ROOT, p)), `enumerated module missing: ${p}`).toBe(true); } @@ -216,87 +649,8 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { } }); - it('three-prong writer coverage holds', () => { - const violations: Violation[] = []; - const allow = new Set(WRITER_ALLOWLIST); - const register = new Set(INFRA_REGISTER); - - for (const rel of sources) { - const raw = readFileSync(join(REPO_ROOT, rel), 'utf8'); - const src = stripComments(raw); - const inAllowlist = allow.has(rel); - const inRegister = register.has(rel); - const isSchemaDefinition = rel === 'packages/db/src/schema.ts'; - - // Runtime code construction: fails anywhere. - if (/\beval\s*\(|\bnew\s+Function\s*\(/.test(src)) { - violations.push({ file: rel, prong: 'code-construction', detail: 'eval/new Function' }); - } - // Non-literal dynamic import: makes the import graph unanalyzable. - if (/\bimport\s*\(\s*(?!['"`])/.test(src)) { - violations.push({ file: rel, prong: 'dynamic-import', detail: 'non-literal import()' }); - } - - // Prong (i): schema-symbol writes — alias-aware. - if (!inAllowlist) { - const aliases = classSymbolAliases(src); - if (aliases.length > 0) { - const writeRe = new RegExp( - `\\.(insert|update|delete)\\s*\\(\\s*(${aliases.join('|')})\\b`, - 'g', - ); - for (const m of src.matchAll(writeRe)) { - violations.push({ - file: rel, - prong: 'i-symbol', - detail: `.${m[1]}(${m[2]}) outside the writer allowlist`, - }); - } - } - } - - // Prong (ii): class-table names in SQL strings/templates. - // Schema definitions and generated migrations are excluded (contract); - // migrations live outside the scanned source roots already. - if (!inAllowlist && !isSchemaDefinition) { - const tableRe = new RegExp(`\\b(${CLASS_TABLES.join('|')})\\b`); - const sqlContextRe = - /\b(select|insert\s+into|update|delete\s+from|join|truncate|alter\s+table|drop\s+table|references)\b/i; - for (const span of stringSpans(src)) { - if (tableRe.test(span) && sqlContextRe.test(span)) { - violations.push({ - file: rel, - prong: 'ii-literal', - detail: `class-table name in SQL context: ${span.slice(0, 80)}`, - }); - } - } - } - - // Prong (iii): raw-execution primitives outside allowlist ∪ register. - if (!inAllowlist && !inRegister) { - const rawPatterns: Array<[RegExp, string]> = [ - [/\bsql\.raw\s*\(/, 'sql.raw()'], - [/\.unsafe\s*\(/, '.unsafe()'], - // `.execute(sql\`...\`)` is exempt: the tagged template keeps the - // SQL literal in source, where prong (ii) scans it. The flagged - // forms are the ones whose SQL content is not statically visible - // at the call site: `.execute(variable)` and `.execute("string")`. - [/\b(?:db|database|tx|trx)\.execute\s*\((?!\s*sql`)/, 'raw db.execute()'], - [/from\s*['"](?:postgres|pg|@electric-sql\/pglite)['"]/, 'direct driver import'], - [ - /\bimport\s*\(\s*['"](?:postgres|pg|@electric-sql\/pglite)['"]\s*\)/, - 'dynamic driver import', - ], - ]; - for (const [re, label] of rawPatterns) { - if (re.test(src)) { - violations.push({ file: rel, prong: 'iii-raw-execution', detail: label }); - } - } - } - } - + it('three-prong writer coverage holds over the production tree', () => { + const violations = files.flatMap((f) => analyzeFile(f, conduits, fileSet)); expect( violations, violations.map((v) => `[prong ${v.prong}] ${v.file}: ${v.detail}`).join('\n'), @@ -305,22 +659,14 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { it('migration-runner import edges are exactly the closed importer enumeration', () => { const offenders: string[] = []; - for (const rel of sources) { - if (rel.startsWith('packages/db/src/')) continue; // the runner's own package - const src = stripComments(readFileSync(join(REPO_ROOT, rel), 'utf8')); - const symbolRe = new RegExp(`\\b(${MIGRATION_RUNNER_SYMBOLS.join('|')})\\b`); - if (!symbolRe.test(src)) continue; - // An import edge is a static value import from the db package (or its - // migrate module) naming a runner symbol, or a literal dynamic - // import('@mosaicstack/db') in a file that uses a runner symbol. - // `import type` is erased at runtime and is not an edge; parameter - // injection (the gateway schema-check module) has no edge. - const staticEdge = new RegExp( - `import\\s*(?!type\\b)\\{[^}]*\\b(${MIGRATION_RUNNER_SYMBOLS.join('|')})\\b[^}]*\\}\\s*from\\s*['"](@mosaicstack/db|[^'"]*migrate(\\.js)?)['"]`, - ).test(src); - const dynamicEdge = /import\s*\(\s*['"]@mosaicstack\/db['"]\s*\)/.test(src); - if ((staticEdge || dynamicEdge) && !MIGRATION_RUNNER_IMPORTERS.includes(rel)) { - offenders.push(rel); + for (const f of files) { + if (f.rel.startsWith('packages/db/src/')) continue; // the runner's own package + if (!new RegExp(`\\b(${MIGRATION_RUNNER_SYMBOLS.join('|')})\\b`).test(f.code)) continue; + if ( + hasSymbolImportEdge(f.code, MIGRATION_RUNNER_SYMBOLS, '@mosaicstack/db') && + !MIGRATION_RUNNER_IMPORTERS.includes(f.rel) + ) { + offenders.push(f.rel); } } expect(offenders, `unenumerated migration-runner importers:\n${offenders.join('\n')}`).toEqual( @@ -330,20 +676,121 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { it('migrate-tier import edges are exactly the closed importer enumeration', () => { const offenders: string[] = []; - for (const rel of sources) { - if (rel === 'packages/storage/src/migrate-tier.ts') continue; - const src = stripComments(readFileSync(join(REPO_ROOT, rel), 'utf8')); - // Edge = direct module-path import, or a value import of a migrate-tier - // symbol from the storage package barrel (the barrel is itself - // enumerated as a re-exporter, so barrel consumers must not escape). + for (const f of files) { + if (f.rel === 'packages/storage/src/migrate-tier.ts') continue; const pathEdge = - /from\s*['"][^'"]*migrate-tier(\.js)?['"]/.test(src) || - /import\s*\(\s*['"][^'"]*migrate-tier(\.js)?['"]\s*\)/.test(src); - const barrelEdge = new RegExp( - `import\\s*(?!type\\b)\\{[^}]*\\b(${MIGRATE_TIER_SYMBOLS.join('|')})\\b[^}]*\\}\\s*from\\s*['"]@mosaicstack/storage['"]`, - ).test(src); - if ((pathEdge || barrelEdge) && !MIGRATE_TIER_IMPORTERS.includes(rel)) offenders.push(rel); + /from\s*['"][^'"]*migrate-tier(\.js)?['"]/.test(f.code) || + /import\s*\(\s*['"][^'"]*migrate-tier(\.js)?['"]\s*\)/.test(f.code); + const symbolEdge = + new RegExp(`\\b(${MIGRATE_TIER_SYMBOLS.join('|')})\\b`).test(f.code) && + hasSymbolImportEdge(f.code, MIGRATE_TIER_SYMBOLS, '@mosaicstack/storage'); + if ((pathEdge || symbolEdge) && !MIGRATE_TIER_IMPORTERS.includes(f.rel)) { + offenders.push(f.rel); + } } expect(offenders, `unenumerated migrate-tier importers:\n${offenders.join('\n')}`).toEqual([]); }); + + it('db-factory (createDb/createPgliteDb) import edges are exactly the closed importer enumeration', () => { + const offenders: string[] = []; + for (const f of files) { + if (f.rel.startsWith('packages/db/src/')) continue; // the factories' own package + if (!new RegExp(`\\b(${DB_FACTORY_SYMBOLS.join('|')})\\b`).test(f.code)) continue; + if ( + hasSymbolImportEdge(f.code, DB_FACTORY_SYMBOLS, '@mosaicstack/db') && + !DB_FACTORY_IMPORTERS.includes(f.rel) + ) { + offenders.push(f.rel); + } + } + expect(offenders, `unenumerated db-factory importers:\n${offenders.join('\n')}`).toEqual([]); + }); + + // ------------------------------------------------------------------------- + // Permanent evasion controls: every known escape from the M4-1a review must + // be flagged by the analyzer, and the two legitimate controls must pass. + // Synthetic files run through the exact production analyzer. + // ------------------------------------------------------------------------- + const EVASIONS: Array<{ name: string; src: string }> = [ + { + name: 'E1 namespace import write', + src: `import * as dbSchema from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(dbSchema.companies).values({}); }`, + }, + { + name: 'E2 re-export laundering (conduit consumer)', + src: `import { companies } from './evasion-barrel.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(companies).values({}); }`, + }, + { + name: 'E3 renamed createDb handle', + src: `import { createDb } from '@mosaicstack/db';\nexport async function f(t: string) { const d = createDb('u'); await d.execute('DELETE FROM ' + t); }`, + }, + { + name: 'E4 aliased sql.raw', + src: `import { sql as q } from 'drizzle-orm';\nimport { db } from './x.js';\nexport async function f(t: string) { await db.execute(q.raw('TRUNCATE ' + t)); }`, + }, + { + name: 'E5 interpolated tagged template on db receiver', + src: `import { sql } from 'drizzle-orm';\nimport { db } from './x.js';\nexport async function f() { const tbl = 'hierarchy_grants'; await db.execute(sql\`DELETE FROM \${tbl}\`); }`, + }, + { + name: 'E6 driver client via import', + src: `import postgres from 'postgres';\nexport async function f(t: string) { const c = postgres('u'); await c.unsafe('TRUNCATE ' + t); }`, + }, + { + name: 'E7 DI-shaped driver client query', + src: `export class R { constructor(private client: { query(s: string): Promise }) {}\n async f(t: string) { await this.client.query('TRUNCATE ' + t); } }`, + }, + { + name: 'E8 string containing // does not hide code', + src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { const x = 'oops//'; await db.insert(companies).values({}); }`, + }, + { + name: 'C-literal SQL string with class table', + src: `export const q = 'DELETE FROM hierarchy_grants WHERE role = $1';`, + }, + { + name: 'C-eval', + src: `export function f(s: string) { return eval(s); }`, + }, + ]; + const CLEAN_CONTROLS: Array<{ name: string; src: string }> = [ + { + name: 'clean: schema read via select', + src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { return db.select().from(companies); }`, + }, + { + name: 'clean: comment mentioning a table is not SQL', + src: `// syncs hierarchy_grants downstream\nexport const n = 1;`, + }, + ]; + + it('analyzer flags every known evasion form', () => { + // A synthetic conduit so E2 exercises the fixpoint through a real re-export. + const barrel: FileFacts = { + rel: 'packages/db/src/evasion-barrel.ts', + ...lexSource(`export { companies } from '@mosaicstack/db';`), + }; + for (const e of EVASIONS) { + const rel = 'packages/db/src/evasion-sample.ts'; + const synthetic: FileFacts = { rel, ...lexSource(e.src) }; + const synthSet = new Set([...fileSet, rel, barrel.rel]); + const synthConduits = computeSchemaConduits([...files, barrel, synthetic], synthSet); + const v = analyzeFile(synthetic, synthConduits, synthSet); + expect(v.length, `evasion not caught: ${e.name}`).toBeGreaterThan(0); + } + }); + + it('analyzer passes legitimate non-writer code (no false positives on controls)', () => { + for (const c of CLEAN_CONTROLS) { + const rel = 'packages/db/src/clean-sample.ts'; + const synthetic: FileFacts = { rel, ...lexSource(c.src) }; + const synthSet = new Set([...fileSet, rel]); + const synthConduits = computeSchemaConduits([...files, synthetic], synthSet); + const v = analyzeFile(synthetic, synthConduits, synthSet); + expect( + v, + `false positive on ${c.name}: ${v.map((x) => `${x.prong}:${x.detail}`).join('; ')}`, + ).toEqual([]); + } + }); }); -- 2.54.0 From 7dedc8d3c0eaa392ca64212118ce0f995e6f60cc Mon Sep 17 00:00:00 2001 From: fred Date: Thu, 27 Aug 2026 16:38:48 -0500 Subject: [PATCH 03/10] fix(db): close round-2 review evasions in writer-coverage assertion Per the second M4-1a detached review (REQUEST_CHANGES, R1-R8): - R1: prong (i) now tracks namespace destructuring, nested namespace re-exports (ns.schema.companies), and literal dynamic-import bindings (destructured and namespace) per contract rev 9. - R2: capability-conduit fixpoint (computeCapabilityConduits) closes driver/factory laundering through export-from modules. - R3: dynamic-import check is per call site with a full-literal tail match, so concatenated specifiers no longer pass. - R4: computed-member calls with literal verb names (obj['insert'], obj['query']) and spaced member access are flagged. - R5: prong (ii) adjacency tolerates schema qualification, interposed block comments, COPY, and escaped quotes. - R6/R8: header documents KNOWN RESIDUALS (DI receiver rename in capability-free files, computed non-literal member access, scan perimeter) with the measured counterfactuals. - createRequire fails outside a 5-module measured register (R1 route). - 15 new permanent evasion controls (E9-E23) with helper-file conduits run through the production analyzer. Calibration: tree-wide prong test green with zero new exclusions; tsc, eslint, and the full package suite pass. --- .../db/src/hierarchy-writer-coverage.test.ts | 393 ++++++++++++++++-- 1 file changed, 347 insertions(+), 46 deletions(-) diff --git a/packages/db/src/hierarchy-writer-coverage.test.ts b/packages/db/src/hierarchy-writer-coverage.test.ts index af973c62..ec1271df 100644 --- a/packages/db/src/hierarchy-writer-coverage.test.ts +++ b/packages/db/src/hierarchy-writer-coverage.test.ts @@ -28,11 +28,31 @@ * receiver backstop still fires on db/client-shaped receivers. * `sql.raw` is tracked through import aliasing and namespaces. * - * Runtime code-construction primitives (eval, new Function) and non-literal - * dynamic imports fail anywhere — allowlist and register included. This is - * deliberately stricter than the contract's minimum: an unanalyzable import - * or constructed code defeats every static prong, so there is no enumerated - * disposition path for them. + * Runtime code-construction primitives (eval, new Function) fail anywhere — + * allowlist and register included: constructed code defeats every static + * prong, so there is no enumerated disposition path for it. Unanalyzable + * import routes (a dynamic import whose specifier is not a single string + * literal, and createRequire) fail everywhere except their own enumerated + * disposition registers below. A LITERAL dynamic import is an import edge + * like any static import (contract rev 9): its bindings (destructured or + * namespace) feed prong (i) exactly as static import bindings do. + * + * KNOWN RESIDUALS (deliberate, reviewed trade-offs — not claims of closure): + * - In a file with NO detected raw capability (no driver/factory import on + * any tracked route), prong (iii) falls back to a conventional-receiver + * backstop (db/client/pool/tx/…). A dependency-injected handle bound to + * an unconventional name in such a file is an OPEN evasion path. The + * counterfactual was measured: any-receiver flagging in capability-free + * files false-positives on real non-SQL .execute()/.query() sites + * (runtime-provider-registry, connector-lease, the log CLI), so the + * residual is accepted and reviews of DI provider modules carry it. + * - Computed member access with a NON-literal name (obj[verb]()) is not + * statically resolvable; literal computed access (obj['insert']()) is + * flagged. Constructing the verb at runtime is adjacent to eval and is + * expected to be caught in review. + * - The scan perimeter is //src for the three roots; production + * TS outside a src/ directory (e.g. packages/mosaic/framework/**) is not + * scanned (verified free of db/driver/execute references at review time). * * The writer allowlist names hierarchy command/repository modules ONLY. It is * empty today: the hierarchy command family (M4-1b) has not landed, so no @@ -140,6 +160,18 @@ const MIGRATE_TIER_IMPORTERS: string[] = [ const DYNAMIC_IMPORT_REGISTER: string[] = [ 'plugins/macp/src/index.ts', // loads the ACP runtime SDK from a configured sdkRoot; no db access ]; +/** + * Enumerated disposition for createRequire (an unanalyzable CJS import + * route). Every entry uses it for package.json version reads or module + * resolution only (measured); every other prong still applies in full. + */ +const CREATE_REQUIRE_REGISTER: string[] = [ + 'packages/mosaic/src/cli.ts', // package.json version read + 'packages/mosaic/src/commands/gateway/daemon.ts', // module path resolution + 'packages/mosaic/src/commands/launch.ts', // package.json version read + resolution + 'packages/mosaic/src/commands/lease-activation-probe.ts', // injectable module resolver default + 'plugins/macp/src/index.ts', // OpenCode SDK resolution +]; /** Importers of the db-handle factories (measured set; a new importer must be reviewed in). */ const DB_FACTORY_SYMBOLS = ['createDb', 'createPgliteDb']; const DB_FACTORY_IMPORTERS: string[] = [ @@ -435,51 +467,155 @@ interface ClassAliases { namespaces: string[]; // namespace identifiers over a schema source } +/** Parse an import named-binding list ("a, b as c") into locals bound to class symbols. */ +function importBindings(namedList: string, named: Set): void { + for (const part of namedList.split(',')) { + const seg = part.trim(); + if (!seg || seg.startsWith('type ')) continue; + const asMatch = /^(\w+)\s+as\s+(\w+)$/.exec(seg); + const original = asMatch ? asMatch[1]! : seg; + const local = asMatch ? asMatch[2]! : seg; + if (CLASS_SYMBOLS.includes(original)) named.add(local); + } +} + +/** Parse a destructuring pattern ("a, b: c") into locals bound to class symbols. */ +function destructureBindings(pattern: string, named: Set): void { + for (const part of pattern.split(',')) { + const seg = part.trim(); + if (!seg) continue; + const m = /^(\w+)\s*(?::\s*(\w+))?$/.exec(seg); + if (m && CLASS_SYMBOLS.includes(m[1]!)) named.add(m[2] ?? m[1]!); + } +} + +function isSchemaSpecifier( + rel: string, + spec: string, + conduits: Set, + fileSet: Set, +): boolean { + if (spec === '@mosaicstack/db') return true; + const r = resolveSpecifier(rel, spec, fileSet); + return r !== null && conduits.has(r); +} + function classAliases(f: FileFacts, conduits: Set, fileSet: Set): ClassAliases { const named = new Set(); const namespaces = new Set(); for (const m of f.code.matchAll(IMPORT_RE)) { const [, typeOnly, , namedList, nsName, , spec] = m; if (typeOnly) continue; - const fromSchema = - spec === '@mosaicstack/db' || - (() => { - const r = resolveSpecifier(f.rel, spec!, fileSet); - return r !== null && conduits.has(r); - })(); - if (!fromSchema) continue; + if (!isSchemaSpecifier(f.rel, spec!, conduits, fileSet)) continue; if (nsName) namespaces.add(nsName); - if (namedList) { - for (const part of namedList.split(',')) { - const seg = part.trim(); - if (!seg || seg.startsWith('type ')) continue; - const asMatch = /^(\w+)\s+as\s+(\w+)$/.exec(seg); - const original = asMatch ? asMatch[1]! : seg; - const local = asMatch ? asMatch[2]! : seg; - if (CLASS_SYMBOLS.includes(original)) named.add(local); - } + if (namedList) importBindings(namedList, named); + } + // Literal dynamic imports of a schema source are import edges like any + // other (contract rev 9): both binding shapes feed prong (i). + for (const m of f.code.matchAll( + /(?:const|let|var)\s*(?:\{([^}]*)\}|(\w+))\s*=\s*await\s+import\s*\(\s*(['"])([^'"]+)\3\s*\)/g, + )) { + const [, pattern, nsName, , spec] = m; + if (!isSchemaSpecifier(f.rel, spec!, conduits, fileSet)) continue; + if (nsName) namespaces.add(nsName); + if (pattern) destructureBindings(pattern, named); + } + // Destructuring from a schema namespace binds class symbols to locals: + // `import * as s from '@mosaicstack/db'; const { companies } = s;` + for (const ns of [...namespaces]) { + for (const m of f.code.matchAll( + new RegExp(`(?:const|let|var)\\s*\\{([^}]*)\\}\\s*=\\s*${ns}\\b`, 'g'), + )) { + destructureBindings(m[1]!, named); } } return { named: [...named], namespaces: [...namespaces] }; } +/** + * Capability conduits: modules that re-export raw-execution capability, so a + * consumer can obtain it without a literal driver/factory specifier. + * `driver` — re-exports (star, named, or default) from a driver package or + * another driver conduit; importing ANYTHING from one confers driver + * capability (the conduit module itself is additionally flagged by prong + * (iii)'s literal check, so these routes fail at both ends). + * `factory` — re-exports createDb/createPgliteDb from the db package or + * another factory conduit; importing from one while referencing a factory + * symbol confers factory capability. + */ +function computeCapabilityConduits( + files: FileFacts[], + fileSet: Set, +): { driver: Set; factory: Set } { + const driver = new Set(); + const factory = new Set(); + const isDriverSpec = (rel: string, spec: string): boolean => { + if (DRIVER_SPECIFIERS.includes(spec)) return true; + const r = resolveSpecifier(rel, spec, fileSet); + return r !== null && driver.has(r); + }; + const isFactorySpec = (rel: string, spec: string): boolean => { + if (spec === '@mosaicstack/db') return true; + const r = resolveSpecifier(rel, spec, fileSet); + return r !== null && factory.has(r); + }; + let changed = true; + while (changed) { + changed = false; + for (const f of files) { + for (const m of f.code.matchAll(EXPORT_FROM_RE)) { + if (m[1]) continue; // export type — erased + const spec = m[3]!; + if (!driver.has(f.rel) && isDriverSpec(f.rel, spec)) { + driver.add(f.rel); + changed = true; + } + if (!factory.has(f.rel) && isFactorySpec(f.rel, spec)) { + const named = m[2]; + if (named === undefined || /\b(createDb|createPgliteDb)\b/.test(named)) { + factory.add(f.rel); + changed = true; + } + } + } + // `export { default as x } from 'postgres'` matches EXPORT_FROM_RE's + // named branch above; `export x from` is not valid syntax — covered. + } + } + return { driver, factory }; +} + /** Value-import edge naming one of `symbols` (named import from anywhere, or literal dynamic package import while using the symbol). */ function hasSymbolImportEdge(code: string, symbols: string[], packageName: string): boolean { + const pkg = packageName.replace('/', '\\/'); const staticEdge = new RegExp( `import\\s*(?!type\\b)(?:\\w+\\s*,\\s*)?\\{[^}]*\\b(${symbols.join('|')})\\b[^}]*\\}\\s*from\\s*['"][^'"]+['"]`, ).test(code); const usesSymbol = new RegExp(`\\b(${symbols.join('|')})\\b`).test(code); - const dynamicEdge = - usesSymbol && - new RegExp(`import\\s*\\(\\s*['"]${packageName.replace('/', '\\/')}['"]\\s*\\)`).test(code); - return staticEdge || dynamicEdge; + const dynamicEdge = usesSymbol && new RegExp(`import\\s*\\(\\s*['"]${pkg}['"]\\s*\\)`).test(code); + // Namespace form: `import * as ns from ''` + `ns.` usage. + let nsEdge = false; + for (const m of code.matchAll( + new RegExp(`import\\s*\\*\\s*as\\s+(\\w+)\\s*from\\s*['"]${pkg}['"]`, 'g'), + )) { + if (new RegExp(`\\b${m[1]}\\s*\\.\\s*(${symbols.join('|')})\\b`).test(code)) nsEdge = true; + } + return staticEdge || dynamicEdge || nsEdge; } // --------------------------------------------------------------------------- // The analyzer — pure over (rel, source) so the evasion controls below can // feed synthetic files through the exact production logic. // --------------------------------------------------------------------------- -function analyzeFile(f: FileFacts, conduits: Set, fileSet: Set): Violation[] { +interface AnalysisCtx { + fileSet: Set; + conduits: Set; // schema-symbol sources (prong i) + driverConduits: Set; // driver-capability re-exporters (prong iii) + factoryConduits: Set; // factory-capability re-exporters (prong iii) +} + +function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { + const { fileSet, conduits, driverConduits, factoryConduits } = ctx; const violations: Violation[] = []; const rel = f.rel; const code = f.code; @@ -491,9 +627,20 @@ function analyzeFile(f: FileFacts, conduits: Set, fileSet: Set): if (/\beval\s*\(|\bnew\s+Function\s*\(/.test(code)) { violations.push({ file: rel, prong: 'code-construction', detail: 'eval/new Function' }); } - // Non-literal dynamic import: makes the import graph unanalyzable. - if (!DYNAMIC_IMPORT_REGISTER.includes(rel) && /\bimport\s*\(\s*(?!['"])/.test(code)) { - violations.push({ file: rel, prong: 'dynamic-import', detail: 'non-literal import()' }); + // Dynamic import whose specifier is not a single string literal: the + // import graph becomes unanalyzable. Checked per call site, so a literal + // first fragment ('x' + y) does not slip past. + if (!DYNAMIC_IMPORT_REGISTER.includes(rel)) { + for (const m of code.matchAll(/\bimport\s*\(/g)) { + const tail = code.slice((m.index ?? 0) + m[0].length); + if (!/^\s*(['"])(?:[^'"\\]|\\.)*?\1\s*[,)]/.test(tail)) { + violations.push({ file: rel, prong: 'dynamic-import', detail: 'non-literal import()' }); + } + } + } + // createRequire: an unanalyzable CJS import route. + if (!CREATE_REQUIRE_REGISTER.includes(rel) && /\bcreateRequire\s*\(/.test(code)) { + violations.push({ file: rel, prong: 'dynamic-import', detail: 'createRequire()' }); } const aliases = classAliases(f, conduits, fileSet); @@ -502,11 +649,13 @@ function analyzeFile(f: FileFacts, conduits: Set, fileSet: Set): if (!inAllowlist) { const targets: string[] = [...aliases.named]; for (const ns of aliases.namespaces) { - for (const s of CLASS_SYMBOLS) targets.push(`${ns}\\.${s}`); + // Allow intermediate property segments: ns.schema.companies (nested + // namespace re-exports) as well as ns.companies. + for (const s of CLASS_SYMBOLS) targets.push(`${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*${s}`); } if (targets.length > 0) { const writeRe = new RegExp( - `\\.(insert|update|delete)\\s*\\(\\s*(${targets.join('|')})\\b`, + `\\.\\s*(insert|update|delete)\\s*\\(\\s*(${targets.join('|')})\\b`, 'g', ); for (const m of code.matchAll(writeRe)) { @@ -519,13 +668,43 @@ function analyzeFile(f: FileFacts, conduits: Set, fileSet: Set): } } + // Computed member access with a literal verb name (obj['insert'](…)) + // bypasses dot-based call detection; the tree has no legitimate use of the + // form (measured), so it fails outright. Write verbs fail outside the + // allowlist; execution verbs fail outside allowlist ∪ register. + if (!inAllowlist) { + const writeBracket = /\[\s*['"](insert|update|delete)['"]\s*\]\s*\(/g; + for (const m of code.matchAll(writeBracket)) { + violations.push({ + file: rel, + prong: 'i-symbol', + detail: `['${m[1]}']() computed-member write call`, + }); + } + if (!inRegister) { + for (const m of code.matchAll(/\[\s*['"](execute|query|unsafe)['"]\s*\]\s*\(/g)) { + violations.push({ + file: rel, + prong: 'iii-raw-execution', + detail: `['${m[1]}']() computed-member execution call`, + }); + } + } + } + // Prong (ii): class-table names in string/template spans with SQL context. - // The SQL keyword must be ADJACENT to the table name (optionally quoted): - // co-residence anywhere in one span over-matches prose (English "from" plus - // "pnpm workspaces" in an embedded doc string is not SQL). + // The SQL keyword must be ADJACENT to the table name — co-residence + // anywhere in one span over-matches prose ("pnpm workspaces" plus an + // unrelated "from" in an embedded doc string is not SQL). Adjacency + // tolerates schema qualification (public.hierarchy_grants), interposed + // block comments, and quoting (including escaped quotes in span text). if (!inAllowlist && !isSchemaDefinition) { + const kw = + '(?:insert\\s+into|update|delete\\s+from|from|join|truncate(?:\\s+table)?|alter\\s+table|drop\\s+table|references|into|copy)'; + const gap = '(?:\\s|/\\*[\\s\\S]*?\\*/)+'; + const q = `(?:\\\\?["'\`])?`; const sqlAdjacentRe = new RegExp( - `\\b(insert\\s+into|update|delete\\s+from|from|join|truncate(\\s+table)?|alter\\s+table|drop\\s+table|references|into)\\s+["'\`]?(${CLASS_TABLES.join('|')})\\b`, + `\\b${kw}${gap}${q}(?:\\w+\\s*\\.\\s*)?${q}(${CLASS_TABLES.join('|')})\\b`, 'i', ); for (const span of f.spans) { @@ -541,15 +720,28 @@ function analyzeFile(f: FileFacts, conduits: Set, fileSet: Set): // Prong (iii): content-independent raw execution. if (!inAllowlist && !inRegister) { - const driverImport = new RegExp( + const literalDriver = new RegExp( `(from\\s*|import\\s*\\(\\s*)['"](${DRIVER_SPECIFIERS.map((s) => s.replace('/', '\\/')).join('|')})['"]`, ).test(code); + // Any value import from a driver conduit confers driver capability. + let conduitDriver = false; + let factoryConduitImport = false; + for (const m of f.code.matchAll(IMPORT_RE)) { + if (m[1]) continue; // import type — erased + const r = resolveSpecifier(rel, m[6]!, fileSet); + if (r !== null && driverConduits.has(r)) conduitDriver = true; + if (r !== null && factoryConduits.has(r)) factoryConduitImport = true; + } + const driverImport = literalDriver || conduitDriver; + const usesFactorySymbol = /\b(createDb|createPgliteDb)\b/.test(code); const factoryImport = hasSymbolImportEdge(code, DB_FACTORY_SYMBOLS, '@mosaicstack/db') || + (factoryConduitImport && usesFactorySymbol) || (aliases.namespaces.length > 0 && - new RegExp(`\\b(${aliases.namespaces.join('|')})\\.(createDb|createPgliteDb)\\b`).test( - code, - )); + usesFactorySymbol && + new RegExp( + `\\b(${aliases.namespaces.join('|')})(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(createDb|createPgliteDb)\\b`, + ).test(code)); if (driverImport) { violations.push({ file: rel, prong: 'iii-raw-execution', detail: 'direct driver import' }); // A driver client exposes query/execute/unsafe as raw primitives: @@ -624,6 +816,13 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { return { rel, code, spans }; }); const conduits = computeSchemaConduits(files, fileSet); + const capConduits = computeCapabilityConduits(files, fileSet); + const ctx: AnalysisCtx = { + fileSet, + conduits, + driverConduits: capConduits.driver, + factoryConduits: capConduits.factory, + }; it('scans a non-empty production source set including plugins', () => { expect(files.length).toBeGreaterThan(100); @@ -638,6 +837,7 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { ...MIGRATE_TIER_IMPORTERS, ...DB_FACTORY_IMPORTERS, ...DYNAMIC_IMPORT_REGISTER, + ...CREATE_REQUIRE_REGISTER, ]) { expect(existsSync(join(REPO_ROOT, p)), `enumerated module missing: ${p}`).toBe(true); } @@ -650,7 +850,7 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { }); it('three-prong writer coverage holds over the production tree', () => { - const violations = files.flatMap((f) => analyzeFile(f, conduits, fileSet)); + const violations = files.flatMap((f) => analyzeFile(f, ctx)); expect( violations, violations.map((v) => `[prong ${v.prong}] ${v.file}: ${v.detail}`).join('\n'), @@ -711,7 +911,11 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { // be flagged by the analyzer, and the two legitimate controls must pass. // Synthetic files run through the exact production analyzer. // ------------------------------------------------------------------------- - const EVASIONS: Array<{ name: string; src: string }> = [ + const EVASIONS: Array<{ + name: string; + src: string; + extras?: Array<{ rel: string; src: string }>; + }> = [ { name: 'E1 namespace import write', src: `import * as dbSchema from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(dbSchema.companies).values({}); }`, @@ -752,6 +956,85 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { name: 'C-eval', src: `export function f(s: string) { return eval(s); }`, }, + // --- round-2 review shapes (R1–R5) --- + { + name: 'E9 namespace destructuring', + src: `import * as dbSchema from '@mosaicstack/db';\nimport { db } from './x.js';\nconst { companies } = dbSchema;\nexport async function f() { await db.insert(companies).values({}); }`, + }, + { + name: 'E10 nested namespace re-export', + src: `import * as ns from './evasion-mid.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(ns.schema.companies).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid.ts', + src: `export * as schema from '@mosaicstack/db';`, + }, + ], + }, + { + name: 'E11 literal dynamic import, destructured binding', + src: `import { db } from './x.js';\nexport async function f() { const { companies } = await import('@mosaicstack/db'); await db.insert(companies).values({}); }`, + }, + { + name: 'E12 literal dynamic import, namespace binding', + src: `import { db } from './x.js';\nexport async function f() { const m = await import('@mosaicstack/db'); await db.insert(m.companies).values({}); }`, + }, + { + name: 'E13 createRequire outside its register', + src: `import { createRequire } from 'node:module';\nconst require = createRequire(import.meta.url);\nexport const pg = require('postgres');`, + }, + { + name: 'E14 factory capability laundered through re-export', + src: `import * as dbns from './evasion-mid2.js';\nexport async function f(t: string) { await dbns.createDb('u').execute('DELETE FROM ' + t); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid2.ts', + src: `export { createDb } from '@mosaicstack/db';`, + }, + ], + }, + { + name: 'E15 driver default laundered through re-export', + src: `import pg from './evasion-mid3.js';\nexport async function f(t: string) { const c = pg('u'); await c.unsafe('TRUNCATE ' + t); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid3.ts', + src: `export { default } from 'postgres';`, + }, + ], + }, + { + name: 'E16 concatenated dynamic-import specifier', + src: `export async function f() { const m = await import('@mosaicstack/' + 'db'); return m; }`, + }, + { + name: 'E17 computed-member write call', + src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db['insert'](companies).values({}); }`, + }, + { + name: 'E18 spaced member access write', + src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db . insert (companies).values({}); }`, + }, + { + name: 'E19 computed-member execution call', + src: `export class R { constructor(private client: { query(s: string): Promise }) {}\n async f(t: string) { await this.client['query']('TRUNCATE ' + t); } }`, + }, + { + name: 'E20 schema-qualified table in SQL span', + src: `export const q = 'DELETE FROM public.hierarchy_grants WHERE role = $1';`, + }, + { + name: 'E21 comment interposed in SQL span', + src: `export const q = 'DELETE FROM /* audit */ hierarchy_grants WHERE role = $1';`, + }, + { + name: 'E22 COPY statement in SQL span', + src: `export const q = 'COPY hierarchy_grants FROM STDIN';`, + }, + { + name: 'E23 escaped-backtick-quoted table in SQL span', + src: 'export const q = `DELETE FROM \\`hierarchy_grants\\` WHERE role = 1`;', + }, ]; const CLEAN_CONTROLS: Array<{ name: string; src: string }> = [ { @@ -773,9 +1056,20 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { for (const e of EVASIONS) { const rel = 'packages/db/src/evasion-sample.ts'; const synthetic: FileFacts = { rel, ...lexSource(e.src) }; - const synthSet = new Set([...fileSet, rel, barrel.rel]); - const synthConduits = computeSchemaConduits([...files, barrel, synthetic], synthSet); - const v = analyzeFile(synthetic, synthConduits, synthSet); + const extraFacts: FileFacts[] = (e.extras ?? []).map((x) => ({ + rel: x.rel, + ...lexSource(x.src), + })); + const synthFiles = [...files, barrel, ...extraFacts, synthetic]; + const synthSet = new Set([...fileSet, rel, barrel.rel, ...extraFacts.map((x) => x.rel)]); + const synthCap = computeCapabilityConduits(synthFiles, synthSet); + const synthCtx: AnalysisCtx = { + fileSet: synthSet, + conduits: computeSchemaConduits(synthFiles, synthSet), + driverConduits: synthCap.driver, + factoryConduits: synthCap.factory, + }; + const v = analyzeFile(synthetic, synthCtx); expect(v.length, `evasion not caught: ${e.name}`).toBeGreaterThan(0); } }); @@ -784,9 +1078,16 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { for (const c of CLEAN_CONTROLS) { const rel = 'packages/db/src/clean-sample.ts'; const synthetic: FileFacts = { rel, ...lexSource(c.src) }; + const synthFiles = [...files, synthetic]; const synthSet = new Set([...fileSet, rel]); - const synthConduits = computeSchemaConduits([...files, synthetic], synthSet); - const v = analyzeFile(synthetic, synthConduits, synthSet); + const synthCap = computeCapabilityConduits(synthFiles, synthSet); + const synthCtx: AnalysisCtx = { + fileSet: synthSet, + conduits: computeSchemaConduits(synthFiles, synthSet), + driverConduits: synthCap.driver, + factoryConduits: synthCap.factory, + }; + const v = analyzeFile(synthetic, synthCtx); expect( v, `false positive on ${c.name}: ${v.map((x) => `${x.prong}:${x.detail}`).join('; ')}`, -- 2.54.0 From f44db58b0dbe15f553f02a580fa0dc412349b22c Mon Sep 17 00:00:00 2001 From: fred Date: Thu, 27 Aug 2026 17:04:40 -0500 Subject: [PATCH 04/10] fix(db): propagate symbol identity through conduits in writer-coverage assertion Round-3 review remediation (G1-G6): - export-map schema conduits: renames propagate through export-from, export-of-local, and export-const-from-namespace routes (G1) - fail-closed namespace destructure over-approximation + depth-2 alias derivation (const t = ns.companies, const { schema } = ns) (G1) - factory capability: consumer-side name gate dropped for conduit imports; export-of-local factory bindings make a module a factory conduit (G2) - createRequire tracked through import aliases and dynamic destructure (G3) - literal dynamic import of a schema/factory/driver source outside the tracked const-await binding shape is a violation (.then/deferred/ Promise.all) (G4) - prong (ii): quoted schema qualifier and SQL line-comment gaps (G5) - computed eval, spaced execution verbs, bracket-form and parenthesized write arguments, spaced sql.raw (G6) - value-flow residual documented in KNOWN RESIDUALS with counterfactuals - 16 new permanent controls (E24-E39) --- .../db/src/hierarchy-writer-coverage.test.ts | 515 ++++++++++++++---- 1 file changed, 418 insertions(+), 97 deletions(-) diff --git a/packages/db/src/hierarchy-writer-coverage.test.ts b/packages/db/src/hierarchy-writer-coverage.test.ts index ec1271df..747e62f4 100644 --- a/packages/db/src/hierarchy-writer-coverage.test.ts +++ b/packages/db/src/hierarchy-writer-coverage.test.ts @@ -9,9 +9,17 @@ * (i) Symbol prong — write references (insert/update/delete) to the * class-table schema symbols occur only in allowlisted modules. * Schema symbols are tracked through named imports (aliased or not), - * namespace imports, and re-export conduits: any scanned module that - * re-exports the schema (or another conduit) is itself treated as a - * schema source, computed to a fixpoint. + * namespace imports, and re-export conduits. Conduits carry an + * EXPORT MAP (per-module named exports and namespace exports), + * computed to a fixpoint, so a rename at the export site + * (`export { companies as c } from`), an export of a locally bound + * alias (`import { companies }; export { companies as co }`), and a + * binding derived from a namespace (`export const co = ns.companies`) + * all propagate symbol identity to the consumer. Destructuring a + * NON-class property from a schema namespace over-approximates: the + * binding is treated as a schema namespace itself (fail-closed), and + * single-alias derivation from a namespace (`const t = ns.companies`) + * is folded in to a fixed depth. * (ii) Literal prong — a class-table name inside a string or template * span that also carries SQL context fails outside the allowlist. * Spans are produced by a real lexer, so comments cannot hide code @@ -28,14 +36,23 @@ * receiver backstop still fires on db/client-shaped receivers. * `sql.raw` is tracked through import aliasing and namespaces. * - * Runtime code-construction primitives (eval, new Function) fail anywhere — - * allowlist and register included: constructed code defeats every static - * prong, so there is no enumerated disposition path for it. Unanalyzable - * import routes (a dynamic import whose specifier is not a single string - * literal, and createRequire) fail everywhere except their own enumerated - * disposition registers below. A LITERAL dynamic import is an import edge - * like any static import (contract rev 9): its bindings (destructured or - * namespace) feed prong (i) exactly as static import bindings do. + * Runtime code-construction primitives (eval, new Function — spelled + * directly or as a literal computed member like `globalThis['eval']`) fail + * anywhere — allowlist and register included: constructed code defeats every + * static prong, so there is no enumerated disposition path for it. + * Unanalyzable import routes fail everywhere except their own enumerated + * disposition registers below: a dynamic import whose specifier is not a + * single string literal, and createRequire (tracked through import aliasing + * — `createRequire as x` — and destructuring from the module namespace; a + * route that hides the NAME entirely, e.g. re-exporting createRequire from a + * helper, falls under the value-flow residual). A LITERAL dynamic import is + * an import edge like any static import (contract rev 9): its bindings + * (destructured or namespace) feed prong (i) exactly as static import + * bindings do — and to keep that claim sound, a literal dynamic import of a + * schema/factory/driver source OUTSIDE the tracked + * `const X = await import('…')` binding shape (`.then` chains, deferred + * awaits, Promise.all) is itself a violation: bindings the analyzer cannot + * track are not allowed to exist for capability-bearing modules. * * KNOWN RESIDUALS (deliberate, reviewed trade-offs — not claims of closure): * - In a file with NO detected raw capability (no driver/factory import on @@ -50,6 +67,17 @@ * statically resolvable; literal computed access (obj['insert']()) is * flagged. Constructing the verb at runtime is adjacent to eval and is * expected to be caught in review. + * - General VALUE FLOW is not modeled. The analyzer tracks import edges, + * re-export chains, and single-step alias/destructure derivations from a + * namespace binding (to depth 2) — not arbitrary assignment chains, + * function returns, or method extraction. Demonstrated escapes in this + * class: `const u = this.client.unsafe; u.call(this.client, s)` (the + * verb never appears as a member call), an alias chain three or more + * steps deep, and a helper function that returns a schema symbol. + * Closing it requires data-flow analysis (a type-checker-backed rewrite, + * tracked for M4-1b consideration); the counterfactual — flagging every + * bare identifier call — false-positives on essentially all callback + * code. Reviews of modules touching db handles carry this residual. * - The scan perimeter is //src for the three roots; production * TS outside a src/ directory (e.g. packages/mosaic/framework/**) is not * scanned (verified free of db/driver/execute references at review time). @@ -415,46 +443,109 @@ interface Violation { } /** - * Compute the fixpoint set of "schema sources": module paths from which a - * class-table symbol can be imported. Seeds: the schema module and the db - * package barrel (plus the bare '@mosaicstack/db' specifier, handled - * separately). Any scanned module that re-exports from a schema source - * (star, or a named list carrying a class symbol) — or imports class symbols - * and re-exports those local names — joins the set. + * Schema exports of one module in the conduit graph: `named` are exported + * identifiers bound to a class-table symbol (under WHATEVER exported name — + * renames propagate); `ns` are exported identifiers that are themselves + * namespaces over a schema source (`export * as x from …`). */ -function computeSchemaConduits(files: FileFacts[], fileSet: Set): Set { - const conduits = new Set(['packages/db/src/schema.ts', 'packages/db/src/index.ts']); - const isSchemaSpec = (rel: string, spec: string): boolean => { - if (spec === '@mosaicstack/db') return true; - const resolved = resolveSpecifier(rel, spec, fileSet); - return resolved !== null && conduits.has(resolved); - }; +interface SchemaExports { + named: Set; + ns: Set; +} +type SchemaConduits = Map; + +/** Exported class-symbol names reachable through `spec` from `rel` (null = not a schema source). */ +function schemaExportsOf( + rel: string, + spec: string, + conduits: SchemaConduits, + fileSet: Set, +): SchemaExports | null { + if (spec === '@mosaicstack/db') return { named: new Set(CLASS_SYMBOLS), ns: new Set() }; + const r = resolveSpecifier(rel, spec, fileSet); + return r !== null ? (conduits.get(r) ?? null) : null; +} + +/** + * Compute the fixpoint map of "schema sources": module → the exported names + * under which a class-table symbol (or a schema namespace) is reachable from + * it. Seeds: the schema module and the db package barrel (the bare + * '@mosaicstack/db' specifier is handled in schemaExportsOf). Renames are + * propagated: `export { companies as c } from …` exports `c`, an + * `export { x as y }` of a local class binding exports `y`, and + * `export const y = ns.companies` over a schema namespace exports `y`. + */ +function computeSchemaConduits(files: FileFacts[], fileSet: Set): SchemaConduits { + const conduits: SchemaConduits = new Map([ + ['packages/db/src/schema.ts', { named: new Set(CLASS_SYMBOLS), ns: new Set() }], + ['packages/db/src/index.ts', { named: new Set(CLASS_SYMBOLS), ns: new Set() }], + ]); let changed = true; while (changed) { changed = false; for (const f of files) { - if (conduits.has(f.rel)) continue; - let isConduit = false; + const mine: SchemaExports = conduits.get(f.rel) ?? { + named: new Set(), + ns: new Set(), + }; + const before = mine.named.size + mine.ns.size; for (const m of f.code.matchAll(EXPORT_FROM_RE)) { if (m[1]) continue; // export type — erased - if (!isSchemaSpec(f.rel, m[3]!)) continue; - if (m[2] === undefined) { - isConduit = true; // export * from schema source - } else if (CLASS_SYMBOLS.some((s) => new RegExp(`\\b${s}\\b`).test(m[2]!))) { - isConduit = true; + const src = schemaExportsOf(f.rel, m[3]!, conduits, fileSet); + if (src === null || (src.named.size === 0 && src.ns.size === 0)) continue; + const starAs = /export\s*\*\s*as\s+(\w+)/.exec(m[0]); + if (starAs) { + mine.ns.add(starAs[1]!); // export * as x from schema source + } else if (m[2] === undefined) { + for (const n of src.named) mine.named.add(n); // export * from … + for (const n of src.ns) mine.ns.add(n); + } else { + for (const part of m[2].split(',')) { + const seg = part.trim(); + if (!seg || seg.startsWith('type ')) continue; + const asMatch = /^(\w+)\s+as\s+(\w+)$/.exec(seg); + const original = asMatch ? asMatch[1]! : seg; + const exported = asMatch ? asMatch[2]! : seg; + if (src.named.has(original)) mine.named.add(exported); + if (src.ns.has(original)) mine.ns.add(exported); + } } } - if (!isConduit) { - const aliases = classAliases(f, conduits, fileSet); - if (aliases.named.length > 0) { - const exported = [...f.code.matchAll(/export\s*\{([^}]*)\}(?!\s*from)/g)] - .map((m) => m[1]!) - .join(','); - if (aliases.named.some((a) => new RegExp(`\\b${a}\\b`).test(exported))) isConduit = true; + // Exports of local bindings: `export { x as y }` where x is a local + // class alias (or namespace), and `export const y = ns.`. + const aliases = classAliases(f, conduits, fileSet); + if (aliases.named.length > 0 || aliases.namespaces.length > 0) { + for (const m of f.code.matchAll(/export\s*\{([^}]*)\}(?!\s*from)/g)) { + for (const part of m[1]!.split(',')) { + const seg = part.trim(); + if (!seg || seg.startsWith('type ')) continue; + const asMatch = /^(\w+)\s+as\s+(\w+)$/.exec(seg); + const local = asMatch ? asMatch[1]! : seg; + const exported = asMatch ? asMatch[2]! : seg; + if (aliases.named.includes(local)) mine.named.add(exported); + if (aliases.namespaces.includes(local)) mine.ns.add(exported); + } + } + for (const ns of aliases.namespaces) { + for (const m of f.code.matchAll( + new RegExp( + `export\\s+(?:const|let|var)\\s+(\\w+)\\s*=\\s*${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(?:${CLASS_SYMBOLS.join('|')})\\b`, + 'g', + ), + )) { + mine.named.add(m[1]!); + } + } + for (const local of aliases.named) { + for (const m of f.code.matchAll( + new RegExp(`export\\s+(?:const|let|var)\\s+(\\w+)\\s*=\\s*${local}\\b`, 'g'), + )) { + mine.named.add(m[1]!); + } } } - if (isConduit) { - conduits.add(f.rel); + if (mine.named.size + mine.ns.size > before) { + conduits.set(f.rel, mine); changed = true; } } @@ -467,48 +558,60 @@ interface ClassAliases { namespaces: string[]; // namespace identifiers over a schema source } -/** Parse an import named-binding list ("a, b as c") into locals bound to class symbols. */ -function importBindings(namedList: string, named: Set): void { +/** + * Parse an import named-binding list ("a, b as c") against the SOURCE's + * schema exports, adding locals bound to class symbols / schema namespaces. + */ +function importBindings( + namedList: string, + src: SchemaExports, + named: Set, + namespaces: Set, +): void { for (const part of namedList.split(',')) { const seg = part.trim(); if (!seg || seg.startsWith('type ')) continue; const asMatch = /^(\w+)\s+as\s+(\w+)$/.exec(seg); const original = asMatch ? asMatch[1]! : seg; const local = asMatch ? asMatch[2]! : seg; - if (CLASS_SYMBOLS.includes(original)) named.add(local); + if (src.named.has(original)) named.add(local); + if (src.ns.has(original)) namespaces.add(local); } } -/** Parse a destructuring pattern ("a, b: c") into locals bound to class symbols. */ -function destructureBindings(pattern: string, named: Set): void { +/** + * Parse a destructuring pattern ("a, b: c, d = x") over a schema source. + * A class-symbol property binds a named alias; ANY other destructured + * property is over-approximated as a schema namespace (it may be a nested + * namespace such as `const { schema } = ns` — fail-closed). + */ +function destructureBindings( + pattern: string, + src: SchemaExports, + named: Set, + namespaces: Set, +): void { for (const part of pattern.split(',')) { const seg = part.trim(); if (!seg) continue; - const m = /^(\w+)\s*(?::\s*(\w+))?$/.exec(seg); - if (m && CLASS_SYMBOLS.includes(m[1]!)) named.add(m[2] ?? m[1]!); + const m = /^(\w+)\s*(?::\s*(\w+))?\s*(?:=[\s\S]*)?$/.exec(seg); + if (!m) continue; + const local = m[2] ?? m[1]!; + if (src.named.has(m[1]!)) named.add(local); + else namespaces.add(local); } } -function isSchemaSpecifier( - rel: string, - spec: string, - conduits: Set, - fileSet: Set, -): boolean { - if (spec === '@mosaicstack/db') return true; - const r = resolveSpecifier(rel, spec, fileSet); - return r !== null && conduits.has(r); -} - -function classAliases(f: FileFacts, conduits: Set, fileSet: Set): ClassAliases { +function classAliases(f: FileFacts, conduits: SchemaConduits, fileSet: Set): ClassAliases { const named = new Set(); const namespaces = new Set(); for (const m of f.code.matchAll(IMPORT_RE)) { const [, typeOnly, , namedList, nsName, , spec] = m; if (typeOnly) continue; - if (!isSchemaSpecifier(f.rel, spec!, conduits, fileSet)) continue; + const src = schemaExportsOf(f.rel, spec!, conduits, fileSet); + if (src === null || (src.named.size === 0 && src.ns.size === 0)) continue; if (nsName) namespaces.add(nsName); - if (namedList) importBindings(namedList, named); + if (namedList) importBindings(namedList, src, named, namespaces); } // Literal dynamic imports of a schema source are import edges like any // other (contract rev 9): both binding shapes feed prong (i). @@ -516,17 +619,33 @@ function classAliases(f: FileFacts, conduits: Set, fileSet: Set) /(?:const|let|var)\s*(?:\{([^}]*)\}|(\w+))\s*=\s*await\s+import\s*\(\s*(['"])([^'"]+)\3\s*\)/g, )) { const [, pattern, nsName, , spec] = m; - if (!isSchemaSpecifier(f.rel, spec!, conduits, fileSet)) continue; + const src = schemaExportsOf(f.rel, spec!, conduits, fileSet); + if (src === null || (src.named.size === 0 && src.ns.size === 0)) continue; if (nsName) namespaces.add(nsName); - if (pattern) destructureBindings(pattern, named); + if (pattern) destructureBindings(pattern, src, named, namespaces); } - // Destructuring from a schema namespace binds class symbols to locals: - // `import * as s from '@mosaicstack/db'; const { companies } = s;` - for (const ns of [...namespaces]) { - for (const m of f.code.matchAll( - new RegExp(`(?:const|let|var)\\s*\\{([^}]*)\\}\\s*=\\s*${ns}\\b`, 'g'), - )) { - destructureBindings(m[1]!, named); + // Aliases derived FROM a schema namespace, to a bounded depth (2 passes): + // const { companies } = ns; → named alias + // const { schema } = ns; → nested namespace (fail-closed) + // const t = ns.companies; → named alias + // const s2 = ns.schema; → namespace alias + const nsSrc: SchemaExports = { named: new Set(CLASS_SYMBOLS), ns: new Set() }; + for (let pass = 0; pass < 2; pass += 1) { + for (const ns of [...namespaces]) { + for (const m of f.code.matchAll( + new RegExp(`(?:const|let|var)\\s*\\{([^}]*)\\}\\s*=\\s*(?:await\\s+)?${ns}\\b`, 'g'), + )) { + destructureBindings(m[1]!, nsSrc, named, namespaces); + } + for (const m of f.code.matchAll( + new RegExp( + `(?:const|let|var)\\s+(\\w+)\\s*=\\s*${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(\\w+)\\b`, + 'g', + ), + )) { + if (CLASS_SYMBOLS.includes(m[2]!)) named.add(m[1]!); + else namespaces.add(m[1]!); + } } } return { named: [...named], namespaces: [...namespaces] }; @@ -540,8 +659,10 @@ function classAliases(f: FileFacts, conduits: Set, fileSet: Set) * capability (the conduit module itself is additionally flagged by prong * (iii)'s literal check, so these routes fail at both ends). * `factory` — re-exports createDb/createPgliteDb from the db package or - * another factory conduit; importing from one while referencing a factory - * symbol confers factory capability. + * another factory conduit (via `export … from`, or by exporting a local + * binding of the factory under any name); importing ANYTHING from one + * confers factory capability — the conduit may rename the symbol, so there + * is no consumer-side name gate. */ function computeCapabilityConduits( files: FileFacts[], @@ -580,6 +701,58 @@ function computeCapabilityConduits( } // `export { default as x } from 'postgres'` matches EXPORT_FROM_RE's // named branch above; `export x from` is not valid syntax — covered. + // Export-of-local factory bindings: a module that BINDS the factory + // (named import, namespace member, or tracked dynamic import) and + // exports that binding under any name is a factory conduit even with + // no `export … from` clause (`export const mk = mod.createDb`). + if (!factory.has(f.rel)) { + const locals = new Set(); + const nss = new Set(); + for (const im of f.code.matchAll(IMPORT_RE)) { + const [, typeOnly, , namedList, nsName, , spec] = im; + if (typeOnly || !isFactorySpec(f.rel, spec!)) continue; + if (nsName) nss.add(nsName); + if (namedList) { + for (const part of namedList.split(',')) { + const am = /^(createDb|createPgliteDb)(?:\s+as\s+(\w+))?$/.exec(part.trim()); + if (am) locals.add(am[2] ?? am[1]!); + } + } + } + for (const dm of f.code.matchAll( + /(?:const|let|var)\s*(?:\{([^}]*)\}|(\w+))\s*=\s*await\s+import\s*\(\s*(['"])([^'"]+)\3\s*\)/g, + )) { + const [, pattern, nsName, , spec] = dm; + if (!isFactorySpec(f.rel, spec!)) continue; + if (nsName) nss.add(nsName); + if (pattern) { + for (const part of pattern.split(',')) { + const pm = /^(createDb|createPgliteDb)\s*(?::\s*(\w+))?/.exec(part.trim()); + if (pm) locals.add(pm[2] ?? pm[1]!); + } + } + } + for (const ns of nss) { + for (const am of f.code.matchAll( + new RegExp( + `(?:const|let|var)\\s+(\\w+)\\s*=\\s*${ns}\\s*\\.\\s*(?:createDb|createPgliteDb)\\b`, + 'g', + ), + )) { + locals.add(am[1]!); + } + } + for (const local of locals) { + if ( + new RegExp(`export\\s+(?:const|let|var|function)\\s+${local}\\b`).test(f.code) || + new RegExp(`export\\s*\\{[^}]*\\b${local}\\b[^}]*\\}`).test(f.code) + ) { + factory.add(f.rel); + changed = true; + break; + } + } + } } } return { driver, factory }; @@ -609,7 +782,7 @@ function hasSymbolImportEdge(code: string, symbols: string[], packageName: strin // --------------------------------------------------------------------------- interface AnalysisCtx { fileSet: Set; - conduits: Set; // schema-symbol sources (prong i) + conduits: SchemaConduits; // schema-symbol sources with exported names (prong i) driverConduits: Set; // driver-capability re-exporters (prong iii) factoryConduits: Set; // factory-capability re-exporters (prong iii) } @@ -623,39 +796,90 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { const inRegister = INFRA_REGISTER.includes(rel); const isSchemaDefinition = rel === 'packages/db/src/schema.ts'; - // Runtime code construction: fails anywhere. - if (/\beval\s*\(|\bnew\s+Function\s*\(/.test(code)) { + // Runtime code construction: fails anywhere. Covers direct calls and + // literal computed access (window['eval'], globalThis['Function']). + if (/\beval\s*\(|\bnew\s+Function\s*\(|\[\s*['"](eval|Function)['"]\s*\]/.test(code)) { violations.push({ file: rel, prong: 'code-construction', detail: 'eval/new Function' }); } // Dynamic import whose specifier is not a single string literal: the // import graph becomes unanalyzable. Checked per call site, so a literal - // first fragment ('x' + y) does not slip past. + // first fragment ('x' + y) does not slip past. A LITERAL import of a + // schema/factory source must additionally sit in the tracked binding shape + // (const X = await import(…)) — any other consumption of its promise + // (.then, deferred await, array wrapping) hides the binding from prong (i), + // so it fails closed here. if (!DYNAMIC_IMPORT_REGISTER.includes(rel)) { for (const m of code.matchAll(/\bimport\s*\(/g)) { - const tail = code.slice((m.index ?? 0) + m[0].length); - if (!/^\s*(['"])(?:[^'"\\]|\\.)*?\1\s*[,)]/.test(tail)) { + const idx = m.index ?? 0; + const tail = code.slice(idx + m[0].length); + const lit = /^\s*(['"])((?:[^'"\\]|\\.)*?)\1\s*[,)]/.exec(tail); + if (!lit) { violations.push({ file: rel, prong: 'dynamic-import', detail: 'non-literal import()' }); + continue; + } + const spec = lit[2]!; + const schemaSrc = schemaExportsOf(rel, spec, conduits, fileSet); + const factoryLike = + spec === '@mosaicstack/db' || + (() => { + const r = resolveSpecifier(rel, spec, fileSet); + return r !== null && (factoryConduits.has(r) || driverConduits.has(r)); + })() || + DRIVER_SPECIFIERS.includes(spec); + if (schemaSrc !== null || factoryLike) { + const before = code.slice(Math.max(0, idx - 200), idx); + if (!/(?:const|let|var)\s*(?:\{[^}]*\}|\w+)\s*=\s*await\s*$/.test(before)) { + violations.push({ + file: rel, + prong: 'dynamic-import', + detail: `literal import('${spec}') outside the tracked binding shape`, + }); + } } } } - // createRequire: an unanalyzable CJS import route. - if (!CREATE_REQUIRE_REGISTER.includes(rel) && /\bcreateRequire\s*\(/.test(code)) { - violations.push({ file: rel, prong: 'dynamic-import', detail: 'createRequire()' }); + // createRequire: an unanalyzable CJS import route. Tracked by name AND + // through import aliases (import { createRequire as x } / destructured + // from a dynamic module import). + if (!CREATE_REQUIRE_REGISTER.includes(rel)) { + const crNames = new Set(); + if (/\bcreateRequire\s*\(/.test(code)) crNames.add('createRequire'); + for (const m of code.matchAll(/import\s*\{([^}]*)\}\s*from\s*['"](?:node:)?module['"]/g)) { + const alias = /\bcreateRequire\s+as\s+(\w+)/.exec(m[1]!); + if (alias) crNames.add(alias[1]!); + } + for (const m of code.matchAll( + /(?:const|let|var)\s*\{([^}]*)\}\s*=\s*await\s+import\s*\(\s*['"](?:node:)?module['"]\s*\)/g, + )) { + const alias = /\bcreateRequire\s*:\s*(\w+)/.exec(m[1]!); + if (alias) crNames.add(alias[1]!); + } + for (const n of crNames) { + if (n === 'createRequire' || new RegExp(`\\b${n}\\s*\\(`).test(code)) { + violations.push({ file: rel, prong: 'dynamic-import', detail: 'createRequire()' }); + break; + } + } } const aliases = classAliases(f, conduits, fileSet); // Prong (i): schema-symbol writes — alias-, namespace-, and conduit-aware. if (!inAllowlist) { - const targets: string[] = [...aliases.named]; + // Word boundaries live inside each alternative: the bracket form ends in + // `]` (non-word), where a trailing `\b` could never match. + const targets: string[] = [...aliases.named].map((a) => `${a}\\b`); + const symAlt = CLASS_SYMBOLS.join('|'); for (const ns of aliases.namespaces) { - // Allow intermediate property segments: ns.schema.companies (nested - // namespace re-exports) as well as ns.companies. - for (const s of CLASS_SYMBOLS) targets.push(`${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*${s}`); + // Allow intermediate property segments (ns.schema.companies — nested + // namespace re-exports) and literal computed access (ns['companies']). + targets.push(`${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(?:${symAlt})\\b`); + targets.push(`${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\[\\s*['"\`](?:${symAlt})['"\`]\\s*\\]`); } if (targets.length > 0) { + // `\(\s*\(*` tolerates argument parenthesization: .insert((companies)). const writeRe = new RegExp( - `\\.\\s*(insert|update|delete)\\s*\\(\\s*(${targets.join('|')})\\b`, + `\\.\\s*(insert|update|delete)\\s*\\(\\s*\\(*\\s*(${targets.join('|')})`, 'g', ); for (const m of code.matchAll(writeRe)) { @@ -701,10 +925,10 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { if (!inAllowlist && !isSchemaDefinition) { const kw = '(?:insert\\s+into|update|delete\\s+from|from|join|truncate(?:\\s+table)?|alter\\s+table|drop\\s+table|references|into|copy)'; - const gap = '(?:\\s|/\\*[\\s\\S]*?\\*/)+'; + const gap = '(?:\\s|/\\*[\\s\\S]*?\\*/|--[^\\n]*\\n)+'; const q = `(?:\\\\?["'\`])?`; const sqlAdjacentRe = new RegExp( - `\\b${kw}${gap}${q}(?:\\w+\\s*\\.\\s*)?${q}(${CLASS_TABLES.join('|')})\\b`, + `\\b${kw}${gap}${q}(?:${q}\\w+${q}\\s*\\.\\s*)?${q}(${CLASS_TABLES.join('|')})\\b`, 'i', ); for (const span of f.spans) { @@ -733,20 +957,22 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { if (r !== null && factoryConduits.has(r)) factoryConduitImport = true; } const driverImport = literalDriver || conduitDriver; - const usesFactorySymbol = /\b(createDb|createPgliteDb)\b/.test(code); + // Factory capability: a named/namespace edge to the factory symbols, OR + // ANY value import from a factory conduit — the conduit may re-export + // the factory under a different name (export { createDb as default }), + // so the consumer-side name gate is dropped for conduit imports. const factoryImport = + factoryConduitImport || hasSymbolImportEdge(code, DB_FACTORY_SYMBOLS, '@mosaicstack/db') || - (factoryConduitImport && usesFactorySymbol) || (aliases.namespaces.length > 0 && - usesFactorySymbol && new RegExp( `\\b(${aliases.namespaces.join('|')})(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(createDb|createPgliteDb)\\b`, ).test(code)); if (driverImport) { violations.push({ file: rel, prong: 'iii-raw-execution', detail: 'direct driver import' }); // A driver client exposes query/execute/unsafe as raw primitives: - // flag them all on any receiver, any argument. - for (const m of code.matchAll(/\.(execute|query|unsafe)\s*\(/g)) { + // flag them all on any receiver, any argument, spaced or not. + for (const m of code.matchAll(/\.\s*(execute|query|unsafe)\s*\(/g)) { violations.push({ file: rel, prong: 'iii-raw-execution', @@ -758,7 +984,7 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { // the relational builder, and unrelated .query() methods are common), // so factory capability flags execute/unsafe on any receiver and query // only on the db/client-shaped receiver backstop below. - for (const m of code.matchAll(/\.(execute|unsafe)\s*\(/g)) { + for (const m of code.matchAll(/\.\s*(execute|unsafe)\s*\(/g)) { violations.push({ file: rel, prong: 'iii-raw-execution', @@ -770,7 +996,7 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { // DI residual backstop: db/client-shaped receivers fire regardless of // detected capability (a handle can arrive by injection). for (const m of code.matchAll( - /\b(?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)\.(execute|query|unsafe)\s*\(/g, + /\b(?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)\s*\.\s*(execute|query|unsafe)\s*\(/g, )) { violations.push({ file: rel, @@ -793,10 +1019,10 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { else if (asMatch) sqlAliases.add(asMatch[1]!); } } - if (nsName) sqlAliases.add(`${nsName}\\.sql`); + if (nsName) sqlAliases.add(`${nsName}\\s*\\.\\s*sql`); } if (sqlAliases.size > 0) { - const rawRe = new RegExp(`\\b(${[...sqlAliases].join('|')})\\.raw\\s*\\(`); + const rawRe = new RegExp(`\\b(${[...sqlAliases].join('|')})\\s*\\.\\s*raw\\s*\\(`); if (rawRe.test(code)) { violations.push({ file: rel, prong: 'iii-raw-execution', detail: 'sql.raw()' }); } @@ -1035,6 +1261,101 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { name: 'E23 escaped-backtick-quoted table in SQL span', src: 'export const q = `DELETE FROM \\`hierarchy_grants\\` WHERE role = 1`;', }, + // --- round-3 review shapes (G1–G6) --- + { + name: 'E24 export-site rename through conduit', + src: `import { c } from './evasion-mid4.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid4.ts', + src: `export { companies as c } from '@mosaicstack/db';`, + }, + ], + }, + { + name: 'E25 export of locally bound alias, renamed', + src: `import { co } from './evasion-mid5.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(co).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid5.ts', + src: `import { companies } from '@mosaicstack/db';\nexport { companies as co };`, + }, + ], + }, + { + name: 'E26 alias-export helper over dynamic namespace', + src: `import { co } from './evasion-mid6.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(co).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid6.ts', + src: `const mod = await import('@mosaicstack/db');\nexport const co = mod.companies;`, + }, + ], + }, + { + name: 'E27 single-const alias from namespace', + src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nconst t = ns.companies;\nexport async function f() { await db.insert(t).values({}); }`, + }, + { + name: 'E28 non-class destructure from namespace (fail-closed)', + src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nconst { schema } = ns;\nexport async function f() { await db.insert(schema.companies).values({}); }`, + }, + { + name: 'E29 destructure with default value', + src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nconst { companies: co = null } = ns;\nexport async function f() { await db.insert(co).values({}); }`, + }, + { + name: 'E30 factory renamed to default through conduit', + src: `import createDbNow from './evasion-mid7.js';\nexport async function f(t: string) { await createDbNow('u').execute('DELETE FROM ' + t); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid7.ts', + src: `export { createDb as default } from '@mosaicstack/db';`, + }, + ], + }, + { + name: 'E31 factory alias-export helper over dynamic namespace', + src: `import { mk } from './evasion-mid8.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid8.ts', + src: `const mod = await import('@mosaicstack/db');\nexport const mk = mod.createDb;`, + }, + ], + }, + { + name: 'E32 createRequire import alias', + src: `import { createRequire as mkReq } from 'node:module';\nconst req = mkReq(import.meta.url);\nexport const pg = req('postgres');`, + }, + { + name: 'E33 then-form dynamic schema import', + src: `import { db } from './x.js';\nexport function f() { return import('@mosaicstack/db').then((m) => db.insert(m.companies).values({})); }`, + }, + { + name: 'E34 quoted schema qualifier in SQL span', + src: `export const q = 'DELETE FROM "public".hierarchy_grants WHERE role = $1';`, + }, + { + name: 'E35 SQL line comment interposed in SQL span', + src: 'export const q = `DELETE FROM -- audit\nhierarchy_grants`;', + }, + { + name: 'E36 spaced member access on conventional receiver', + src: `export class R { constructor(private db: { query(s: string): Promise }) {}\n async f(t: string) { await this.db . query ('TRUNCATE ' + t); } }`, + }, + { + name: 'E37 bracket-form schema argument', + src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(ns['companies']).values({}); }`, + }, + { + name: 'E38 parenthesized schema argument', + src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert((companies)).values({}); }`, + }, + { + name: 'E39 computed-member eval', + src: `export function f(s: string) { return (globalThis as never)['eval'](s); }`, + }, ]; const CLEAN_CONTROLS: Array<{ name: string; src: string }> = [ { -- 2.54.0 From 3dcfb264c5cc7e80492570c4eb620abb08374e51 Mon Sep 17 00:00:00 2001 From: fred Date: Thu, 27 Aug 2026 17:28:57 -0500 Subject: [PATCH 05/10] fix(db): close round-4 review evasions in writer-coverage assertion Default-export slot treated as an export name at both ends (helper-side export-default passes, consumer-side bare-default-import resolution); namespace-member write targets match the source's exported names via a per-file memberSyms union; factory conduits carry a name map computed to a fixpoint so renames propagate hop by hop (export-from, export-of-local, export default); tracked dynamic imports of capability conduits confer capability; factory destructure-rename from a schema namespace; static module-namespace createRequire destructure; spread-argument writes; DCL keywords (GRANT/REVOKE ON, LOCK TABLE); bracketed conventional DI receiver. Permanent controls E40-E53. --- .../db/src/hierarchy-writer-coverage.test.ts | 380 ++++++++++++++---- 1 file changed, 298 insertions(+), 82 deletions(-) diff --git a/packages/db/src/hierarchy-writer-coverage.test.ts b/packages/db/src/hierarchy-writer-coverage.test.ts index 747e62f4..fc5265fd 100644 --- a/packages/db/src/hierarchy-writer-coverage.test.ts +++ b/packages/db/src/hierarchy-writer-coverage.test.ts @@ -15,11 +15,17 @@ * (`export { companies as c } from`), an export of a locally bound * alias (`import { companies }; export { companies as co }`), and a * binding derived from a namespace (`export const co = ns.companies`) - * all propagate symbol identity to the consumer. Destructuring a - * NON-class property from a schema namespace over-approximates: the - * binding is treated as a schema namespace itself (fail-closed), and - * single-alias derivation from a namespace (`const t = ns.companies`) - * is folded in to a fixed depth. + * all propagate symbol identity to the consumer. The DEFAULT-export + * slot is an export name like any other (`export { companies as + * default } from`, `export default companies`), so a bare default + * import of a conduit resolves against the map too; namespace-member + * write targets match the source's EXPORTED names — renames included + * (`M.co` after `export { companies as co } from`) — not just the + * original class symbols. Destructuring a NON-class property from a + * schema namespace over-approximates: the binding is treated as a + * schema namespace itself (fail-closed), and single-alias derivation + * from a namespace (`const t = ns.companies`) is folded in to a + * fixed depth. * (ii) Literal prong — a class-table name inside a string or template * span that also carries SQL context fails outside the allowlist. * Spans are produced by a real lexer, so comments cannot hide code @@ -27,7 +33,13 @@ * migrations are excluded from this prong only (per contract). * (iii) Raw-execution prong — content-independent. A file is RAW-CAPABLE * when it imports a database driver or the createDb/createPgliteDb - * factories (directly or via namespace). In a raw-capable file + * factories — directly, via namespace, by destructuring a factory + * symbol out of a schema namespace, or from a capability CONDUIT. + * Conduits carry their own exported-name maps computed to a + * fixpoint, so a factory renamed at any hop (`export { createDb as + * mk } from`, `export default createDb`) still marks every module + * down the chain, and a tracked dynamic import of a conduit is an + * import edge like a static one. In a raw-capable file * outside the allowlist and register, EVERY `.execute(`, `.query(`, * and `.unsafe(` call fails, on any receiver, with any argument — * there is no tagged-template exemption (§6.3(b): "regardless of what @@ -43,7 +55,9 @@ * Unanalyzable import routes fail everywhere except their own enumerated * disposition registers below: a dynamic import whose specifier is not a * single string literal, and createRequire (tracked through import aliasing - * — `createRequire as x` — and destructuring from the module namespace; a + * — `createRequire as x` — and destructuring from the module namespace, + * whether that namespace came from a static `import * as` or a tracked + * dynamic import; a * route that hides the NAME entirely, e.g. re-exporting createRequire from a * helper, falls under the value-flow residual). A LITERAL dynamic import is * an import edge like any static import (contract rev 9): its bindings @@ -543,6 +557,21 @@ function computeSchemaConduits(files: FileFacts[], fileSet: Set): Schema mine.named.add(m[1]!); } } + // The default slot is an export name like any other: + // `export default companies;` / `export default mod.companies;` + for (const m of f.code.matchAll(/export\s+default\s+(\w+)\s*;/g)) { + if (aliases.named.includes(m[1]!)) mine.named.add('default'); + if (aliases.namespaces.includes(m[1]!)) mine.ns.add('default'); + } + for (const ns of aliases.namespaces) { + if ( + new RegExp( + `export\\s+default\\s+${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(?:${CLASS_SYMBOLS.join('|')})\\b`, + ).test(f.code) + ) { + mine.named.add('default'); + } + } } if (mine.named.size + mine.ns.size > before) { conduits.set(f.rel, mine); @@ -556,6 +585,10 @@ function computeSchemaConduits(files: FileFacts[], fileSet: Set): Schema interface ClassAliases { named: string[]; // local identifiers bound to class-table symbols namespaces: string[]; // namespace identifiers over a schema source + // Member names under which a class symbol is reachable on SOME imported + // schema source (class symbols plus every renamed conduit export the file + // imports) — the alternation for namespace-member write targets. + memberSyms: Set; } /** @@ -605,13 +638,21 @@ function destructureBindings( function classAliases(f: FileFacts, conduits: SchemaConduits, fileSet: Set): ClassAliases { const named = new Set(); const namespaces = new Set(); + const memberSyms = new Set(CLASS_SYMBOLS); for (const m of f.code.matchAll(IMPORT_RE)) { - const [, typeOnly, , namedList, nsName, , spec] = m; + const [, typeOnly, defaultWith, namedList, nsName, defaultBare, spec] = m; if (typeOnly) continue; const src = schemaExportsOf(f.rel, spec!, conduits, fileSet); if (src === null || (src.named.size === 0 && src.ns.size === 0)) continue; + for (const n of src.named) memberSyms.add(n); if (nsName) namespaces.add(nsName); if (namedList) importBindings(namedList, src, named, namespaces); + // A default import binds whatever the source exports as `default`. + const dflt = defaultWith ?? defaultBare; + if (dflt) { + if (src.named.has('default')) named.add(dflt); + if (src.ns.has('default')) namespaces.add(dflt); + } } // Literal dynamic imports of a schema source are import edges like any // other (contract rev 9): both binding shapes feed prong (i). @@ -621,6 +662,7 @@ function classAliases(f: FileFacts, conduits: SchemaConduits, fileSet: Set, ): { driver: Set; factory: Set } { const driver = new Set(); - const factory = new Set(); + // Factory conduits carry a NAME MAP (module → exported names under which a + // factory is reachable) so renames propagate hop by hop, exactly like the + // schema export map — a literal-name gate at any hop would launder. + const factoryNames = new Map>(); const isDriverSpec = (rel: string, spec: string): boolean => { if (DRIVER_SPECIFIERS.includes(spec)) return true; const r = resolveSpecifier(rel, spec, fileSet); return r !== null && driver.has(r); }; - const isFactorySpec = (rel: string, spec: string): boolean => { - if (spec === '@mosaicstack/db') return true; + const factoryNamesOf = (rel: string, spec: string): Set | null => { + if (spec === '@mosaicstack/db') return new Set(DB_FACTORY_SYMBOLS); const r = resolveSpecifier(rel, spec, fileSet); - return r !== null && factory.has(r); + return r !== null ? (factoryNames.get(r) ?? null) : null; }; let changed = true; while (changed) { @@ -691,71 +740,93 @@ function computeCapabilityConduits( driver.add(f.rel); changed = true; } - if (!factory.has(f.rel) && isFactorySpec(f.rel, spec)) { - const named = m[2]; - if (named === undefined || /\b(createDb|createPgliteDb)\b/.test(named)) { - factory.add(f.rel); - changed = true; - } - } } // `export { default as x } from 'postgres'` matches EXPORT_FROM_RE's // named branch above; `export x from` is not valid syntax — covered. - // Export-of-local factory bindings: a module that BINDS the factory - // (named import, namespace member, or tracked dynamic import) and - // exports that binding under any name is a factory conduit even with - // no `export … from` clause (`export const mk = mod.createDb`). - if (!factory.has(f.rel)) { - const locals = new Set(); - const nss = new Set(); - for (const im of f.code.matchAll(IMPORT_RE)) { - const [, typeOnly, , namedList, nsName, , spec] = im; - if (typeOnly || !isFactorySpec(f.rel, spec!)) continue; - if (nsName) nss.add(nsName); - if (namedList) { - for (const part of namedList.split(',')) { - const am = /^(createDb|createPgliteDb)(?:\s+as\s+(\w+))?$/.exec(part.trim()); - if (am) locals.add(am[2] ?? am[1]!); - } - } - } - for (const dm of f.code.matchAll( - /(?:const|let|var)\s*(?:\{([^}]*)\}|(\w+))\s*=\s*await\s+import\s*\(\s*(['"])([^'"]+)\3\s*\)/g, - )) { - const [, pattern, nsName, , spec] = dm; - if (!isFactorySpec(f.rel, spec!)) continue; - if (nsName) nss.add(nsName); - if (pattern) { - for (const part of pattern.split(',')) { - const pm = /^(createDb|createPgliteDb)\s*(?::\s*(\w+))?/.exec(part.trim()); - if (pm) locals.add(pm[2] ?? pm[1]!); - } - } - } - for (const ns of nss) { - for (const am of f.code.matchAll( - new RegExp( - `(?:const|let|var)\\s+(\\w+)\\s*=\\s*${ns}\\s*\\.\\s*(?:createDb|createPgliteDb)\\b`, - 'g', - ), - )) { - locals.add(am[1]!); - } - } - for (const local of locals) { - if ( - new RegExp(`export\\s+(?:const|let|var|function)\\s+${local}\\b`).test(f.code) || - new RegExp(`export\\s*\\{[^}]*\\b${local}\\b[^}]*\\}`).test(f.code) - ) { - factory.add(f.rel); - changed = true; - break; + const mine = factoryNames.get(f.rel) ?? new Set(); + const before = mine.size; + for (const m of f.code.matchAll(EXPORT_FROM_RE)) { + if (m[1]) continue; + const src = factoryNamesOf(f.rel, m[3]!); + if (src === null || src.size === 0) continue; + if (m[2] === undefined) { + // `export *` / `export * as x` — over-approximated to the source's + // names (membership is what confers capability on consumers). + for (const n of src) mine.add(n); + } else { + for (const part of m[2].split(',')) { + const seg = part.trim(); + if (!seg || seg.startsWith('type ')) continue; + const am = /^(\w+)\s+as\s+(\w+)$/.exec(seg); + if (src.has(am ? am[1]! : seg)) mine.add(am ? am[2]! : seg); } } } + // Locals bound to a factory (named import — aliased or not — namespace + // member extraction, or tracked dynamic-import destructure), then + // exported under ANY name and by ANY form, braces or default included + // (`export const mk = mod.createDb`, `export default createDb`). + const locals = new Set(); + const nss = new Map>(); + for (const im of f.code.matchAll(IMPORT_RE)) { + const [, typeOnly, , namedList, nsName, , spec] = im; + if (typeOnly) continue; + const src = factoryNamesOf(f.rel, spec!); + if (src === null || src.size === 0) continue; + if (nsName) nss.set(nsName, src); + if (namedList) { + for (const part of namedList.split(',')) { + const am = /^(\w+)(?:\s+as\s+(\w+))?$/.exec(part.trim()); + if (am && src.has(am[1]!)) locals.add(am[2] ?? am[1]!); + } + } + } + for (const dm of f.code.matchAll( + /(?:const|let|var)\s*(?:\{([^}]*)\}|(\w+))\s*=\s*await\s+import\s*\(\s*(['"])([^'"]+)\3\s*\)/g, + )) { + const [, pattern, nsName, , spec] = dm; + const src = factoryNamesOf(f.rel, spec!); + if (src === null || src.size === 0) continue; + if (nsName) nss.set(nsName, src); + if (pattern) { + for (const part of pattern.split(',')) { + const pm = /^(\w+)\s*(?::\s*(\w+))?/.exec(part.trim()); + if (pm && src.has(pm[1]!)) locals.add(pm[2] ?? pm[1]!); + } + } + } + for (const [ns, src] of nss) { + for (const am of f.code.matchAll( + new RegExp(`(?:const|let|var)\\s+(\\w+)\\s*=\\s*${ns}\\s*\\.\\s*(\\w+)\\b`, 'g'), + )) { + if (src.has(am[2]!)) locals.add(am[1]!); + } + } + if (locals.size > 0) { + for (const m of f.code.matchAll(/export\s*\{([^}]*)\}(?!\s*from)/g)) { + for (const part of m[1]!.split(',')) { + const seg = part.trim(); + if (!seg || seg.startsWith('type ')) continue; + const am = /^(\w+)(?:\s+as\s+(\w+))?$/.exec(seg); + if (am && locals.has(am[1]!)) mine.add(am[2] ?? am[1]!); + } + } + for (const local of locals) { + if (new RegExp(`export\\s+(?:const|let|var|function)\\s+${local}\\b`).test(f.code)) { + mine.add(local); + } + if (new RegExp(`export\\s+default\\s+${local}\\b`).test(f.code)) { + mine.add('default'); + } + } + } + if (mine.size > before) { + factoryNames.set(f.rel, mine); + changed = true; + } } } - return { driver, factory }; + return { driver, factory: new Set(factoryNames.keys()) }; } /** Value-import edge naming one of `symbols` (named import from anywhere, or literal dynamic package import while using the symbol). */ @@ -854,6 +925,25 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { const alias = /\bcreateRequire\s*:\s*(\w+)/.exec(m[1]!); if (alias) crNames.add(alias[1]!); } + // Destructuring from a STATIC or dynamically-bound module namespace: + // import * as M from 'node:module'; const { createRequire: x } = M; + const modNs = new Set(); + for (const m of code.matchAll(/import\s*\*\s*as\s+(\w+)\s*from\s*['"](?:node:)?module['"]/g)) { + modNs.add(m[1]!); + } + for (const m of code.matchAll( + /(?:const|let|var)\s+(\w+)\s*=\s*await\s+import\s*\(\s*['"](?:node:)?module['"]\s*\)/g, + )) { + modNs.add(m[1]!); + } + for (const ns of modNs) { + for (const m of code.matchAll( + new RegExp(`(?:const|let|var)\\s*\\{([^}]*)\\}\\s*=\\s*${ns}\\b`, 'g'), + )) { + const alias = /\bcreateRequire\s*(?::\s*(\w+))?/.exec(m[1]!); + if (alias) crNames.add(alias[1] ?? 'createRequire'); + } + } for (const n of crNames) { if (n === 'createRequire' || new RegExp(`\\b${n}\\s*\\(`).test(code)) { violations.push({ file: rel, prong: 'dynamic-import', detail: 'createRequire()' }); @@ -869,7 +959,9 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { // Word boundaries live inside each alternative: the bracket form ends in // `]` (non-word), where a trailing `\b` could never match. const targets: string[] = [...aliases.named].map((a) => `${a}\\b`); - const symAlt = CLASS_SYMBOLS.join('|'); + // Namespace members match the source's EXPORTED names (renames included), + // not just the original class symbols. + const symAlt = [...aliases.memberSyms].join('|'); for (const ns of aliases.namespaces) { // Allow intermediate property segments (ns.schema.companies — nested // namespace re-exports) and literal computed access (ns['companies']). @@ -877,9 +969,10 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { targets.push(`${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\[\\s*['"\`](?:${symAlt})['"\`]\\s*\\]`); } if (targets.length > 0) { - // `\(\s*\(*` tolerates argument parenthesization: .insert((companies)). + // The argument prefix tolerates parenthesization, spread, and array + // wrapping: .insert((companies)), .insert(...[companies]). const writeRe = new RegExp( - `\\.\\s*(insert|update|delete)\\s*\\(\\s*\\(*\\s*(${targets.join('|')})`, + `\\.\\s*(insert|update|delete)\\s*\\(\\s*(?:(?:\\(|\\[|\\.\\.\\.)\\s*)*(${targets.join('|')})`, 'g', ); for (const m of code.matchAll(writeRe)) { @@ -924,7 +1017,7 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { // block comments, and quoting (including escaped quotes in span text). if (!inAllowlist && !isSchemaDefinition) { const kw = - '(?:insert\\s+into|update|delete\\s+from|from|join|truncate(?:\\s+table)?|alter\\s+table|drop\\s+table|references|into|copy)'; + '(?:insert\\s+into|update|delete\\s+from|from|join|truncate(?:\\s+table)?|alter\\s+table|drop\\s+table|references|into|copy|on|lock(?:\\s+table)?)'; const gap = '(?:\\s|/\\*[\\s\\S]*?\\*/|--[^\\n]*\\n)+'; const q = `(?:\\\\?["'\`])?`; const sqlAdjacentRe = new RegExp( @@ -956,6 +1049,13 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { if (r !== null && driverConduits.has(r)) conduitDriver = true; if (r !== null && factoryConduits.has(r)) factoryConduitImport = true; } + // A TRACKED dynamic import of a conduit is an import edge like any + // static one (contract rev 9) — for capability too, not just prong (i). + for (const m of f.code.matchAll(/=\s*await\s+import\s*\(\s*(['"])([^'"]+)\1\s*\)/g)) { + const r = resolveSpecifier(rel, m[2]!, fileSet); + if (r !== null && driverConduits.has(r)) conduitDriver = true; + if (r !== null && factoryConduits.has(r)) factoryConduitImport = true; + } const driverImport = literalDriver || conduitDriver; // Factory capability: a named/namespace edge to the factory symbols, OR // ANY value import from a factory conduit — the conduit may re-export @@ -967,6 +1067,12 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { (aliases.namespaces.length > 0 && new RegExp( `\\b(${aliases.namespaces.join('|')})(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(createDb|createPgliteDb)\\b`, + ).test(code)) || + // Destructuring a factory symbol OUT of a schema namespace confers + // capability whatever the local rename: const { createDb: mk } = dbns. + (aliases.namespaces.length > 0 && + new RegExp( + `(?:const|let|var)\\s*\\{[^}]*\\b(?:createDb|createPgliteDb)\\b[^}]*\\}\\s*=\\s*(?:await\\s+)?(?:${aliases.namespaces.join('|')})\\b`, ).test(code)); if (driverImport) { violations.push({ file: rel, prong: 'iii-raw-execution', detail: 'direct driver import' }); @@ -995,8 +1101,10 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { if (!driverImport) { // DI residual backstop: db/client-shaped receivers fire regardless of // detected capability (a handle can arrive by injection). + // The receiver may be a dotted name OR a literal bracketed member with + // a conventional name: this['db'].query(…). for (const m of code.matchAll( - /\b(?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)\s*\.\s*(execute|query|unsafe)\s*\(/g, + /(?:\b(?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)|\[\s*['"](?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)['"]\s*\])\s*\.\s*(execute|query|unsafe)\s*\(/g, )) { violations.push({ file: rel, @@ -1356,6 +1464,114 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { name: 'E39 computed-member eval', src: `export function f(s: string) { return (globalThis as never)['eval'](s); }`, }, + { + name: 'E40 schema symbol renamed to default through conduit', + src: `import c from './evasion-mid9.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid9.ts', + src: `export { companies as default } from '@mosaicstack/db';`, + }, + ], + }, + { + name: 'E41 export default of locally bound schema symbol', + src: `import c from './evasion-mid10.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid10.ts', + src: `import { companies } from '@mosaicstack/db';\nexport default companies;`, + }, + ], + }, + { + name: 'E42 export default of dynamic namespace member', + src: `import c from './evasion-mid11.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid11.ts', + src: `const mod = await import('@mosaicstack/db');\nexport default mod.companies;`, + }, + ], + }, + { + name: 'E43 namespace member under renamed export', + src: `import * as M from './evasion-mid12.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(M.co).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid12.ts', + src: `export { companies as co } from '@mosaicstack/db';`, + }, + ], + }, + { + name: 'E44 dynamic namespace member under renamed export', + src: `import { db } from './x.js';\nconst M = await import('./evasion-mid13.js');\nexport async function f() { await db.insert(M.co).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid13.ts', + src: `export { companies as co } from '@mosaicstack/db';`, + }, + ], + }, + { + name: 'E45 two-hop factory export-from rename', + src: `import { mk } from './evasion-mid15.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid14.ts', + src: `export { createDb as mk } from '@mosaicstack/db';`, + }, + { + rel: 'packages/db/src/evasion-mid15.ts', + src: `export { mk } from './evasion-mid14.js';`, + }, + ], + }, + { + name: 'E46 export default of locally bound factory', + src: `import mk from './evasion-mid16.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid16.ts', + src: `import { createDb } from '@mosaicstack/db';\nexport default createDb;`, + }, + ], + }, + { + name: 'E47 tracked dynamic import of a factory conduit', + src: `const F = await import('./evasion-mid17.js');\nexport async function f(t: string) { await F.mk('u').execute('DELETE FROM ' + t); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid17.ts', + src: `export { createDb as mk } from '@mosaicstack/db';`, + }, + ], + }, + { + name: 'E48 factory destructure-rename from schema namespace', + src: `import * as dbns from '@mosaicstack/db';\nconst { createDb: mk } = dbns;\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`, + }, + { + name: 'E49 createRequire destructured from static module namespace', + src: `import * as M from 'node:module';\nconst { createRequire: mkReq } = M;\nconst req = mkReq(import.meta.url);\nexport const pg = req('postgres');`, + }, + { + name: 'E50 spread-argument schema write', + src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(...[companies]).values({}); }`, + }, + { + name: 'E51 GRANT ON class table in SQL span', + src: `export const q = 'GRANT SELECT ON hierarchy_grants TO auditor';`, + }, + { + name: 'E52 LOCK TABLE class table in SQL span', + src: `export const q = 'LOCK TABLE hierarchy_grants IN ACCESS EXCLUSIVE MODE';`, + }, + { + name: 'E53 bracketed conventional receiver', + src: `export class R { async f(t: string) { await this['db'].query('TRUNCATE ' + t); } }`, + }, ]; const CLEAN_CONTROLS: Array<{ name: string; src: string }> = [ { -- 2.54.0 From 743d884bfce466abb2bccebdc33b25d7ce867b00 Mon Sep 17 00:00:00 2001 From: fred Date: Thu, 27 Aug 2026 17:50:44 -0500 Subject: [PATCH 06/10] fix(db): close round-5 review evasions in writer-coverage assertion Unify member-access matching on shared DOT/BRACKET_OPEN/MEMBER_SEG shapes (dot, bracket, ?. and ! forms) across both conduit computations, the write targets, the factory-import clause, and the DI backstop. Treat export-default as an expression at both ends: strip parens/assertions, classify bare-word vs namespace-member chain, on the schema AND capability sides. Add the capability-side export-default ns-member pass (round-5 finding 2). Admit bare SQL qualifier words (TABLE, ONLY, IF EXISTS) between keyword and table name in prong (ii). Controls E54-E68. --- .../db/src/hierarchy-writer-coverage.test.ts | 243 +++++++++++++++--- 1 file changed, 210 insertions(+), 33 deletions(-) diff --git a/packages/db/src/hierarchy-writer-coverage.test.ts b/packages/db/src/hierarchy-writer-coverage.test.ts index fc5265fd..4c9ee996 100644 --- a/packages/db/src/hierarchy-writer-coverage.test.ts +++ b/packages/db/src/hierarchy-writer-coverage.test.ts @@ -87,7 +87,9 @@ * function returns, or method extraction. Demonstrated escapes in this * class: `const u = this.client.unsafe; u.call(this.client, s)` (the * verb never appears as a member call), an alias chain three or more - * steps deep, and a helper function that returns a schema symbol. + * steps deep, a helper function that returns a schema symbol, and an + * export whose expression COMPUTES the value (a ternary, a call result) + * rather than naming a binding or member. * Closing it requires data-flow analysis (a type-checker-backed rewrite, * tracked for M4-1b consideration); the counterfactual — flagging every * bare identifier call — false-positives on essentially all callback @@ -444,6 +446,33 @@ const IMPORT_RE = const EXPORT_FROM_RE = /export\s*(type\s+)?(?:\{([^}]*)\}|\*(?:\s*as\s+\w+)?)\s*from\s*['"]([^'"]+)['"]/g; +// Member-access shapes, tolerant of optional chaining and non-null +// assertions (ns?.companies, ns!.companies, ns?.['companies']) — a `?.` or +// `!` between receiver and member must not break a match. +const DOT = `\\s*(?:\\?\\.|!\\s*\\.|\\.)\\s*`; +const BRACKET_OPEN = `\\s*(?:\\?\\.|!)?\\s*\\[`; +const MEMBER_SEG = `(?:${DOT}\\w+|${BRACKET_OPEN}\\s*['"\`]\\w+['"\`]\\s*\\])`; +/** Terminal member access matching one of `alt`'s names, dot or bracket form. */ +const memberTail = (alt: string): string => + `(?:${DOT}(?:${alt})\\b|${BRACKET_OPEN}\\s*['"\`](?:${alt})['"\`]\\s*\\])`; +/** + * Reduce an exported expression to its core: strip parenthesization and + * trailing type assertions (`(companies)`, `companies as unknown as object`) + * so `export default ` passes see the binding under the dressing. + */ +function stripExprDressing(raw: string): string { + let expr = raw.trim(); + for (let prev = ''; prev !== expr; ) { + prev = expr; + expr = expr + .replace(/^\(\s*/, '') + .replace(/\s*\)$/, '') + .replace(/\s+as\s+[^()]+$/, '') + .trim(); + } + return expr; +} + interface FileFacts { rel: string; code: string; @@ -543,7 +572,7 @@ function computeSchemaConduits(files: FileFacts[], fileSet: Set): Schema for (const ns of aliases.namespaces) { for (const m of f.code.matchAll( new RegExp( - `export\\s+(?:const|let|var)\\s+(\\w+)\\s*=\\s*${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(?:${CLASS_SYMBOLS.join('|')})\\b`, + `export\\s+(?:const|let|var)\\s+(\\w+)\\s*=\\s*${ns}${MEMBER_SEG}*${memberTail(CLASS_SYMBOLS.join('|'))}`, 'g', ), )) { @@ -552,24 +581,28 @@ function computeSchemaConduits(files: FileFacts[], fileSet: Set): Schema } for (const local of aliases.named) { for (const m of f.code.matchAll( - new RegExp(`export\\s+(?:const|let|var)\\s+(\\w+)\\s*=\\s*${local}\\b`, 'g'), + new RegExp(`export\\s+(?:const|let|var)\\s+(\\w+)\\s*=\\s*\\(*\\s*${local}\\b`, 'g'), )) { mine.named.add(m[1]!); } } - // The default slot is an export name like any other: - // `export default companies;` / `export default mod.companies;` - for (const m of f.code.matchAll(/export\s+default\s+(\w+)\s*;/g)) { - if (aliases.named.includes(m[1]!)) mine.named.add('default'); - if (aliases.namespaces.includes(m[1]!)) mine.ns.add('default'); - } - for (const ns of aliases.namespaces) { - if ( - new RegExp( - `export\\s+default\\s+${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(?:${CLASS_SYMBOLS.join('|')})\\b`, - ).test(f.code) - ) { - mine.named.add('default'); + // The default slot is an export name like any other, whatever the + // expression dressing: bare local, parenthesized, type-asserted, or + // a namespace member chain in dot or bracket form, semicolon or not. + for (const m of f.code.matchAll(/export\s+default\s+([^;\n]+)/g)) { + const expr = stripExprDressing(m[1]!); + if (/^\w+$/.test(expr)) { + if (aliases.named.includes(expr)) mine.named.add('default'); + if (aliases.namespaces.includes(expr)) mine.ns.add('default'); + } else { + for (const ns of aliases.namespaces) { + if ( + new RegExp(`^${ns}${MEMBER_SEG}*${memberTail(CLASS_SYMBOLS.join('|'))}$`).test(expr) + ) { + mine.named.add('default'); + break; + } + } } } } @@ -681,11 +714,11 @@ function classAliases(f: FileFacts, conduits: SchemaConduits, fileSet: Set 0) { @@ -815,8 +851,32 @@ function computeCapabilityConduits( if (new RegExp(`export\\s+(?:const|let|var|function)\\s+${local}\\b`).test(f.code)) { mine.add(local); } - if (new RegExp(`export\\s+default\\s+${local}\\b`).test(f.code)) { - mine.add('default'); + // A derived binding exported under a NEW name re-exports the + // capability under that name: `export const mk2 = (mk);` + for (const dm of f.code.matchAll( + new RegExp(`export\\s+(?:const|let|var)\\s+(\\w+)\\s*=\\s*\\(*\\s*${local}\\b`, 'g'), + )) { + mine.add(dm[1]!); + } + } + } + // The default slot, under any expression dressing — a local factory + // binding (`export default (createDb)`) or a namespace factory member + // (`export default mod.createDb`, `export default mod['createDb']`) — + // mirrors the schema side's default-export pass. + for (const m of f.code.matchAll(/export\s+default\s+([^;\n]+)/g)) { + const expr = stripExprDressing(m[1]!); + if (/^\w+$/.test(expr)) { + if (locals.has(expr)) mine.add('default'); + } else { + for (const [ns, src] of nss) { + const t = new RegExp( + `^${ns}(?:${DOT}(\\w+)\\b|${BRACKET_OPEN}\\s*['"\`](\\w+)['"\`]\\s*\\])$`, + ).exec(expr); + if (t && src.has((t[1] ?? t[2])!)) { + mine.add('default'); + break; + } } } } @@ -837,12 +897,13 @@ function hasSymbolImportEdge(code: string, symbols: string[], packageName: strin ).test(code); const usesSymbol = new RegExp(`\\b(${symbols.join('|')})\\b`).test(code); const dynamicEdge = usesSymbol && new RegExp(`import\\s*\\(\\s*['"]${pkg}['"]\\s*\\)`).test(code); - // Namespace form: `import * as ns from ''` + `ns.` usage. + // Namespace form: `import * as ns from ''` + `ns.` usage + // (dot or bracket member, optional-chain/non-null tolerant). let nsEdge = false; for (const m of code.matchAll( new RegExp(`import\\s*\\*\\s*as\\s+(\\w+)\\s*from\\s*['"]${pkg}['"]`, 'g'), )) { - if (new RegExp(`\\b${m[1]}\\s*\\.\\s*(${symbols.join('|')})\\b`).test(code)) nsEdge = true; + if (new RegExp(`\\b${m[1]}${memberTail(symbols.join('|'))}`).test(code)) nsEdge = true; } return staticEdge || dynamicEdge || nsEdge; } @@ -964,9 +1025,9 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { const symAlt = [...aliases.memberSyms].join('|'); for (const ns of aliases.namespaces) { // Allow intermediate property segments (ns.schema.companies — nested - // namespace re-exports) and literal computed access (ns['companies']). - targets.push(`${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(?:${symAlt})\\b`); - targets.push(`${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\[\\s*['"\`](?:${symAlt})['"\`]\\s*\\]`); + // namespace re-exports), literal computed access (ns['companies']), + // and optional-chain/non-null markers (ns?.companies, ns!.companies). + targets.push(`${ns}${MEMBER_SEG}*${memberTail(symAlt)}`); } if (targets.length > 0) { // The argument prefix tolerates parenthesization, spread, and array @@ -1013,15 +1074,19 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { // The SQL keyword must be ADJACENT to the table name — co-residence // anywhere in one span over-matches prose ("pnpm workspaces" plus an // unrelated "from" in an embedded doc string is not SQL). Adjacency - // tolerates schema qualification (public.hierarchy_grants), interposed - // block comments, and quoting (including escaped quotes in span text). + // tolerates schema qualification (public.hierarchy_grants), bare + // qualifier words (TABLE, ONLY, IF EXISTS), interposed block comments, + // and quoting (including escaped quotes in span text). if (!inAllowlist && !isSchemaDefinition) { const kw = '(?:insert\\s+into|update|delete\\s+from|from|join|truncate(?:\\s+table)?|alter\\s+table|drop\\s+table|references|into|copy|on|lock(?:\\s+table)?)'; const gap = '(?:\\s|/\\*[\\s\\S]*?\\*/|--[^\\n]*\\n)+'; + // Bare qualifier words may sit between keyword and table name: + // GRANT … ON TABLE t, DELETE FROM ONLY t, DROP TABLE IF EXISTS t. + const qual = `(?:(?:table|only|if\\s+exists)${gap})*`; const q = `(?:\\\\?["'\`])?`; const sqlAdjacentRe = new RegExp( - `\\b${kw}${gap}${q}(?:${q}\\w+${q}\\s*\\.\\s*)?${q}(${CLASS_TABLES.join('|')})\\b`, + `\\b${kw}${gap}${qual}${q}(?:${q}\\w+${q}\\s*\\.\\s*)?${q}(${CLASS_TABLES.join('|')})\\b`, 'i', ); for (const span of f.spans) { @@ -1066,7 +1131,7 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { hasSymbolImportEdge(code, DB_FACTORY_SYMBOLS, '@mosaicstack/db') || (aliases.namespaces.length > 0 && new RegExp( - `\\b(${aliases.namespaces.join('|')})(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(createDb|createPgliteDb)\\b`, + `\\b(?:${aliases.namespaces.join('|')})${MEMBER_SEG}*${memberTail('createDb|createPgliteDb')}`, ).test(code)) || // Destructuring a factory symbol OUT of a schema namespace confers // capability whatever the local rename: const { createDb: mk } = dbns. @@ -1102,9 +1167,13 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { // DI residual backstop: db/client-shaped receivers fire regardless of // detected capability (a handle can arrive by injection). // The receiver may be a dotted name OR a literal bracketed member with - // a conventional name: this['db'].query(…). + // a conventional name — this['db'].query(…) — and the final member + // access tolerates ?. and ! markers (this.db?.query(…)). for (const m of code.matchAll( - /(?:\b(?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)|\[\s*['"](?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)['"]\s*\])\s*\.\s*(execute|query|unsafe)\s*\(/g, + new RegExp( + `(?:\\b(?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)|\\[\\s*['"](?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)['"]\\s*\\])${DOT}(execute|query|unsafe)\\s*\\(`, + 'g', + ), )) { violations.push({ file: rel, @@ -1572,6 +1641,114 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { name: 'E53 bracketed conventional receiver', src: `export class R { async f(t: string) { await this['db'].query('TRUNCATE ' + t); } }`, }, + { + name: 'E54 parenthesized default export of schema symbol', + src: `import c from './evasion-mid18.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid18.ts', + src: `import { companies } from '@mosaicstack/db';\nexport default (companies);`, + }, + ], + }, + { + name: 'E55 bracket-member default export over dynamic namespace', + src: `import c from './evasion-mid19.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid19.ts', + src: `const mod = await import('@mosaicstack/db');\nexport default mod['companies'];`, + }, + ], + }, + { + name: 'E56 type-asserted default export of schema symbol', + src: `import c from './evasion-mid20.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid20.ts', + src: `import { companies } from '@mosaicstack/db';\nexport default companies as unknown as object;`, + }, + ], + }, + { + name: 'E57 export-const bracket member of schema namespace', + src: `import { co } from './evasion-mid21.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(co).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid21.ts', + src: `import * as ns from '@mosaicstack/db';\nexport const co = ns['companies'];`, + }, + ], + }, + { + name: 'E58 default export of dynamic namespace factory member', + src: `import mk from './evasion-mid22.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid22.ts', + src: `const mod = await import('@mosaicstack/db');\nexport default mod.createDb;`, + }, + ], + }, + { + name: 'E59 default export of static namespace factory member', + src: `import mk from './evasion-mid23.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid23.ts', + src: `import * as ns from '@mosaicstack/db';\nexport default ns.createDb;`, + }, + ], + }, + { + name: 'E60 export-const bracket factory member', + src: `import { mk } from './evasion-mid24.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid24.ts', + src: `import * as ns from '@mosaicstack/db';\nexport const mk = ns['createDb'];`, + }, + ], + }, + { + name: 'E61 parenthesized default export of factory local', + src: `import mk from './evasion-mid25.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid25.ts', + src: `import { createDb } from '@mosaicstack/db';\nexport default (createDb);`, + }, + ], + }, + { + name: 'E62 GRANT ON TABLE class table in SQL span', + src: `export const q = 'GRANT SELECT ON TABLE hierarchy_grants TO auditor';`, + }, + { + name: 'E63 DROP TABLE IF EXISTS class table in SQL span', + src: `export const q = 'DROP TABLE IF EXISTS hierarchy_grants';`, + }, + { + name: 'E64 DELETE FROM ONLY class table in SQL span', + src: `export const q = 'DELETE FROM ONLY hierarchy_grants WHERE id = $1';`, + }, + { + name: 'E65 optional-chained namespace write target', + src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(ns?.companies).values({}); }`, + }, + { + name: 'E66 non-null-asserted namespace write target', + src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(ns!.companies).values({}); }`, + }, + { + name: 'E67 optional-chained DI receiver', + src: `export class R { constructor(private db?: { query(s: string): Promise }) {}\n async f(t: string) { await this.db?.query('TRUNCATE ' + t); } }`, + }, + { + name: 'E68 optional-chained bracketed DI receiver', + src: `export class R { async f(t: string) { await this['db']?.query('TRUNCATE ' + t); } }`, + }, ]; const CLEAN_CONTROLS: Array<{ name: string; src: string }> = [ { -- 2.54.0 From 4cad960796c4984a9be891bd8dc8160dee55ce6f Mon Sep 17 00:00:00 2001 From: fred Date: Thu, 27 Aug 2026 18:12:45 -0500 Subject: [PATCH 07/10] fix(db): close round-6 review evasions in writer-coverage assertion stripExprDressing also strips trailing non-null assertions and satisfies clauses, so export-default dressing (companies!, createDb satisfies T, (x as unknown)!) resolves at both ends. The three ns-member right-hand sides tolerate parenthesization. Every verb matcher admits the optional- call form (?.() via a shared CALL_OPEN shape. A computed member call whose key is a text-only template literal fails closed (its text never reaches lexed code); interpolated keys stay under the non-literal residual. Document the DB_FACTORY_IMPORTERS ns-destructure enumeration blind spot as a residual. Controls E69-E81. --- .../db/src/hierarchy-writer-coverage.test.ts | 197 ++++++++++++++++-- 1 file changed, 174 insertions(+), 23 deletions(-) diff --git a/packages/db/src/hierarchy-writer-coverage.test.ts b/packages/db/src/hierarchy-writer-coverage.test.ts index 4c9ee996..77f13ee6 100644 --- a/packages/db/src/hierarchy-writer-coverage.test.ts +++ b/packages/db/src/hierarchy-writer-coverage.test.ts @@ -79,8 +79,20 @@ * residual is accepted and reviews of DI provider modules carry it. * - Computed member access with a NON-literal name (obj[verb]()) is not * statically resolvable; literal computed access (obj['insert']()) is - * flagged. Constructing the verb at runtime is adjacent to eval and is - * expected to be caught in review. + * flagged, and a computed member CALL whose key is a text-only template + * literal (obj[`insert`]()) fails closed anywhere — template text never + * reaches the lexer's code output, so such a call is indistinguishable + * from a runtime-constructed verb (a template key WITH interpolation is + * a non-literal computed member, above). Constructing the verb at + * runtime is adjacent to eval and is expected to be caught in review. + * - The DB_FACTORY_IMPORTERS enumeration counts the import edges + * hasSymbolImportEdge can see (named import, literal dynamic package + * import with symbol use, namespace member use). Destructuring a factory + * out of a schema namespace (`const { createDb } = dbns`) confers + * capability for prong (iii) but is NOT visible to the enumeration, so a + * file on that route joins the importer set only via review; prong (iii) + * still flags any execution in it. The enumeration is a measured set, + * not a soundness claim. * - General VALUE FLOW is not modeled. The analyzer tracks import edges, * re-export chains, and single-step alias/destructure derivations from a * namespace binding (to depth 2) — not arbitrary assignment chains, @@ -456,9 +468,11 @@ const MEMBER_SEG = `(?:${DOT}\\w+|${BRACKET_OPEN}\\s*['"\`]\\w+['"\`]\\s*\\])`; const memberTail = (alt: string): string => `(?:${DOT}(?:${alt})\\b|${BRACKET_OPEN}\\s*['"\`](?:${alt})['"\`]\\s*\\])`; /** - * Reduce an exported expression to its core: strip parenthesization and - * trailing type assertions (`(companies)`, `companies as unknown as object`) - * so `export default ` passes see the binding under the dressing. + * Reduce an exported expression to its core: strip parenthesization, + * trailing type assertions (`as …`, `satisfies …`), and trailing non-null + * assertions (`(companies)`, `companies as unknown as object`, + * `companies satisfies object`, `companies!`, `(companies as any)!`) so + * `export default ` passes see the binding under the dressing. */ function stripExprDressing(raw: string): string { let expr = raw.trim(); @@ -468,11 +482,16 @@ function stripExprDressing(raw: string): string { .replace(/^\(\s*/, '') .replace(/\s*\)$/, '') .replace(/\s+as\s+[^()]+$/, '') + .replace(/\s+satisfies\s+[^()]+$/, '') + .replace(/\s*!+$/, '') .trim(); } return expr; } +/** Call-open shape tolerant of the optional-call form: `f(…)` or `f?.(…)`. */ +const CALL_OPEN = `\\s*(?:\\?\\.)?\\s*\\(`; + interface FileFacts { rel: string; code: string; @@ -572,7 +591,7 @@ function computeSchemaConduits(files: FileFacts[], fileSet: Set): Schema for (const ns of aliases.namespaces) { for (const m of f.code.matchAll( new RegExp( - `export\\s+(?:const|let|var)\\s+(\\w+)\\s*=\\s*${ns}${MEMBER_SEG}*${memberTail(CLASS_SYMBOLS.join('|'))}`, + `export\\s+(?:const|let|var)\\s+(\\w+)\\s*=\\s*\\(*\\s*${ns}${MEMBER_SEG}*${memberTail(CLASS_SYMBOLS.join('|'))}`, 'g', ), )) { @@ -587,8 +606,9 @@ function computeSchemaConduits(files: FileFacts[], fileSet: Set): Schema } } // The default slot is an export name like any other, whatever the - // expression dressing: bare local, parenthesized, type-asserted, or - // a namespace member chain in dot or bracket form, semicolon or not. + // expression dressing: bare local, parenthesized, type-asserted + // (`as`/`satisfies`), non-null-asserted (`companies!`), or a + // namespace member chain in dot or bracket form, semicolon or not. for (const m of f.code.matchAll(/export\s+default\s+([^;\n]+)/g)) { const expr = stripExprDressing(m[1]!); if (/^\w+$/.test(expr)) { @@ -714,7 +734,7 @@ function classAliases(f: FileFacts, conduits: SchemaConduits, fileSet: Set(); - if (/\bcreateRequire\s*\(/.test(code)) crNames.add('createRequire'); + if (new RegExp(`\\bcreateRequire${CALL_OPEN}`).test(code)) crNames.add('createRequire'); for (const m of code.matchAll(/import\s*\{([^}]*)\}\s*from\s*['"](?:node:)?module['"]/g)) { const alias = /\bcreateRequire\s+as\s+(\w+)/.exec(m[1]!); if (alias) crNames.add(alias[1]!); @@ -1006,7 +1045,7 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { } } for (const n of crNames) { - if (n === 'createRequire' || new RegExp(`\\b${n}\\s*\\(`).test(code)) { + if (n === 'createRequire' || new RegExp(`\\b${n}${CALL_OPEN}`).test(code)) { violations.push({ file: rel, prong: 'dynamic-import', detail: 'createRequire()' }); break; } @@ -1033,7 +1072,7 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { // The argument prefix tolerates parenthesization, spread, and array // wrapping: .insert((companies)), .insert(...[companies]). const writeRe = new RegExp( - `\\.\\s*(insert|update|delete)\\s*\\(\\s*(?:(?:\\(|\\[|\\.\\.\\.)\\s*)*(${targets.join('|')})`, + `\\.\\s*(insert|update|delete)${CALL_OPEN}\\s*(?:(?:\\(|\\[|\\.\\.\\.)\\s*)*(${targets.join('|')})`, 'g', ); for (const m of code.matchAll(writeRe)) { @@ -1051,7 +1090,10 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { // form (measured), so it fails outright. Write verbs fail outside the // allowlist; execution verbs fail outside allowlist ∪ register. if (!inAllowlist) { - const writeBracket = /\[\s*['"](insert|update|delete)['"]\s*\]\s*\(/g; + const writeBracket = new RegExp( + `\\[\\s*['"](insert|update|delete)['"]\\s*\\]${CALL_OPEN}`, + 'g', + ); for (const m of code.matchAll(writeBracket)) { violations.push({ file: rel, @@ -1060,7 +1102,9 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { }); } if (!inRegister) { - for (const m of code.matchAll(/\[\s*['"](execute|query|unsafe)['"]\s*\]\s*\(/g)) { + for (const m of code.matchAll( + new RegExp(`\\[\\s*['"](execute|query|unsafe)['"]\\s*\\]${CALL_OPEN}`, 'g'), + )) { violations.push({ file: rel, prong: 'iii-raw-execution', @@ -1142,8 +1186,9 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { if (driverImport) { violations.push({ file: rel, prong: 'iii-raw-execution', detail: 'direct driver import' }); // A driver client exposes query/execute/unsafe as raw primitives: - // flag them all on any receiver, any argument, spaced or not. - for (const m of code.matchAll(/\.\s*(execute|query|unsafe)\s*\(/g)) { + // flag them all on any receiver, any argument, spaced, optional-call + // (`?.(`) or not. + for (const m of code.matchAll(new RegExp(`\\.\\s*(execute|query|unsafe)${CALL_OPEN}`, 'g'))) { violations.push({ file: rel, prong: 'iii-raw-execution', @@ -1155,7 +1200,7 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { // the relational builder, and unrelated .query() methods are common), // so factory capability flags execute/unsafe on any receiver and query // only on the db/client-shaped receiver backstop below. - for (const m of code.matchAll(/\.\s*(execute|unsafe)\s*\(/g)) { + for (const m of code.matchAll(new RegExp(`\\.\\s*(execute|unsafe)${CALL_OPEN}`, 'g'))) { violations.push({ file: rel, prong: 'iii-raw-execution', @@ -1171,7 +1216,7 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { // access tolerates ?. and ! markers (this.db?.query(…)). for (const m of code.matchAll( new RegExp( - `(?:\\b(?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)|\\[\\s*['"](?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)['"]\\s*\\])${DOT}(execute|query|unsafe)\\s*\\(`, + `(?:\\b(?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)|\\[\\s*['"](?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)['"]\\s*\\])${DOT}(execute|query|unsafe)${CALL_OPEN}`, 'g', ), )) { @@ -1199,7 +1244,7 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { if (nsName) sqlAliases.add(`${nsName}\\s*\\.\\s*sql`); } if (sqlAliases.size > 0) { - const rawRe = new RegExp(`\\b(${[...sqlAliases].join('|')})\\s*\\.\\s*raw\\s*\\(`); + const rawRe = new RegExp(`\\b(${[...sqlAliases].join('|')})\\s*\\.\\s*raw${CALL_OPEN}`); if (rawRe.test(code)) { violations.push({ file: rel, prong: 'iii-raw-execution', detail: 'sql.raw()' }); } @@ -1749,6 +1794,112 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { name: 'E68 optional-chained bracketed DI receiver', src: `export class R { async f(t: string) { await this['db']?.query('TRUNCATE ' + t); } }`, }, + { + name: 'E69 non-null-asserted default export of schema symbol', + src: `import c from './evasion-mid26.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid26.ts', + src: `import { companies } from '@mosaicstack/db';\nexport default companies!;`, + }, + ], + }, + { + name: 'E70 satisfies-dressed default export of schema symbol', + src: `import c from './evasion-mid27.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid27.ts', + src: `import { companies } from '@mosaicstack/db';\nexport default companies satisfies object;`, + }, + ], + }, + { + name: 'E71 paren-plus-assertion-plus-non-null default export', + src: `import c from './evasion-mid28.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid28.ts', + src: `import { companies } from '@mosaicstack/db';\nexport default (companies as unknown)!;`, + }, + ], + }, + { + name: 'E72 non-null-asserted ns-member default export (dynamic ns)', + src: `import c from './evasion-mid29.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid29.ts', + src: `const mod = await import('@mosaicstack/db');\nexport default mod.companies!;`, + }, + ], + }, + { + name: 'E73 parenthesized ns-member export-const of schema symbol', + src: `import { co } from './evasion-mid30.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(co).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid30.ts', + src: `import * as ns from '@mosaicstack/db';\nexport const co = (ns.companies);`, + }, + ], + }, + { + name: 'E74 non-null-asserted default export of factory local', + src: `import mk from './evasion-mid31.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid31.ts', + src: `import { createDb } from '@mosaicstack/db';\nexport default createDb!;`, + }, + ], + }, + { + name: 'E75 non-null-asserted ns-member factory default export', + src: `import mk from './evasion-mid32.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid32.ts', + src: `import * as ns from '@mosaicstack/db';\nexport default ns.createDb!;`, + }, + ], + }, + { + name: 'E76 satisfies-dressed default export of factory local', + src: `import mk from './evasion-mid33.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid33.ts', + src: `import { createDb } from '@mosaicstack/db';\nexport default createDb satisfies typeof createDb;`, + }, + ], + }, + { + name: 'E77 parenthesized ns-member factory derivation re-exported', + src: `import mk from './evasion-mid34.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid34.ts', + src: `import * as ns from '@mosaicstack/db';\nconst mk = (ns.createDb);\nexport default mk;`, + }, + ], + }, + { + name: 'E78 optional-call write verb', + src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert?.(companies).values({}); }`, + }, + { + name: 'E79 optional-call DI receiver chain', + src: `export class R { constructor(private pool?: { execute(s: string): Promise }) {}\n async f(t: string) { await this.pool?.execute?.('TRUNCATE ' + t); } }`, + }, + { + name: 'E80 optional-call execution in factory-capable file', + src: `import { createDb } from '@mosaicstack/db';\nexport async function f(t: string) { const d = createDb('u'); await d.execute?.('DELETE FROM ' + t); }`, + }, + { + name: 'E81 template-literal computed member call', + src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db[\`insert\`](companies).values({}); }`, + }, ]; const CLEAN_CONTROLS: Array<{ name: string; src: string }> = [ { -- 2.54.0 From f9a05bba92bb95655f9c6eb80b422022e29430b7 Mon Sep 17 00:00:00 2001 From: fred Date: Thu, 27 Aug 2026 18:37:53 -0500 Subject: [PATCH 08/10] fix(db): close round-7 review evasions in writer-coverage assertion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The text-only template-key rule fires on any computed member — access or call — not only the call form, so template-keyed write targets, export expressions, and receivers fail closed at their origin. Declaration exports and derivations tolerate a type annotation and prior declarators via shared TYPE_ANN/DECL_LIST shapes at all five declarator sites. Invoking a write/exec verb through .apply/.call/.bind fails closed anywhere (both member names statically visible, unlike the method-extraction residual). The evasion test counts violations across the whole synthetic chain, since some routes fail closed at the helper. Controls E82-E92; clean control pinning the interpolated-key discriminator. --- .../db/src/hierarchy-writer-coverage.test.ts | 186 +++++++++++++++--- 1 file changed, 163 insertions(+), 23 deletions(-) diff --git a/packages/db/src/hierarchy-writer-coverage.test.ts b/packages/db/src/hierarchy-writer-coverage.test.ts index 77f13ee6..bdfd738c 100644 --- a/packages/db/src/hierarchy-writer-coverage.test.ts +++ b/packages/db/src/hierarchy-writer-coverage.test.ts @@ -15,7 +15,10 @@ * (`export { companies as c } from`), an export of a locally bound * alias (`import { companies }; export { companies as co }`), and a * binding derived from a namespace (`export const co = ns.companies`) - * all propagate symbol identity to the consumer. The DEFAULT-export + * all propagate symbol identity to the consumer. Declaration exports + * tolerate a type annotation and prior declarators + * (`export const co: typeof companies = companies`, + * `export const dummy = 0, co = companies`). The DEFAULT-export * slot is an export name like any other (`export { companies as * default } from`, `export default companies`), so a bare default * import of a conduit resolves against the map too; namespace-member @@ -79,12 +82,15 @@ * residual is accepted and reviews of DI provider modules carry it. * - Computed member access with a NON-literal name (obj[verb]()) is not * statically resolvable; literal computed access (obj['insert']()) is - * flagged, and a computed member CALL whose key is a text-only template - * literal (obj[`insert`]()) fails closed anywhere — template text never - * reaches the lexer's code output, so such a call is indistinguishable - * from a runtime-constructed verb (a template key WITH interpolation is - * a non-literal computed member, above). Constructing the verb at - * runtime is adjacent to eval and is expected to be caught in review. + * flagged, and a computed member — access or call, in any position — + * whose key is a text-only template literal (obj[`insert`](), + * ns[`companies`]) fails closed anywhere — template text never reaches + * the lexer's code output, so such a member is indistinguishable from a + * runtime-constructed one (a template key WITH interpolation is a + * non-literal computed member, above). Invoking a write/exec verb via + * `.apply`/`.call`/`.bind` likewise fails closed anywhere. Constructing + * the member at runtime is adjacent to eval and is expected to be caught + * in review. * - The DB_FACTORY_IMPORTERS enumeration counts the import edges * hasSymbolImportEdge can see (named import, literal dynamic package * import with symbol use, namespace member use). Destructuring a factory @@ -492,6 +498,19 @@ function stripExprDressing(raw: string): string { /** Call-open shape tolerant of the optional-call form: `f(…)` or `f?.(…)`. */ const CALL_OPEN = `\\s*(?:\\?\\.)?\\s*\\(`; +/** + * Declaration-head shapes: a declarator's initializer may sit behind a type + * annotation (`export const co: typeof companies = companies`) or behind + * prior declarators (`export const dummy = 0, co = companies`). TYPE_ANN + * admits `=>` inside the type text but stops at a bare `=` (the + * initializer); DECL_LIST skips prior declarators whose initializers are + * comma-free. Both are approximations of the declarator grammar — exotic + * prior initializers (an array or call containing a comma) fall to the + * value-flow residual. + */ +const TYPE_ANN = `(?:\\s*:\\s*(?:[^=;\\n]|=>)*?)?`; +const DECL_LIST = `(?:[\\w$]+${TYPE_ANN}\\s*=\\s*[^,;\\n]*,\\s*)*`; + interface FileFacts { rel: string; code: string; @@ -591,7 +610,7 @@ function computeSchemaConduits(files: FileFacts[], fileSet: Set): Schema for (const ns of aliases.namespaces) { for (const m of f.code.matchAll( new RegExp( - `export\\s+(?:const|let|var)\\s+(\\w+)\\s*=\\s*\\(*\\s*${ns}${MEMBER_SEG}*${memberTail(CLASS_SYMBOLS.join('|'))}`, + `export\\s+(?:const|let|var)\\s+${DECL_LIST}(\\w+)${TYPE_ANN}\\s*=\\s*\\(*\\s*${ns}${MEMBER_SEG}*${memberTail(CLASS_SYMBOLS.join('|'))}`, 'g', ), )) { @@ -600,7 +619,10 @@ function computeSchemaConduits(files: FileFacts[], fileSet: Set): Schema } for (const local of aliases.named) { for (const m of f.code.matchAll( - new RegExp(`export\\s+(?:const|let|var)\\s+(\\w+)\\s*=\\s*\\(*\\s*${local}\\b`, 'g'), + new RegExp( + `export\\s+(?:const|let|var)\\s+${DECL_LIST}(\\w+)${TYPE_ANN}\\s*=\\s*\\(*\\s*${local}\\b`, + 'g', + ), )) { mine.named.add(m[1]!); } @@ -734,7 +756,7 @@ function classAliases(f: FileFacts, conduits: SchemaConduits, fileSet: Set { name: 'E81 template-literal computed member call', src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db[\`insert\`](companies).values({}); }`, }, + { + name: 'E82 template-literal computed member as write target', + src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(ns[\`companies\`]).values({}); }`, + }, + { + name: 'E83 template-literal computed member in schema default export', + src: `import c from './evasion-mid35.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid35.ts', + src: `import * as ns from '@mosaicstack/db';\nexport default ns[\`companies\`];`, + }, + ], + }, + { + name: 'E84 template-literal computed member in factory extraction', + src: `import { mk } from './evasion-mid36.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid36.ts', + src: `import * as ns from '@mosaicstack/db';\nexport const mk = ns[\`createDb\`];`, + }, + ], + }, + { + name: 'E85 type-annotated re-export of schema binding', + src: `import { co } from './evasion-mid37.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(co).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid37.ts', + src: `import { companies } from '@mosaicstack/db';\nexport const co: typeof companies = companies;`, + }, + ], + }, + { + name: 'E86 second-declarator re-export of schema binding', + src: `import { co } from './evasion-mid38.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(co).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid38.ts', + src: `import { companies } from '@mosaicstack/db';\nexport const dummy = 0,\n co = companies;`, + }, + ], + }, + { + name: 'E87 type-annotated re-export of factory binding', + src: `import { mk } from './evasion-mid39.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid39.ts', + src: `import { createDb } from '@mosaicstack/db';\nexport const mk: typeof createDb = createDb;`, + }, + ], + }, + { + name: 'E88 second-declarator re-export of factory binding', + src: `import { mk } from './evasion-mid40.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid40.ts', + src: `import { createDb } from '@mosaicstack/db';\nexport const d0 = 0,\n mk = createDb;`, + }, + ], + }, + { + name: 'E89 apply-invoked write verb', + src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert.apply(db, [companies]).values({}); }`, + }, + { + name: 'E90 call-invoked write verb', + src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert.call(db, companies).values({}); }`, + }, + { + name: 'E91 apply-invoked bracket verb in capability-free file', + src: `export class R { constructor(private pool: { execute(s: string): Promise }) {}\n async f(t: string) { await this.pool['execute'].apply(this.pool, ['TRUNCATE ' + t]); } }`, + }, + { + name: 'E92 template-keyed receiver verb in capability-free file', + src: `export class R { constructor(private pool: { execute(s: string): Promise }) {}\n async f(t: string) { await this.pool[\`execute\`].apply(this.pool, ['TRUNCATE ' + t]); } }`, + }, ]; const CLEAN_CONTROLS: Array<{ name: string; src: string }> = [ { @@ -1910,6 +2042,10 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { name: 'clean: comment mentioning a table is not SQL', src: `// syncs hierarchy_grants downstream\nexport const n = 1;`, }, + { + name: 'clean: interpolated template key is a non-literal computed member, not a text-only key', + src: `export function g(o: Record void>, k: string) { o[\`\${k}\`](); }`, + }, ]; it('analyzer flags every known evasion form', () => { @@ -1934,7 +2070,11 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { driverConduits: synthCap.driver, factoryConduits: synthCap.factory, }; - const v = analyzeFile(synthetic, synthCtx); + // An evasion is caught when ANY file in the chain is flagged: some + // laundering routes fail closed at the HELPER (origin), not the + // consumer — e.g. a template-keyed conduit export — which keeps the + // chain out of the tree just as effectively. + const v = [synthetic, ...extraFacts].flatMap((ff) => analyzeFile(ff, synthCtx)); expect(v.length, `evasion not caught: ${e.name}`).toBeGreaterThan(0); } }); -- 2.54.0 From f3250e32d7b9da16399a7a97604c602349c4becb Mon Sep 17 00:00:00 2001 From: fred Date: Thu, 27 Aug 2026 19:02:44 -0500 Subject: [PATCH 09/10] fix(db): close round-8 review evasions in writer-coverage assertion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Dressed quoted computed keys (as/satisfies/!) fail closed anywhere: the static key survives the dressing but breaks every ['name'] matcher, and matcher tolerance cannot span types containing ']' (as Foo['x']), so the shape itself is the trigger (with an !(?!=) guard for ordinary comparisons). - Escape-built quoted keys (\u/\x/octal) fail closed: statically resolvable, so outside the non-literal computed-member residual. - Reflect verb indirection fails closed: any Reflect.* call naming a write/exec verb in its argument text (Reflect.apply(db.insert, ...), Reflect.get(db, 'insert')). - CODE_SHAPE_REGISTER: enumerated disposition path for the fail-closed code-shape rules (template key, dressed/escaped key, apply/call/bind, Reflect) — a reviewed legitimate hit is registered, never resolved by weakening the shape. Empty today; eval/new Function stays unconditional. - KNOWN RESIDUALS: test-file/out-of-src modules named as import-graph conduit blind spots; TYPE_ANN/DECL_LIST single-line limit documented. - Controls E93-E102 (dressed/escaped keys at write target, conduit export, default export, single-file factory extraction; Reflect.apply/get) plus a clean control pinning the !== guard. --- .../db/src/hierarchy-writer-coverage.test.ts | 226 ++++++++++++++---- 1 file changed, 180 insertions(+), 46 deletions(-) diff --git a/packages/db/src/hierarchy-writer-coverage.test.ts b/packages/db/src/hierarchy-writer-coverage.test.ts index bdfd738c..0c6e0bcd 100644 --- a/packages/db/src/hierarchy-writer-coverage.test.ts +++ b/packages/db/src/hierarchy-writer-coverage.test.ts @@ -82,15 +82,20 @@ * residual is accepted and reviews of DI provider modules carry it. * - Computed member access with a NON-literal name (obj[verb]()) is not * statically resolvable; literal computed access (obj['insert']()) is - * flagged, and a computed member — access or call, in any position — - * whose key is a text-only template literal (obj[`insert`](), - * ns[`companies`]) fails closed anywhere — template text never reaches - * the lexer's code output, so such a member is indistinguishable from a - * runtime-constructed one (a template key WITH interpolation is a - * non-literal computed member, above). Invoking a write/exec verb via - * `.apply`/`.call`/`.bind` likewise fails closed anywhere. Constructing - * the member at runtime is adjacent to eval and is expected to be caught - * in review. + * flagged, and the statically-resolvable DISGUISES of a literal key fail + * closed — access or call, in any position: a text-only template-literal + * key (obj[`insert`](), ns[`companies`] — template text never reaches + * the lexer's code output), a quoted key carrying expression dressing + * (`ns['companies' as const]`, `ns['companies'!]`), and a quoted key + * built from string escapes (`\u`/`\x`/octal). (A template key WITH + * interpolation is a non-literal computed member, above.) Invoking a + * write/exec verb via `.apply`/`.call`/`.bind`, or through any + * `Reflect.*` call naming a verb, fails closed the same way. These + * code-shape rules match ordinary syntax over ordinary method names, so + * they carry their own enumerated disposition (CODE_SHAPE_REGISTER, + * empty today): a reviewed legitimate hit is registered, never resolved + * by weakening the shape. Constructing the member at runtime is adjacent + * to eval and is expected to be caught in review. * - The DB_FACTORY_IMPORTERS enumeration counts the import edges * hasSymbolImportEdge can see (named import, literal dynamic package * import with symbol use, namespace member use). Destructuring a factory @@ -115,6 +120,12 @@ * - The scan perimeter is //src for the three roots; production * TS outside a src/ directory (e.g. packages/mosaic/framework/**) is not * scanned (verified free of db/driver/execute references at review time). + * Files excluded from the scan — test files and out-of-src modules — are + * also invisible as import-graph CONDUITS: test files are emitted to + * dist, so a production module could launder a symbol or capability + * through a re-export in one. Importing a test module from production + * code is anomalous and review-visible; the blind spot is accepted as a + * residual, not closed. * * The writer allowlist names hierarchy command/repository modules ONLY. It is * empty today: the hierarchy command family (M4-1b) has not landed, so no @@ -247,6 +258,20 @@ const DB_FACTORY_IMPORTERS: string[] = [ 'packages/storage/src/cli.ts', 'packages/storage/src/migrate-tier.ts', ]; +/** + * Enumerated disposition for the fail-closed CODE-SHAPE rules (text-only + * template keys, dressed/escape-built quoted keys, verb + * `.apply`/`.call`/`.bind`, `Reflect.*` verb indirection). Those shapes use + * ordinary syntax over ordinary method names (`query`, `delete`), so a + * legitimate hit is possible — e.g. a non-SQL `.query.bind(this)` on a + * log-shaped service. Such a hit is registered here with a justification, + * reviewed under §5.1, and is never resolved by weakening the shape. Empty + * today: the production tree has zero occurrences of any of these shapes + * (calibrated by the full-tree test). Exemption covers the shape rules ONLY — + * every prong still applies in full. eval/new Function stays unconditional: + * constructed code has no disposition path. + */ +const CODE_SHAPE_REGISTER: string[] = []; const SCAN_ROOTS = ['apps', 'packages', 'plugins']; const EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts']); @@ -506,7 +531,9 @@ const CALL_OPEN = `\\s*(?:\\?\\.)?\\s*\\(`; * initializer); DECL_LIST skips prior declarators whose initializers are * comma-free. Both are approximations of the declarator grammar — exotic * prior initializers (an array or call containing a comma) fall to the - * value-flow residual. + * value-flow residual. Both are also SINGLE-LINE shapes: a multiline type + * annotation (prettier keeps one only past the print width) falls to the + * value-flow residual too. */ const TYPE_ANN = `(?:\\s*:\\s*(?:[^=;\\n]|=>)*?)?`; const DECL_LIST = `(?:[\\w$]+${TYPE_ANN}\\s*=\\s*[^,;\\n]*,\\s*)*`; @@ -987,42 +1014,93 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { ) { violations.push({ file: rel, prong: 'code-construction', detail: 'eval/new Function' }); } - // A computed member — access or call — whose key is a text-only template - // literal is invisible to every member and verb matcher (the lexer routes - // template text to spans, so the key survives in code as two ADJACENT - // backticks — the discriminator against backticks inside quoted-string - // prose, whose text stays in code, and against interpolated keys, whose - // `${…}` expression stays in code between the backticks). Runtime code - // construction's sibling: fail-closed anywhere, in any position (write - // target, export expression, receiver, call). A template key WITH - // interpolation is a non-literal computed member (documented residual - // above). - if (new RegExp(`\\[\\s*\`\`\\s*\\]`).test(code)) { - violations.push({ - file: rel, - prong: 'code-construction', - detail: 'template-literal computed member', - }); - } - // Invoking a write/exec verb through Function.prototype indirection - // (`db.insert.apply(db, [companies])`, `d.execute.call(d, s)`, - // `db.insert.bind(db)`) hides the argument shape from every verb matcher - // while both member names stay statically visible — unlike the - // method-EXTRACTION residual, where the verb never appears as a member. - // Fail-closed anywhere: no legitimate site invokes a builder verb this way - // (calibrated clean over the production tree). - if ( - new RegExp( - `(?:${DOT}(?:insert|update|delete|execute|query|unsafe|raw)\\b|` + - `\\[\\s*['"](?:insert|update|delete|execute|query|unsafe|raw)['"]\\s*\\])` + - `${DOT}(?:apply|call|bind)${CALL_OPEN}`, - ).test(code) - ) { - violations.push({ - file: rel, - prong: 'code-construction', - detail: 'verb apply/call/bind indirection', - }); + // Fail-closed CODE-SHAPE rules: statically-resolvable disguises of a + // literal member key or verb invocation. They match ordinary syntax, so a + // reviewed legitimate hit is dispositioned through CODE_SHAPE_REGISTER — + // the shapes themselves are never weakened. + if (!CODE_SHAPE_REGISTER.includes(rel)) { + // A computed member — access or call — whose key is a text-only template + // literal is invisible to every member and verb matcher (the lexer routes + // template text to spans, so the key survives in code as two ADJACENT + // backticks — the discriminator against backticks inside quoted-string + // prose, whose text stays in code, and against interpolated keys, whose + // `${…}` expression stays in code between the backticks). Runtime code + // construction's sibling: fail-closed in any position (write target, + // export expression, receiver, call). A template key WITH interpolation + // is a non-literal computed member (documented residual above). + if (new RegExp(`\\[\\s*\`\`\\s*\\]`).test(code)) { + violations.push({ + file: rel, + prong: 'code-construction', + detail: 'template-literal computed member', + }); + } + // A quoted computed key carrying expression dressing + // (`ns['companies' as const]`, `ns['companies' satisfies 'companies']`, + // `ns['companies'!]`) keeps its static value while breaking every + // `['name']` matcher — the bracket alternatives require the closing + // quote to touch the `]`. Tolerance inside the matchers cannot span the + // class (the dressing's type text may itself contain a `]`, e.g. + // `as Foo['x']`), so the SHAPE fails closed: a quote-close followed by + // `!`/`as`/`satisfies` inside a bracket. The `!(?!=)` guard keeps + // ordinary comparisons (`o['k'] !== x` — dressing AFTER the bracket) + // clean. + if ( + new RegExp(`\\[\\s*(['"])(?:(?!\\1)[^\\n])*\\1\\s*(?:!(?!=)|as\\s|satisfies\\s)`).test(code) + ) { + violations.push({ + file: rel, + prong: 'code-construction', + detail: 'dressed computed-member key', + }); + } + // A quoted key built from string escapes (a `\` + `u`/`x`/octal-digit + // sequence whose decoded text is a plain identifier, e.g. a key spelling + // `companies` with its first letter unicode-escaped) stays invisible to + // every `\w+`-keyed matcher. The key IS statically + // resolvable, so it is not under the non-literal residual: any bracketed + // quoted key containing an identifier-capable escape fails closed. + if (new RegExp(`\\[\\s*(['"])[^'"\\n]*\\\\[ux0-7][^'"\\n]*\\1\\s*\\]`).test(code)) { + violations.push({ + file: rel, + prong: 'code-construction', + detail: 'escape-built computed-member key', + }); + } + // Invoking a write/exec verb through Function.prototype indirection + // (`db.insert.apply(db, [companies])`, `d.execute.call(d, s)`, + // `db.insert.bind(db)`) hides the argument shape from every verb matcher + // while both member names stay statically visible — unlike the + // method-EXTRACTION residual, where the verb never appears as a member. + if ( + new RegExp( + `(?:${DOT}(?:insert|update|delete|execute|query|unsafe|raw)\\b|` + + `\\[\\s*['"](?:insert|update|delete|execute|query|unsafe|raw)['"]\\s*\\])` + + `${DOT}(?:apply|call|bind)${CALL_OPEN}`, + ).test(code) + ) { + violations.push({ + file: rel, + prong: 'code-construction', + detail: 'verb apply/call/bind indirection', + }); + } + // `Reflect` reaches the same members without member syntax: + // `Reflect.apply(db.insert, db, [companies])`, `Reflect.get(db, + // 'insert')`, `Reflect.getOwnPropertyDescriptor(db, 'execute')`. Any + // Reflect call whose argument text (up to the first `)`) names a verb + // fails closed — the tree has zero Reflect call sites (measured). + if ( + new RegExp( + `\\bReflect${DOT}[\\w$]+${CALL_OPEN}[^)\\n]*\\b(?:insert|update|delete|execute|query|unsafe|raw)\\b`, + ).test(code) + ) { + violations.push({ + file: rel, + prong: 'code-construction', + detail: 'Reflect verb indirection', + }); + } } // Dynamic import whose specifier is not a single string literal: the // import graph becomes unanalyzable. Checked per call site, so a literal @@ -2032,6 +2110,58 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { name: 'E92 template-keyed receiver verb in capability-free file', src: `export class R { constructor(private pool: { execute(s: string): Promise }) {}\n async f(t: string) { await this.pool[\`execute\`].apply(this.pool, ['TRUNCATE ' + t]); } }`, }, + { + name: 'E93 as-dressed computed key as write target', + src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(ns['companies' as const]).values({}); }`, + }, + { + name: 'E94 satisfies-dressed computed key as write target', + src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(ns['companies' satisfies 'companies']).values({}); }`, + }, + { + name: 'E95 non-null-dressed computed key as write target', + src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(ns['companies'!]).values({}); }`, + }, + { + name: 'E96 escape-built computed key as write target', + src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(ns['\\u0063ompanies']).values({}); }`, + }, + { + name: 'E97 dressed computed key in schema conduit export', + src: `import { co } from './evasion-mid41.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(co).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid41.ts', + src: `import * as ns from '@mosaicstack/db';\nexport const co = ns['companies' as const];`, + }, + ], + }, + { + name: 'E98 dressed computed key in schema default export', + src: `import c from './evasion-mid42.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid42.ts', + src: `import * as ns from '@mosaicstack/db';\nexport default ns['companies' as const];`, + }, + ], + }, + { + name: 'E99 dressed computed key in single-file factory extraction', + src: `import * as ns from '@mosaicstack/db';\nexport async function f(t: string) { const mk = ns['createDb' as const]; const d = mk('u'); await d.execute('DELETE FROM ' + t); }`, + }, + { + name: 'E100 Reflect.apply of a write verb', + src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await Reflect.apply(db.insert, db, [companies]); }`, + }, + { + name: 'E101 Reflect.apply of an execution verb in capability-free file', + src: `export class R { constructor(private pool: { execute(s: string): Promise }) {}\n async f(t: string) { await Reflect.apply(this.pool.execute, this.pool, ['TRUNCATE ' + t]); } }`, + }, + { + name: 'E102 Reflect.get extraction of an execution verb', + src: `import { db } from './x.js';\nexport async function f(t: string) { const fn = Reflect.get(db, 'execute') as (s: string) => Promise; await fn.call(db, 'DELETE FROM ' + t); }`, + }, ]; const CLEAN_CONTROLS: Array<{ name: string; src: string }> = [ { @@ -2046,6 +2176,10 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { name: 'clean: interpolated template key is a non-literal computed member, not a text-only key', src: `export function g(o: Record void>, k: string) { o[\`\${k}\`](); }`, }, + { + name: 'clean: strict-inequality after a bracket member is not key dressing', + src: `export function h(o: Record) { return o['kind'] !== 'x'; }`, + }, ]; it('analyzer flags every known evasion form', () => { -- 2.54.0 From 14457a83227c58f5a065f8b4bf6d664260411658 Mon Sep 17 00:00:00 2001 From: fred Date: Thu, 27 Aug 2026 19:24:49 -0500 Subject: [PATCH 10/10] fix(db): close round-9 review evasions in writer-coverage assertion --- .../db/src/hierarchy-writer-coverage.test.ts | 117 ++++++++++++++---- 1 file changed, 94 insertions(+), 23 deletions(-) diff --git a/packages/db/src/hierarchy-writer-coverage.test.ts b/packages/db/src/hierarchy-writer-coverage.test.ts index 0c6e0bcd..dc3bf0b2 100644 --- a/packages/db/src/hierarchy-writer-coverage.test.ts +++ b/packages/db/src/hierarchy-writer-coverage.test.ts @@ -85,12 +85,18 @@ * flagged, and the statically-resolvable DISGUISES of a literal key fail * closed — access or call, in any position: a text-only template-literal * key (obj[`insert`](), ns[`companies`] — template text never reaches - * the lexer's code output), a quoted key carrying expression dressing - * (`ns['companies' as const]`, `ns['companies'!]`), and a quoted key + * the lexer's code output), a quoted OR template key carrying expression + * dressing (`ns['companies' as const]`, `ns['companies'!]`, + * `ns[`companies` as const]` — the dressed rule's key class includes the + * backtick, so the lexed empty backtick pair matches), and a quoted key * built from string escapes (`\u`/`\x`/octal). (A template key WITH * interpolation is a non-literal computed member, above.) Invoking a - * write/exec verb via `.apply`/`.call`/`.bind`, or through any - * `Reflect.*` call naming a verb, fails closed the same way. These + * write/exec verb via `.apply`/`.call`/`.bind` fails closed the same + * way, and ANY appearance of the `Reflect` identifier fails closed + * outright — verb detection through Reflect is not boundable (argument + * windows stop at newlines and nested parens, the method can be + * bracket-spelled, the object aliased), and the tree has zero + * occurrences outside test files. These * code-shape rules match ordinary syntax over ordinary method names, so * they carry their own enumerated disposition (CODE_SHAPE_REGISTER, * empty today): a reviewed legitimate hit is registered, never resolved @@ -1035,18 +1041,25 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { detail: 'template-literal computed member', }); } - // A quoted computed key carrying expression dressing + // A quoted OR template computed key carrying expression dressing // (`ns['companies' as const]`, `ns['companies' satisfies 'companies']`, - // `ns['companies'!]`) keeps its static value while breaking every - // `['name']` matcher — the bracket alternatives require the closing - // quote to touch the `]`. Tolerance inside the matchers cannot span the - // class (the dressing's type text may itself contain a `]`, e.g. - // `as Foo['x']`), so the SHAPE fails closed: a quote-close followed by - // `!`/`as`/`satisfies` inside a bracket. The `!(?!=)` guard keeps - // ordinary comparisons (`o['k'] !== x` — dressing AFTER the bracket) - // clean. + // `ns['companies'!]`, and the template forms `ns[\`companies\` as + // const]`, `ns[\`companies\`!]`) keeps its static value while breaking + // every `['name']` matcher — the bracket alternatives require the + // closing quote to touch the `]`, and a dressed TEMPLATE key also breaks + // the template rule above (dressing intervenes before the `]`). + // Tolerance inside the matchers cannot span the class (the dressing's + // type text may itself contain a `]`, e.g. `as Foo['x']`), so the SHAPE + // fails closed: a quote-close followed by `!`/`as`/`satisfies` inside a + // bracket. The key class includes the backtick: a dressed template key + // survives in lexed code as the empty adjacent-backtick pair, which the + // empty-key case of this regex matches (an interpolated key keeps its + // `\${…}` between the backticks and stays clean unless dressed — a + // dressed interpolated key fires too, an over-match dispositioned like + // any other shape hit). The `!(?!=)` guard keeps ordinary comparisons + // (`o['k'] !== x` — dressing AFTER the bracket) clean. if ( - new RegExp(`\\[\\s*(['"])(?:(?!\\1)[^\\n])*\\1\\s*(?:!(?!=)|as\\s|satisfies\\s)`).test(code) + new RegExp(`\\[\\s*(['"\`])(?:(?!\\1)[^\\n])*\\1\\s*(?:!(?!=)|as\\s|satisfies\\s)`).test(code) ) { violations.push({ file: rel, @@ -1087,18 +1100,22 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { } // `Reflect` reaches the same members without member syntax: // `Reflect.apply(db.insert, db, [companies])`, `Reflect.get(db, - // 'insert')`, `Reflect.getOwnPropertyDescriptor(db, 'execute')`. Any - // Reflect call whose argument text (up to the first `)`) names a verb - // fails closed — the tree has zero Reflect call sites (measured). - if ( - new RegExp( - `\\bReflect${DOT}[\\w$]+${CALL_OPEN}[^)\\n]*\\b(?:insert|update|delete|execute|query|unsafe|raw)\\b`, - ).test(code) - ) { + // 'insert')`, `Reflect.getOwnPropertyDescriptor(db, 'execute')`. Verb + // detection through Reflect is not boundable: any argument window stops + // at a newline (prettier breaks a >100-col call one-arg-per-line, moving + // the verb past it) or at the first `)` (a nested-paren first argument + // closes it early), the method can be bracket-spelled + // (`Reflect['apply']`), and the object can be aliased + // (`const R = Reflect`). So the IDENTIFIER is the shape: any `Reflect` + // token in lexed code fails closed — member access, aliasing, or + // argument passing alike. The tree has zero occurrences outside test + // files (measured); a reviewed legitimate use is register-dispositioned. + // (`\b` keeps compound identifiers like `ReflectHelper` clean.) + if (new RegExp(`\\bReflect\\b`).test(code)) { violations.push({ file: rel, prong: 'code-construction', - detail: 'Reflect verb indirection', + detail: 'Reflect indirection', }); } } @@ -2162,6 +2179,52 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { name: 'E102 Reflect.get extraction of an execution verb', src: `import { db } from './x.js';\nexport async function f(t: string) { const fn = Reflect.get(db, 'execute') as (s: string) => Promise; await fn.call(db, 'DELETE FROM ' + t); }`, }, + { + name: 'E103 as-dressed template key as write target', + src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(ns[\`companies\` as const]).values({}); }`, + }, + { + name: 'E104 as-dressed template key as verb call', + src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db[\`insert\` as const](companies).values({}); }`, + }, + { + name: 'E105 non-null-dressed template key as write target', + src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(ns[\`companies\`!]).values({}); }`, + }, + { + name: 'E106 dressed template receiver verb in capability-free file', + src: `export class R { constructor(private pool: { query(s: string): Promise }) {}\n async f(t: string) { await this.pool[\`query\` as const]('TRUNCATE ' + t); } }`, + }, + { + name: 'E107 dressed template key in schema conduit export', + src: `import { co } from './evasion-mid43.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(co).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid43.ts', + src: `import * as ns from '@mosaicstack/db';\nexport const co = ns[\`companies\` as const];`, + }, + ], + }, + { + name: 'E108 dressed template key in single-file factory extraction', + src: `import * as ns from '@mosaicstack/db';\nexport async function f(t: string) { const mk = ns[\`createDb\` as const]; const d = mk('u'); await d.execute('DELETE FROM ' + t); }`, + }, + { + name: 'E109 multiline Reflect.apply in prettier-broken shape', + src: `export class R { constructor(private pool: { execute(s: string): Promise }) {}\n async f(t: string) {\n await Reflect.apply(\n this.pool.execute,\n this.pool,\n ['TRUNCATE ' + t],\n );\n } }`, + }, + { + name: 'E110 bracket-spelled Reflect method', + src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await Reflect['apply'](db.insert, db, [companies]); }`, + }, + { + name: 'E111 Reflect.apply with nested-paren first argument', + src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await Reflect.apply((fn: () => unknown) => fn(), db.insert, [db, [companies]]); }`, + }, + { + name: 'E112 aliased Reflect', + src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { const R = Reflect; await R.apply(db.insert, db, [companies]); }`, + }, ]; const CLEAN_CONTROLS: Array<{ name: string; src: string }> = [ { @@ -2180,6 +2243,14 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { name: 'clean: strict-inequality after a bracket member is not key dressing', src: `export function h(o: Record) { return o['kind'] !== 'x'; }`, }, + { + name: 'clean: undressed interpolated template key stays a non-literal member under the backtick key class', + src: `export function j(o: Record, k: string) { return o[\`\${k}\`]; }`, + }, + { + name: 'clean: compound identifier containing Reflect is not the Reflect object', + src: `export class ReflectHelper { reflectStyle = 1; }\n// Reflect in a comment is prose, not code`, + }, ]; it('analyzer flags every known evasion form', () => { -- 2.54.0