diff --git a/financial_forecasting/auth.py b/financial_forecasting/auth.py index e0ad7c9f..3ead86f0 100644 --- a/financial_forecasting/auth.py +++ b/financial_forecasting/auth.py @@ -142,6 +142,50 @@ async def get_current_user_dep(request: Request) -> Optional[Dict]: _BEDROCK_INTERNAL_API_KEY = os.getenv("BEDROCK_INTERNAL_API_KEY", "") +# HTTP methods considered "writes" for the kill switch below. Idempotent reads +# (GET, HEAD, OPTIONS) remain available even when the switch is on so that +# Pebble's research/lookup paths keep working during write-side incidents. +_WRITE_METHODS = frozenset({"POST", "PUT", "PATCH", "DELETE"}) + +# Default scope grant for the internal-key principal. v1.0 grants the +# superset; later phases tighten via per-call X-Pebble-Scopes header. +# `*` is interpreted as "all scopes" by check_permission_or_internal. +_DEFAULT_INTERNAL_SCOPES = ("*",) + + +def _pebble_writes_disabled() -> bool: + """Read the kill switch fresh on every call so operators can flip it + without a redeploy. Truthy values: "true" / "1" / "yes" (case-insensitive). + """ + return os.getenv("PEBBLE_WRITES_DISABLED", "").strip().lower() in {"true", "1", "yes"} + + +def _parse_scopes(header_value: str) -> tuple[str, ...]: + """Parse the optional X-Pebble-Scopes header. Comma-separated scope + strings; empty / unset header = full grant via the wildcard ``*``. + """ + raw = (header_value or "").strip() + if not raw: + return _DEFAULT_INTERNAL_SCOPES + scopes = tuple(s.strip() for s in raw.split(",") if s.strip()) + return scopes or _DEFAULT_INTERNAL_SCOPES + + +def _is_valid_originating_user_email(email: str) -> bool: + """Lightweight shape check on X-Originating-User. We don't verify + membership in org_users here (would require a DB hit on every + request); the route-level audit insert (FK to org_users via + originating_user_email lookup) is the durable check. Spoofs against + the API key still produce auditable rows. + """ + if not email or len(email) > 254: + return False + if "@" not in email or email.startswith("@") or email.endswith("@"): + return False + if any(c.isspace() for c in email): + return False + return True + async def require_auth_or_internal(request: Request) -> Dict: """Authorize via internal API key (service-to-service) or user JWT. @@ -150,13 +194,101 @@ async def require_auth_or_internal(request: Request) -> Dict: synthetic service user dict. Otherwise falls back to require_auth. Dev mode: if BEDROCK_INTERNAL_API_KEY is empty, internal key check is skipped and only JWT auth is tried. + + **Mandatory headers when using internal-key auth (Phase 0.2):** + + - ``X-Originating-User``: the human whose session triggered this + service-to-service call. Required on EVERY internal-key request, + reads and writes alike. The email is propagated to + ``bedrock.pebble_write_audit`` and to permission resolution so + that Pebble acts as a delegated principal, not a god principal. + Three independent adversarial reviews flagged the un-attributed + "service:pebble" pattern as a 1.0 blocker; this enforcement + closes that gap. + - ``X-Request-Id``: a UUIDv7 (or any UUID) for replay defense via + ``UNIQUE(request_id)`` on ``bedrock.pebble_write_audit``. Format + checked here; uniqueness checked at the audit-row INSERT in the + route handler. + + Optional header: + + - ``X-Pebble-Scopes``: comma-separated scopes the caller is + requesting for THIS request. Default = ``("*",)`` (full grant). + ``check_permission_or_internal`` verifies the matching scope is + present. Future tightening: Pebble will request narrow scopes + per call instead of carrying the full grant. + + Kill switch: if PEBBLE_WRITES_DISABLED env is truthy AND the caller + is using a valid internal key AND the request method is a write + (POST/PUT/PATCH/DELETE), return 503. Reads via internal key and all + JWT-authenticated requests are unaffected. """ internal_key = request.headers.get("X-Internal-Key", "") if _BEDROCK_INTERNAL_API_KEY and internal_key: if hmac.compare_digest(internal_key, _BEDROCK_INTERNAL_API_KEY): + if _pebble_writes_disabled() and request.method in _WRITE_METHODS: + logger.warning( + "pebble_writes_disabled: blocking %s %s for service caller", + request.method, + request.url.path, + ) + raise HTTPException( + status_code=503, + detail={ + "error": "pebble_writes_disabled", + "message": ( + "Pebble service-account writes are temporarily " + "disabled. Reads remain available." + ), + }, + ) + + originating_user = request.headers.get("X-Originating-User", "").strip() + if not _is_valid_originating_user_email(originating_user): + logger.warning( + "internal_key_missing_originating_user: %s %s", + request.method, + request.url.path, + ) + raise HTTPException( + status_code=401, + detail={ + "error": "originating_user_required", + "message": ( + "X-Originating-User header is required on every " + "internal-key request. Pebble acts on behalf of a " + "specific user, never as itself." + ), + }, + ) + + request_id = request.headers.get("X-Request-Id", "").strip() + if not request_id: + logger.warning( + "internal_key_missing_request_id: %s %s", + request.method, + request.url.path, + ) + raise HTTPException( + status_code=401, + detail={ + "error": "request_id_required", + "message": ( + "X-Request-Id header is required on every " + "internal-key request for replay defense. " + "Pass a UUID." + ), + }, + ) + + scopes = _parse_scopes(request.headers.get("X-Pebble-Scopes", "")) + return { "user_id": "service:pebble", "email": "pebble@internal", "is_service": True, + "originating_user_email": originating_user, + "request_id": request_id, + "scopes": scopes, } return await require_auth(request) diff --git a/financial_forecasting/db/migrations/2026-05-06-pebble-write-audit.sql b/financial_forecasting/db/migrations/2026-05-06-pebble-write-audit.sql new file mode 100644 index 00000000..1eb8bee0 --- /dev/null +++ b/financial_forecasting/db/migrations/2026-05-06-pebble-write-audit.sql @@ -0,0 +1,150 @@ +-- 2026-05-06: bedrock.pebble_write_audit + bedrock.search_audit_access_log +-- +-- Phase 0.1 + 0.7 of the Pebble 1.0 plan (tasks/pebble-search-spec.md). +-- +-- Why: +-- When Pebble (or any service-to-service caller using +-- X-Internal-Key) writes through Bedrock's API, today's audit +-- trail collapses to a single synthetic user "service:pebble" +-- across logger.info lines and SF "LastModifiedById" — the +-- originating end-user is unrecoverable. Three independent +-- reviews (security/UX/architecture adversaries) flagged this +-- as a 1.0 blocker. +-- +-- bedrock.pebble_write_audit captures every internal-key write +-- with the originating user, request id, route, payload hash, +-- and response status. UNIQUE(request_id) doubles as the +-- idempotency / replay-defense store for X-Request-Id (Phase +-- 0.7) — a duplicate request_id within 24h yields a 409 from +-- the API rather than a duplicate write. +-- +-- bedrock.search_audit_access_log captures admins reading +-- other users' search history when query_text is revealed. +-- Lives separately from the search_audit table itself so that +-- RTBF / DSAR purges of search_audit cannot also erase the +-- accountability trail of who looked at what. +-- +-- Related: +-- * tasks/pebble-search-spec.md (decisions §3, §4) +-- * tasks/pebble-search-spec-security.md §1.5, §2, §10.4 +-- * tasks/pebble-overhaul-plan.md §0.1, §0.7 +-- * financial_forecasting/auth.py:require_auth_or_internal +-- (enforces X-Originating-User; this table receives the +-- audit row from each write route) +-- +-- Idempotent — safe to re-run. +-- +-- Apply as bedrock owner: +-- psql "$DATABASE_URL" -f 2026-05-06-pebble-write-audit.sql + +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE EXTENSION IF NOT EXISTS citext; + +CREATE TABLE IF NOT EXISTS bedrock.pebble_write_audit ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(), + + -- Idempotency / replay defense. Required on every internal-key + -- write; UNIQUE means a 24h replay returns 409 from INSERT. + request_id UUID NOT NULL, + + -- Routing / classification. + route TEXT NOT NULL, -- e.g. "/api/opportunities/update-stage" + http_method TEXT NOT NULL CHECK (http_method IN + ('POST', 'PUT', 'PATCH', 'DELETE')), + + -- Subject. nullable so an early-failure audit row (e.g. invalid + -- payload) still gets written for forensics. + sf_object_type TEXT, -- 'Opportunity' | 'Account' | 'Contact' | 'Task' | 'Payment' | 'Award' | 'Project' + sf_object_id TEXT, + + -- Attribution. originating_user_email is the human whose + -- session triggered the workflow; service_user is "service:pebble" + -- (or future siblings). Both required. + originating_user_email CITEXT NOT NULL, + service_user TEXT NOT NULL DEFAULT 'service:pebble', + + -- What was sent. payload_hash = sha256 of canonical JSON; full + -- payload kept for 90 days where the route opts in (sensitive + -- routes pass NULL to suppress). + payload_hash TEXT, + payload JSONB, + + -- What came back. + response_status SMALLINT NOT NULL, + response_summary JSONB, -- e.g. {"award_created": true, "stage": "..."} + error_class TEXT, -- ExceptionClass name when status >= 400 + + -- Side-effect attestation. ensure_for_opp + future event-bus + -- subscribers write back here so a forensic reader can see the + -- full chain from one INSERT (vs. correlating across 3 tables). + side_effects JSONB, -- e.g. {"award_created": true, "activity_logged": true} + + -- Latency for ops dashboards. + latency_ms INT, + + -- Multi-tenant outer guard. Single value today, present so + -- search_audit / pebble_audit share an enforcement pattern. + org_id TEXT NOT NULL DEFAULT 'pursuit', + + -- Replay defense: the duplicate-request_id detector relies on this. + UNIQUE (request_id) +); + +CREATE INDEX IF NOT EXISTS idx_pebble_write_audit_user_time + ON bedrock.pebble_write_audit(originating_user_email, occurred_at DESC); +CREATE INDEX IF NOT EXISTS idx_pebble_write_audit_object + ON bedrock.pebble_write_audit(sf_object_type, sf_object_id, occurred_at DESC) + WHERE sf_object_id IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_pebble_write_audit_errors + ON bedrock.pebble_write_audit(occurred_at DESC) + WHERE response_status >= 400; +CREATE INDEX IF NOT EXISTS idx_pebble_write_audit_route_time + ON bedrock.pebble_write_audit(route, occurred_at DESC); + +COMMENT ON TABLE bedrock.pebble_write_audit IS + 'Audit row for every internal-key write through Bedrock (Phase 0.1). UNIQUE(request_id) doubles as 24h replay defense (Phase 0.7).'; +COMMENT ON COLUMN bedrock.pebble_write_audit.originating_user_email IS + 'Mandatory. The human whose session triggered the workflow. Sourced from X-Originating-User header. require_auth_or_internal rejects calls without it.'; +COMMENT ON COLUMN bedrock.pebble_write_audit.request_id IS + 'Mandatory. UUIDv7 from X-Request-Id header. Replay window = 24h via UNIQUE constraint; deduplicate at the route via INSERT...ON CONFLICT (request_id) DO NOTHING RETURNING id.'; + +-- --------------------------------------------------------------------------- +-- Admin access log for revealing search_audit.query_text +-- --------------------------------------------------------------------------- +-- search_audit itself comes in a later migration (Layer 1). This table is +-- separate so RTBF/DSAR purges of search_audit cannot also erase +-- accountability. +CREATE TABLE IF NOT EXISTS bedrock.search_audit_access_log ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + accessed_at TIMESTAMPTZ NOT NULL DEFAULT now(), + admin_email CITEXT NOT NULL, + target_user_email CITEXT NOT NULL, + revealed_query_text BOOLEAN NOT NULL, + reason TEXT NOT NULL, + request_id UUID NOT NULL, + org_id TEXT NOT NULL DEFAULT 'pursuit' +); + +CREATE INDEX IF NOT EXISTS idx_search_audit_access_admin + ON bedrock.search_audit_access_log(admin_email, accessed_at DESC); +CREATE INDEX IF NOT EXISTS idx_search_audit_access_target + ON bedrock.search_audit_access_log(target_user_email, accessed_at DESC); + +COMMENT ON TABLE bedrock.search_audit_access_log IS + 'Append-only log of admins viewing other users search history with query_text revealed. Survives RTBF purges.'; + +-- --------------------------------------------------------------------------- +-- Grants — bedrock_user owns these tables and writes through the API. +-- --------------------------------------------------------------------------- +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'bedrock_user') THEN + GRANT SELECT, INSERT ON bedrock.pebble_write_audit TO bedrock_user; + GRANT SELECT, INSERT ON bedrock.search_audit_access_log TO bedrock_user; + -- UPDATE intentionally NOT granted on pebble_write_audit: + -- audit rows are append-only (immutability matters for forensics). + -- DELETE limited to a separate retention job role, added when + -- the 90d cleanup script lands. + END IF; +END $$; diff --git a/financial_forecasting/db/migrations/2026-05-06-search-audit.sql b/financial_forecasting/db/migrations/2026-05-06-search-audit.sql new file mode 100644 index 00000000..098953e8 --- /dev/null +++ b/financial_forecasting/db/migrations/2026-05-06-search-audit.sql @@ -0,0 +1,143 @@ +-- 2026-05-06: bedrock.search_audit +-- +-- Layer 1 / security spec §2. Every search query — Find or Ask, +-- human or service — gets one row here. +-- +-- Why: +-- * Compliance / data-access reviews — every query the team +-- makes is recoverable. +-- * Click-through analytics — drives ranking-quality dashboards. +-- * Anomaly detection — exfiltration patterns ("user reading +-- every contact in an hour") are visible to the batch job +-- described in security spec §4. +-- * Debug — "user said they searched X and got Y" is +-- reconstructible. +-- +-- Schema decisions (security spec §2): +-- * query_text stored full at v1.0, length-capped 256 chars at +-- the API. Default dashboards key on query_text_hash; raw +-- query_text reads gated on `manage_users_roles` and +-- themselves audited via bedrock.search_audit_access_log +-- (already shipped in 2026-05-06-pebble-write-audit.sql). +-- * 90 days hot in this table; older rows archived to GCS +-- monthly via Parquet (separate retention job). +-- * Two attribution columns: user_email is the bearer (e.g. +-- pebble@internal for service callers); originating_user_email +-- is the human whose session triggered Pebble. Both required +-- when caller is service. +-- * org_id outer guard from day 1 even though Pursuit is single +-- tenant today. +-- +-- Idempotent — safe to re-run. + +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE EXTENSION IF NOT EXISTS citext; + +CREATE TABLE IF NOT EXISTS bedrock.search_audit ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(), + + -- Correlation. query_id groups one query → its results → its + -- click. request_id is the per-HTTP-call idempotency key + -- (Phase 0.7) so a retried search query shows up as one row, + -- not duplicates. + query_id UUID NOT NULL, + request_id UUID NOT NULL, + + -- Attribution. + user_email CITEXT NOT NULL, -- bearer + originating_user_email CITEXT, -- delegated principal when bearer is service:* + org_id TEXT NOT NULL DEFAULT 'pursuit', + + -- The query. + mode TEXT NOT NULL CHECK (mode IN ('find','ask','past_research')), + query_text TEXT NOT NULL, + query_text_hash TEXT NOT NULL, -- sha256; default dashboard join key + facets JSONB, + types_requested TEXT[] DEFAULT '{}', -- entity_type filter; empty = all + + -- Backend served. + backend_used TEXT NOT NULL CHECK (backend_used IN + ('postgres_fts','pgvector','sf_sosl', + 'sf_sosl_fallback','cache_hit','degraded_empty')), + -- Per-backend latency breakdown for SLO drill-down. + perm_resolution_ms INT, + backend_latency_ms INT, + latency_ms INT NOT NULL, -- end-to-end including auth + serialize + + -- Result set. + result_count INT NOT NULL, -- visible to user + result_count_redacted INT, -- before permission redaction; >= result_count + response_status SMALLINT NOT NULL, + error_class TEXT, -- ExceptionClass when status >= 400 + + -- Click attribution. Updated by /api/search/click; left NULL + -- when no click happened. NULL is itself a metric (0-click + -- queries = poor ranking signal). + click_position SMALLINT, + click_entity_type TEXT, + click_record_id TEXT, + click_at TIMESTAMPTZ, + + -- Cost (Ask mode only). Mirrors pebble_daily_usage shape so + -- per-user budget enforcement reads from one source. + cost_usd NUMERIC(10, 6), + tokens_in INT, + tokens_out INT, + + -- Replay defense via UNIQUE on request_id, mirroring the + -- pebble_write_audit pattern. Safe across the 24h replay + -- window. + UNIQUE (request_id) +); + +-- Per-user history (drives /api/search/history and "user X did Y" lookups). +CREATE INDEX IF NOT EXISTS idx_search_audit_user_time + ON bedrock.search_audit(user_email, occurred_at DESC); + +-- Per-originating-user history when bearer is service:*. Lets us +-- ask "what has Pebble done on Jac's behalf today?" +CREATE INDEX IF NOT EXISTS idx_search_audit_origin_time + ON bedrock.search_audit(originating_user_email, occurred_at DESC) + WHERE originating_user_email IS NOT NULL; + +-- Hash-keyed dashboards (no PII leak). +CREATE INDEX IF NOT EXISTS idx_search_audit_qhash_time + ON bedrock.search_audit(query_text_hash, occurred_at DESC); + +-- Click correlation. +CREATE INDEX IF NOT EXISTS idx_search_audit_qid + ON bedrock.search_audit(query_id); + +-- 0-click queries (ranking-quality signal). +CREATE INDEX IF NOT EXISTS idx_search_audit_no_click + ON bedrock.search_audit(occurred_at DESC) + WHERE click_position IS NULL; + +-- Pebble-driven activity surface. +CREATE INDEX IF NOT EXISTS idx_search_audit_pebble + ON bedrock.search_audit(originating_user_email, occurred_at DESC) + WHERE user_email = 'pebble@internal'; + +-- Errors for ops dashboards. +CREATE INDEX IF NOT EXISTS idx_search_audit_errors + ON bedrock.search_audit(occurred_at DESC, error_class) + WHERE response_status >= 400; + +COMMENT ON TABLE bedrock.search_audit IS + 'Audit row for every search query (Phase 1.3). Populated by routes/search.py via FastAPI BackgroundTask. UNIQUE(request_id) doubles as 24h replay defense.'; +COMMENT ON COLUMN bedrock.search_audit.originating_user_email IS + 'When user_email is service:* (e.g. pebble@internal), this carries the human whose session triggered the workflow. Required for service callers; null for direct human searches.'; +COMMENT ON COLUMN bedrock.search_audit.result_count_redacted IS + 'Count BEFORE permission redaction. result_count_redacted >= result_count always. Delta is the leak surface — a high-delta-low-result query may indicate a permission misconfig.'; + +-- Grants +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'bedrock_user') THEN + GRANT SELECT, INSERT, UPDATE ON bedrock.search_audit TO bedrock_user; + -- UPDATE granted because click attribution post-update on existing + -- rows is the click endpoint's job (only the click_* columns). + -- DELETE intentionally NOT granted — retention via separate role. + END IF; +END $$; diff --git a/financial_forecasting/db/migrations/2026-05-06-search-doc.sql b/financial_forecasting/db/migrations/2026-05-06-search-doc.sql new file mode 100644 index 00000000..c656e385 --- /dev/null +++ b/financial_forecasting/db/migrations/2026-05-06-search-doc.sql @@ -0,0 +1,271 @@ +-- 2026-05-06: bedrock.search_doc + bedrock.search_index_queue +-- +-- Layer 1 of the Pebble 1.0 search system. The denormalized, +-- cross-entity search index that GlobalSearch Find mode and Pebble's +-- Ask-mode tool calls both query against. Replaces the SOSL passthrough +-- at routes/salesforce_search.py. +-- +-- Design — see tasks/pebble-search-spec-backend.md §2 for the full +-- argument. Headlines: +-- +-- * One denormalized table, one row per searchable entity. Cross- +-- entity ranking uses a single query plan vs UNION ALL of N +-- per-entity indexes. +-- * tsvector + GIN on the composed search_vector for lexical match. +-- * pg_trgm + GIN on search_text for typo tolerance ("ed millr" → +-- "Ed Miller"). +-- * pgvector halfvec(768) embedding column reserved for v1.1 +-- semantic-recall overlay; not populated in v1.0. +-- * Permission columns are FK-shaped (owner_sf_id, owner_email, +-- account_sf_id, visibility) so the API layer can compose a +-- pre-filter accessible-id subquery without denormalizing +-- per-user ACL into rows. See backend spec §4 for the rejection +-- of the post-filter and ACL-array alternatives. +-- * Soft-delete via partial index — tombstones in the heap, never +-- in the GIN posting list. +-- * Multi-tenant org_id from day 1, even though Pursuit is single +-- tenant today (security spec §7). +-- +-- search_index_queue is the durable backbone of the indexer worker +-- pattern (backend spec §3). Every entity write enqueues a row; +-- LISTEN/NOTIFY wakes the worker; FOR UPDATE SKIP LOCKED lets us +-- shard later without changing the schema. +-- +-- Idempotent — safe to re-run. + +CREATE EXTENSION IF NOT EXISTS pg_trgm; +CREATE EXTENSION IF NOT EXISTS vector; + +-- --------------------------------------------------------------------------- +-- search_doc +-- --------------------------------------------------------------------------- + +CREATE TABLE IF NOT EXISTS bedrock.search_doc ( + -- Composite identity. The 11-value CHECK constraint is + -- intentionally explicit (not ENUM) so adding a new entity_type + -- is a plain ALTER TABLE rather than the multi-step ALTER TYPE + -- dance Postgres requires for enum mutation. + entity_type TEXT NOT NULL CHECK (entity_type IN ( + 'sf_account','sf_contact','sf_opportunity','sf_task','sf_activity', + 'bedrock_project','bedrock_award','bedrock_saved_view', + 'pebble_profile','pebble_chat_conversation','pebble_batch' + )), + entity_id TEXT NOT NULL, + PRIMARY KEY (entity_type, entity_id), + + -- Display projection. The frontend renders these directly; no + -- post-query JOIN to source tables on the read path. + title TEXT NOT NULL, + subtitle TEXT, + href TEXT NOT NULL, + + -- Lexical search. + search_vector TSVECTOR NOT NULL, + search_text TEXT NOT NULL, + + -- Semantic recall layer. Reserved for v1.1; v1.0 leaves this + -- column NULL. Defining it now means we don't have to + -- ALTER TABLE on a 200k-row hot table later. + embedding halfvec(768), + + -- Permission columns. Pre-filter via accessible-id subquery + -- (backend spec §4). owner_sf_id is the SF user id when the + -- entity is SF-mirrored; owner_email is the Bedrock-side + -- owner. account_sf_id is the parent account for permission + -- inheritance (Contact / Opp / Task visible to whoever can + -- see the parent Account, plus their direct owner). + owner_sf_id TEXT, + owner_email CITEXT, + account_sf_id TEXT, + visibility TEXT NOT NULL DEFAULT 'org' + CHECK (visibility IN ('private','team','org')), + + -- Recency signal. Computed at ranking time as + -- ts_rank_cd(...) * exp(-EXTRACT(epoch FROM now() - activity_at) / (86400 * 30)) + -- so half-life is tunable without reindexing. + activity_at TIMESTAMPTZ, + + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + deleted_at TIMESTAMPTZ, + + -- Indexer metadata. source_version is the source row's + -- version (e.g. SF SystemModstamp or bedrock row's updated_at) + -- so the indexer can skip re-indexing when the source hasn't + -- changed since indexed_at. + indexed_at TIMESTAMPTZ NOT NULL DEFAULT now(), + source_version TIMESTAMPTZ, + + -- Multi-tenant outer guard. + org_id TEXT NOT NULL DEFAULT 'pursuit' +); + +-- Lexical search index. Partial on deleted_at IS NULL means +-- soft-deleted tombstones never enter the GIN posting list — they +-- still consume heap, but search ignores them. +CREATE INDEX IF NOT EXISTS idx_search_doc_fts + ON bedrock.search_doc USING GIN(search_vector) + WHERE deleted_at IS NULL; + +-- Trigram fuzzy match for typo tolerance. Same partial predicate. +CREATE INDEX IF NOT EXISTS idx_search_doc_trgm + ON bedrock.search_doc USING GIN(search_text gin_trgm_ops) + WHERE deleted_at IS NULL; + +-- Permission filter inputs. +CREATE INDEX IF NOT EXISTS idx_search_doc_owner_sf + ON bedrock.search_doc(owner_sf_id) + WHERE deleted_at IS NULL AND owner_sf_id IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_search_doc_owner_email + ON bedrock.search_doc(owner_email) + WHERE deleted_at IS NULL AND owner_email IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_search_doc_account + ON bedrock.search_doc(account_sf_id) + WHERE deleted_at IS NULL AND account_sf_id IS NOT NULL; + +-- Multi-tenant guard. +CREATE INDEX IF NOT EXISTS idx_search_doc_org + ON bedrock.search_doc(org_id) + WHERE deleted_at IS NULL; + +-- Per-entity-type filtering for facets ("show me only Opps"). +CREATE INDEX IF NOT EXISTS idx_search_doc_entity + ON bedrock.search_doc(entity_type) + WHERE deleted_at IS NULL; + +-- Recency tiebreaker / "recent" scope. +CREATE INDEX IF NOT EXISTS idx_search_doc_activity + ON bedrock.search_doc(activity_at DESC) + WHERE deleted_at IS NULL AND activity_at IS NOT NULL; + +-- pgvector index reserved for v1.1. Created as a no-op now (no +-- rows have embeddings yet) so the schema is fixed-shape before +-- backfill begins. +CREATE INDEX IF NOT EXISTS idx_search_doc_embedding_hnsw + ON bedrock.search_doc USING hnsw (embedding halfvec_cosine_ops) + WHERE embedding IS NOT NULL; + +-- --------------------------------------------------------------------------- +-- search_vector composer trigger. Single weighting policy so the +-- relevance contract is in one place, not scattered across writers. +-- --------------------------------------------------------------------------- + +CREATE OR REPLACE FUNCTION bedrock.search_doc_compose_vector() RETURNS trigger AS $$ +BEGIN + NEW.search_vector := + setweight(to_tsvector('english', COALESCE(NEW.title, '')), 'A') || + setweight(to_tsvector('english', COALESCE(NEW.subtitle, '')), 'B') || + setweight(to_tsvector('english', COALESCE(NEW.search_text, '')), 'C'); + NEW.updated_at := now(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_trigger WHERE tgname = 'trg_search_doc_compose' + ) THEN + CREATE TRIGGER trg_search_doc_compose + BEFORE INSERT OR UPDATE ON bedrock.search_doc + FOR EACH ROW EXECUTE FUNCTION bedrock.search_doc_compose_vector(); + END IF; +END $$; + +-- --------------------------------------------------------------------------- +-- search_index_queue — durable hand-off from source-table writes +-- to the asynchronous indexer worker. +-- --------------------------------------------------------------------------- + +CREATE TABLE IF NOT EXISTS bedrock.search_index_queue ( + id BIGSERIAL PRIMARY KEY, + entity_type TEXT NOT NULL, + entity_id TEXT NOT NULL, + op TEXT NOT NULL CHECK (op IN ('upsert','delete')), + enqueued_at TIMESTAMPTZ NOT NULL DEFAULT now(), + attempt_count INT NOT NULL DEFAULT 0, + last_error TEXT, + + -- Coalesce: multiple enqueues of the same entity collapse to + -- one row; ON CONFLICT bumps enqueued_at so the worker + -- processes them in FIFO order without missing any. + UNIQUE (entity_type, entity_id, op) +); + +-- Drain index. Worker uses +-- SELECT ... FROM bedrock.search_index_queue +-- ORDER BY enqueued_at ASC FOR UPDATE SKIP LOCKED LIMIT 100 +-- so this index serves both the order-by and the lock-skip. +CREATE INDEX IF NOT EXISTS idx_search_queue_drain + ON bedrock.search_index_queue(enqueued_at); + +-- Failed-row index. Periodic reconciliation reads attempt_count > 0 +-- to surface stuck rows. +CREATE INDEX IF NOT EXISTS idx_search_queue_stuck + ON bedrock.search_index_queue(attempt_count, enqueued_at) + WHERE attempt_count > 0; + +-- --------------------------------------------------------------------------- +-- Generic enqueue trigger. Source-table triggers reference this +-- with a TG_ARGV[0] entity_type tag. Bedrock-native source tables +-- get one CREATE TRIGGER apiece in subsequent migrations. +-- --------------------------------------------------------------------------- + +CREATE OR REPLACE FUNCTION bedrock.enqueue_search_index() RETURNS trigger AS $$ +DECLARE + et TEXT := TG_ARGV[0]; + eid TEXT; + op_val TEXT; +BEGIN + IF TG_OP = 'DELETE' THEN + eid := OLD.id::TEXT; + op_val := 'delete'; + ELSE + eid := NEW.id::TEXT; + op_val := 'upsert'; + END IF; + + INSERT INTO bedrock.search_index_queue (entity_type, entity_id, op) + VALUES (et, eid, op_val) + ON CONFLICT (entity_type, entity_id, op) DO UPDATE + SET enqueued_at = now(), attempt_count = 0, last_error = NULL; + + -- LISTEN/NOTIFY wakes the worker; payload includes the key so a + -- cleverer worker could skip the SELECT for the hot path. + PERFORM pg_notify('bedrock_search_index_queue', et || ':' || eid); + + RETURN COALESCE(NEW, OLD); +END; +$$ LANGUAGE plpgsql; + +-- --------------------------------------------------------------------------- +-- updated_at trigger reuses the existing helper. +-- --------------------------------------------------------------------------- + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_trigger WHERE tgname = 'trg_search_doc_updated_at' + ) THEN + CREATE TRIGGER trg_search_doc_updated_at + BEFORE UPDATE ON bedrock.search_doc + FOR EACH ROW EXECUTE FUNCTION bedrock.set_updated_at(); + END IF; +END $$; + +COMMENT ON TABLE bedrock.search_doc IS + 'Denormalized cross-entity search index (Phase 1.1). One row per searchable entity; populated by the queue-drain indexer worker. Permission filtering is pre-filter via accessible-id subquery.'; +COMMENT ON COLUMN bedrock.search_doc.embedding IS + 'pgvector halfvec(768) for v1.1 semantic-recall overlay. NULL in v1.0 — schema reserved so backfill never has to ALTER.'; +COMMENT ON TABLE bedrock.search_index_queue IS + 'Durable queue from source-table writes to the indexer worker. ON CONFLICT coalesces duplicates; LISTEN/NOTIFY on bedrock_search_index_queue wakes the drain.'; + +-- Grants +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'bedrock_user') THEN + GRANT SELECT, INSERT, UPDATE, DELETE ON bedrock.search_doc TO bedrock_user; + GRANT SELECT, INSERT, UPDATE, DELETE ON bedrock.search_index_queue TO bedrock_user; + GRANT USAGE ON SEQUENCE bedrock.search_index_queue_id_seq TO bedrock_user; + END IF; +END $$; diff --git a/financial_forecasting/db/migrations/2026-05-06-search-source-triggers.sql b/financial_forecasting/db/migrations/2026-05-06-search-source-triggers.sql new file mode 100644 index 00000000..618b6e86 --- /dev/null +++ b/financial_forecasting/db/migrations/2026-05-06-search-source-triggers.sql @@ -0,0 +1,82 @@ +-- 2026-05-06: source-table triggers wiring bedrock entities to the +-- search index queue. +-- +-- Layer 1.6 of the Pebble 1.0 plan. Adds AFTER-INSERT/UPDATE/DELETE +-- triggers on the bedrock-side source tables so every write enqueues +-- a search_index_queue row. The indexer worker drains the queue and +-- composes search_doc rows. +-- +-- Sources wired in this migration: +-- * bedrock.project → entity_type 'bedrock_project' +-- * bedrock.saved_view → entity_type 'bedrock_saved_view' +-- * bedrock.pebble_research_sessions → entity_type 'pebble_profile' +-- (only when status='completed' — +-- in-progress sessions don't +-- appear in search) +-- +-- SF-mirrored sources (Account, Contact, Opportunity, Task, Activity) +-- get wired in a separate migration once the SF mirror tables exist +-- (Layer 1.4). Award (bedrock.award) is wired here too — name is +-- composed from the linked Opportunity which we don't yet mirror; for +-- v1 the composer can defer until Layer 1.4 lands. +-- +-- Idempotent — DROP TRIGGER IF EXISTS before each CREATE. + +-- --------------------------------------------------------------------------- +-- bedrock.project +-- --------------------------------------------------------------------------- + +DROP TRIGGER IF EXISTS trg_project_search_index ON bedrock.project; +CREATE TRIGGER trg_project_search_index + AFTER INSERT OR UPDATE OR DELETE ON bedrock.project + FOR EACH ROW EXECUTE FUNCTION bedrock.enqueue_search_index('bedrock_project'); + +-- --------------------------------------------------------------------------- +-- bedrock.saved_view +-- --------------------------------------------------------------------------- + +DROP TRIGGER IF EXISTS trg_saved_view_search_index ON bedrock.saved_view; +CREATE TRIGGER trg_saved_view_search_index + AFTER INSERT OR UPDATE OR DELETE ON bedrock.saved_view + FOR EACH ROW EXECUTE FUNCTION bedrock.enqueue_search_index('bedrock_saved_view'); + +-- --------------------------------------------------------------------------- +-- bedrock.pebble_research_sessions +-- +-- Only enqueue when status transitions into 'completed' OR for +-- already-completed rows on UPDATE. In-progress sessions don't appear +-- in search; the composer would return None and we'd waste a queue +-- cycle. Use a WHEN clause to short-circuit at the trigger. +-- --------------------------------------------------------------------------- + +DROP TRIGGER IF EXISTS trg_pebble_research_sessions_search_index ON bedrock.pebble_research_sessions; +CREATE TRIGGER trg_pebble_research_sessions_search_index + AFTER INSERT OR UPDATE ON bedrock.pebble_research_sessions + FOR EACH ROW + WHEN (NEW.status = 'completed') + EXECUTE FUNCTION bedrock.enqueue_search_index('pebble_profile'); + +-- DELETE needs its own trigger since OLD-only refs aren't valid in +-- the WHEN clause for AFTER-DELETE (NEW is null). +DROP TRIGGER IF EXISTS trg_pebble_research_sessions_search_delete ON bedrock.pebble_research_sessions; +CREATE TRIGGER trg_pebble_research_sessions_search_delete + AFTER DELETE ON bedrock.pebble_research_sessions + FOR EACH ROW EXECUTE FUNCTION bedrock.enqueue_search_index('pebble_profile'); + +-- --------------------------------------------------------------------------- +-- bedrock.award +-- +-- The composer for entity_type='bedrock_award' isn't shipped yet +-- (needs Opp.Name from a future SF mirror). We still wire the trigger +-- so by the time the composer lands, the queue has every existing +-- award row already enqueued — no separate backfill pass needed. +-- +-- Until the composer is registered, the indexer logs +-- 'no_composer' and leaves the queue row in place (silent skip, +-- no failure bump). Confirmed in test_drain_once_no_composer_leaves_queue_intact. +-- --------------------------------------------------------------------------- + +DROP TRIGGER IF EXISTS trg_award_search_index ON bedrock.award; +CREATE TRIGGER trg_award_search_index + AFTER INSERT OR UPDATE OR DELETE ON bedrock.award + FOR EACH ROW EXECUTE FUNCTION bedrock.enqueue_search_index('bedrock_award'); diff --git a/financial_forecasting/db/migrations/2026-05-06-sf-picklist-cache.sql b/financial_forecasting/db/migrations/2026-05-06-sf-picklist-cache.sql new file mode 100644 index 00000000..7f1c924d --- /dev/null +++ b/financial_forecasting/db/migrations/2026-05-06-sf-picklist-cache.sql @@ -0,0 +1,100 @@ +-- 2026-05-06: bedrock.sf_picklist_cache +-- +-- Phase 0.9 of the Pebble 1.0 plan. Single source of truth for +-- Salesforce picklist values (Opportunity.StageName today; extends +-- to other picklists as needed) so that the eight independent +-- locations carrying renamed stage strings (`models.py`, +-- `frontend-v2/lib/stages.ts`, `funnelStages.ts`, +-- `StageProgression.tsx`, `Cleanup.tsx`, `crm_parser.py`, +-- `opportunities_extra.py:53`, `awards_service.ELIGIBLE_STAGES_BY_RECORD_TYPE`) +-- collapse into one fetch. +-- +-- Why: +-- Per `feedback_sf_stages_sacred.md`: SF stages are sacred — +-- never hide/deprecate/reclassify them on the SF side. But +-- stages DO get renamed on the SF side (commit 58c360e was +-- "rename SF stages to match new picklist labels"), and when +-- they do, eight files in the Bedrock codebase need +-- hand-edits. This cache is the canonical Bedrock-side +-- reflection of what's in SF — one fetch, everyone uses it. +-- +-- Design: +-- One row per (object, picklist_field, record_type, value). +-- `record_type` is nullable — when null, the value is +-- available across all record types. Refreshed nightly via a +-- background job calling SF describeSObject. 24h TTL on +-- `services/sf_stages.py` reads. +-- +-- Related: +-- * tasks/pebble-search-spec.md decision §6 (multi-tenant column) +-- * tasks/pebble-overhaul-plan.md §0.9 +-- * services/sf_stages.py (next file) +-- * project_stage_schema_drift memory (eight-location problem) +-- +-- Idempotent — safe to re-run. + +CREATE TABLE IF NOT EXISTS bedrock.sf_picklist_cache ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + + -- Composite identity + sf_object TEXT NOT NULL, -- 'Opportunity' | 'Account' | ... + field_name TEXT NOT NULL, -- 'StageName' | 'Type' | ... + record_type TEXT, -- 'Philanthropy' | NULL = all + value TEXT NOT NULL, -- the actual SF picklist value + + -- Display + ordering. Mirror of SF metadata so we don't + -- per-page-fetch. Order is important — the funnel display in + -- frontend-v2/lib/funnelStages.ts encodes a left-to-right + -- progression. + label TEXT NOT NULL, + sort_order INT NOT NULL DEFAULT 0, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + is_default BOOLEAN NOT NULL DEFAULT FALSE, + + -- Bucket annotations. Reporting semantics live as buckets ON + -- TOP of stages — never replacing them. Encoded as a string + -- array so we can have e.g. + -- ['REVENUE_EARNING','OPEN_PIPELINE'] without joining a + -- second table. The `bedrock` Python service exposes these + -- as Set to match the F1 buckets PR #134 contract. + buckets TEXT[] NOT NULL DEFAULT '{}', + + -- Probability for forecast math. Mirrors `models.py` static + -- table; live picklist values without an entry default to + -- the median bucket prob. + probability SMALLINT, + + -- Sync metadata. Used by `services/sf_stages.py` cache to + -- decide refresh urgency. + fetched_at TIMESTAMPTZ NOT NULL DEFAULT now(), + refresh_after TIMESTAMPTZ NOT NULL DEFAULT (now() + INTERVAL '24 hours'), + + -- Multi-tenant outer guard. + org_id TEXT NOT NULL DEFAULT 'pursuit', + + UNIQUE (org_id, sf_object, field_name, record_type, value) +); + +CREATE INDEX IF NOT EXISTS idx_sf_picklist_cache_lookup + ON bedrock.sf_picklist_cache(sf_object, field_name, record_type, sort_order) + WHERE is_active = TRUE; +CREATE INDEX IF NOT EXISTS idx_sf_picklist_cache_buckets + ON bedrock.sf_picklist_cache USING GIN (buckets); +CREATE INDEX IF NOT EXISTS idx_sf_picklist_cache_stale + ON bedrock.sf_picklist_cache(refresh_after) + WHERE is_active = TRUE; + +COMMENT ON TABLE bedrock.sf_picklist_cache IS + 'Canonical Bedrock-side reflection of Salesforce picklist values. services/sf_stages.py is the read API. Refreshed via nightly background job.'; +COMMENT ON COLUMN bedrock.sf_picklist_cache.buckets IS + 'Reporting bucket annotations on top of SF stages. Examples: {"REVENUE_EARNING","OPEN_PIPELINE"}. Buckets layer on top of stages — never replace them.'; +COMMENT ON COLUMN bedrock.sf_picklist_cache.record_type IS + 'NULL means "applies to all record types". Mirrors how SF stores per-RecordType picklist subset rules.'; + +-- Grants +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'bedrock_user') THEN + GRANT SELECT, INSERT, UPDATE ON bedrock.sf_picklist_cache TO bedrock_user; + END IF; +END $$; diff --git a/financial_forecasting/db/migrations/2026-05-07-pebble-chat-scratchpad.sql b/financial_forecasting/db/migrations/2026-05-07-pebble-chat-scratchpad.sql new file mode 100644 index 00000000..54d93fcf --- /dev/null +++ b/financial_forecasting/db/migrations/2026-05-07-pebble-chat-scratchpad.sql @@ -0,0 +1,110 @@ +-- 2026-05-07: bedrock.pebble_chat_scratchpad +-- +-- Externalized state for the Pebble chat orchestrator. Every step of +-- every conversation persists here: plan emission, tool calls, tool +-- results, evaluations, render, conflicts, checkpoints, errors. The +-- table is the foundation for replay, debugging, training-signal +-- extraction, and the human-visible "what is Pebble doing" plan view. +-- +-- Design — see tasks/pebble-bi-architect.md "Schemas" section. Headlines: +-- +-- * One row per step. Tree-shaped via parent_step_id so re-plans +-- branch (the original plan stays for forensics; the re-plan +-- hangs off the failed evaluation step's id). +-- * step_type CHECK constraint enumerates the legal step kinds — +-- same set the orchestrator emits as SSE events to the FE. +-- * tool_name + tool_args + tool_result NULL when the step isn't +-- a tool call (e.g. plan emission, evaluation). +-- * cost_usd + duration_ms drive bounded-autonomy enforcement and +-- post-run cost telemetry. +-- * org_id outermost guard from day 1. user_email is required so +-- the audit + replay surfaces don't have an "unattributed" row. +-- * Indices: (conversation_id, step_number) for replay; (user_email, +-- created_at) for "what has user X asked recently"; partial on +-- errors so the alerting query stays fast. +-- +-- Idempotent. + +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE EXTENSION IF NOT EXISTS citext; + +CREATE TABLE IF NOT EXISTS bedrock.pebble_chat_scratchpad ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + conversation_id UUID NOT NULL, + parent_step_id UUID, -- nullable; root steps point at none + step_number INT NOT NULL, -- monotonic within conversation_id + step_type TEXT NOT NULL CHECK (step_type IN ( + 'plan', + 'tool_call', + 'tool_result', + 'evaluation', + 'render', + 'conflict', + 'checkpoint', + 'error' + )), + + -- Tool details. NULL for non-tool steps. + tool_name TEXT, + tool_args JSONB, + tool_result JSONB, + + -- Plan / evaluation details. JSONB-shaped; readers know how to + -- interpret based on step_type. + payload JSONB, + + -- Resource accounting. + cost_usd NUMERIC(10, 6), + duration_ms INT, + tokens_in INT, + tokens_out INT, + + -- Attribution. user_email is the originating human (Pebble is + -- always delegated). org_id is the multi-tenant outermost guard. + user_email CITEXT NOT NULL, + org_id TEXT NOT NULL DEFAULT 'pursuit', + + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- Replay index: walking a conversation step-by-step. +CREATE INDEX IF NOT EXISTS idx_chat_scratchpad_conv_step + ON bedrock.pebble_chat_scratchpad(conversation_id, step_number); + +-- Per-user history: drives /pebble/activity surface. +CREATE INDEX IF NOT EXISTS idx_chat_scratchpad_user_time + ON bedrock.pebble_chat_scratchpad(user_email, created_at DESC); + +-- Tree: child-step lookups when reconstructing branched re-plans. +CREATE INDEX IF NOT EXISTS idx_chat_scratchpad_parent + ON bedrock.pebble_chat_scratchpad(parent_step_id) + WHERE parent_step_id IS NOT NULL; + +-- Errors / checkpoints: for ops dashboards + anomaly detection. +CREATE INDEX IF NOT EXISTS idx_chat_scratchpad_errors + ON bedrock.pebble_chat_scratchpad(created_at DESC) + WHERE step_type IN ('error', 'conflict', 'checkpoint'); + +-- Tool-call cost rollup: the daily-cost cap (Phase 0.4 in +-- pebble_proxy.py) reads pebble_daily_usage; the per-conversation +-- cap reads from this index. +CREATE INDEX IF NOT EXISTS idx_chat_scratchpad_conv_cost + ON bedrock.pebble_chat_scratchpad(conversation_id, cost_usd) + WHERE cost_usd IS NOT NULL; + +COMMENT ON TABLE bedrock.pebble_chat_scratchpad IS + 'Externalized state for Pebble chat orchestrator. One row per step. Replay-able. tasks/pebble-bi-architect.md'; +COMMENT ON COLUMN bedrock.pebble_chat_scratchpad.parent_step_id IS + 'NULL for root steps. Points at the parent step_id when this step is part of a re-plan or branched execution. Tree shape preserves the original failed plan for forensics.'; +COMMENT ON COLUMN bedrock.pebble_chat_scratchpad.step_type IS + 'plan = planner emitted a Plan; tool_call = orchestrator invoked a tool; tool_result = tool returned; evaluation = evaluator scored a response; render = renderer composed final answer; conflict = two tool results disagreed; checkpoint = human-in-loop pause (e.g. propose_write); error = step failed with cause in payload.'; + +-- Grants +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'bedrock_user') THEN + GRANT SELECT, INSERT ON bedrock.pebble_chat_scratchpad TO bedrock_user; + -- No UPDATE: scratchpad is append-only. No DELETE: retention + -- job uses a separate role. + END IF; +END $$; diff --git a/financial_forecasting/db/migrations/2026-05-18-pebble-access-launch-dark.sql b/financial_forecasting/db/migrations/2026-05-18-pebble-access-launch-dark.sql new file mode 100644 index 00000000..b4661f01 --- /dev/null +++ b/financial_forecasting/db/migrations/2026-05-18-pebble-access-launch-dark.sql @@ -0,0 +1,151 @@ +-- 2026-05-18: pebble_access permission key + per-user override mechanism +-- + JP-only launch-dark seed +-- +-- Wave 0 of the Pebble L2 Research Swarm plan, launch-dark cohort. +-- +-- Why: +-- Pebble (existing + new L2 swarm) is landing on main as a +-- launch-dark feature: code is shipped, but only jp@pursuit.org +-- can access it. Even other admins (Jac, etc.) cannot see the +-- Pebble sidebar entry or hit any /api/pebble/* route. +-- +-- Two existing facts in the permission system make this +-- non-trivial to express via the current permission_profile + +-- user_config tables: +-- +-- 1. routes/permissions.py:113-115 — Admin profile FORCES +-- every key in PERMISSION_KEYS to true via setdefault. +-- So simply adding a new permission key would auto-grant +-- it to every Admin, defeating the JP-only intent. +-- +-- 2. bedrock.user_config has no per-user override mechanism +-- today — it links org_user_id → profile_id, period. +-- Per-user grants require either a new profile per user +-- (proliferation) or a new override column. +-- +-- This migration solves both: +-- +-- A. Adds pebble_access: false to each of the 4 profiles +-- (Admin, RM, Executive, PM). pebble_access will be +-- explicitly excluded from the Admin auto-fill in +-- routes/permissions.py via a new ADMIN_AUTOFILL_EXCLUDED +-- constant (paired migration in that file). +-- +-- B. Adds bedrock.user_config.permission_overrides JSONB +-- column defaulting to '{}'. get_user_permissions applies +-- overrides as the LAST layer (profile → admin-autofill → +-- overrides) so a per-user override can both grant +-- (pebble_access=true) and deny (pebble_access=false). +-- +-- C. Seeds permission_overrides = {"pebble_access": true} +-- for jp@pursuit.org's user_config row, creating the +-- row if it doesn't exist yet. +-- +-- This generalizes beyond launch-dark Pebble: any future +-- per-user grant or deny ("Jac needs view_projects=false during +-- audit period") uses the same column. +-- +-- Future widening: +-- When Pebble opens up beyond JP, change the per-profile default +-- from false → true for the desired profiles (e.g. Admin) via a +-- follow-up migration. The user_config overrides continue to win +-- for any individual exception. +-- +-- Related: +-- * routes/permissions.py PERMISSION_KEYS + get_user_permissions +-- * routes/pebble_proxy.py require_ask_perm +-- * pebble/main.py check_pebble_perm +-- * frontend-v2 useUserPermissions + AppShell sidebar +-- * ~/.claude/plans/glistening-crafting-matsumoto.md (the gate +-- discussion follows JP's 2026-05-18 directive: "Pebble is +-- accessible to jp@pursuit.org when logged in. Only JP can +-- access it, not even other admins.") +-- +-- Idempotent — safe to re-run. + +-- --------------------------------------------------------------------------- +-- A. Add pebble_access to all 4 profiles, default false +-- --------------------------------------------------------------------------- +-- The jsonb_set with create_missing=true ensures the key lands even +-- if a profile somehow lacked it before. WHERE clause limits to known +-- production profile names so a hand-rolled experimental profile +-- isn't accidentally rewritten. +UPDATE bedrock.permission_profile +SET permissions = jsonb_set( + COALESCE(permissions, '{}'::jsonb), + '{pebble_access}', + 'false'::jsonb, + true + ), + updated_at = now() +WHERE name IN ('Admin', 'Relationship Manager', 'Executive', 'Project Manager'); + +-- --------------------------------------------------------------------------- +-- B. Add permission_overrides JSONB column to user_config +-- --------------------------------------------------------------------------- +-- DEFAULT '{}' so the JOIN-and-merge in get_user_permissions never +-- sees NULL. NOT NULL for the same reason — simpler to reason about. +ALTER TABLE bedrock.user_config + ADD COLUMN IF NOT EXISTS permission_overrides JSONB NOT NULL DEFAULT '{}'::jsonb; + +COMMENT ON COLUMN bedrock.user_config.permission_overrides IS + 'Per-user permission overlay. Applied AFTER profile permissions and Admin auto-fill in routes/permissions.py:get_user_permissions. A key set here wins regardless of profile. Used for launch-dark gates and individual exceptions.'; + +-- --------------------------------------------------------------------------- +-- C. JP-only launch-dark seed +-- --------------------------------------------------------------------------- +-- Three steps: +-- 1. Locate jp@pursuit.org in public.org_users (may not exist +-- on a fresh local dev DB). +-- 2. Ensure a user_config row exists for that org_user_id. +-- 3. Merge {"pebble_access": true} into permission_overrides. +-- +-- Wrapped in a DO block so we can NOTICE-and-continue when +-- public.org_users isn't reachable (e.g. local dev without the +-- learning platform schema). +DO $$ +DECLARE + jp_org_user_id UUID; + admin_profile_id UUID; +BEGIN + BEGIN + SELECT id INTO jp_org_user_id + FROM public.org_users + WHERE LOWER(email) = 'jp@pursuit.org' + LIMIT 1; + EXCEPTION + WHEN insufficient_privilege THEN + RAISE NOTICE 'Skipped JP seed — restricted access to public.org_users'; + RETURN; + WHEN undefined_table THEN + RAISE NOTICE 'Skipped JP seed — public.org_users does not exist on this DB'; + RETURN; + END; + + IF jp_org_user_id IS NULL THEN + RAISE NOTICE 'jp@pursuit.org not in public.org_users yet — re-run this migration after first JP login, or insert manually.'; + RETURN; + END IF; + + -- Pre-resolve Admin profile id. If we end up creating a fresh + -- user_config row for JP (no row exists yet) we MUST point it at + -- Admin — without this, the new row would have profile_id=NULL + -- and get_user_permissions would fall through to the default + -- profile (Relationship Manager) at next login, silently + -- demoting JP to RM with only the pebble_access override. The + -- ON CONFLICT path doesn't touch profile_id so existing rows + -- keep their current profile. + SELECT id INTO admin_profile_id + FROM bedrock.permission_profile + WHERE name = 'Admin' + LIMIT 1; + + INSERT INTO bedrock.user_config (org_user_id, profile_id, permission_overrides) + VALUES (jp_org_user_id, admin_profile_id, '{"pebble_access": true}'::jsonb) + ON CONFLICT (org_user_id) DO UPDATE SET + permission_overrides = COALESCE(bedrock.user_config.permission_overrides, '{}'::jsonb) + || '{"pebble_access": true}'::jsonb, + updated_at = now(); + + RAISE NOTICE 'JP launch-dark gate seeded for jp@pursuit.org (org_user_id=%, admin_profile_id=%)', jp_org_user_id, admin_profile_id; +END $$; diff --git a/financial_forecasting/db/migrations/2026-05-18-pebble-ledger-instrumentation.sql b/financial_forecasting/db/migrations/2026-05-18-pebble-ledger-instrumentation.sql new file mode 100644 index 00000000..21d63e5b --- /dev/null +++ b/financial_forecasting/db/migrations/2026-05-18-pebble-ledger-instrumentation.sql @@ -0,0 +1,187 @@ +-- 2026-05-18: pebble_harness_log cache-aware columns + pebble_tool_call_log +-- +-- Wave 0 of the Pebble L2 Research Swarm plan +-- (~/.claude/plans/glistening-crafting-matsumoto.md §4.12). +-- +-- Why: +-- The plan replaces estimate-based budget enforcement with a +-- real-time ledger. The Anthropic SDK response.usage object +-- returns four token counts, not two: +-- input_tokens, output_tokens, +-- cache_creation_input_tokens, cache_read_input_tokens +-- Today pebble_harness_log only captures the first two. The +-- cache_creation tokens are priced at 1.25× the normal input +-- rate; cache_read tokens are priced at 0.10× — a 10x cost cut +-- when re-using a stable system-prompt prefix across calls. +-- Without recording them, the ledger over-reports cost on +-- cache hits and under-reports on cache creation, and the +-- cockpit's "Cache: 74%" hit-rate display is impossible. +-- +-- pebble_tool_call_log captures non-LLM tool calls (FEC, +-- ProPublica, OpenCorporates HTTP fetches) which today +-- bypass ModelClient entirely and are therefore invisible +-- to the cost surface. Free APIs cost $0 USD but the count +-- and rate-limit-remaining matter for runaway detection +-- (Meta-Observer §4.4 needs tool-call counts to compute +-- "tool-call burn rate vs allocated cap"). +-- +-- New columns on pebble_harness_log — session_id, purpose, +-- cluster, tier, provider — let the cockpit query +-- "what did session X spend by cluster" without parsing +-- pebble_research_sessions.agents_log_json (which is +-- opaque blob today). +-- +-- Related: +-- * ~/.claude/plans/glistening-crafting-matsumoto.md §4.5, §4.12 +-- * pebble/model_client.py:197-201, :263-266, :279-286 +-- * pebble/storage/db.py:92-141 (log_harness_outcome, +-- increment_daily_usage) +-- * pebble/harness.py:549-559 (HarnessResult.tokens_used) +-- +-- Idempotent — safe to re-run. ADD COLUMN IF NOT EXISTS guards +-- against re-application. +-- +-- Apply as bedrock owner: +-- psql "$DATABASE_URL" -f 2026-05-18-pebble-ledger-instrumentation.sql + +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- --------------------------------------------------------------------------- +-- Extend pebble_harness_log with cache-aware token columns + run context +-- --------------------------------------------------------------------------- +-- Defaults to 0 on existing rows so historical analytics don't break. +ALTER TABLE bedrock.pebble_harness_log + ADD COLUMN IF NOT EXISTS cache_creation_input_tokens INTEGER NOT NULL DEFAULT 0, + ADD COLUMN IF NOT EXISTS cache_read_input_tokens INTEGER NOT NULL DEFAULT 0, + ADD COLUMN IF NOT EXISTS provider TEXT, + ADD COLUMN IF NOT EXISTS model_id TEXT, + ADD COLUMN IF NOT EXISTS session_id UUID, + ADD COLUMN IF NOT EXISTS purpose TEXT, + ADD COLUMN IF NOT EXISTS cluster TEXT, + ADD COLUMN IF NOT EXISTS tier TEXT, + ADD COLUMN IF NOT EXISTS redo_attempt SMALLINT NOT NULL DEFAULT 0; + +-- Run-level rollup queries hit this index first. +CREATE INDEX IF NOT EXISTS idx_pebble_harness_session_time + ON bedrock.pebble_harness_log(session_id, created_at) + WHERE session_id IS NOT NULL; + +-- Cluster-level slicing for cockpit per-tile cost breakdown. +CREATE INDEX IF NOT EXISTS idx_pebble_harness_cluster_time + ON bedrock.pebble_harness_log(cluster, created_at DESC) + WHERE cluster IS NOT NULL; + +-- Purpose-level rollup: "how much did synthesis cost across all runs today" +CREATE INDEX IF NOT EXISTS idx_pebble_harness_purpose_time + ON bedrock.pebble_harness_log(purpose, created_at DESC) + WHERE purpose IS NOT NULL; + +COMMENT ON COLUMN bedrock.pebble_harness_log.cache_creation_input_tokens IS + 'Anthropic SDK response.usage.cache_creation_input_tokens. Priced at 1.25x normal input rate. 0 for OpenRouter / non-Anthropic providers.'; +COMMENT ON COLUMN bedrock.pebble_harness_log.cache_read_input_tokens IS + 'Anthropic SDK response.usage.cache_read_input_tokens. Priced at 0.10x normal input rate. Drives the cockpit cache-hit-ratio display.'; +COMMENT ON COLUMN bedrock.pebble_harness_log.session_id IS + 'The Pebble research run that owns this call. NULL for legacy rows pre-2026-05-18.'; +COMMENT ON COLUMN bedrock.pebble_harness_log.purpose IS + 'Logical role of this call in the swarm: doer | verifier | probe | capacity | propensity | affinity | synthesis | meta_observer | replan | escalation | quorum.'; +COMMENT ON COLUMN bedrock.pebble_harness_log.cluster IS + 'Cluster name when call originated inside one: cluster_a_financial | cluster_b_affiliations | cluster_c_public_profile | cluster_d_network | cluster_e_giving_trends | cluster_f_org_intel. NULL for orchestrator-level calls.'; +COMMENT ON COLUMN bedrock.pebble_harness_log.tier IS + 'Research tier the run was committed to: T1 | T2 | T3 | T4.'; +COMMENT ON COLUMN bedrock.pebble_harness_log.redo_attempt IS + 'Doer/Verifier loop iteration. 0 = fresh attempt, 1 = first redo, 2 = second redo. Caps from TierBudget.'; + +-- --------------------------------------------------------------------------- +-- pebble_tool_call_log — non-LLM tool calls (HTTP to FEC/ProPublica/etc.) +-- --------------------------------------------------------------------------- +-- These don't pass through ModelClient so they're missing from +-- pebble_harness_log. Meta-Observer needs the count + rate-limit +-- visibility for runaway detection; cockpit shows tool-call burn +-- vs budgeted cap per cluster. Cache hits on bedrock.pebble_api_cache +-- are marked so the cache-hit-ratio extends to data sources, not +-- just LLM calls. +CREATE TABLE IF NOT EXISTS bedrock.pebble_tool_call_log ( + id BIGSERIAL PRIMARY KEY, + occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(), + + -- Owning run. + session_id UUID NOT NULL, + + -- Logical tool path. e.g.: + -- 'fec.search_contributions' + -- 'propublica.download_990_xml' + -- 'opencorporates.search_officers' + -- 'sec.search_cik' + -- 'web_search.search_person' + tool TEXT NOT NULL, + + -- Cluster + agent attribution. Same conventions as + -- pebble_harness_log.cluster. + cluster TEXT, + agent_name TEXT, + + -- $ cost. 0 for free APIs (FEC, EDGAR, USAspending, ProPublica, + -- Federal Register, Wikipedia, FINRA). Populated for paid tiers + -- (OpenCorporates paid plan, Serper). Drives the cockpit's + -- "tool cost" sub-bucket. + cost_usd NUMERIC NOT NULL DEFAULT 0, + + -- Execution outcome. + success BOOLEAN NOT NULL, + elapsed_ms INTEGER NOT NULL, + bytes_returned INTEGER, + + -- Cache hit on bedrock.pebble_api_cache. When TRUE, cost_usd + -- is 0 regardless of provider rate. + cache_hit BOOLEAN NOT NULL DEFAULT FALSE, + + -- Rate-limit budget at time of response. Lets Meta-Observer + -- detect approaching rate-limit exhaustion (e.g. ProPublica + -- 1-XML-per-minute). + rate_limit_remaining INTEGER, + rate_limit_reset_at TIMESTAMPTZ, + + -- Error class for failures (HTTPError, Timeout, RateLimitError, ...). + error_class TEXT, + + -- Audit attribution. Required. + originating_user_email TEXT NOT NULL, + + -- Multi-tenant outer guard (matches pebble_write_audit pattern). + org_id TEXT NOT NULL DEFAULT 'pursuit' +); + +-- Hot path: per-session rollup for cockpit + ledger. +CREATE INDEX IF NOT EXISTS idx_pebble_tool_session + ON bedrock.pebble_tool_call_log(session_id, occurred_at); + +-- Per-tool analytics ("ProPublica failure rate this week"). +CREATE INDEX IF NOT EXISTS idx_pebble_tool_kind_time + ON bedrock.pebble_tool_call_log(tool, occurred_at DESC); + +-- Failure / rate-limit forensics. +CREATE INDEX IF NOT EXISTS idx_pebble_tool_failures + ON bedrock.pebble_tool_call_log(occurred_at DESC) + WHERE success = FALSE; + +CREATE INDEX IF NOT EXISTS idx_pebble_tool_user_time + ON bedrock.pebble_tool_call_log(originating_user_email, occurred_at DESC); + +COMMENT ON TABLE bedrock.pebble_tool_call_log IS + 'Per-call log for non-LLM tool invocations (HTTP fetches to FEC, ProPublica, OpenCorporates, etc.). Complement to pebble_harness_log which only captures LLM calls. Append-only; retain at least 90 days for cost forensics.'; +COMMENT ON COLUMN bedrock.pebble_tool_call_log.cache_hit IS + 'TRUE when the call short-circuited via bedrock.pebble_api_cache. cost_usd is 0 when TRUE regardless of provider rate.'; +COMMENT ON COLUMN bedrock.pebble_tool_call_log.rate_limit_remaining IS + 'Rate-limit budget at time of response (from provider response headers). Lets Meta-Observer detect approaching exhaustion before it triggers throttling.'; + +-- --------------------------------------------------------------------------- +-- Grants +-- --------------------------------------------------------------------------- +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'bedrock_user') THEN + GRANT SELECT, INSERT ON bedrock.pebble_tool_call_log TO bedrock_user; + GRANT USAGE, SELECT ON SEQUENCE bedrock.pebble_tool_call_log_id_seq TO bedrock_user; + -- pebble_harness_log already has grants from init.sql; no change needed. + END IF; +END $$; diff --git a/financial_forecasting/db/migrations/2026-05-18-pebble-network-and-giving.sql b/financial_forecasting/db/migrations/2026-05-18-pebble-network-and-giving.sql new file mode 100644 index 00000000..fd79f4c5 --- /dev/null +++ b/financial_forecasting/db/migrations/2026-05-18-pebble-network-and-giving.sql @@ -0,0 +1,265 @@ +-- 2026-05-18: pebble_network_edges + pebble_giving_history +-- +-- Wave 0 of the Pebble L2 Research Swarm plan +-- (~/.claude/plans/glistening-crafting-matsumoto.md §5.1, §5.2, §5.4). +-- +-- Why: +-- HNW research without a relationship graph is flat text. A +-- prospect's value to a fundraising team comes from their +-- network: who they sit on boards with, who they give +-- alongside, who their advisors are, what foundations they +-- control. Today's Pebble emits a flat list of Claim objects +-- (pebble.schemas.profile.Profile.claims) with no edges. +-- +-- Cluster D (Network Mapping, §5.1) emits four edge types via +-- four parallel doers: +-- D1 co_board — 990 officer overlap across orgs +-- D2 co_donor — FEC committee co-appearance +-- D3 family_candidate — surname + city heuristic (low conf) +-- D4 professional_peer — LDA co-registrant +-- Plus future advisor / spouse / co_trustee edge types as data +-- sources expand. +-- +-- pebble_network_edges stores one row per detected edge with +-- a verifier verdict, strength score, and evidence URL. The +-- peer_person_normalized column ("lastname|firstinitial|state") +-- lets a future cross-prospect graph (Wave 8, post-1.0) +-- compose edges into a property graph queryable by +-- "show me everyone Pursuit has researched who shares a +-- board seat with X" — without backfill. +-- +-- Cluster E (Giving Trends, §5.2) emits one row per (prospect, +-- giving_kind, year_or_cycle) tuple capturing multi-year +-- philanthropic / political / federal-received totals. Today +-- ProPublica's filings_with_data array is fetched but only +-- the latest tax year is parsed; the historical pattern +-- (5-year escalation? plateau? decline?) is a meaningful +-- capacity + propensity signal. +-- +-- direct_grant_into_foundation is the giving_kind for §5.6: +-- when a prospect controls foundation X and X's Schedule B +-- lists other foundations giving INTO it, those flows are +-- recorded here so the synthesis stage can surface "Y +-- Foundation has given $200K/yr to Z Foundation since 2019" +-- as an affinity signal. +-- +-- Related: +-- * ~/.claude/plans/glistening-crafting-matsumoto.md §5.1-5.6 +-- * pebble/clusters/org_intelligence.py:118-235 (990 XML +-- officer parsing, reused by Cluster D1) +-- * pebble/data_sources/propublica.py:76-91 (filings_with_data, +-- reused by Cluster E1 multi-year extraction) +-- * pebble/data_sources/fec.py (Cluster D2 co-donor) +-- * pebble/data_sources/lda.py (Cluster D4 professional peers) +-- +-- Idempotent — safe to re-run. +-- +-- Apply as bedrock owner: +-- psql "$DATABASE_URL" -f 2026-05-18-pebble-network-and-giving.sql + +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- --------------------------------------------------------------------------- +-- pebble_network_edges — Cluster D output +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS bedrock.pebble_network_edges ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(), + + session_id UUID NOT NULL, + prospect_id TEXT NOT NULL, + + edge_type TEXT NOT NULL CHECK (edge_type IN ( + 'co_board', + 'co_donor', + 'family_candidate', + 'professional_peer', + 'advisor', + 'spouse', + 'co_trustee' + )), + + -- Peer subject. At least one of peer_person_name or + -- peer_org_name must be non-null (enforced by application + -- code; could add CHECK constraint when subject types + -- stabilize across edge types). + peer_person_name TEXT, + + -- Normalized form for cross-prospect dedup + future graph. + -- Format: "lastname|firstinitial|state" (lowercase, ASCII- + -- folded). NULL when edge subject is an organization. + peer_person_normalized TEXT, + + peer_org_name TEXT, + peer_org_ein TEXT, + + -- Bridge context: the org or committee that connects the + -- prospect to the peer. + via_org_name TEXT, + via_org_ein TEXT, + via_committee_id TEXT, + via_client TEXT, -- LDA client_id for D4 + + -- Temporal anchor. For co_board: tax year of 990 filing. + -- For co_donor: FEC cycle ("2024" or "2023-2024"). For + -- family_candidate: not applicable (NULL). + year_or_cycle TEXT, + + -- Strength: implementation-defined per edge type. + -- co_board: log(1 + count_of_shared_orgs) + -- co_donor: log(1 + shared_committee_count) + -- * log(1 + joint_amount_bucket) + -- family_candidate: surname_rarity_score * shared_org_count + -- professional_peer: shared_filing_count + strength_score NUMERIC, + + -- Verifier verdict — see §10 Decision 4 for default confidence + -- by edge type. + confidence TEXT NOT NULL + CHECK (confidence IN ('high','medium','low')), + + -- Mandatory citation; required for cockpit + audit trail. + evidence_url TEXT NOT NULL, + evidence_excerpt TEXT, + + -- Verifier loop outcome at admission. + verified BOOLEAN NOT NULL DEFAULT FALSE, + verifier_note TEXT, -- e.g. "parse_failed" → low confidence + + -- Origin attribution. Examples: "cluster_d.doer_d1", + -- "cluster_d.doer_d2". + discovered_by TEXT, + + -- Audit attribution. + originating_user_email TEXT, + + -- Multi-tenant outer guard. + org_id TEXT NOT NULL DEFAULT 'pursuit' +); + +-- Per-prospect edge browse + cockpit "Final profile" render. +CREATE INDEX IF NOT EXISTS idx_pebble_edges_prospect_type + ON bedrock.pebble_network_edges(prospect_id, edge_type); + +-- Per-session: load all edges produced by one run. +CREATE INDEX IF NOT EXISTS idx_pebble_edges_session + ON bedrock.pebble_network_edges(session_id); + +-- Cross-prospect graph: "who else have we researched that +-- shares this peer?" +CREATE INDEX IF NOT EXISTS idx_pebble_edges_peer_norm + ON bedrock.pebble_network_edges(peer_person_normalized) + WHERE peer_person_normalized IS NOT NULL; + +-- "What org bridges the most prospects?" — promotes a foundation +-- to "key network org" when it surfaces in 3+ co_board edges. +CREATE INDEX IF NOT EXISTS idx_pebble_edges_via_org + ON bedrock.pebble_network_edges(via_org_ein) + WHERE via_org_ein IS NOT NULL; + +-- Confidence filtering for cockpit ("show me only high-conf edges"). +CREATE INDEX IF NOT EXISTS idx_pebble_edges_confidence + ON bedrock.pebble_network_edges(prospect_id, confidence); + +COMMENT ON TABLE bedrock.pebble_network_edges IS + 'Cluster D output: structured relationship edges between a prospect and other persons or organizations. One row per detected edge. peer_person_normalized enables a future cross-prospect graph query without backfill.'; +COMMENT ON COLUMN bedrock.pebble_network_edges.peer_person_normalized IS + 'Format "lastname|firstinitial|state" — lowercase, ASCII-folded. NULL when subject is an organization. The dedup + future-graph join key.'; +COMMENT ON COLUMN bedrock.pebble_network_edges.confidence IS + 'Verifier verdict. Defaults by edge type (Decision 4): co_board=high (when explicit title+recent year), co_donor=medium, family_candidate=low, professional_peer=medium. Verifier can promote/demote.'; +COMMENT ON COLUMN bedrock.pebble_network_edges.strength_score IS + 'Per-edge-type formula (Plan §5.1). Bigger = stronger evidence. NOT a confidence — confidence is a discrete label, strength is continuous.'; + +-- --------------------------------------------------------------------------- +-- pebble_giving_history — Cluster E output +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS bedrock.pebble_giving_history ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(), + + session_id UUID NOT NULL, + prospect_id TEXT NOT NULL, + + giving_kind TEXT NOT NULL CHECK (giving_kind IN ( + 'philanthropic', -- E1: foundations controlled + 'political', -- E2: FEC contributions + 'federal_recipient', -- E3: USAspending awards + 'direct_grant_into_foundation' -- §5.6: Schedule B of recipient orgs + )), + + -- Calendar year ("2024") for philanthropic/federal_recipient; + -- FEC cycle ("2023-2024") for political; flexible. + year_or_cycle TEXT NOT NULL, + + total_usd NUMERIC, + + -- Per-record sub-data: + -- philanthropic: [{recipient_name, recipient_ein, amount, + -- purpose}] + -- political: [{committee_id, committee_name, amount, party}] + -- federal_recipient: [{award_id, agency, amount, period}] + -- direct_grant_into_foundation: [{donor_org_name, donor_ein, + -- amount, year}] + top_recipients_json JSONB, + + -- Which foundation channeled this giving (for philanthropic). + -- Lets the cockpit show "Maria's giving via Y Foundation was + -- $X in 2024". + via_org_ein TEXT, + via_org_name TEXT, + + evidence_url TEXT NOT NULL, + evidence_excerpt TEXT, + + -- Trend bucket — populated by E1/E2 when ≥3 data points + -- exist. NULL otherwise. Column is nullable (no NOT NULL), so + -- NULL bypasses the CHECK entirely — IN-list does NOT include + -- NULL because `x IN (..., NULL)` collapses to NULL not TRUE + -- in SQL semantics (see test_migration_lints.py L3). + trend_direction TEXT CHECK (trend_direction IS NULL OR trend_direction IN ( + 'increasing','flat','declining' + )), + trend_year_span SMALLINT, -- # of years in trend + + -- Ideology cluster (E2 political only). Same nullable + IS NULL + -- pattern as trend_direction above. + ideology_cluster TEXT CHECK (ideology_cluster IS NULL OR ideology_cluster IN ( + 'liberal','conservative','mixed','unknown' + )), + + discovered_by TEXT, -- "cluster_e.doer_e1" etc. + originating_user_email TEXT, + org_id TEXT NOT NULL DEFAULT 'pursuit' +); + +-- Hot path: per-prospect timeline. +CREATE INDEX IF NOT EXISTS idx_pebble_giving_prospect + ON bedrock.pebble_giving_history(prospect_id, giving_kind, year_or_cycle); + +-- Per-session rollup. +CREATE INDEX IF NOT EXISTS idx_pebble_giving_session + ON bedrock.pebble_giving_history(session_id); + +-- "What years was a foundation active?" — drives the Capacity +-- Estimator's foundation-throughput proxy. +CREATE INDEX IF NOT EXISTS idx_pebble_giving_via_org + ON bedrock.pebble_giving_history(via_org_ein, year_or_cycle) + WHERE via_org_ein IS NOT NULL; + +COMMENT ON TABLE bedrock.pebble_giving_history IS + 'Cluster E output: multi-year giving timeline. One row per (prospect, giving_kind, year_or_cycle). Drives the cockpit timeline view + the Capacity Estimator (§5.5).'; +COMMENT ON COLUMN bedrock.pebble_giving_history.giving_kind IS + 'philanthropic = foundation-controlled giving from 990s; political = FEC contributions; federal_recipient = USAspending awards received; direct_grant_into_foundation = Schedule B inbound to a foundation the prospect controls.'; + +-- --------------------------------------------------------------------------- +-- Grants +-- --------------------------------------------------------------------------- +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'bedrock_user') THEN + GRANT SELECT, INSERT, UPDATE ON bedrock.pebble_network_edges TO bedrock_user; + GRANT SELECT, INSERT, UPDATE ON bedrock.pebble_giving_history TO bedrock_user; + -- UPDATE allowed because verifier verdicts can promote / demote + -- confidence and discovered_by post-insert during the loop. + END IF; +END $$; diff --git a/financial_forecasting/db/migrations/2026-05-18-pebble-swarm-runtime.sql b/financial_forecasting/db/migrations/2026-05-18-pebble-swarm-runtime.sql new file mode 100644 index 00000000..8cd4d1d8 --- /dev/null +++ b/financial_forecasting/db/migrations/2026-05-18-pebble-swarm-runtime.sql @@ -0,0 +1,215 @@ +-- 2026-05-18: pebble_meta_alerts + pebble_research_action_idempotency +-- + pebble_scratchpad.events_jsonl +-- +-- Wave 0 of the Pebble L2 Research Swarm plan +-- (~/.claude/plans/glistening-crafting-matsumoto.md §4.4, §6). +-- +-- Why: +-- The Meta-Observer (§4.4) watches a research run and intervenes +-- when a team goes off the rails — stall, runaway cost, +-- verifier-rejection spike, divergence between clusters, conflict +-- spike, low-novelty churn, prompt-injection signature hits, +-- cost-cap breach. Each action (warn / throttle / abort cluster / +-- replan / halt) is a structured event the cockpit renders in +-- the "Meta-Observer feed" panel AND a row the user can review +-- post-hoc to understand why a run looked unusual. +-- +-- pebble_meta_alerts persists those events. Lives separately +-- from pebble_harness_log (which is per-LLM-call) and +-- pebble_conflict_log (which is per-claim-pair) so the cockpit +-- can render a clean "what did Meta-Observer do during this +-- run" timeline without joining across three tables. +-- +-- pebble_research_action_idempotency backs the abort / abort- +-- cluster / continue-to-T4 control endpoints. The endpoint +-- mints a UUIDv7 X-Request-Id; duplicate requests within 24h +-- return 409 from a no-op insert rather than re-aborting a +-- cluster that's already been re-spawned by replan or +-- re-running a T4 expansion. Same pattern as +-- pebble_write_audit's UNIQUE(request_id) replay defense. +-- +-- pebble_scratchpad.events_jsonl is the append-only event log +-- that the SSE stream replays from. Today's scratchpad_json +-- column holds a single state-of-the-world blob (upserted on +-- each scratchpad.save_scratchpad call); the new events_jsonl +-- column accumulates ordered scratchpad events (cluster.start, +-- verifier.approve, claim.admit, meta.warn, ...) so a cold +-- SSE subscriber can replay from ?since_seq=0. +-- +-- Related: +-- * ~/.claude/plans/glistening-crafting-matsumoto.md §4.2 (scratchpad), +-- §4.4 (meta-observer), §6 (cockpit + SSE), §10 D17-D20 +-- * pebble/storage/db.py:990-1021 (save_scratchpad / update_scratchpad) +-- * tasks/pebble-adversary-security.md M3 (idempotency replay defense) +-- +-- Idempotent — safe to re-run. +-- +-- Apply as bedrock owner: +-- psql "$DATABASE_URL" -f 2026-05-18-pebble-swarm-runtime.sql + +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- --------------------------------------------------------------------------- +-- Extend pebble_scratchpad with append-only event log +-- --------------------------------------------------------------------------- +-- JSONB so the SSE replay handler can filter by event kind at the DB +-- level (`jsonb_path_query`) for ?since_seq=N catch-up. Defaults to +-- empty array so legacy rows continue to read fine. +ALTER TABLE bedrock.pebble_scratchpad + ADD COLUMN IF NOT EXISTS events_jsonl JSONB NOT NULL DEFAULT '[]'::jsonb, + ADD COLUMN IF NOT EXISTS last_event_seq BIGINT NOT NULL DEFAULT 0, + ADD COLUMN IF NOT EXISTS originating_user_email TEXT, + ADD COLUMN IF NOT EXISTS tier TEXT, + -- run_status is nullable + no DEFAULT so existing (historical) + -- scratchpad rows are NOT mass-marked as 'running' when the column + -- lands. The orchestrator sets 'running' on insert + transitions + -- it to terminal states; cockpit treats NULL as legacy / unknown. + -- CHECK allows NULL plus the five real states. + ADD COLUMN IF NOT EXISTS run_status TEXT + CHECK (run_status IS NULL OR run_status IN ('running','done','aborted','halted','failed')); + +-- SSE replay queries by session_id ordered by last_event_seq. +-- The existing UNIQUE idx_pebble_sp_session(session_id) covers +-- the lookup; new index would be duplicative. + +COMMENT ON COLUMN bedrock.pebble_scratchpad.events_jsonl IS + 'Append-only ordered list of ScratchpadEvent objects. SSE replay reads from here when subscriber is cold. Each event has shape: {seq, ts, kind, cluster, actor, payload}.'; +COMMENT ON COLUMN bedrock.pebble_scratchpad.last_event_seq IS + 'Monotonic counter. SSE subscriber sends ?since_seq=N to resume.'; +COMMENT ON COLUMN bedrock.pebble_scratchpad.run_status IS + 'Top-level run state set by the L2 orchestrator. NULL on historical (pre-2026-05-18) rows — cockpit treats NULL as legacy/unknown. Real states: running | done | aborted | halted | failed. Distinct from status (legacy free-text).'; + +-- --------------------------------------------------------------------------- +-- pebble_meta_alerts — Meta-Observer interventions +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS bedrock.pebble_meta_alerts ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(), + + session_id UUID NOT NULL, + + -- What was detected. + alert_kind TEXT NOT NULL CHECK (alert_kind IN ( + 'stall', + 'runaway', + 'off_rails', + 'divergence', + 'conflict_spike', + 'low_novelty', + 'cost_80', + 'cost_100', + 'injection_signature', + 'global_cost_breach' + )), + + -- What was done about it. + severity TEXT NOT NULL CHECK (severity IN ( + 'warn', + 'throttle', + 'abort_cluster', + 'replan', + 'halt' + )), + + -- Cluster being acted on (null for run-level alerts like cost_100). + cluster TEXT, + + -- Human-readable action description, for cockpit display. + -- Examples: + -- "Cluster A verifier rejection rate 60% (3/5); injected one-shot tightening hint" + -- "Cluster B stall: no scratchpad event in 45s; aborting" + -- "Cost 80% of TierBudget; signaling orchestrator to drop optional clusters" + action_taken TEXT NOT NULL, + + -- Structured detection payload for forensics + cassette tests. + -- Examples: + -- {"rejection_rate": 0.6, "last_5_outcomes": [...]} + -- {"silent_seconds": 45, "last_event_seq": 47} + -- {"cost_used_usd": 0.62, "tier_cap_usd": 0.75} + payload_json JSONB, + + -- Did the Meta-Observer use its $0.01 LLM budget for this decision? + -- TRUE = ambiguous case warranted Haiku introspection. + -- FALSE = deterministic threshold tripped. + llm_introspection BOOLEAN NOT NULL DEFAULT FALSE, + llm_cost_usd NUMERIC NOT NULL DEFAULT 0, + + -- Audit attribution. Required. + originating_user_email TEXT NOT NULL, + + -- Multi-tenant outer guard. + org_id TEXT NOT NULL DEFAULT 'pursuit' +); + +CREATE INDEX IF NOT EXISTS idx_pebble_meta_session_time + ON bedrock.pebble_meta_alerts(session_id, occurred_at); +CREATE INDEX IF NOT EXISTS idx_pebble_meta_kind_time + ON bedrock.pebble_meta_alerts(alert_kind, occurred_at DESC); +CREATE INDEX IF NOT EXISTS idx_pebble_meta_severity_time + ON bedrock.pebble_meta_alerts(severity, occurred_at DESC) + WHERE severity IN ('halt', 'abort_cluster'); +CREATE INDEX IF NOT EXISTS idx_pebble_meta_user_time + ON bedrock.pebble_meta_alerts(originating_user_email, occurred_at DESC); + +COMMENT ON TABLE bedrock.pebble_meta_alerts IS + 'Meta-Observer interventions: detected anomaly + chosen action. One row per intervention. Append-only. Cockpit renders these as the "Meta-Observer feed" panel; ops dashboards aggregate by kind to spot systemic issues.'; +COMMENT ON COLUMN bedrock.pebble_meta_alerts.llm_introspection IS + 'TRUE when Meta-Observer used its bounded ($0.01/run) Haiku budget to disambiguate. FALSE when a deterministic threshold tripped.'; +COMMENT ON COLUMN bedrock.pebble_meta_alerts.payload_json IS + 'Structured detection details for forensics + cassette test assertions. Schema varies by alert_kind.'; + +-- --------------------------------------------------------------------------- +-- pebble_research_action_idempotency — abort/continue replay defense +-- --------------------------------------------------------------------------- +-- Pattern mirrors bedrock.pebble_write_audit.UNIQUE(request_id). +-- The control endpoints (abort, abort-cluster, continue-to-T4) +-- accept X-Request-Id and insert here BEFORE acting. Duplicate +-- request_id within 24h returns the prior response (cached +-- response_summary) rather than re-acting. Cleanup job (separate +-- migration when retention rules ship) prunes rows > 24h. +CREATE TABLE IF NOT EXISTS bedrock.pebble_research_action_idempotency ( + request_id UUID PRIMARY KEY, + occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(), + + session_id UUID NOT NULL, + action TEXT NOT NULL CHECK (action IN ( + 'abort', + 'abort_cluster', + 'continue_t4', + 'pause', + 'resume' + )), + action_params JSONB, + + response_status SMALLINT NOT NULL, + response_summary JSONB, + + originating_user_email TEXT NOT NULL, + org_id TEXT NOT NULL DEFAULT 'pursuit' +); + +CREATE INDEX IF NOT EXISTS idx_pebble_action_session_time + ON bedrock.pebble_research_action_idempotency(session_id, occurred_at DESC); +-- TTL cleanup index. Plain occurred_at — NOT a partial index with +-- `WHERE occurred_at < now() - INTERVAL '24 hours'` because PostgreSQL +-- requires index predicates to use IMMUTABLE functions only, and now() +-- is STABLE. A range scan on the cleanup job (DELETE WHERE +-- occurred_at < now() - INTERVAL '24 hours') uses this index just as +-- well at our row volume. +CREATE INDEX IF NOT EXISTS idx_pebble_action_occurred_at + ON bedrock.pebble_research_action_idempotency(occurred_at); + +COMMENT ON TABLE bedrock.pebble_research_action_idempotency IS + 'Replay defense for swarm control endpoints (abort, abort-cluster, continue-to-T4). UNIQUE(request_id) means a duplicate request returns the cached response_summary rather than re-acting. 24h retention.'; + +-- --------------------------------------------------------------------------- +-- Grants +-- --------------------------------------------------------------------------- +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'bedrock_user') THEN + GRANT SELECT, INSERT ON bedrock.pebble_meta_alerts TO bedrock_user; + GRANT SELECT, INSERT ON bedrock.pebble_research_action_idempotency TO bedrock_user; + -- pebble_scratchpad already has full grants from init.sql. + END IF; +END $$; diff --git a/financial_forecasting/frontend-v2/.gitignore b/financial_forecasting/frontend-v2/.gitignore index 17b53275..66eac361 100644 --- a/financial_forecasting/frontend-v2/.gitignore +++ b/financial_forecasting/frontend-v2/.gitignore @@ -4,6 +4,13 @@ dist-ssr *.local .vite +# TypeScript composite-build artifacts. ``tsc -b`` (used by ``npm run +# build``) emits these alongside .ts files. Regenerated on every +# build; checking them in causes spurious diffs. +*.tsbuildinfo +vite.config.d.ts +vite.config.js + # Re-include src/lib/ — the root .gitignore globs `lib/` for Python's # virtualenv layout, which incidentally catches our React lib/ folder. !src/lib/ diff --git a/financial_forecasting/frontend-v2/package-lock.json b/financial_forecasting/frontend-v2/package-lock.json index 5f540279..fe21d4c7 100644 --- a/financial_forecasting/frontend-v2/package-lock.json +++ b/financial_forecasting/frontend-v2/package-lock.json @@ -33,22 +33,35 @@ "tailwind-merge": "^3.3.1" }, "devDependencies": { + "@testing-library/jest-dom": "^6.7.0", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", "@types/node": "^25.6.0", "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", "@vitejs/plugin-react": "^4.3.4", + "@vitest/coverage-v8": "^3.2.4", "autoprefixer": "^10.4.22", "eslint": "^9.19.0", "eslint-plugin-react": "^7.37.4", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.18", + "jsdom": "^26.1.0", "postcss": "^8.5.6", "tailwindcss": "^3.4.19", "tailwindcss-animate": "^1.0.7", "typescript": "^5.7.2", - "vite": "^6.1.0" + "vite": "^6.1.0", + "vitest": "^3.2.4" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -62,6 +75,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -296,6 +344,16 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -344,6 +402,131 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -1034,6 +1217,34 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1122,6 +1333,17 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -2562,6 +2784,104 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2607,6 +2927,17 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/d3-array": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", @@ -2670,6 +3001,13 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2741,66 +3079,235 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" }, - "engines": { - "node": ">=0.4.0" + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "dev": true, "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" + "tinyrainbow": "^2.0.0" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://opencollective.com/vitest" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", "dev": true, "license": "MIT" }, @@ -2844,6 +3351,16 @@ "node": ">=10" } }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", @@ -2982,6 +3499,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -3153,6 +3699,16 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -3243,6 +3799,23 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3260,6 +3833,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -3390,6 +3973,13 @@ "node": ">= 8" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -3403,6 +3993,20 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -3531,6 +4135,20 @@ "node": ">=12" } }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -3613,12 +4231,29 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/decimal.js-light": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3671,6 +4306,16 @@ "node": ">=0.4.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -3704,6 +4349,14 @@ "node": ">=0.10.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3718,6 +4371,13 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.345", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.345.tgz", @@ -3725,6 +4385,26 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-abstract": { "version": "1.24.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", @@ -3840,6 +4520,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -4173,6 +4860,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -4189,6 +4886,16 @@ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4350,6 +5057,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", @@ -4519,6 +5243,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4532,6 +5278,32 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -4665,8 +5437,69 @@ "node": ">= 0.4" } }, - "node_modules/ignore": { - "version": "5.3.2", + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, @@ -4712,6 +5545,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -4910,6 +5753,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -4996,6 +5849,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -5155,6 +6015,60 @@ "dev": true, "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -5173,6 +6087,22 @@ "node": ">= 0.4" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", @@ -5203,6 +6133,46 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -5346,6 +6316,13 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -5365,6 +6342,68 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5419,6 +6458,16 @@ "node": ">= 0.6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -5432,6 +6481,16 @@ "node": "*" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5513,6 +6572,13 @@ "node": ">=0.10.0" } }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5699,6 +6765,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5712,6 +6785,19 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5739,6 +6825,47 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5984,6 +7111,44 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -6250,6 +7415,20 @@ "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", @@ -6405,6 +7584,13 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -6484,6 +7670,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -6648,6 +7854,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/sonner": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", @@ -6668,7 +7894,21 @@ "node": ">=0.10.0" } }, - "node_modules/stop-iteration-iterator": { + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", @@ -6682,6 +7922,60 @@ "node": ">= 0.4" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -6780,6 +8074,62 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -6793,6 +8143,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -6842,6 +8212,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwind-merge": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", @@ -6922,6 +8299,60 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -6951,6 +8382,20 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -6999,6 +8444,56 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -7012,6 +8507,32 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -7353,6 +8874,29 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/vite/node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -7384,6 +8928,153 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -7489,6 +9180,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -7499,6 +9207,130 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/financial_forecasting/frontend-v2/package.json b/financial_forecasting/frontend-v2/package.json index 3ef50750..ef836e86 100644 --- a/financial_forecasting/frontend-v2/package.json +++ b/financial_forecasting/frontend-v2/package.json @@ -9,7 +9,10 @@ "build": "tsc -b && vite build", "preview": "vite preview", "lint": "eslint . --max-warnings=0", - "typecheck": "tsc -b --noEmit" + "typecheck": "tsc -b", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage" }, "dependencies": { "@radix-ui/react-dialog": "^1.1.15", @@ -50,6 +53,12 @@ "tailwindcss": "^3.4.19", "tailwindcss-animate": "^1.0.7", "typescript": "^5.7.2", - "vite": "^6.1.0" + "vite": "^6.1.0", + "vitest": "^3.2.4", + "@vitest/coverage-v8": "^3.2.4", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@testing-library/jest-dom": "^6.7.0", + "jsdom": "^26.1.0" } } diff --git a/financial_forecasting/frontend-v2/src/App.tsx b/financial_forecasting/frontend-v2/src/App.tsx index 607078bf..7ef34c70 100644 --- a/financial_forecasting/frontend-v2/src/App.tsx +++ b/financial_forecasting/frontend-v2/src/App.tsx @@ -3,6 +3,7 @@ import { Toaster } from "sonner"; import { AppShell } from "./components/AppShell"; import { AuthGate } from "./components/AuthGate"; +import { PebbleAccessGate } from "./components/PebbleAccessGate"; import { DashboardPage } from "./pages/Dashboard"; import { AccountsPage } from "./pages/Accounts"; import { AccountDetailPage } from "./pages/AccountDetail"; @@ -17,6 +18,7 @@ import { TasksPage } from "./pages/Tasks"; import { ContactsPage } from "./pages/Contacts"; import { ContactDetailPage } from "./pages/ContactDetail"; import { LoginPage } from "./pages/Login"; +import { PebblePage } from "./pages/Pebble"; import { SettingsPage } from "./pages/Settings"; import { CashFlowPage } from "./pages/CashFlow"; import { PlatformIntakePage } from "./pages/PlatformIntake"; @@ -52,6 +54,14 @@ export default function App() { } /> } /> } /> + + + + } + /> } /> } /> diff --git a/financial_forecasting/frontend-v2/src/components/AppShell.tsx b/financial_forecasting/frontend-v2/src/components/AppShell.tsx index 49ac890c..65d69b12 100644 --- a/financial_forecasting/frontend-v2/src/components/AppShell.tsx +++ b/financial_forecasting/frontend-v2/src/components/AppShell.tsx @@ -20,6 +20,7 @@ import { import { GlobalSearch } from "@/components/GlobalSearch"; import { cn } from "@/lib/utils"; import { useCurrentUser, useSalesforceStatus, startSalesforceConnect } from "@/services/auth"; +import { usePebbleAccess } from "@/services/permissions"; const NAV_GROUPS = [ { @@ -47,6 +48,19 @@ const NAV_GROUPS = [ // still wired so direct URLs continue to work. ], }, + { + label: "Pebble", + items: [ + // Pebble's home page (Layer B0 of the overhaul plan — port from + // legacy /pebble in v1). Sparkles icon shared with Cleanup; both + // use it semantically (cleanup = magic, pebble = AI). Permission- + // gated by use_pebble_chat / use_pebble_research at the route + // level — sidebar entry is shown for everyone so users discover + // it exists; gated routes show a "request access" panel for + // un-permissioned users (added in B0.6 follow-up). + { to: "/pebble", label: "Ask Pebble", icon: MessageSquarePlus }, + ], + }, ] as const; const NAV_COLLAPSED_W = 52; @@ -157,6 +171,15 @@ function Sidebar({ }) { const { data: user } = useCurrentUser(); const sf = useSalesforceStatus(); + const pebbleAccess = usePebbleAccess(); + + // Filter NAV_GROUPS: hide the Pebble group unless the launch-dark gate + // grants this user pebble_access. Strict (false-while-loading) hook used + // intentionally — JP-only on main means non-JP users must never see the + // group flash visible during the /api/permissions/me round-trip. + const visibleNavGroups = NAV_GROUPS.filter( + (group) => group.label !== "Pebble" || pebbleAccess, + ); return (