From c4565a00f4e073d09f626fbdda134feffed5cb14 Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Tue, 29 Sep 2026 17:28:25 -0400 Subject: [PATCH 01/51] Preserve local BackIntel PoC foundation and frontend references --- .dockerignore | 1 + .gitignore | 2 + Dockerfile.runtime | 11 +- README.md | 139 +- aegra.json | 8 +- compose.capabilities.yml | 73 ++ config/real_models.json | 49 + config/simulation/equipment.json | 150 +++ config/simulation/support.json | 218 ++++ docs/architecture/repository-map.md | 30 +- docs/design/decision-workspace-concept.md | 64 + docs/design/decision-workspace-mockup.png | Bin 0 -> 234552 bytes docs/design/decision-workspace-mockup.svg | 133 ++ .../decision-workspace-reference-lock.json | 32 + docs/design/references/front-inbox-mobbin.jpg | Bin 0 -> 48692 bytes docs/design/references/shadcn-sidebar.jpg | Bin 0 -> 24490 bytes docs/research/issue-prediction-results.md | 48 + docs/research/issue-review-artifact.md | 25 + docs/research/issue-review-feasibility.md | 45 + docs/roadmap/capability-reference.md | 1145 +++++++++++++++++ docs/roadmap/poc-recovery-plan.md | 2 +- docs/roadmap/v0.1.0-development-roadmap.md | 746 +---------- migrations/0007_capability_evidence.sql | 24 + migrations/0008_capability_background.sql | 35 + migrations/0009_model_requests.sql | 32 + requirements.models.txt | 6 + runtime/artifacts.py | 212 +++ runtime/attention.py | 68 + runtime/audience_server.py | 217 ++++ runtime/capability_graph.py | 76 ++ runtime/capability_pipeline.py | 169 +++ runtime/contracts.py | 202 +++ runtime/evidence.py | 90 ++ runtime/generated_artifacts.py | 121 ++ runtime/jev.py | 16 +- runtime/jobs.py | 193 +++ runtime/observations.py | 141 ++ runtime/prediction.py | 387 ++++++ runtime/real_models.py | 181 +++ runtime/real_pipeline.py | 249 ++++ runtime/real_semantics.py | 231 ++++ runtime/sandbox.py | 123 ++ runtime/simulation.py | 239 ++++ runtime/simulation_graph.py | 53 + runtime/synthetic.py | 22 + scripts/audience_demo.py | 35 + scripts/capability_demo.py | 196 +++ scripts/package_capabilities.py | 101 ++ scripts/prepare_real_models.py | 35 + scripts/real_capabilities.py | 68 + scripts/real_demo.py | 305 +++++ scripts/real_model_probe.py | 116 ++ scripts/simulate.py | 60 + scripts/validation/check_audience_ui.cjs | 111 ++ scripts/validation/check_audiences.py | 163 +++ scripts/validation/check_capabilities.py | 43 + .../validation/check_capability_restore.py | 96 ++ .../validation/check_real_driver_runtime.py | 106 ++ scripts/validation/check_real_followups.py | 149 +++ .../validation/check_real_model_restore.py | 218 ++++ scripts/validation/check_real_models.py | 86 ++ scripts/validation/check_real_package.py | 77 ++ .../validation/check_real_package_browser.cjs | 46 + scripts/validation/check_runtime.py | 2 +- scripts/validation/check_sandbox.py | 58 + tabellio.validation.json | 60 +- tests/test_capabilities.py | 369 ++++++ tests/test_real_driver.py | 114 ++ tests/test_real_integrations.py | 273 ++++ tests/test_simulation.py | 107 ++ tests/test_simulation_runtime.py | 29 + 71 files changed, 8285 insertions(+), 746 deletions(-) create mode 100644 compose.capabilities.yml create mode 100644 config/real_models.json create mode 100644 config/simulation/equipment.json create mode 100644 config/simulation/support.json create mode 100644 docs/design/decision-workspace-concept.md create mode 100644 docs/design/decision-workspace-mockup.png create mode 100644 docs/design/decision-workspace-mockup.svg create mode 100644 docs/design/decision-workspace-reference-lock.json create mode 100644 docs/design/references/front-inbox-mobbin.jpg create mode 100644 docs/design/references/shadcn-sidebar.jpg create mode 100644 docs/research/issue-prediction-results.md create mode 100644 docs/research/issue-review-artifact.md create mode 100644 docs/research/issue-review-feasibility.md create mode 100644 docs/roadmap/capability-reference.md create mode 100644 migrations/0007_capability_evidence.sql create mode 100644 migrations/0008_capability_background.sql create mode 100644 migrations/0009_model_requests.sql create mode 100644 requirements.models.txt create mode 100644 runtime/artifacts.py create mode 100644 runtime/attention.py create mode 100644 runtime/audience_server.py create mode 100644 runtime/capability_graph.py create mode 100644 runtime/capability_pipeline.py create mode 100644 runtime/contracts.py create mode 100644 runtime/evidence.py create mode 100644 runtime/generated_artifacts.py create mode 100644 runtime/jobs.py create mode 100644 runtime/observations.py create mode 100644 runtime/prediction.py create mode 100644 runtime/real_models.py create mode 100644 runtime/real_pipeline.py create mode 100644 runtime/real_semantics.py create mode 100644 runtime/sandbox.py create mode 100644 runtime/simulation.py create mode 100644 runtime/simulation_graph.py create mode 100644 runtime/synthetic.py create mode 100644 scripts/audience_demo.py create mode 100644 scripts/capability_demo.py create mode 100644 scripts/package_capabilities.py create mode 100644 scripts/prepare_real_models.py create mode 100644 scripts/real_capabilities.py create mode 100644 scripts/real_demo.py create mode 100644 scripts/real_model_probe.py create mode 100644 scripts/simulate.py create mode 100644 scripts/validation/check_audience_ui.cjs create mode 100644 scripts/validation/check_audiences.py create mode 100644 scripts/validation/check_capabilities.py create mode 100644 scripts/validation/check_capability_restore.py create mode 100644 scripts/validation/check_real_driver_runtime.py create mode 100644 scripts/validation/check_real_followups.py create mode 100644 scripts/validation/check_real_model_restore.py create mode 100644 scripts/validation/check_real_models.py create mode 100644 scripts/validation/check_real_package.py create mode 100644 scripts/validation/check_real_package_browser.cjs create mode 100644 scripts/validation/check_sandbox.py create mode 100644 tests/test_capabilities.py create mode 100644 tests/test_real_driver.py create mode 100644 tests/test_real_integrations.py create mode 100644 tests/test_simulation.py create mode 100644 tests/test_simulation_runtime.py diff --git a/.dockerignore b/.dockerignore index 8464ac2..ef788f4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,6 @@ * !Dockerfile.runtime +!requirements.models.txt !aegra.json !runtime/ !runtime/** diff --git a/.gitignore b/.gitignore index 091e7a6..329832f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ /artifacts/validation/ +/artifacts/Models/ __pycache__/ +.venv/ *.py[cod] .env # Graphify generates a graph in the working directory and a cache beside scanned files. diff --git a/Dockerfile.runtime b/Dockerfile.runtime index ed23ef8..564f9b2 100644 --- a/Dockerfile.runtime +++ b/Dockerfile.runtime @@ -3,15 +3,20 @@ WORKDIR /app ARG BACKINTEL_CANDIDATE_SHA ENV BACKINTEL_CANDIDATE_SHA=$BACKINTEL_CANDIDATE_SHA RUN pip install --no-cache-dir aegra-api==0.10.5 langchain-typesafe==0.0.1a3 +ARG BACKINTEL_WITH_MODELS=0 +COPY requirements.models.txt ./requirements.models.txt +RUN if [ "$BACKINTEL_WITH_MODELS" = "1" ]; then \ + apt-get update && apt-get install -y --no-install-recommends libgomp1 && rm -rf /var/lib/apt/lists/* && \ + pip install --no-cache-dir torch==2.14.0 --index-url https://download.pytorch.org/whl/cpu && \ + pip install --no-cache-dir -r requirements.models.txt; \ + fi COPY aegra.json ./aegra.json COPY config ./config COPY runtime ./runtime COPY migrations ./migrations COPY scripts ./scripts COPY data/profiles ./data/profiles -COPY tests/test_replay_ledger.py ./tests/test_replay_ledger.py -COPY tests/test_costs.py ./tests/test_costs.py -COPY tests/test_jev.py ./tests/test_jev.py +COPY tests ./tests ENV AEGRA_CONFIG=/app/aegra.json PYTHONUNBUFFERED=1 EXPOSE 2026 CMD ["uvicorn", "aegra_api.main:app", "--host", "0.0.0.0", "--port", "2026"] diff --git a/README.md b/README.md index 70e55d7..fda1d2b 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,143 @@ # BackIntel -Local Olist Seller Performance PoC: reconciled facts, background review enrichment, versioned semantic changes, and a source-linked manager review. This checkout is the integration home. The website remains a separate project. +BackIntel targets a recurring operational problem: before a team can decide what needs attention, someone must gather scattered records, read free-text notes, reconcile changes, judge significance, and assemble a review people can trust. New information can make that review stale and force the same work again. This is the problem hypothesis we are testing; its frequency and cost still need validation with a real team. +The intended outcome is a current, prioritized review package showing what changed, what might happen next, what evidence supports each finding, and what still needs human judgment. BackIntel automates preparation and follow-up through approved data admission, structured interpretation, prediction, attention tracking, and reports. Operations analysts and supervisors retain responsibility for investigation, intervention, and business decisions. + +The current implementation is a predefined background workflow with model-assisted interpretation and prediction. Code controls its sequence, schedules, and permissions. It is developed against synthetic scenarios; the existing Olist PoC remains an optional integration example. Whole-job replacement, real-world predictive accuracy, and staff time saved have not been demonstrated. + +- [Current roadmap](docs/roadmap/v0.1.0-development-roadmap.md) — six steps to test and demonstrate issue review +- [Detailed capability reference](docs/roadmap/capability-reference.md) — full acceptance contract and supporting research - [Current repository map](docs/architecture/repository-map.md) - [Recovery plan and cleanup record](docs/roadmap/poc-recovery-plan.md) -- [Product contract and later release gates](docs/roadmap/v0.1.0-development-roadmap.md) -## Current local runtime +## Run the capability simulation + +The [active goal](docs/roadmap/v0.1.0-development-roadmap.md) requires real Jev interpretation and real CatBoost/TabICLv2 execution on synthetic data, followed by analysis, attention and useful stakeholder reports. Actual local predictors, durable follow-ups and report packaging now work with clearly labelled Jev fixtures. One actual Jev probe passed; the complete two-scenario Jev run remains unfinished. The command below is a development fixture; its simulated answers cannot satisfy final completion. Google TabFM is a separate model choice. + +```sh +python3 -m scripts.simulate +``` + +Two synthetic domains use one engine: JSON support tickets and CSV equipment readings. No dataset download, model key, server, or dependency installation is needed. Reports and JSON outputs appear in `~/Library/Application Support/BackIntel/Evidence/Simulation`. + +The scenarios exercise mapped ingestion, source lineage, rule-based simulated extraction, comparisons, missing observations, injected failure/retry, duplicate replay, simulated schedule events, acknowledgment/staleness/recovery, and audience-specific reports. Each artifact labels its synthetic inputs and simulation boundaries. Forecast output is an unevaluated linear stand-in; generated-code execution is deferred. + +Use `--scenario support` or `--scenario equipment` to select one; use `--output /absolute/path` to choose another output location. `python3 -m unittest discover -s tests -p test_simulation.py -v` runs the offline workflow checks. + +The new `capability_simulation` graph also plugs into the existing LangGraph/PostgreSQL runtime. Its registration is source code until an approved runtime update; the offline command already exercises the same core functions. + +The durable development journey uses a separate local service, preserving the installed Olist service and its data: + +```sh +.venv/bin/python -m scripts.capability_demo --development --demo-id demo-v1 --verify-recovery --wait +``` + +This builds the isolated service from `Dockerfile.runtime` and starts it on localhost:2027. Both synthetic scenarios complete durable source changes, comparisons, prediction-driven attention, model-update controls, audience reports and actual sandbox execution of a fixed generated-view fixture. The command restarts only the isolated runtime while work is pending, verifies replay, then packages scoped HTML/JSON/CSV reports, source code, execution logs and review receipts. Exported HTML is read-only and links to local evidence files, so it can be read without the viewer. It uses simulated predictors; final real-model acceptance remains separate. + +Use a fresh demonstration ID for `--verify-recovery`. To resume or replay that same stream, reuse its ID without that flag. Inspect with `python3 -m scripts.capability_demo --status --demo-id demo-v1`. Add `--serve` to the command to open the scoped local viewer after packaging; it prints a loopback login URL and the path to private one-hour audience tokens. Stop the viewer with Ctrl-C. The native scheduler stops after the bounded run, or after two minutes if the client disconnects. Each invocation retains its own receipt directory under `artifacts/validation/CapabilityDemo//Attempt...`; the isolated database volume retains recovery state. + +## Approved local model and sandbox checks + +Real CatBoost and TabICLv2 now execute classification and regression on synthetic structured features. The full facts-plus-real-Jev comparison remains unfinished. Model use requires the matching local approval record and pinned weights; it never authorizes paid Jev calls. The original Olist runtime stays separate. + +After preparing the approved `.venv`, model directory and a dedicated `test_` database, run: + +```sh +export BACKINTEL_MODEL_DIR="$PWD/artifacts/Models" +export BACKINTEL_APP_DATABASE_URL="$BACKINTEL_TEST_DATABASE_URL" +OMP_NUM_THREADS=2 OPENBLAS_NUM_THREADS=2 MKL_NUM_THREADS=2 HF_HUB_OFFLINE=1 \ + .venv/bin/python -m scripts.validation.check_real_models +.venv/bin/python -m scripts.validation.check_sandbox +``` + +The sandbox check uses the existing local `backintel-capability-demo-runtime` image and actually attempts prohibited operations. Results are retained under `artifacts/validation/RealPredictors` and `artifacts/validation/Sandbox`. These checks establish subsystem behavior on an uncommitted working tree, not final acceptance. The sandbox returns an untrusted candidate for later review; it does not publish a report. + +The optional model-enabled image also runs both predictors on CPU, using the approved local weights and two threads. Build it with `BACKINTEL_WITH_MODELS=1 docker compose -f compose.capabilities.yml build runtime`, then start the isolated service with `docker compose -f compose.capabilities.yml up --detach --wait --no-build runtime`. The default image build omits model dependencies. No provider credential is configured by Compose. + +Prepare committed synthetic histories through that service without spending money: + +```sh +.venv/bin/python -m scripts.real_capabilities prepare \ + --scenario support --demo-id real-demo-v1 --request-id history-preparation +``` + +Repeat with `--scenario equipment` for the second domain. Reusing the same request and demonstration identities replays the completed stage. `interpret` requires the exact source hash, a separately approved provider authorization and an ephemeral runtime credential. `compare` rejects incomplete or simulated Jev history and unknown actual charges. The stages now include the durable follow-up path described below; the complete real-run driver remains unfinished. Receipts remain under `artifacts/validation/RealPipeline`. + +Actual Linux model checks and file/checkpoint recovery passed in `artifacts/validation/RealPredictorsLinux` and `artifacts/validation/RealModelRecovery`. Recovery restored the approved files into a temporary directory and reproduced all 24 recorded predictions. That earlier check used the existing isolated evidence database. Combined recovery now reproduces 50 predictions and replays 90 completed jobs from restored database and model files, using Jev fixtures. Final actual-Jev recovery remains pending. + +The real path now also prepares future arrivals and corrections as immutable, time-scoped synthetic records: + +```sh +.venv/bin/python -m scripts.real_capabilities prepare_followups \ + --scenario support --demo-id real-demo-v1 --request-id future-source-preparation +``` + +Future records stay outside earlier feature cutoffs. After separate approval covering the prepared history and future sources, the `start` stage accepts `--authorization ` and durably queues interpretation, comparison and follow-ups. Each scenario needs a scope covering 27 source versions. The existing one-request probe cannot authorize this batch. Every paid request still checks its own budget and approval; failed or uncertain requests stop subsequent work for that task. + +Both scenarios completed 45 durable jobs with actual local predictors and **fixture Jev responses**. The check advances due timestamps in an isolated test database; it does not prove a live paid Jev journey or native-clock execution of that journey. Corrections invalidate earlier predictions and exclude superseded sources from subsequent training. Replaying all completed jobs changes no records and makes no extra fixture calls. The receipt is `artifacts/validation/RealFollowups/Attempt333bb285ed2d/Followups.json`. + +Prepare both scenarios without provider access: + +```sh +.venv/bin/python -m scripts.real_demo --demo-id real-driver-v1 --prepare-only +``` + +This builds the isolated model-enabled service and writes `AuthorizationScopes.json` under `artifacts/validation/RealDemo//Attempt...`. Each scope names the exact task, questions and 27 source versions. Startup refuses to replace a runtime with pending work. Use `--no-start` to reuse an existing compatible service and resume its workload. + +After separate approval for both complete scopes, use the same demonstration ID: + +```sh +.venv/bin/python -m scripts.real_demo --demo-id real-driver-v1 \ + --authorizations /absolute/path/Authorizations.json --verify-recovery +``` + +The authorization file maps scenario names to existing approved authorization records. Writing this file does not grant approval: + +```json +{"support": "existing-approved-support-id", "equipment": "existing-approved-equipment-id"} +``` + +The driver checks both scopes, request limits, pricing, measured budget and expiry before retrieving the existing Keychain credential. It places the credential in an owner-bound, task-bound, expiring file on runtime memory storage (`tmpfs`, directory `0700`, file `0600`). It never places the key in command arguments or Docker environment configuration. Cleanup removes only that attempt's credential and scheduler. An optional restart preserves pending work and reinstalls the authorized credential afterward. A failed or uncertain provider request stops further paid work for that task. + +Preparation, unauthorized-run denial, scoped native-clock scheduling and restart recovery passed without paid calls. Full paid execution remains unverified. The approved one-request Jev probe passed at a measured provider charge of $0.000011592. It returned `typesafe/jev-1.13-20260917` for the requested `jev-1.13` alias. Its saved response was accepted after a narrow model-name compatibility fix, without another paid call. That approval does not authorize either complete source scope. See `artifacts/validation/RealModelPreparation/ProbeAcceptance.json`. + +Packaging has explicit `development`, `real` and `predictor_fixture` modes. Real mode requires actual predictors, actual Jev and complete provider billing. Fixture mode labels simulated text findings and separates fictional charges from actual spending. Both ordinary and sandbox-generated offline reports passed desktop/mobile checks. All results remain working-tree evidence; paid Jev and exact committed-candidate acceptance are separate gates. + +## Local audience reports + +The working tree now produces audience-scoped briefings, analysis, CSV/JSON exports and source evidence. Operators can review and correct observations; read-only audiences cannot. Corrections preserve history and refresh the report. Generated layouts execute in the restricted host sandbox and require a separate local review. Development checks still use explicitly simulated models. + +After running the updated development code against an isolated database, open reports for its completed task IDs: + +```sh +export BACKINTEL_APP_DATABASE_URL="$BACKINTEL_TEST_DATABASE_URL" +.venv/bin/python -m scripts.audience_demo \ + --task support-audience-v1 --task equipment-audience-v1 \ + --access-file artifacts/validation/Audiences/local-access.json +``` + +The task IDs must match the chosen demonstration ID; `audience-v1` is an example. Open the printed loopback login page and use the appropriate token from the new private access file. Tokens last one hour, bind a task version and audience, and never appear in URLs or logs. The access file must not already exist. Stop the viewer with Ctrl-C. The isolated development image now includes the audience workflow; the preserved Olist service remains unchanged. + +Direct checks: `python -m scripts.validation.check_audiences` exercises actual HTTP authorization, report actions and generated-code review against an explicitly isolated test database. The browser companion is `scripts/validation/check_audience_ui.cjs`; it uses Playwright, a live local viewer and the private fixture emitted by the direct check. Evidence includes desktop/mobile captures, keyboard source inspection and table scrolling, saved corrections, and denied access. Full real-model packaging remains unfinished. + +A separate restore check backs up the completed development database, restores it into a fresh `test_` database on the existing isolated validation PostgreSQL container, compares accepted evidence, replays completed jobs, and removes the temporary clone: + +```sh +.venv/bin/python -m scripts.validation.check_capability_restore --demo-id demo-v1 +``` + +This check currently requires the local `backintel-capability-test` PostgreSQL container on port 55436. Its backup and receipt remain under `artifacts/validation/CapabilityRestore`. To restore an isolated predictor evidence database and approved model files together, set both database variables to that source database and run: + +```sh +.venv/bin/python -m scripts.validation.check_real_model_restore \ + --predictor-receipt /absolute/path/PredictorReceipt.json --restore-database \ + --predictor-image sha256: +``` + +Omit `--predictor-image` only when the host libraries match the prepared packages. The check uses a fresh temporary database on the validation server, restores package/checkpoint files into a temporary directory, reproduces recorded predictions and replays completed jobs. It preserves a backup and receipt, then removes both temporary restores. It does not restore the live scheduler or broker, and fixture Jev evidence cannot establish actual Jev recovery. + +## Preserved Olist runtime The local stack now runs from this canonical checkout. Runtime candidate: `741773df289866437b067c306d6fefea1b415691`. The eight historical backend worktrees have been deleted; their commits remain in `main` and the recovery bundle. The separate website remains untouched. @@ -33,7 +164,7 @@ export OPENROUTER_API_KEY="" Cutover and cleanup receipts: `~/Library/Application Support/BackIntel/Evidence/PoCRecovery/{cutover-acceptance,cleanup-receipt}.json`. Recovery set: `~/Library/Application Support/BackIntel/Recovery/20260926T170122/runtime-cutover`. Migration 0006 changes the snapshot uniqueness key; rollback requires restoring the coordinated application/checkpoint/broker backups before starting the previous image. -## Run the local PoC +## Run the preserved Olist PoC Use the existing Python/PostgreSQL/Aegra stack. The demo defaults to replaying recorded observations and makes **no inference calls**. Missing recordings are an error; they are never replaced with fabricated Jev output. diff --git a/aegra.json b/aegra.json index 7bad2eb..66e40fc 100644 --- a/aegra.json +++ b/aegra.json @@ -1,7 +1,11 @@ { - "dependencies": ["."], + "dependencies": [ + "." + ], "graphs": { "olist_fixture": "./runtime/graph.py:graph", - "olist_review_enrichment": "./runtime/review_graph.py:graph" + "olist_review_enrichment": "./runtime/review_graph.py:graph", + "capability_simulation": "./runtime/simulation_graph.py:graph", + "capability_platform": "./runtime/capability_graph.py:graph" } } diff --git a/compose.capabilities.yml b/compose.capabilities.yml new file mode 100644 index 0000000..5103194 --- /dev/null +++ b/compose.capabilities.yml @@ -0,0 +1,73 @@ +name: backintel-capability-demo +services: + postgres: + image: pgvector/pgvector:pg18 + environment: + POSTGRES_USER: capability_demo + POSTGRES_DB: test_backintel_demo + POSTGRES_HOST_AUTH_METHOD: trust + ports: + - "127.0.0.1:55437:5432" + volumes: + - capability_db:/var/lib/postgresql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U capability_demo -d test_backintel_demo"] + interval: 2s + retries: 20 + redis: + image: redis:7.2.5-alpine + command: ["redis-server", "--appendonly", "yes"] + volumes: + - capability_broker:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 2s + retries: 20 + runtime: + build: + context: . + dockerfile: Dockerfile.runtime + args: + BACKINTEL_CANDIDATE_SHA: ${BACKINTEL_CANDIDATE_SHA:-working-tree} + BACKINTEL_WITH_MODELS: ${BACKINTEL_WITH_MODELS:-0} + command: ["sh", "-c", "python -m runtime.bootstrap && exec uvicorn aegra_api.main:app --host 0.0.0.0 --port 2026"] + environment: + DATABASE_URL: postgresql://capability_demo@postgres:5432/test_backintel_demo + BACKINTEL_APP_DATABASE_URL: postgresql://capability_demo@postgres:5432/test_backintel_demo + BACKINTEL_TEST_DATABASE_URL: postgresql://capability_demo@postgres:5432/test_backintel_demo + REDIS_URL: redis://redis:6379/0 + REDIS_BROKER_ENABLED: "true" + AUTH_TYPE: noop + WORKER_COUNT: "1" + N_JOBS_PER_WORKER: "1" + CRON_ALLOW_SECONDS_SCHEDULE: "true" + CRON_POLL_INTERVAL_SECONDS: "1" + OTEL_TARGETS: "" + OTEL_CONSOLE_EXPORT: "false" + BACKINTEL_REPORT_DIR: /reports + BACKINTEL_MODEL_DIR: /models + BACKINTEL_PROVIDER_CREDENTIAL_FILE: /run/backintel-credentials/openrouter.json + OMP_NUM_THREADS: "2" + MKL_NUM_THREADS: "2" + OPENBLAS_NUM_THREADS: "2" + HF_HUB_OFFLINE: "1" + cpus: 2.0 + tmpfs: + - /run/backintel-credentials:mode=0700,size=1m + ports: + - "127.0.0.1:2027:2026" + volumes: + - ./artifacts/validation/CapabilityDemo/Reports:/reports + - ./artifacts/Models:/models + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:2026/health',timeout=2)"] + interval: 3s + retries: 30 +volumes: + capability_db: + capability_broker: diff --git a/config/real_models.json b/config/real_models.json new file mode 100644 index 0000000..1fbf5d2 --- /dev/null +++ b/config/real_models.json @@ -0,0 +1,49 @@ +{ + "schema": "backintel-real-models/v1", + "catboost": { + "package": "catboost==1.2.10", + "license": "Apache-2.0", + "source": "https://github.com/catboost/catboost", + "parameters": { + "iterations": 40, + "depth": 3, + "learning_rate": 0.08, + "random_seed": 42, + "thread_count": 2, + "verbose": false, + "allow_writing_files": false + } + }, + "tabiclv2": { + "package": "tabicl==2.2.0", + "repository": "jingang/TabICL", + "revision": "4dcd344ece2c00be9e831fdd35bed57b5ad83e19", + "license": "bsd-3-clause", + "source": "https://huggingface.co/jingang/TabICL", + "checkpoints": { + "classification": { + "file": "tabicl-classifier-v2-20260212.ckpt", + "bytes": 110368038, + "sha256": "bdc7dbd5e4ff21f8f0456fcf90c6b7cdf72dbea960f2d05b19bec19f9b3d4ed0" + }, + "regression": { + "file": "tabicl-regressor-v2-20260212.ckpt", + "bytes": 114324594, + "sha256": "0db9cb538f114e79026bf08f45f41ad8dd7ad2de2aaca9a5ca8cd3bd9748ae7a" + } + }, + "parameters": { + "device": "cpu", + "n_estimators": 1, + "n_jobs": 1, + "batch_size": 1, + "random_state": 42, + "verbose": false + } + }, + "limits": { + "max_training_rows": 64, + "max_prediction_rows": 64, + "max_cpu_threads": 2 + } +} diff --git a/config/simulation/equipment.json b/config/simulation/equipment.json new file mode 100644 index 0000000..8febe26 --- /dev/null +++ b/config/simulation/equipment.json @@ -0,0 +1,150 @@ +{ + "schema": "backintel-simulation/v1", + "id": "equipment", + "title": "Synthetic equipment monitoring", + "audience": "Maintenance coordinator", + "signal_label": "Readings above 80 degrees", + "fields": { + "id": "reading", + "entity": "asset", + "period": "cycle", + "content": "temperature" + }, + "extractor": { + "kind": "above", + "threshold": 80 + }, + "policy": { + "attention_rate": 0.5, + "stale_after": 5 + }, + "events": [ + { + "at": 0, + "action": "arrival", + "period": "baseline" + }, + { + "at": 1, + "action": "arrival", + "period": "baseline" + }, + { + "at": 2, + "action": "arrival", + "period": "change", + "fail_once": true + }, + { + "at": 3, + "action": "retry", + "period": "change" + }, + { + "at": 4, + "action": "acknowledge" + }, + { + "at": 10, + "action": "tick" + }, + { + "at": 11, + "action": "arrival", + "period": "change" + }, + { + "at": 12, + "action": "schedule", + "period": "missing" + }, + { + "at": 13, + "action": "schedule", + "period": "recovery" + } + ], + "source": { + "format": "csv", + "data": "reading,asset,cycle,temperature\nR01,PumpA,baseline,40\nR02,PumpB,baseline,45\nR03,PumpB,baseline,\nR04,PumpA,change,95\nR05,PumpB,change,98\nR06,PumpB,change,40\nR07,PumpB,change,\nR08,PumpA,missing,\nR09,PumpA,recovery,35\nR10,PumpB,recovery,40\n" + }, + "task": { + "schema": "backintel-task/v1", + "id": "equipment", + "entity": "equipment asset", + "fields": { + "id": "reading", + "entity": "asset", + "event_at": "occurred_at", + "available_at": "arrived_at", + "revision": "revision", + "content": "inspection" + }, + "measures": [ + { + "id": "temperature", + "field": "temperature", + "unit": "celsius", + "nullable": true + } + ], + "questions": [ + { + "id": "wear", + "prompt": "What wear level does the synthetic inspection report?", + "type": "number", + "rule": { + "kind": "number" + } + } + ], + "target": { + "id": "next-window-vibration", + "kind": "regression", + "unit": "mm/s", + "horizon": 2, + "minimum_train": 8, + "holdout_fraction": 0.25 + }, + "audiences": [ + { + "id": "operator", + "view": "analysis", + "entities": [], + "can_correct": true + }, + { + "id": "manager", + "view": "briefing", + "entities": [ + "Pump-A" + ], + "can_correct": false + }, + { + "id": "analyst", + "view": "export", + "entities": [], + "can_correct": false + } + ], + "policy": { + "max_rows": 100, + "max_attempts": 2, + "max_provider_calls": 200, + "entry": 0.6, + "clear": 0.3, + "cooldown": 3, + "stale_after": 6, + "response_deadline": 4, + "signal_question": "wear", + "signal_scale": 10, + "prediction_scale": 5 + }, + "observation_provider": { + "name": "synthetic-jev-contract", + "version": "1", + "implementation_mode": "simulated" + } + } +} diff --git a/config/simulation/support.json b/config/simulation/support.json new file mode 100644 index 0000000..22f152a --- /dev/null +++ b/config/simulation/support.json @@ -0,0 +1,218 @@ +{ + "schema": "backintel-simulation/v1", + "id": "support", + "title": "Synthetic support service", + "audience": "Service operations lead", + "signal_label": "Access interruption mentions", + "fields": { + "id": "ticket", + "entity": "queue", + "period": "window", + "content": "message" + }, + "extractor": { + "kind": "keywords", + "terms": [ + "blocked", + "failed" + ] + }, + "policy": { + "attention_rate": 0.5, + "stale_after": 5 + }, + "events": [ + { + "at": 0, + "action": "arrival", + "period": "baseline" + }, + { + "at": 1, + "action": "arrival", + "period": "baseline" + }, + { + "at": 2, + "action": "arrival", + "period": "change", + "fail_once": true + }, + { + "at": 3, + "action": "retry", + "period": "change" + }, + { + "at": 4, + "action": "acknowledge" + }, + { + "at": 10, + "action": "tick" + }, + { + "at": 11, + "action": "arrival", + "period": "change" + }, + { + "at": 12, + "action": "schedule", + "period": "missing" + }, + { + "at": 13, + "action": "schedule", + "period": "recovery" + } + ], + "source": { + "format": "json", + "data": [ + { + "ticket": "T01", + "queue": "Accounts", + "window": "baseline", + "message": "Password reset completed" + }, + { + "ticket": "T02", + "queue": "Access", + "window": "baseline", + "message": "Login now works" + }, + { + "ticket": "T03", + "queue": "Access", + "window": "baseline", + "message": null + }, + { + "ticket": "T04", + "queue": "Accounts", + "window": "change", + "message": "Account blocked after reset" + }, + { + "ticket": "T05", + "queue": "Access", + "window": "change", + "message": "Login failed twice" + }, + { + "ticket": "T06", + "queue": "Access", + "window": "change", + "message": "Service working" + }, + { + "ticket": "T07", + "queue": "Access", + "window": "change", + "message": "" + }, + { + "ticket": "T08", + "queue": "Accounts", + "window": "missing", + "message": null + }, + { + "ticket": "T09", + "queue": "Accounts", + "window": "recovery", + "message": "Access restored" + }, + { + "ticket": "T10", + "queue": "Access", + "window": "recovery", + "message": "Login works" + } + ] + }, + "task": { + "schema": "backintel-task/v1", + "id": "support", + "entity": "service queue", + "fields": { + "id": "ticket", + "entity": "queue", + "event_at": "occurred_at", + "available_at": "arrived_at", + "revision": "revision", + "content": "message" + }, + "measures": [ + { + "id": "load", + "field": "volume", + "unit": "tickets", + "nullable": true + } + ], + "questions": [ + { + "id": "risk", + "prompt": "Does this message report a service failure?", + "type": "boolean", + "rule": { + "kind": "keywords", + "terms": [ + "blocked", + "failed" + ] + } + } + ], + "target": { + "id": "next-window-breach", + "kind": "classification", + "unit": "probability", + "horizon": 2, + "minimum_train": 8, + "holdout_fraction": 0.25 + }, + "audiences": [ + { + "id": "operator", + "view": "analysis", + "entities": [], + "can_correct": true + }, + { + "id": "manager", + "view": "briefing", + "entities": [ + "Accounts" + ], + "can_correct": false + }, + { + "id": "analyst", + "view": "export", + "entities": [], + "can_correct": false + } + ], + "policy": { + "max_rows": 100, + "max_attempts": 2, + "max_provider_calls": 200, + "entry": 0.6, + "clear": 0.3, + "cooldown": 3, + "stale_after": 6, + "response_deadline": 4, + "signal_question": "risk", + "signal_scale": 1, + "prediction_scale": 1 + }, + "observation_provider": { + "name": "synthetic-jev-contract", + "version": "1", + "implementation_mode": "simulated" + } + } +} diff --git a/docs/architecture/repository-map.md b/docs/architecture/repository-map.md index 69e03cb..c3dfacd 100644 --- a/docs/architecture/repository-map.md +++ b/docs/architecture/repository-map.md @@ -4,7 +4,19 @@ Maintained integration checkout: `/Users/hudson/Documents/GitHub/BackIntel`. | Surface | Source | Responsibility | | --- | --- | --- | -| Product contract | [Roadmap](../roadmap/v0.1.0-development-roadmap.md) | User, Olist boundaries, required outcomes and deferred release work | +| Product contract | [Roadmap](../roadmap/v0.1.0-development-roadmap.md) and [capability reference](../roadmap/capability-reference.md) | Current issue-review demo steps; full 16-capability acceptance contract and historical plans in the reference | +| Capability simulation | [simulation.py](../../runtime/simulation.py), [CLI](../../scripts/simulate.py), [scenarios](../../config/simulation) | Mapped JSON/CSV inputs, simulated extraction/events, comparisons, attention lifecycle, source-linked artifacts | +| Generic runtime integration | [simulation_graph.py](../../runtime/simulation_graph.py) | Same simulator through existing LangGraph and PostgreSQL ledger; source registration pending runtime update | +| Portable evidence | [contracts.py](../../runtime/contracts.py), [evidence.py](../../runtime/evidence.py), [observations.py](../../runtime/observations.py) | Revision admission, immutable lineage, typed development extraction and correction history; real generic Jev integration pending | +| Predictive lifecycle | [prediction.py](../../runtime/prediction.py), [synthetic.py](../../runtime/synthetic.py) | Time-safe features/outcomes, real evaluation calculations, selection/rollback/scoring; real CatBoost/TabICLv2 structured-only paths checked; real semantic comparison pending | +| Real model boundary | [real_semantics.py](../../runtime/real_semantics.py), [real_models.py](../../runtime/real_models.py), [pinned models](../../config/real_models.json), [probe](../../scripts/real_model_probe.py) | Approved local model execution; separate paid approval, durable request ledger, no uncertain retry; actual Jev probe pending; CPU container execution and model-file recovery checked | +| Real staged execution | [real_pipeline.py](../../runtime/real_pipeline.py), [stage command](../../scripts/real_capabilities.py), [model recovery](../../scripts/validation/check_real_model_restore.py) | Committed history/future plans, one durable start, real predictor updates and replay checked with fixture Jev; combined driver implemented; single actual Jev probe passed, full paid journey pending | +| Restricted generated code | [sandbox.py](../../runtime/sandbox.py), [direct check](../../scripts/validation/check_sandbox.py) | Actual Docker isolation and rejection evidence; host-only execution, bounded logs, constrained generated layouts, audience-scoped preview and retained local reviews; included in the development package; explicit real and predictor-fixture packaging; fixture path checked | +| Audience artifacts and access | [artifacts.py](../../runtime/artifacts.py), [audience_server.py](../../runtime/audience_server.py), [generated_artifacts.py](../../runtime/generated_artifacts.py), [local viewer](../../scripts/audience_demo.py) | Token-bound task/audience access; briefings, analysis, exports, source/history, corrections and reviews; actual sandbox-generated layouts; checked on fixtures | +| Background capabilities | [capability_pipeline.py](../../runtime/capability_pipeline.py), [capability_graph.py](../../runtime/capability_graph.py), [jobs.py](../../runtime/jobs.py), [attention.py](../../runtime/attention.py), [development command](../../scripts/capability_demo.py) | Durable jobs/triggers and local attention; Aegra native cron owns timers; both updated development scenarios verified on isolated localhost:2027; persisted forecast analysis drives attention; real model integrations remain unfinished | +| Development packaging and recovery | [development command](../../scripts/capability_demo.py), [package writer](../../scripts/package_capabilities.py), [restore check](../../scripts/validation/check_capability_restore.py), [compose](../../compose.capabilities.yml) | Direct canonical-image build; pending-work restart, API replay, scoped reports and sandbox review; isolated database restore and cleanup; combined database/model recovery verified on real predictors with fixture Jev; actual Jev recovery pending | +| Real demonstration command | [driver](../../scripts/real_demo.py), [runtime check](../../scripts/validation/check_real_driver_runtime.py), [driver checks](../../tests/test_real_driver.py) | Both scenario scopes checked before credential access; owned expiring tmpfs credential; bounded task-scoped native scheduler; unpaid preparation, denial and restart verified; paid execution pending | +| Real report packaging | [writer](../../scripts/package_capabilities.py), [package check](../../scripts/validation/check_real_package.py), [browser check](../../scripts/validation/check_real_package_browser.cjs) | Explicit development, real and predictor-fixture modes; fixture charges separate from actual billing; scoped offline and sandbox-generated reports checked at desktop/mobile widths | | Local entrypoint | [poc.py](../../scripts/poc.py) | Database setup, recorded-batch admission, receipt inspection | | Source facts | [load_olist_facts.py](../../scripts/data/load_olist_facts.py), [migrations](../../migrations) | Fingerprints, row lineage, reconciled relational facts | | Background execution | [review_graph.py](../../runtime/review_graph.py), [aegra.json](../../aegra.json) | Accepted review batch → signals → comparison → report | @@ -16,6 +28,20 @@ Maintained integration checkout: `/Users/hudson/Documents/GitHub/BackIntel`. | Runtime fixture | [graph.py](../../runtime/graph.py) | Synthetic delay/partition probe for bounded recovery tests; not Olist processing | | Validation | [manifest](../../tabellio.validation.json), [tests](../../tests) | Exact-commit offline contracts plus separately recorded product evidence | +```mermaid +flowchart LR + J[Synthetic JSON source] --> M[Configurable input mapping] + V[Synthetic CSV source] --> M + M --> O[Source-linked observations] + O --> C[Shared comparisons and attention states] + C --> A[Briefing and evidence artifacts] + G[Existing LangGraph and result ledger] --> M +``` + +Two source shapes exercise the same capability code. Rule extraction, virtual triggers, projection, injected failures, and inbox delivery are simulated. Runtime integration uses the existing application ledger; it does not create another execution engine. The installed Olist runtime remains unchanged until a separate update. + +## Preserved Olist integration example + ```mermaid flowchart LR S[Approved Olist snapshot] --> F[Reconciled PostgreSQL facts] @@ -37,3 +63,5 @@ All eight historical backend worktrees and their obsolete local branch reference ## Report presentation contract Keep the existing report's system font, navy text, neutral tables, semantic HTML, source links, and explicit caveats. Add one sampled-change section using those same primitives; no new frontend stack or new brand direction. Required rendered states: populated integrated report on 1440px desktop and 390px mobile, keyboard-open source evidence, safe long source IDs, and horizontally scrollable tables without page overflow. No screenshot baseline is promoted automatically. + +The audience surface uses the existing report presentation contract as its reference lock: system font, navy text, neutral tables, native forms and details, no external media or new frontend framework. Required states are scoped/empty/denied reports, saved corrections, and accepted/rejected generated candidates at 1440px and 390px. Tables scroll locally with keyboard access; source identifiers wrap. Review evidence is under `artifacts/validation/AudienceUI/8b06b63f-612c-4f36-87f0-ea92c8652785`. This is a working-tree check, not a promoted baseline or final release. diff --git a/docs/design/decision-workspace-concept.md b/docs/design/decision-workspace-concept.md new file mode 100644 index 0000000..6b31f2a --- /dev/null +++ b/docs/design/decision-workspace-concept.md @@ -0,0 +1,64 @@ +# Decision workspace concept + +## Purpose + +Help a reviewer decide whether a BackIntel finding needs action, inspect its source, and record a reasoned decision. The first example is public-issue review. The layout is a concept, not a working application or proof of prediction quality. + +**Design status — 2026-09-29:** the first SVG mockup was rejected by the user. It is retained as an earlier draft and must not be used as an approved reference. The revised direction below is a proposal grounded in inspected interfaces. + +## Revised visual direction + +Primary reference: [Front's inbox email thread on Mobbin](https://mobbin.com/explore/screens/3e29dd35-0c1d-4f71-831b-af35c13bf1ec). Borrow its compact navigation, scannable queue, restrained selection state, and focused reading pane. Adapt its composer region into a human decision form. Open source implementation reference: [shadcn sidebar-07](https://ui.shadcn.com/view/new-york-v4/sidebar-07), inspected in a browser. + +![Observed Front inbox reference on Mobbin](references/front-inbox-mobbin.jpg) + +Use a neutral canvas, thin dividers, compact 32–36px controls, readable body text, and one accent for selection and primary action. Evidence opens on demand in a drawer or tab so the finding has room to read. Keep source facts, interpretation, and predictions visibly distinct through labels and grouping. Avoid oversized cards, decorative shadows, a permanently expanded third evidence column, large summary banners, and a contrasting navy navigation panel. The [reference lock](decision-workspace-reference-lock.json) records these constraints; the direction has not received user approval. + +## What determines the view + +The workflow defines the reader, decision, timing, and actions. A fixed layout then composes reusable sections: queue, case, observed facts, interpretation, optional prediction, evidence, decision, and later outcome. The agent supplies findings within bounded fields; it does not generate executable frontend code or choose arbitrary controls at runtime. A new workflow can omit or rearrange sections through reviewed configuration. + +## Proposed data flow + +1. The existing Python workflow collects source records, produces facts and findings, and keeps evidence references. +2. A versioned, audience-filtered decision record supplies the frontend. It keeps facts, interpretations, predictions, human decisions, and outcomes separate. +3. The frontend renders the appropriate view and sends human decisions to the server for durable storage. Browser storage is not the system of record. +4. The same record can feed a case view, alert, or summary. Prediction displays carry their evaluation status; experimental predictions do not set queue priority. + +The current audience server already has artifact JSON and review routes, but the public-issue prototype is separate. Connecting these requires an issue adapter and a reviewed decision-record contract; the mockup does not imply that integration exists. + +## Frontend options for the demo + +| Option | Fit | Added work | +| --- | --- | --- | +| Vite + React | Recommended for an interactive, client-rendered review demo backed by Python. Reusable components can render multiple workflows. | Define the shared record, adapter, API, and durable review write. | +| Next.js | Useful if the product later needs server-rendered pages, built-in server routes, or a larger web application. | Adds a second server layer that this local review demo does not yet need. | +| Reflex | Python-first alternative for a compact interactive demonstration. Can use the same record and decision rules. | Build and test a separate UI implementation and its state integration; do not fork the data contract. | + +**Decision proposed:** build the main demo with Vite + React + TypeScript, Tailwind CSS 4, and shadcn-compatible Radix primitives. Provide the requested small Reflex comparison using the same decision record. Reflex uses its own Python components and Radix Themes styling; matching the design means sharing token values and behavior, not assuming React component files can be imported unchanged. + +## Local infrastructure checked + +- `IntelIP/IntelIPWebsite/package.json` declares React 19, Tailwind 4, Radix controls, Instrument Sans, and Phosphor icons. Its actual `src/components/ui/` contains button, badge, card, tabs, separator, toggle, and toggle-group primitives. Inspect and adapt those before adding equivalents. +- Its `components.json` configures an `@intelip` registry at `http://localhost:3000/r/{name}.json`. A read-only connection check found no server listening on that port. Registry configuration is not proof of an operational shared component service. +- The IntelIP design skill and website instructions refer to design-system paths that are absent from the inspected locations. Treat those path references as stale; use present component source and CSS as evidence. +- BackIntel has Python artifact and review machinery but no frontend package manifest. These frontend packages are proposed, not installed in BackIntel. + +## Proposed CSS and component stack + +| Layer | Choice | Purpose | +| --- | --- | --- | +| Interactive views | React + TypeScript, built with Vite | Queue, case reader, source drawer, and saved review state. | +| Styling | Tailwind CSS 4 + semantic CSS variables | One consistent system for spacing, colors, typography, and responsive layout. Tailwind supplies utilities; the reference and tokens determine the appearance. | +| Controls | Existing shadcn-compatible primitives, backed by Radix | Reuse buttons, tabs, menus, dialogs, and keyboard/focus behavior. Use only the needed components. | +| Typography and icons | Instrument Sans + Phosphor | Match verified local package conventions with one font and icon family. | +| Data boundary | Audience-filtered Python API | Shared decision records and server-side review persistence. The browser never decides evidence access. | +| Python alternative | Reflex + Radix Themes + shared token values | Demonstrate the same review task with Python-authored views. | + +Official setup and alternative styling references: [shadcn Vite](https://ui.shadcn.com/docs/installation/vite), [Tailwind 4 support](https://ui.shadcn.com/docs/tailwind-v4), [Reflex styling](https://reflex.dev/docs/styling/overview), and [Reflex theming](https://reflex.dev/docs/styling/theming). + +Next.js remains an option if a server-rendered web application becomes a requirement. It does not determine the visual design. The next visual artifact should be a rendered component prototype showing the queue, selected case, evidence drawer, and decision form, before connecting live model execution. + +## First direct check + +Using one public issue, a reviewer can identify the source facts, distinguish interpretation from prediction, open evidence, save a decision with a reason, and later see the outcome. Repeat the display with a clearly simulated equipment case to test reuse of the layout, not equipment accuracy or operational readiness. diff --git a/docs/design/decision-workspace-mockup.png b/docs/design/decision-workspace-mockup.png new file mode 100644 index 0000000000000000000000000000000000000000..6f4206fc95bdd5619f6cb991e14b47ed6ea904de GIT binary patch literal 234552 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%0A^vEA2sBP)3` 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

BlB_nh+rzGQ1c>h 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/design/decision-workspace-mockup.svg b/docs/design/decision-workspace-mockup.svg new file mode 100644 index 0000000..b15cf71 --- /dev/null +++ b/docs/design/decision-workspace-mockup.svg @@ -0,0 +1,133 @@ + + BackIntel decision workspace concept + A visual mockup showing an issue queue, one case with facts and an experimental prediction, source evidence, and human decision controls. + + + + + + + + + + + + BackIntel + WORKSPACES + + + Issue review + Equipment watch + + Demo layout preview + RECORDS + Decisions + Reports + + Concept mockup · Not live + + Issue review + See what needs attention. Check the evidence. Record a decision. + + + Public issue demo + + + TODAY'S REVIEW + 3 cases need a human decision + + 7 cases checked + Original sources available + + Prediction status + Experimental only + + + Cases + 3 open + + ⌕ Search issues + NEEDS DECISION + + + + Issue #284 + Payment failure reported + Open 12 days · Review needed + + Issue #291 + Checkout error · Open 9 days + + Issue #305 + Refund request · Open 6 days + + REVIEWED + 4 cases have saved decisions + + View weekly summary → + + + ISSUE #284 / DECISION RECORD + Payment failure reported + + Needs review + Opened 12 days ago · Original public issue + + + OBSERVED FACTS + The opening report describes a payment failure. + No resolution appears in the collected record. + Facts come from the source snapshot, not the model. + + + BACKINTEL FINDING + This unresolved payment problem merits a billing review. + Interpretation · Reviewer should check before acting. + + PREDICTION · EXPERIMENTAL + Current models did not beat the simple baseline. + + + YOUR DECISION + + Follow up + + No action + + Need more info + + Reason and next step… + Later outcome stays hidden until the first decision is recorded. + + + Evidence + Check before deciding + + ORIGINAL SOURCE + Public issue #284 + Open source record ↗ + Snapshot: Jan 8, 10:32 AM + + SOURCE EXCERPT + “Payment failed at checkout. + The customer cannot complete + the order…” + Illustrative wording for mockup. + + TRACE + Source → facts → finding + → review → later outcome + + View processing history → + diff --git a/docs/design/decision-workspace-reference-lock.json b/docs/design/decision-workspace-reference-lock.json new file mode 100644 index 0000000..4f98cbb --- /dev/null +++ b/docs/design/decision-workspace-reference-lock.json @@ -0,0 +1,32 @@ +{ + "status": "proposal-awaiting-user-design-feedback", + "observed_on": "2026-09-29", + "primary_reference": { + "name": "Front Web Inbox Email Thread on Mobbin", + "url": "https://mobbin.com/explore/screens/3e29dd35-0c1d-4f71-831b-af35c13bf1ec", + "capture": "references/front-inbox-mobbin.jpg", + "preserve": ["compact navigation", "scannable queue", "restrained selection", "focused reading pane", "contextual actions"] + }, + "implementation_reference": { + "name": "shadcn sidebar-07", + "url": "https://ui.shadcn.com/view/new-york-v4/sidebar-07", + "capture": "references/shadcn-sidebar.jpg", + "borrow": ["collapsible navigation", "compact header", "composable controls"] + }, + "proposed_stack": ["Vite", "React", "TypeScript", "Tailwind CSS 4", "shadcn-compatible Radix primitives", "Instrument Sans", "Phosphor"], + "component_sources": ["IntelIP/IntelIPWebsite/src/components/ui", "official shadcn registry for missing controls"], + "role_constraints": { + "color": "neutral canvas; one selection/action accent; status colors reserved for meaningful states", + "type": "Instrument Sans; readable body text; compact metadata", + "controls": "32–36px typical controls; visible focus and accessible labels", + "evidence": "on-demand drawer or tab; preserve access to original source and collection time", + "agent": "supplies bounded content; layout and executable controls follow reviewed templates", + "media": "no decorative bitmap imagery needed" + }, + "reject": ["first SVG mockup", "oversized cards", "decorative shadows", "permanent third evidence column", "large summary banner", "contrasting navy navigation panel"], + "required_views": ["queue", "selected case", "source evidence", "decision form", "later outcome"], + "required_states": ["loading", "empty", "error", "long source text", "keyboard focus", "save success", "save failure", "stale record conflict"], + "required_viewports": ["1440px desktop", "390px mobile"], + "python_alternative": "Reflex components and Radix Themes sharing token values and decision-record behavior; separate UI implementation", + "accepted_visual_baseline": null +} diff --git a/docs/design/references/front-inbox-mobbin.jpg b/docs/design/references/front-inbox-mobbin.jpg new file mode 100644 index 0000000000000000000000000000000000000000..00dd54c82f8a78d9429d2a8d719b8b6ed1480f18 GIT binary patch literal 48692 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 Q{x_u*K+cPI@YkXL0*C3ky#N3J literal 0 HcmV?d00001 diff --git a/docs/design/references/shadcn-sidebar.jpg b/docs/design/references/shadcn-sidebar.jpg new file mode 100644 index 0000000000000000000000000000000000000000..03b067b2feba583269fc43d169e12387ffe73056 GIT binary patch literal 24490 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 Ky~VSGseb{@lcxFr literal 0 HcmV?d00001 diff --git a/docs/research/issue-prediction-results.md b/docs/research/issue-prediction-results.md new file mode 100644 index 0000000..ce3b5bb --- /dev/null +++ b/docs/research/issue-prediction-results.md @@ -0,0 +1,48 @@ +# First actual issue-prediction test + +**Result:** local predictions and historical evaluation now run on a bounded real-data sample. **Neither model beat the simple baseline.** This resolves the two blocks for an exploratory facts-only test, not for the full application or the planned 112-case evaluation. + +## What ran + +- Selected the earlier 20 January candidates for training and the first eight currently searchable February issues for testing, before model execution. Excluded two missing original snapshots without replacement: #201663 and #203938. +- Trained on **19 issues**: seven recorded response gaps and twelve other outcomes. Tested on **seven later issues**: five gaps and two other outcomes. +- Ran the existing pinned CatBoost 1.2.10 and TabICL 2.2.0 classifier implementations locally, with two CPU threads and the already downloaded, hash-verified TabICLv2 checkpoint. +- Used creation-time title/body lengths, code-block presence, link count, hour, and weekday. No future comments, issue identifiers, outcomes, or model-generated text findings entered the inputs. +- Both saved models/context reproduced their predictions after restoration. No paid calls, GitHub writes, or operational model activation occurred. + +## Historical evidence repaired + +The public archive index was missing January 6, 2024, at 22:00 UTC. The full original hourly file was recovered and scanned. All **432 hourly slots** across January 1–9 and February 1–9 are now represented by the index or recovered file. + +All relevant indexed creation, comment, and close/reopen records were reconciled with original archive payloads. Forty-nine original hourly files were fully scanned; total event counts matched the index in all 48 overlapping hours. Historical commenter roles came from the original payloads. Available GitHub comments and state histories were cross-checked. Missing originals remained excluded. + +Labels mean **open at seven days without a qualifying maintainer comment recorded in this frozen public history**. This establishes observable source coverage; it cannot establish that GitHub captured every event or that no private work occurred. Source limits remain visible. All January training outcomes were available before any February test issue opened. + +## Results + +| Method | Probability error: lower is better | Actual gaps among first three selected | +| --- | --- | --- | +| Training-frequency baseline | 0.324 | 2 | +| CatBoost | 0.356 | 1 | +| TabICLv2 | 0.388 | 2 | + +Probability error is the Brier score. The baseline gives every issue the training gap rate (36.8%); ties follow creation order. The oldest-first policy also found two gaps in its first three cases. No model was selected or tuned using these test results. + +All methods predicted below 50% for every test case; all missed the five actual gaps at that threshold. Seven test cases cannot support a general accuracy claim. The training/test gap rates differ substantially (7/19 versus 5/7). Keep the baseline comparison and expand the frozen evaluation before considering operational use. + +## Inspect or reproduce + +The local evidence folder is `~/Library/Application Support/BackIntel/Evidence/IssuePredictionTest/`. + +- `report.html`: seven actual prediction cards, original text, later outcomes, and method comparison. This is an inspection artifact, not the finished reviewer application. +- `predictions.json`, `cohort.json`, and `checks.json`: scores, excluded records, model identities, and direct checks. +- `evidence-manifest.json`: retained file hashes, including the research scripts and source replies. Third-party issue text remains outside the repository. +- `collect.py` retrieves/caches sources; `prepare.py` rebuilds labels and creation-only inputs; `predict.py` invokes existing local predictors; `report.py` renders stored results without inference. + +Offline verification: run `python3 "$HOME/Library/Application Support/BackIntel/Evidence/IssuePredictionTest/report.py"`. Model reproduction from the BackIntel checkout: set `BACKINTEL_MODEL_DIR="$PWD/artifacts/Models"` and `PYTHONPATH="$PWD"`, then run `.venv/bin/python "$HOME/Library/Application Support/BackIntel/Evidence/IssuePredictionTest/predict.py"`. Source retrieval is separate; neither command contacts a paid provider. + +Direct checks passed for label boundaries, bot/author exclusions, close/reopen ordering, future-field exclusion, time-separated evaluation, identical comparison cases, saved-model replay, and offline cohort reproduction. Peak model-process memory was approximately 748 MiB; the model loop including restoration took 8.36 seconds. Local compute cost is unpriced. + +Application runtime files were not changed. This is a source-hash-recorded research execution, not exact-commit product validation. Browser visual review, paid text interpretation, the full 112-case study, and integration into the standing background workflow remain outside this result. + +Sources: [GH Archive](https://www.gharchive.org/), original hourly files listed in local evidence, [GitHub REST](https://docs.github.com/en/rest/issues), and [ClickHouse's public archive index](https://play.clickhouse.com/). Retrieval and execution: September 29, 2026 UTC. diff --git a/docs/research/issue-review-artifact.md b/docs/research/issue-review-artifact.md new file mode 100644 index 0000000..1850a19 --- /dev/null +++ b/docs/research/issue-review-artifact.md @@ -0,0 +1,25 @@ +# Issue-review workspace prototype + +**Purpose:** let a reviewer inspect an original issue, decide what needs follow-up, and keep a portable record of that decision. + +The local prototype uses the seven real cases and saved predictions from the [first prediction test](issue-prediction-results.md). It does not run new models or change GitHub. + +## What the artifact contains + +- An oldest-first queue, search, and reviewed count. Unproven predictions do not determine order. +- Original opening text and a GitHub source link. Current source text may differ from the archived snapshot. +- Collapsed experimental estimates, with the failed baseline comparison explained. +- A human decision and reason/next step, saved in browser storage. Export produces JSON with all seven decisions and source identities. +- A deliberate reveal of later outcomes and recorded events. Later history stays hidden during the initial review. + +**Local files:** `~/Library/Application Support/BackIntel/Evidence/IssueReviewWorkspace/Review.html` is the workspace. `build.py` rebuilds it from saved evidence; `check.cjs` exercises the browser interactions. `BrowserChecks.json` and desktop/mobile screenshots retain the observed results. + +The visual direction reuses the preceding report: light surfaces, system fonts, blue controls, and an amber experimental notice. Native controls and plain-text source rendering keep the prototype small. The reference and required states are recorded in `reference-lock.json` alongside the artifact. + +## Verified boundary + +Browser checks passed for the seven original texts, saved notes after reload, decision export, hidden/revealed outcomes, search/empty states, keyboard navigation, desktop/mobile overflow, and a visible storage-failure path with export recovery. No automatic external requests or runtime errors occurred. Test decisions were made only in an isolated browser context. + +This is a local artifact prototype, not the integrated background application or exact-commit release acceptance. Browser notes can be lost if browser storage is cleared; export is the portable copy. Text interpretation, automated recommendations, shared ownership, and live notifications are not implemented here. No production visual baseline is being approved. + +**Next review:** use one case to judge whether the evidence and decision fields support the actual reviewer’s work. That feedback should shape the final artifact before broader integration. diff --git a/docs/research/issue-review-feasibility.md b/docs/research/issue-review-feasibility.md new file mode 100644 index 0000000..aaae3ee --- /dev/null +++ b/docs/research/issue-review-feasibility.md @@ -0,0 +1,45 @@ +# Issue-review data test — 2026-09-28 + +**Follow-up:** the [first actual prediction test](issue-prediction-results.md) recovered the missing archive hour, established bounded recorded-history outcomes, and executed both local predictors. The initial findings below remain the record of the first, narrower check. + +**Question:** can we recover original issue text and seven-day outcomes well enough to test response-gap predictions? + +**Result: blocked for prediction testing.** The first source check ran on 20 real issues. Original text is largely recoverable, but this bounded check does not establish complete seven-day coverage or two verified outcome classes. No models ran and no GitHub records changed. + +| Check | Result | +| --- | --- | +| Current issue, comment, and event histories collected | 20 of 20; paginated and comment counts reconciled | +| Creation records recovered from original GH Archive files | 19 of 20; recovered creation timestamps match GitHub | +| Currently visible comments within seven days matched to original archives | 34 of 34 | +| Cases with a verified qualifying historical maintainer response | 3 | +| Cases with a verified seven-day response gap | Not established | +| Original versus current text differences | 2 titles and 5 bodies changed | +| Historical role differs from the archive index field | 13 comments | +| Deterministic role, bot, author, and deadline boundary checks | Passed | + +## What the test caught + +- [Issue #201663](https://github.com/microsoft/vscode/issues/201663) has a current creation timestamp but no opening event was found in its expected archive hour. Its original input remains excluded; it was not replaced with current text. +- The public ClickHouse index did not preserve comment roles correctly for this sample. Original archive payloads are required for this target. +- Current GitHub roles can differ from roles recorded at comment creation. Current roles must not determine historical response labels. +- Matching all currently visible comments does not establish that no other activity existed. Only selected creation/comment hours were read, not the full seven-day event windows. No absence-based label was accepted. + +## Scope and limits + +Selected the earliest 20 **currently searchable** non-PR issues created in January 2024, ordered by creation time and issue ID. This is a feasibility sample, not the frozen training cohort: historical issues no longer searchable may change eligibility and ordering. + +The observed histories suggest seven response-gap cases and thirteen other cases, but those counts are **provisional, not training labels**. Positive archived response evidence verifies three non-gap outcomes. Full state and absence coverage remains unfinished. Public archive gaps and deleted activity remain source limitations even after a wider scan. + +The bot exclusions used for this check were `vscodenpa`, `VSCodeTriageBot`, and `github-actions[bot]`, plus accounts marked `Bot` or ending in `[bot]`. Original authors were excluded. Only historical `OWNER`, `MEMBER`, or `COLLABORATOR` comments within the inclusive seven-day window qualified. + +## Next action + +Resolve creation and full-window coverage before freezing the model cohort. Keep incomplete cases excluded. The recovered sources can support a review-screen preview, with any simulated findings clearly labelled. Do not expand to training on the provisional labels. + +## Evidence and reproduction + +Local evidence is stored at `~/Library/Application Support/BackIntel/Evidence/IssueFeasibility/`: `report.html` shows the 20-case table; `assessment.json` contains individual outcomes and limits; `evidence-manifest.json` records file hashes. Raw third-party content remains outside the repository. + +`collect.py` retrieves/caches read-only API records and the discovery index; `check_archive.py` retrieves selected original events; `assess.py` repeats the assessment offline and checks the label boundaries. Run the last script to reproduce results without network or model calls. These are research scripts, not a new product integration or exact-commit product acceptance. + +Sources: [GH Archive](https://www.gharchive.org/), the hourly files recorded in the evidence manifest, [GitHub issue comments](https://docs.github.com/en/rest/issues/comments), [GitHub issue events](https://docs.github.com/en/rest/issues/events), and the [ClickHouse public query service](https://play.clickhouse.com/). Retrieval: September 28, 2026, America/New_York (September 29 UTC). Model calls: zero. Provider spending: zero. Local compute/network costs were not priced. diff --git a/docs/roadmap/capability-reference.md b/docs/roadmap/capability-reference.md new file mode 100644 index 0000000..8569bdf --- /dev/null +++ b/docs/roadmap/capability-reference.md @@ -0,0 +1,1145 @@ +> Detailed reference retained from the previous roadmap. Follow the [focused roadmap](v0.1.0-development-roadmap.md) for current next steps. Earlier sequences and historical status below are not the current work queue. The full capability acceptance contract remains applicable. + +# BackIntel capability roadmap — synthetic data, real model execution + +**Active direction, corrected 2026-09-26.** Complete the reusable technology capabilities using synthetic source records and known synthetic historical outcomes, with real Jev interpretation and real CatBoost/TabICLv2 execution in the final demonstration. Deterministic simulated responses remain development fixtures and cannot satisfy the final model-execution requirements. Olist is an optional integration example. It does not define the platform user, schema, analysis task, or release sequence. This active plan supersedes the historical Olist release plan retained below. + +**Saved-goal correction:** The goal tool supports status changes but does not support editing the saved objective text. Its original simulation-only objective has not been edited or marked complete. This roadmap records the product owner's superseding finish line; real semantic and predictive model execution is required within this same unfinished goal. + +## Problem we are trying to solve + +**Problem hypothesis for customer validation:** an operations team has records of what is happening, but turning them into a current, defensible decision requires repeated human preparation. Evidence is distributed across structured records and free-text notes. Someone has to assemble context, interpret meaning, reconcile revisions, identify what deserves attention, and explain the result. The repository demonstrates parts of that processing; it does not establish how frequently a real team experiences this pain or what the pain costs. + +**The core burden is repeatedly reconstructing decision context.** A supervisor needs to know which cases deserve attention, what changed since the last review, what might happen next, and whether the supporting information is trustworthy. An analyst must first turn scattered inputs into those answers. If a source changes after the report is prepared, some of that work must be repeated. If nobody repeats it, the review can describe an earlier situation while people act on the current one. + +This creates three related costs to investigate: **preparation effort** spent gathering and reconciling information; **decision delay** while usable context is assembled; and **follow-up effort** spent checking whether concerns remain open, were corrected, or actually resolved. Mistaken classifications, unsupported predictions, or excessive alerts can add more work than automation removes. The value hypothesis must account for that total burden. + +The person doing the preparation and the person accountable for the decision may differ. The analyst needs less repeated assembly and fewer corrections. The supervisor needs a reliable, explainable priority list before the opportunity to act passes. The team lead needs a process that continues when the usual report preparer is unavailable. The economic buyer and the size of each pain remain unvalidated. + +### Current workflow and where it hurts + +The following is the target workflow to investigate with a real team. Consequences are hypotheses, not observed customer outcomes. Capability numbers refer to the full roadmap below. + +| Work people perform | Pain or failure point | Why it matters to the decision | Roadmap response | +| --- | --- | --- | --- | +| Decide what to monitor and what counts as a problem. | Definitions live in a person's judgment or differ between reviewers; the prediction target and time horizon may be unclear. | Two reviewers can prioritize the same case differently, and a model can optimize the wrong outcome. | 1: explicit task, question, target, audience, and policy contracts. | +| Collect and reconcile records. | Exports, identifiers, duplicates, late records, and corrections need manual reconciliation. | Cases can be missed or counted twice; a new export can silently change the basis of a finding. | 2–3: source admission, revisions, and preserved evidence links. | +| Read notes to understand what happened. | Important meaning is buried in text; classification consumes attention and varies between people. | A status field or count can omit the reason a case deserves attention. Ambiguous language can be mistaken for a fact. | 4: typed interpretation, distributions, correction history, and required uncertainty handling. | +| Combine findings with historical facts and outcomes. | Analysts reconstruct which information was available when; outcome labels may be incomplete or arrive later. | A convincing retrospective explanation can become an invalid prediction if it uses information from the future. | 5–6: time-safe features, separately available outcomes, and reproducible predictor preparation. | +| Decide whether a prediction is useful enough to rely on. | A plausible score can be accepted without a fair baseline comparison or a recorded approval decision. | The team may spend time on an inferior model or treat experimental output as operational guidance. | 7–8: comparable evaluation and controlled model activation, fallback, and rollback. | +| Reassess cases as information changes. | Someone must notice the change, remember to rerun the right work, and identify which earlier results are affected. | Decisions can use stale inputs; indiscriminate reruns can duplicate work, retrain needlessly, or repeat charges. | 9–11: targeted updates, durable jobs, and persistent triggers. | +| Prepare and defend the review. | Findings, calculations, source excerpts, and report versions must be assembled again for different audiences. | Review time is spent reconstructing the evidence; users cannot readily tell facts, estimates, and unknowns apart. | 13–14: scoped reports, traceable outputs, and restricted execution for generated artifacts. | +| Keep concerns open until they are actually resolved. | Repeated alerts obscure new information; acknowledgment, silence, and missing data can be mistaken for resolution. | A case can be neglected, repeatedly escalated, or closed without evidence that its condition changed. | 12: persistent attention state, deadlines, staleness, and controlled local delivery. | +| Operate the process through interruptions and increasing workload. | Failed runs, restarts, and unmeasured costs require manual checking and recovery. | The team cannot depend on the review arriving complete, once, and within agreed limits. | 15–16: operational controls and a repeatable integrated demonstration. | + +### Worked use case: preparing a support operations review + +**Illustrative workflow using the existing support domain; not a deployed customer process or a commitment to a first customer.** A supervisor must decide which unresolved service problems require investigation. The raw material includes ticket notes and approved structured facts. Prediction is relevant only if a specific future outcome, usable historical labels, and an action the team can take are defined. + +1. **Before the review, an analyst rebuilds the case context.** A ticket says a problem is resolved, but a later note says the problem returned. The analyst must distinguish the old state from the new statement and connect both to the right case. Counting tickets or copying the latest status does not settle the contradiction. The desired automation preserves both versions, extracts the relevant finding, and makes its evidence and uncertainty visible. +2. **The analyst determines whether the change deserves attention.** The note must be considered alongside current facts and the agreed review policy. If prediction is justified, a model estimates the defined future outcome. The desired result separates the observed change, the model's estimate, and the policy condition that caused the flag. It does not present a prediction as an observed incident or proof of its cause. +3. **The supervisor checks the finding and decides what to do.** A flag without usable source evidence transfers the preparation burden to the supervisor. The review therefore needs the changed condition, supporting records, freshness, uncertainty, and current attention state together. Investigation, assignment, and intervention remain human decisions. +4. **Another correction arrives after the review.** The earlier finding may no longer be valid. The desired workflow identifies affected results, retains the prior explanation, recomputes what changed, and updates the review. It avoids treating every refresh as a new reason to interrupt someone. +5. **At the next review, the concern still needs a disposition.** Acknowledgment means someone saw it; resolution needs evidence under the agreed policy. New outcomes, overdue responses, and stale inputs need distinct treatment. Maintaining that continuity is part of the product, rather than leaving the next analyst to reconstruct it again. + +The intended unit of value is **one current, defensible case in an operational review**, not a model response or a generated report file. A useful case tells a reviewer what changed, which inputs support it, which parts are estimates or unknown, why it meets the attention policy, and whether it is new, ongoing, acknowledged, stale, or resolved. The complete review also makes coverage and missing inputs visible so an empty queue cannot be confused with proof that nothing is wrong. + +### Where the value hypothesis is strongest + +The strongest candidate workflow repeats, uses stable questions, depends partly on text that existing fields do not capture, and requires follow-up as conditions change. The team must have access to the relevant sources and a practical response when a finding matters. Prediction additionally needs a meaningful target, reliable outcomes, and sufficient evaluation data. + +The likely value is less repeated preparation, earlier access to reviewable information, and more consistent follow-up. These are intended outcomes, not measured benefits. Classification accuracy alone is insufficient if verification takes longer than reading the original notes. Prediction accuracy alone is insufficient if the team cannot act on the estimate. More alerts are not evidence of better prioritization. + +The full capability may be unnecessary when the job is a one-time summary or an existing structured report already supports the decision reliably. Missing identifiers, inaccessible sources, unclear ownership, or absent outcome labels can be upstream blockers that interpretation and prediction do not solve. A simpler rule may outperform a model for a particular decision; the comparison must allow that result. + +### Validate the problem with the workflow owner + +Before claiming business value, observe a real review from source collection through follow-up. Ask the preparer to show the last completed review, trace one difficult case to its original records, and show what happened when a source changed or the usual preparer was absent. Identify the decision owner, action deadline, actual intervention, and recorded outcome. Record observed steps separately from interview opinions and product assumptions. Do not infer demand or willingness to pay from the synthetic demonstration. + +| Question to test | Evidence and measurement | +| --- | --- | +| Is repeated preparation a material burden? | Record human minutes spent gathering, reading, reconciling, preparing, checking, correcting, and following up per comparable batch. Include the checking and correction effort required by automation. | +| Does usable information arrive too late? | Measure time from relevant source availability to a reviewable finding and then to human review. Keep source-delivery delay separate from BackIntel processing delay. | +| Are extracted findings trustworthy enough? | Compare a declared sample with independently adjudicated meanings; record incorrect answers, unknowns, and human corrections. Valid JSON alone does not answer this question. | +| Does the queue improve prioritization? | Review flagged and unflagged cases for unnecessary flags and missed cases, using agreed definitions and available outcomes. Separate a correct policy flag from a useful business intervention. | +| Does follow-up remain accurate? | Trace reopened, corrected, acknowledged, stale, and resolved cases across reviews; check duplicate interruptions and cases closed without supporting evidence. | +| Is the total burden lower? | Compare human effort and measured provider/compute costs for the same work and quality requirements. Keep unknown costs explicit; avoid counting shifted review work as savings. | + +Agree on success thresholds with the workflow owner before a pilot. A comparison can use the same frozen inputs for manual and assisted preparation; a live pilot must also examine late arrivals, corrections, and response timing. If the problem is rare, current preparation is already cheap and reliable, or automation adds more checking than it removes, narrow or reject the use case. Improved downstream outcomes require separate evidence about interventions and their effects; a before/after report alone does not establish causation. + +This discovery and pilot work evaluates the business hypothesis. It does not replace or silently expand the existing synthetic-data, real-model technical acceptance contract. + +### Researched next experiment: public issue response-gap review + +**Selected by the user for action planning on 2026-09-28; execution remains pending.** Use public software issue triage as the first real-data evaluation of the support-review pattern. This selection does not approve a dataset/model run or replace the existing release contract. Keep the two synthetic scenarios for controlled capability acceptance. This experiment adds a concrete external example; it does not make GitHub the platform's domain model. + +**User and decision:** a repository's rotating inbox reviewer decides which new issues warrant a closer look before they remain open without a recorded maintainer response. BackIntel prepares a local review queue and evidence packet. It does not assign, label, comment on, or close live issues. + +**Documented process:** VS Code describes a rotating inbox tracker, feature-area owners, and automated classification; people handle issues the bot does not correctly triage. Its automation also manages requests for more information and follow-up. Therefore, the hypothesis is an improvement to evidence preparation and response-gap review alongside existing automation, not replacement of an entirely manual process. These documents establish a real workflow, not measured demand for BackIntel or proof that Microsoft needs this product. See [Issue Tracking](https://github.com/microsoft/vscode/wiki/Issue-Tracking), [Issues Triaging](https://github.com/microsoft/vscode/wiki/Issues-Triaging), and [Automated Issue Triaging](https://github.com/microsoft/vscode/wiki/Automated-Issue-Triaging). + +**Pain to test:** the reviewer must read a new issue, determine whether it describes a failure, check whether reproduction steps and environment details are present, distinguish a reported regression from a general request, and reconstruct subsequent comments and state changes. A bare count, label, or risk score does not provide that decision context. The intended benefit is a review packet that reduces reading and reconstruction while maintaining correction quality and follow-up accuracy. Preparation effort and prioritization benefit remain unmeasured. + +#### Dataset and bounded cohort + +- **Primary source:** [GH Archive](https://www.gharchive.org/) creation-time `IssuesEvent` records for `microsoft/vscode`, with comment and issue-state events. Preserve raw event IDs, timestamps, source locations, and hashes. GH Archive supplies hourly JSON event archives and a BigQuery representation; no paid query or bulk download was performed during this research. +- **Cross-check:** paginate the GitHub issue comments and issue-event APIs for selected records. Present-day issue bodies, labels, assignments, reactions, and comment counts must not substitute for creation-time feature snapshots. Missing event coverage or unavailable records become unknown/excluded cases, not invented negative outcomes. +- **Proposed cohort:** 112 non-pull-request issues with nonempty initial text and a complete observable seven-day outcome window: 64 training issues opened January 1–24, 2024; 16 calibration issues opened February 1–21; 32 held-out issues opened March 1–24. Follow outcomes through March 31. These are target counts, not a completed data profile. Within each window, select the earliest eligible records in `(created_at, issue_id)` order. Freeze the manifest before model execution; do not select on whether a prediction succeeds. +- **Feasibility gate:** first reconstruct 20 creation snapshots and outcome histories. Require both target classes in the proposed training cohort. If coverage, fields, or class balance are inadequate, record the reason and revise the cohort before freezing it. Keep the initial training/context set at 64 rows; a later larger study is separate. +- **Why a 2024 cohort:** GitHub [announced removal of author-association fields from several Events API payloads in 2025](https://github.blog/changelog/2025-08-08-upcoming-changes-to-github-events-api-payloads/). Historical and current schemas cannot be assumed interchangeable. Verify the actual archived schema before relying on association fields. +- **Rights and access:** public visibility is not a blanket redistribution license. Verify applicable source and contribution terms before model use or redistribution; see [GitHub's user-generated content terms](https://docs.github.com/en/site-policy/github-terms/github-terms-of-service#d-user-generated-content). Ship authored synthetic examples and a permitted retrieval/manifest path by default, rather than embedding third-party issue text in an open-source release. The previous model-use approval covers synthetic data, so it does not authorize this real-data experiment or its provider calls. + +**Small source check completed:** GitHub's issue search returned 226 non-PR issues opened January 1–7, 2024. Three records were inspected; two comment histories were checked. [Issue #201650](https://github.com/microsoft/vscode/issues/201650) was created January 1 at 00:54:06 UTC; its first currently visible collaborator comment is January 17 at 09:03:15 UTC, and its returned issue-event history contains no close/reopen events. [Issue #201652](https://github.com/microsoft/vscode/issues/201652) was closed as a duplicate on January 2. This confirms accessible text/timestamps and contrasting histories in a small sample. It does not establish archive completeness, initial-text integrity, cohort label distribution, or model performance. Deleted or otherwise unobservable activity remains a limitation. + +#### Exact prediction question and allowed inputs + +**Proposed target:** at issue creation, estimate whether the issue will be open seven days later without a recorded, non-bot maintainer comment during that interval. This is an observable response-gap proxy. It is not bug severity, customer harm, an official service-level violation, or proof that nobody worked on the issue through another channel. + +Define a qualifying maintainer comment using the archived comment's `OWNER`, `MEMBER`, or `COLLABORATOR` association, excluding the issue author and a frozen bot-account list. Do not rely only on the account `type`: automation can use ordinary user accounts. A contributor outside those roles does not automatically count as a maintainer. Unknown historical association or incomplete required coverage blocks that record's label. + +For `deadline = issue_created_at + 7 days`, label a case `1` only when reconstructed state is open at the deadline and no qualifying comment is recorded by that deadline; label `0` when an observed qualifying response exists or reconstructed state is closed at the deadline. Replay close and reopen events in order. Freeze the treatment of ambiguous histories and exclusions before training. Record outcome availability at the deadline so training cannot use an immature outcome. + +Use two feature sets on identical cases: + +- **Facts only:** creation hour/weekday and deterministic measurements of the initial title/body, such as length, code-block presence, and link count. Exclude usernames and all future comments, labels, assignments, closure fields, and final reaction totals. +- **Facts plus Jev findings:** fixed questions about whether the initial text reports a failure, includes reproduction steps, states expected versus observed behavior, provides environment/version information, and claims something previously worked. Preserve the source text reference, returned distributions, unknowns, and model/request identity. These are claims in the issue text, not verified technical diagnoses. + +Fit the baseline, CatBoost, and TabICLv2 on the same chronological data. Compare facts-only and facts-plus-findings routes. Evaluate probability quality with Brier score and response-gap retrieval at a fixed review capacity, alongside a simple oldest-open-unanswered review policy. The model estimates response-gap likelihood; it must not silently become an urgency or severity score. If semantic findings do not improve prediction, retain the stronger baseline and assess whether the structured review packet still helps the reviewer. + +#### Action plan: testing and visible demonstration + +**Business result to demonstrate:** a rotating inbox reviewer can move from an unread issue to an evidence-linked review decision, then see the packet update when a response or state change arrives. The pain being tested is repeated reading, checking, and reconstruction of the same case. A prediction alone does not satisfy this result. + +**Delivery status:** this is a plan, not an implemented or validated GitHub demo. Existing local reports, audience views, background jobs, and evidence storage provide starting points. The GitHub adapter, historical cohort, uncertainty handling, and scenario-specific views still need work. The current validation manifest also needs complete visual, operational, and security coverage before integrated acceptance can pass. Existing fixture checks and isolated model runs do not prove this new journey. + +**Authority and ownership:** the user owns product tradeoffs, the pilot target, and execution approval. A single implementation owner should integrate the adapter, views, and evidence; a named repository reviewer should adjudicate interpretation examples and run the usefulness pilot. Those people are not yet assigned. This request authorizes the plan only. No Plane changes, provider calls, dataset ingestion, live GitHub changes, deployment, or publication are part of preparing it. The prior synthetic-data model approval does not cover public issue content. Keep at most three work items active; split each milestone into bounded implementation sessions when execution is authorized. + +##### What the audience will see + +Use the existing local audience viewer and report packaging, extending them for this workflow. Keep engineering diagnostics in a separate operator view. + +| View | What it shows | What the reviewer should understand | +| --- | --- | --- | +| Review overview | Repository, cohort dates, replay clock, cases ready for review, incomplete cases, last update, and execution mode. | What information is available now and what is still missing. Progress reflects recorded work, not an animated simulation of live processing. | +| Original issue and findings | Creation-time title/body alongside the five structured questions, source references, supported answers, and explicit unknowns. | What the text actually says, what the model inferred, and what needs checking. Missing reproduction steps must remain missing. | +| Prediction comparison | Creation-time feature cutoff, response-gap definition, five comparable routes, sample counts, probability scores, and fixed-capacity results. | Whether semantic findings add value over simpler methods. Outcome labels stay hidden in the walkthrough until the replay clock reaches their availability time. | +| Review queue and case detail | Source link, review reason, predicted seven-day response-gap likelihood, current observed state, uncertainty, and a local reviewer decision/note. | Where closer review may help. The queue must not present the probability as severity or urgency. No assignment or message is sent to GitHub. | +| Case timeline | Initial snapshot, original prediction, actual recorded comments and close/reopen events, deadline, packet updates, and local attention changes. | How the standing workflow keeps the packet current. Later facts never overwrite the original prediction. | +| Operator evidence | Run and candidate identity, source/model versions, stage results, failures, retries, measured usage, and evidence links. | Which checks passed, failed, or remain blocked, without cluttering the business-facing review. | + +Every screen must distinguish **authored fixture**, **replay of recorded actual model output**, and **new model execution**. Historical source events and deliberately injected test faults must also be visibly different. Replaying stored output does not incur or imply a new provider call. + +##### Milestones and dependency order + +| Milestone | Bounded deliverable and owned surface | Completion check and stop condition | +| --- | --- | --- | +| 1. Establish usable evidence | Data owner reconstructs 20 histories, then freezes the proposed 112-case manifest, exclusions, source rights/access decision, cutoff rules, and label policy described above. No model fitting. | Every admitted case has its original snapshot and sufficient observable outcome coverage; hashes and ordering reproduce. Unknown coverage is excluded with a reason. Stop if trustworthy creation text or outcome labels cannot be recovered; revise the cohort before continuing. | +| 2. Define trustworthy interpretation | Workflow reviewer adjudicates a small development set for the five questions, including absent and ambiguous information. Implementation owner defines the output schema, unknown policy, and deterministic truth fixtures. Repair the validation manifest's missing required coverage in a separate bounded task. | Freeze scoring and acceptance thresholds before held-out model results are inspected. Missing evidence cannot silently become a confident answer. Unsupported outputs fail validation; required validator entries must actually execute checks. | +| 3. Build one complete fixture journey | Implementation owner adds the issue adapter/task contract, explicitly extends the current support/equipment allowlists, and connects normalized sources to findings, prediction, local review, timeline, and artifacts. Use authored fixtures first. Depends on milestone 2; real adapter inputs depend on milestone 1. | One command can produce a clearly labelled fixture package. The reviewer can follow a case across all audience views. Negative-case checks below pass. No fixture output is represented as a real model result. | +| 4. Run the frozen real-data comparison | After separate data/model-use and capped provider approval, run baseline, CatBoost facts, CatBoost facts plus Jev, TabICLv2 facts, and TabICLv2 facts plus Jev. Preserve actual replies, usage, identities, and identical split manifests. Depends on milestones 1–3. | All routes report on the same 32 held-out cases or report the entire comparison blocked; do not silently drop failures. Record Brier score, retrieval at the agreed capacity, and comparison with oldest-open-unanswered. Show losses as well as gains. Missing required usage telemetry blocks completion. | +| 5. Deliver a repeatable demonstration | Package actual outputs and historical replay locally, with an exact candidate identity, operator evidence, starting-state reset, walkthrough, and fixture fault cases. Depends on milestone 4 for a real-data demo; milestone 3 supports a separately labelled fixture preview. | Complete the walkthrough twice from reset without new model calls; predictions and substantive state transitions agree, excluding run IDs and timestamps. Required exact-candidate checks end as passed, failed, or blocked. No missing check counts as passed. | +| 6. Measure reviewer usefulness | Named reviewer compares source-only preparation against the assisted packet on comparable disjoint cases, with counterbalanced order. Record reading, checking, and correction time plus material mistakes. Depends on milestone 5. | Report measured results and limitations. Proposed target: at least 30% less preparation time with no increase in material errors; workflow owner must agree before the pilot. A failed target is an informative result, not a reason to alter cases or claim success. | + +The critical sequence is evidence → interpretation contract → complete fixture journey → approved real comparison → repeatable demonstration → human usefulness pilot. Validation-manifest repair can run alongside data feasibility. Do not begin model experiments while the target, feature cutoff, or interpretation contract is unsettled. + +##### Repeatable walkthrough: approximately ten minutes + +Select cases by a frozen rule before looking at model success: earliest eligible held-out case with an observed qualifying response, earliest observed response-gap case, and earliest closed-at-deadline case. Add the first extraction failure or disagreement in cohort order if one exists. If a category is absent, disclose it and use an authored fixture solely for the missing behavior. The researched public examples above are candidates for explanation, not accepted ground truth until their histories are reconstructed. + +1. **Start at the frozen clock (one minute).** Show the reviewer’s job, source dates, mode badge, and initial queue. State the exact prediction question and the human decision still required. +2. **Inspect the original text (two minutes).** Open a case. Compare its five structured observations with the source. Show one missing or ambiguous field and how the packet asks for human checking. +3. **Explain the prediction (two minutes).** Show only creation-time inputs, the selected route, and its response-gap estimate. Explain selection using calibration data; do not tune the model on the holdout. Reveal the aggregate comparison as a separate retrospective evaluation, not future information available to the simulated reviewer. +4. **Record a local review decision (one minute).** Mark the case as needing investigation or no further local review and add a note. Show that this is a human decision and that GitHub remains unchanged. +5. **Advance historical events (two minutes).** Replay a recorded response or close/reopen sequence and then the seven-day deadline. Show updated current state, attention, and packet freshness beside the unchanged original prediction. Closing a duplicate does not mean the underlying bug was fixed. +6. **Show recovery and the result (two minutes).** In the labelled fixture fault lane, deliver an event twice and interrupt a worker. Resume it and show one accepted result and no duplicate local notification. Finish with the operator’s passed/failed/blocked evidence and measured usage. + +The presentation uses stored actual results by default so a meeting does not depend on provider latency or authorize fresh spending. A new model run is a separate, explicitly approved execution mode. Reset only run-owned demo state; retain source manifests and evidence. No live repository cleanup is needed because no live issue changes are permitted. + +##### Verification matrix: what can be checked deterministically + +| Check | Required direct verification | Expected result | +| --- | --- | --- | +| Source integrity | Rebuild normalized cases from the frozen source manifest; compare hashes and joins. | Identical case inputs; unknown coverage is explicit. Hashes verify replay identity, not completeness of all real-world activity. | +| Seven-day label | Fixture histories cover before/at/after deadline comments, author comments, excluded bots, missing association, and ordered close/reopen events. | Labels follow the frozen rule; uncertain histories stay unknown. Deadline inclusion and simultaneous-event ordering are specified in the contract. | +| Future-data exclusion | Change later comments, final labels, assignments, and outcome fields while holding the creation snapshot fixed. | Creation-time feature hashes and stored original predictions remain unchanged. Later operational state can change. | +| Interpretation | Validate recorded responses against schema and the adjudicated examples; include absent, contradictory, and malformed answers. | Invalid data is rejected; unknowns survive into the UI. Report semantic accuracy against frozen thresholds; deterministic parsing alone does not prove semantic correctness. | +| Fair comparison | Check split membership, outcome availability, identical cases, fixed review capacity, and hand-calculated metric fixtures. | No train/calibration/holdout overlap or unavailable labels; metric calculations agree. Selection uses calibration only. | +| Durable workflow | Replay duplicate events, same request with conflicting input, out-of-order delivery, failure, and interruption around result persistence. | No duplicate accepted result or local notification; conflicts fail clearly; state reconstructs in event order and resumes safely. | +| Paid-call uncertainty | Simulate interruption after provider submission but before acknowledgement. | Mark the result unresolved and stop automatic paid retries; do not claim exactly-once provider execution. | +| Review UI | Browser check and human walkthrough of all six views, including keyboard use, unknowns, loading, failed stages, source links, and local notes. | A reviewer can distinguish evidence, inference, later outcome, and their own decision. Failed or incomplete work is visible. | +| Scope and cost | Verify read-only source access, blocked mutation paths, configured limits, and ledger reconciliation. | No GitHub writes; approved limits hold; missing required cost/usage evidence yields blocked. A post-handler timeout is not a hard execution cap. | +| Repeatability | Replay the same stored replies and events twice from reset; compare substantive artifact fields and state. | Matching results apart from declared volatile fields. New model calls are assessed against frozen quality thresholds, not promised byte-for-byte identical. | + +Run the required static, schema, semantic, workflow, visual, operational, and security checks against the candidate used for the demonstration. Reuse and extend the existing report/viewer paths (`runtime/artifacts.py`, `runtime/audience_server.py`, `scripts/package_capabilities.py`) and package/browser/runtime validators; do not create a second demo platform. No new issue-specific runner or validator is claimed to exist yet. + +Once implementation and a committed candidate are authorized and ready, use the existing validation entrypoint with the repaired manifest: + +```sh +node scripts/validation/run.mjs \ + --manifest tabellio.validation.json \ + --expected-commit "$release_sha" \ + --output artifacts/validation/ReleaseCandidate +``` + +`release_sha` must identify the exact candidate being demonstrated. The package must include its run manifest, replay instructions, metric table, screen captures, and required validator evidence. Local working-tree checks cannot substitute for exact-commit acceptance. + +**Done for the demonstration:** a reviewer can complete the walkthrough, inspect the original evidence and actual model outputs, see a correct update and recovery, and inspect all required results without hidden fixtures or fresh provider calls. This proves a bounded historical review workflow. It does not prove saved time, intervention benefit, broad market demand, or production readiness. Those need the separate human pilot and later prospective evaluation. The existing all-16-capability release contract below remains unchanged. + +#### Why this experiment instead of the other candidates + +| Candidate | Useful capability | Reason not to use it as the first prospective text-to-outcome demonstration | +| --- | --- | --- | +| Existing synthetic support/equipment fixtures | Deterministic software, interruption, and permission checks. | Outcomes are generated by known formulas; they do not validate naturally occurring predictive relationships or real preparation pain. Retain them for technical acceptance. | +| Olist orders and reviews | Commerce facts, review interpretation, and historical operational analysis. | Olist says surveys are sent after receipt or when the estimated delivery date is due. Using that order's later review as an earlier predictor of its delivery outcome leaks information. Earlier reviews could support a different future-window task, but that needs its own contract. See the [publisher's dataset description](https://www.kaggle.com/datasets/olistbr/brazilian-ecommerce). | +| CFPB consumer complaints | Complaint-theme extraction and response monitoring. | Complaints are published after the company responds or after 15 days, whichever comes first; narratives also require consent and scrubbing. A public narrative cannot automatically be treated as available at initial submission to predict the same response. See [CFPB publication rules](https://www.consumerfinance.gov/data-research/consumer-complaints/) and [data-use definitions](https://www.consumerfinance.gov/complaint/data-use/). | + +GH Archive is selected for feasibility assessment because event snapshots can separate initial text from later outcomes, subject to the feasibility and rights gates above. The user has selected this developer-support workflow for planning. Customer demand, commercial clearance, model accuracy, and business improvement remain unproven. + +## Product objective + +Give operations teams a current, evidence-linked review of changed conditions and predicted outcomes, with less repeated preparation and clearer follow-up as the intended benefits. Turn approved heterogeneous data into traceable information and stakeholder outputs through background workflows that do not require a chat prompt for every batch. Keep business decisions and operational interventions with people. + +Develop against varied synthetic scenarios. Interpret their relevant text with real Jev, retain its actual returned findings and request/model identity, build prediction-ready information using facts and findings available at each cutoff, execute real predictors, and feed their predictions into further analysis, attention tracking and useful stakeholder outputs. Dataset adapters, extraction definitions, analysis policies, and audience requirements may vary; the core execution, provenance, result lifecycle, and artifact contracts remain reusable. + +### Workflow and process we automate + +BackIntel automates preparation and follow-up for **recurring operational review and exception management**: assembling scattered records, identifying emerging problems, prioritizing items for review, preparing useful reports, and keeping open concerns current. An exception is a case that meets an agreed condition for human attention. The primary users are operations analysts, reporting coordinators, and team supervisors. + +The standing assignment is: **watch approved information, reassess predefined questions, and surface changes that deserve attention**. New records, corrections, schedules, deadlines, or stale information can start agreed work without another chat prompt. Models run when the workflow calls them; there is no continuously thinking model choosing its own goals. + +People define the entities, input mappings, questions, prediction targets and horizons, attention rules, audiences, and permitted actions. The workflow produces a review package containing what changed, predicted outcomes, supporting evidence, freshness, uncertainty, and unresolved concerns. People investigate, assign work, authorize interventions, and assess actual outcomes. Demonstrated delivery stays within the local inbox/outbox simulator. + +### Where the capability sits in the data lifecycle + +| Stage | Work automated or assisted | Who controls the decision | +| --- | --- | --- | +| Admit and validate | Accept approved batches; identify duplicates, invalid records, late arrivals, and corrections; preserve sources. | Written validation and admission rules. Live source-system integration is a separate requirement for each workflow. | +| Interpret unstructured information | Ask fixed questions about relevant text and convert returned answers into structured observations. | Jev makes a learned interpretation; people define the questions and acceptable interpretation quality. | +| Prepare prediction inputs | Combine facts and observations into versioned, entity-level inputs using only information available at the declared cutoff. | Written mapping, missing-value, versioning, and time-cutoff rules. | +| Compare and predict | Compare the baseline, CatBoost, and TabICLv2 on shared eligible cases; score new inputs with the selected model. | Models supply estimates; code applies evaluation and selection rules. The demo simulates operator approval for activation. | +| Analyze and prioritize | Compare findings and predictions with the configured policy; create or update attention episodes. | Written business rules. The current risk-combination policy is explicitly synthetic. | +| Prepare the operational review | Produce audience-scoped reports and exports with source links, freshness, uncertainty, and simulation labels. | Written report and access rules; people judge usefulness and decide action. | +| Follow up and reassess | Record acknowledgment and resolution, react to deadlines and staleness, admit observed outcomes, and refresh affected results. | Recorded events and written scheduling/update rules; people remain responsible for real operational interventions. | + +For fixed inputs, state, and policies, admission, routing, joins, cutoff checks, and attention rules are deterministic. Interpretation and prediction use learned models. Fixed-model inference may be repeatable, but that does not establish correctness or remove uncertainty; training and live-provider responses require controlled evaluation. + +The main LangGraph graph currently wraps a single operation-dispatch node. BackIntel's Python functions and persisted jobs choose the stages. There is no model planner that independently chooses new questions, retrieves additional sources, selects tools, or changes the analysis strategy. Adding that investigative behavior would be an explicit capability decision. It is not required merely to run the agreed workflow in the background. + +### Roles and tasks affected + +| Role | Routine work the capability could take over | Human responsibility retained | +| --- | --- | --- | +| Operations analyst or reporting coordinator | Assemble records, refresh calculations, maintain review queues, and prepare recurring reports. | Define useful questions, investigate findings, and recommend action. | +| Data classification or review specialist | Read routine text and assign predefined structured findings. | Define categories, adjudicate ambiguity, and check interpretation quality. | +| Support or maintenance triage coordinator | Surface cases meeting attention rules and maintain their review state. | Set priorities, assign owners, handle exceptions, and authorize intervention. | +| Business intelligence analyst | Refresh recurring views and trace displayed results to evidence. | Define metrics, explain causes, and assess business meaning. | +| Data engineer or data scientist | Execute established transformations, model comparisons, and scoring. | Design integrations and targets, validate models, prevent leakage, and maintain the system. | + +These are task-level automation opportunities, not demonstrated replacement of whole jobs. The strongest initial fit is preparing and maintaining an operational review. Engineering and data-science work gains reusable execution machinery, while expert design and judgment remain necessary. + +The support-review example above illustrates this division of work. Equipment maintenance can use the same process shape with different inputs, questions, targets, and policies; transfer to other workflows must be demonstrated rather than assumed. + +### What the PoC proves and what remains unproven + +The capability is being demonstrated across **admitted data → structured findings → time-safe prediction inputs → predictions → an operational review**, with evidence history and background execution around that flow. Synthetic fixtures have exercised admission, corrections, feature preparation, reports, and attention transitions. Actual CatBoost and TabICLv2 have run locally; integrated predictor journeys used simulated Jev findings. One actual Jev request passed, while the complete two-scenario real-Jev journey and exact-candidate acceptance remain unfinished. The capability table below owns the detailed evidence status and release conditions. + +Broad live integrations, representative interpretation quality, real-world predictive accuracy, continuously operating production service, operational interventions, staff time saved, and improved business outcomes are not established by this PoC. The demonstration uses synthetic data, bounded execution windows, a fictional attention policy, simulated model-activation approval, and simulated code-generation input to an actual restricted sandbox. Open-ended investigation and autonomous operational action are outside the current implementation. + +The problem-validation plan above defines the evidence to collect in a real-workflow pilot. Those observations can support a business-value assessment and changes to job duties; passing software checks alone does not. This business framing preserves all 16 capabilities and the real-model finish line below. + +## Full capability list — one technology roadmap + +The platform machinery and the selected models must execute. Synthetic records, deterministic provider fixtures and captured outbound messages support development without coupling it to one dataset. Final acceptance requires actual returned Jev findings and actual CatBoost/TabICLv2 predictions. A configured adapter name or a stored simulated response does not prove model execution. Synthetic-data performance does not establish real-world accuracy or business value. + +| Step | Business objective | Capability and completion condition | Current evidence | +| --- | --- | --- | --- | +| 1. Portable task contracts | Reuse the platform for different decisions. | Configure entities, input mappings, units, questions, measures, targets, prediction horizons, audiences, and policies. Two unrelated scenarios share the same core code. | Implemented and checked for two unrelated synthetic source mappings and task contracts. | +| 2. Source admission and revisions | Keep information current without manual reconciliation. | Admit synthetic CSV/JSON/text batches; identify duplicates, late arrivals, corrections, and schema failures; expose accepted/quarantined dispositions. | Implemented admission, quarantine, duplicate handling and source corrections; direct checks passed. | +| 3. Evidence and lineage | Make every finding explainable and correctable. | Preserve immutable source facts, observations, feature rows, outcomes, predictions, policies, and artifacts as distinct versioned records. Trace outputs to their inputs and retain correction history. | Immutable PostgreSQL evidence and correction history implemented; direct checks passed. | +| 4. Semantic observations | Make unstructured information usable. | Run real Jev against relevant synthetic text through a configurable typed boundary with distributions, unknowns, abstention, retries, caching, provenance and separate corrections. Preserve actual responses and request/model identity. | Typed real-provider boundary checked; one approved actual Jev request now retains its returned finding, request ID, resolved model and measured charge. Complete history/follow-up execution remains unapproved. | +| 5. Point-in-time features and outcomes | Support credible prediction without future information leaking in. | Generate versioned feature snapshots and separately available outcomes from synthetic histories. Enforce cutoff, target eligibility, null, entity-grain, and chronological split rules. | Cutoff-safe feature snapshots, separate outcomes and chronological eligibility checked. | +| 6. Predictor preparation | Support interchangeable predictive approaches. | Execute a native baseline, real CatBoost training/preparation and real TabICLv2 context preparation through common prepare/score interfaces. Retain model/package/weight identities and preparation inputs. | Actual structured-only CatBoost/TabICLv2 classification and regression checked on the host and in the CPU container under local approval; real semantic preparation pending Jev. | +| 7. Predictor evaluation | Determine whether a candidate improves a decision on the supplied data. | Compare real CatBoost and real TabICLv2 using ordinary facts alone and facts plus real Jev findings, alongside the baseline. Use identical eligible cases/cutoffs and actual synthetic historical outcomes; calculate classification/regression metrics, calibration, latency and resources. | All four actual predictor routes ran with fixture Jev features in both domains; metrics remain synthetic. Required facts-plus-real-Jev comparison is pending paid approval. | +| 8. Model lifecycle | Control which predictor influences results. | Persist prepared → evaluated → approved → active → retired states, one active version per task, explicit fallback/shadow behavior, compatibility checks, and rollback. Failed/blocked evidence prevents activation. Approval transitions use a simulated operator in the demo. | Lifecycle controls and actual predictor updates checked with fixture Jev; actual-Jev integrated lifecycle remains unverified. | +| 9. Scoring and controlled updates | Keep predictions current without unnecessary retraining. | Score new eligible feature rows, version predictions, invalidate/recompute affected results after corrections, and distinguish feature refresh, scoring, model preparation, artifact refresh, and notification. Measure synthetic drift and test a bounded update path. | Real predictor follow-ups, correction invalidation, scoring and bounded retraining checked with fixture Jev. Retraining excludes superseded sources; actual Jev remains pending. | +| 10. Durable background jobs | Finish work without constant supervision. | Submit through the generic Aegra API; preserve state after client disconnect; support bounded retry, cancellation, restart, concurrent replay, and conflict rejection. Keep one scheduling owner. | Both fixture journeys completed 45 jobs with actual local predictors. Scoped native-clock preparation survived restart with unrelated work excluded; full paid journey remains unverified. | +| 11. Persistent triggers | Start work when information or deadlines change. | Store event, schedule, deadline, staleness, and on-demand triggers. Recover due work after restart without a sleep loop occupying a worker or repeated manual submissions. | Native Aegra cron and persisted due triggers checked, including retry and resumed work. | +| 12. Attention and local delivery | Surface important changes without duplicate interruptions. | Persist episodes with policy-based entry, updates, acknowledgment, investigation, unknown/stale, resolution and clear rules. Exercise cooldown/hysteresis and response deadlines. Refreshing an artifact need not notify a person. Deliver to a local inbox/outbox simulator only. | Persistent forecast analysis drives attention and local delivery in both real-predictor fixture journeys; actual Jev remains pending. | +| 13. Stakeholder artifacts and access | Give each user a useful view of the same evidence. | Publish versioned briefings, analytical views and exports from accepted result bundles; show actuals/predictions/hypotheses, freshness, coverage, uncertainty and source links. Enforce configured local audience scope and correction/review actions. | Scoped reports, corrections and exports checked. Explicit real/predictor-fixture packaging implemented; actual-predictor fixture reports passed desktop/mobile browser checks. | +| 14. Restricted artifact execution | Safely create new presentations and analyses. | Feed a simulated code-generation response into a real restricted local sandbox: allowlisted input snapshot, protected source/dependencies, bounded resources/time, no host secrets or external network. Build/run/inspect a candidate; retain source, logs, preview and review outcome. Prohibited operations must actually be denied. | Actual sandbox execution, constrained layouts and local reviews are included in the development command and retained report package. | +| 15. Operational controls | Make background operation understandable and bounded. | Track run/stage status, lineage, errors, retries, provider calls, local duration/resources, limits and stop reasons. Exercise backpressure and budget enforcement using synthetic provider usage. Distinguish simulated charges, measured provider charges, and unpriced local compute. | Request admission, limits, leases, backpressure and receipts implemented; full real-provider cost/rate/resource acceptance remains unfinished. | +| 16. Integrated demonstration and cleanup | Deliver one repeatable capability platform. | Run both unrelated scenarios through real Jev interpretation, real prediction methods, further analysis, attention and useful reports, alongside the other workflow and sandbox capabilities. Inject failures/restarts/replays, verify outputs, document one-command operation, and safely retire superseded goal-owned scaffolding. | Real driver and explicit packaging modes implemented. Unpaid preparation, denial gates and native-clock restart passed. Combined fixture database/model recovery reproduced 50 predictions and replayed 90 jobs. The actual single Jev probe passed; full paid execution and approved exact-commit acceptance remain pending. | + + +## Long-running goal contract + +**Outcome:** a reusable local background-intelligence platform that completes synthetic source → immutable evidence → real Jev findings → time-safe features → real CatBoost/TabICLv2 preparation, evaluation and selection → actual predictions → further analysis → attention → useful stakeholder outputs, with durable scheduling and restricted artifact execution. Complete all 16 steps together. Deterministic model fixtures or an isolated subsystem cannot establish completion. + +**Acceptance source:** product-owner direction on 2026-09-26 to pursue all technology capabilities in one long-running goal, corrected the same day to require real Jev and real predictor execution while retaining synthetic source records and historical outcomes. + +**Business scope:** source onboarding, interpretation, prediction, background orchestration, stakeholder artifacts, local attention and oversight, operational controls, and repeatable packaging. Olist remains a preserved optional integration example. Amazon Reviews remains a later real-data scale option. Neither defines the platform's domain model. + +**Real versus simulated:** implement real parsing, storage, lineage, feature construction, metrics, candidate registry, workflow state, timers, local inbox, artifacts and sandbox enforcement. Synthetic source records and known synthetic historical outcomes are allowed. Real Jev must interpret the relevant text and preserve actual returned outputs with request/model identity. Real CatBoost and real TabICLv2 must execute against facts alone and facts plus those Jev findings. Every simulated route retains `implementation_mode=simulated` and cannot satisfy final model acceptance. Simulated code-generation responses may still exercise the actual sandbox. Google TabFM is a distinct research/model choice; do not call TabICLv2 TabFM or claim Google's model executed. + +**Model work remains explicit:** Jev-style observations, point-in-time features, target/outcome contracts, train/context preparation, comparable evaluation, model version approval/activation, scoring, monitoring, corrections, update and rollback all remain required. The current linear projection is insufficient. Classification and regression contracts should be covered by the synthetic scenarios; accuracy thresholds are not business claims. + +**Owned surface:** the existing canonical BackIntel repository, its current runtime stack and application schema, scenario/adaptor configuration, local artifact/sandbox implementation, tests and validation contracts. Reuse existing components before adding abstractions or dependencies. Keep one maintained roadmap and repository map; update their status rather than adding parallel plans or ticket worktrees. + +**Invariants:** unknown is distinct from false; source, model interpretation and observed outcome remain distinguishable; future evidence cannot enter earlier snapshots; an accepted result cannot be silently overwritten; acknowledgment does not clear a condition; stale does not mean resolved; source changes do not automatically retrain or notify; retries cannot duplicate accepted results or deliveries; every material output identifies its source/feature/model/policy versions. Source text/code is untrusted. Model, data, execution and publication authority stay separate. + +**Authority:** local implementation and synthetic-data development authorized. Prefer isolated local test services/databases over modifying the preserved Olist demonstration stack. Paid inference and model-use rights retain separate approval boundaries; prepare concrete bounded runs and obtain approval at the relevant execution boundary. No real external messages, public publication, cloud deployment, production writes, new licensed datasets or unrelated deletion. Preserve credentials, live data and recovery copies. Local commits and pushes still require explicit confirmation; none is approved. No autonomous subagent fan-out is authorized. + +**Required direct checks:** contract/schema failures; point-in-time feature/label isolation; actual evaluation calculations; candidate activation/fallback/rollback; generic server submit/disconnect/restart; persistent due-trigger recovery; replay and concurrent conflict handling; episode and delivery deduplication; output/source consistency; local audience boundaries; rendered desktop/mobile and keyboard journeys; actual sandbox rejection of prohibited reads/network/resource use; budget/rate/resource-stop behavior; one integrated run across both scenarios. Record each required result as passed, failed or blocked on the approved exact candidate, with artifact hashes. Tests serve these outcomes; do not add repeated reviews or proof infrastructure for confidence alone. + +**Cost treatment:** record real execution duration/resource observations and provider usage. Keep simulated billing clearly separate. Zero external model calls means zero provider charges; unpriced local compute remains unknown. Human baseline, representative accuracy and total cost savings remain outside this simulated-technology goal. They must not become prerequisites for coding the simulator, and no savings claim is permitted without later evidence. + +**Done condition:** one documented local command starts a bounded demonstration across both unrelated scenarios. Real Jev interprets synthetic text; actual CatBoost and TabICLv2 run comparable facts-only and facts-plus-Jev evaluations; their predictions drive further analysis, attention and useful stakeholder outputs. Subsequent arrivals, corrections and due events run without repeated manual submissions. All other capability requirements, recovery, portability, audience access, restricted execution and rendered-output checks pass on the approved committed candidate. A blocked provider/model step remains unfinished. Evidence identifies actual requests, models, packages, weights and outputs. No real-world accuracy, time-saving or representative business-value claim follows from synthetic data. Obsolete goal-owned scaffolding is safely retired while user work, live services and recovery evidence remain intact. + +## Dependency-ordered implementation packages + +1. **Portable contracts and evidence:** steps 1–4. Retain reusable source, observation and revision contracts; connect real Jev to the typed boundary and preserve actual requests/responses. Simulated responses remain fast test fixtures. +2. **Prediction lifecycle:** steps 5–9. Retain time-safe feature/outcome histories, evaluation calculations and registry controls; execute actual CatBoost and TabICLv2 adapters with facts-only and facts-plus-real-Jev comparisons. Keep CatBoost training and TabICLv2 context preparation distinct. A model/provider approval boundary blocks only its dependent execution. +3. **Unattended operation:** steps 10–12 and operational controls from step 15. Connect the generic API journey, persistent triggers, durable local episodes and delivery deduplication around the existing runtime. +4. **Stakeholder outputs and execution:** steps 13–14. Add configured local views/access and run simulated generated candidates inside an actual bounded sandbox. +5. **Integrated completion:** finish steps 15–16 with real model execution flowing into further analysis, attention and useful reports. Run affected direct checks, save exact-candidate evidence, document startup, and clean up superseded goal-owned material. Do not substitute deterministic responses for final model execution. + +Build as bounded slices inside this one goal. At each checkpoint record implemented outcome, changed surfaces, direct-check result and remaining dependency. Resume from that checkpoint rather than restarting investigation. Do not stop for non-material uncertainty, human labeling worksheets, or accounting inputs unrelated to the current simulated capability. Stop dependent work only for failed material checks, missing external-action/commit authority, or an actual isolation/data-loss risk; continue independent authorized work where available. + +## Current implementation checkpoint + +The initial uncommitted capability slice runs two synthetic source shapes through one shared engine. It covers parsers, rule-based simulated extraction, basic summaries, a virtual failure/retry/duplicate timeline, missing/stale handling, and HTML/JSON evidence artifacts. The graph wrapper uses the existing PostgreSQL ledger; direct graph/ledger and preserved regression tests passed. Desktop/mobile and keyboard checks passed. Generic source revisions, the predictive lifecycle, persistent triggers, durable local attention, scoped user access, and generated-code sandboxing remain incomplete. Existing installed Olist services have not been updated. + +The former Olist recovery goal remains recorded separately; completion of cleanup and the recorded-data demo is not completion of this new platform goal. Goal registration status must come from the goal tool, not a prose statement in this file. + +**2026-09-26, package 1 implementation:** Added portable typed task contracts to both existing scenarios; immutable PostgreSQL evidence with content hashes and parent links; synthetic CSV/JSON/text-envelope admission with accepted, duplicate, correction, late-revision and quarantine outcomes; typed simulated Jev-style responses with distributions, unknown/abstention, bounded retries and persistent caching; authorized correction chains and historical reads. Original source/observation records remain unchanged. Four direct PostgreSQL checks passed in the isolated `backintel-capability-test` database on localhost:55436. These are working-tree checks, not exact-commit acceptance. The installed Olist stack remains unchanged. Package 2 is next: time-safe features/outcomes, distinct simulated predictor preparation, comparison, activation, scoring and updates. API integration, persistent triggers, audience artifacts and sandbox execution remain unfinished. + +**2026-09-26, package 2 implementation:** Added cutoff-safe feature snapshots and separately available versioned outcomes; chronological train/holdout separation; native empirical baseline plus explicitly simulated CatBoost training-style and TabICLv2 context-style adapters; real classification/regression and calibration calculations on identical holdout cases; persisted preparation/evaluation/approval/activation/retirement/rollback; compatible fallback and shadow scoring; prediction invalidation after corrections, drift measurements and a bounded update plan. Fourteen direct checks passed, including preserved offline simulation behavior. Evidence: `artifacts/validation/CapabilityPackages/checks.json` and `checks.log` (source hashes, no provider calls; local compute unpriced). Exact-commit acceptance remains blocked. Package 3 is next: real generic API jobs, restart recovery, persistent triggers and durable local attention/delivery. Model-update execution must be exercised during integration; a saved update plan alone is insufficient. + +**2026-09-27, current checkpoint:** Packages 1–3 now have a working development journey through the actual generic Aegra API on isolated localhost:2027. Both scenario streams complete bootstrap plus 13 saved follow-ups, including source revisions, outcomes, retry, deadline/acknowledgment/investigation/staleness/resolution, preparation and activation of an updated simulated predictor, and result refresh. Aegra's native persistent cron is the sole scheduler; jobs never wait asleep for due events. Job leases, cancellation, bounded repair, backpressure, per-scenario ordering, immutable attempt history and local delivery deduplication are implemented. Nineteen direct checks passed; the actual background journey passed after repairing two integration failures. An overdue-job ordering failure is preserved in `artifacts/validation/CapabilityDemo/ordering-failure-trial.json`; a negative-zero JSONB hash failure is preserved in `negative-zero-failure-trial.json`. The corrected stream resumed its failed job without discarding prior accepted results. Current receipts: `submission.json`, `background-check.json`; direct-check evidence: `artifacts/validation/CapabilityPackages/checks.json`. These remain working-tree results, not approved exact-commit acceptance. Original Olist service at :2026, data and recovery set remain unchanged. + +**2026-09-27, approved local models and sandbox checkpoint:** The user approved CatBoost and TabICLv2 on synthetic data, with at most 64 training rows and two CPU threads. The two pinned TabICLv2 checkpoints (224,692,632 bytes total) downloaded and passed SHA-256 checks. Actual CatBoost 1.2.10 and TabICL 2.2.0 classifier/regressor paths fit, saved, loaded, and predicted on both scenarios: 18 training rows and six held-out rows per scenario. These are structured-only results, not the required facts-plus-real-Jev comparison or evidence of real-world quality. Receipt: `artifacts/validation/RealPredictors/3a7906ed0bd440d2b2817c280d245bcc.json`. Weights, model packages and local approval records stay ignored under `artifacts/Models/`; the host `.venv` is also ignored. The running isolated API image has not yet received these model changes. + +The generic Jev boundary now has committed request admission, scoped and expiring approval, response caching, retained raw responses, request/model identity and measured-charge requirements. Uncertain requests are never automatically retried. Its fixture checks make no paid calls. A separate one-request paid probe is prepared for `jev-1.13`, using only “Service working” and one question. The existing Keychain credential has not been retrieved. Public pricing is unknown; there is no enforceable dollar cap. Paid approval was requested separately and remains pending. One probe would not authorize a larger batch. Local model approval does not authorize Jev. + +Actual Docker sandbox execution now accepts an allowlisted snapshot and generated Python, with no network, no writable host output, protected source/dependencies, an unprivileged user, and CPU/memory/process/file/time/output limits. The direct check accepted the intended calculation and denied nine prohibited operations, including host reads, protected writes, network, and resource misuse. Container cleanup passed. Source, bounded logs, result, stop reason and image identity are retained in `artifacts/validation/Sandbox/aa673f3e7ced4cb8bbef7956c3bb480c.json`. This proves the host runner; integration with stakeholder artifact review/publication remains unfinished. Generated code in these checks is a fixture. Thirty-four capability/provider-boundary/regression checks passed in `artifacts/validation/CapabilityPackages/checks.json`; all evidence is working-tree evidence. Exact-commit acceptance is still blocked until the complete candidate and commit authority exist. + +**2026-09-27, audience reports and generated-view checkpoint:** `refresh` now creates immutable audience projections from each accepted result bundle. A loopback-only HTTP service binds short-lived private tokens to one task version and audience; HTML, CSV, JSON, historical reports, source references and generated candidates enforce that scope. Operators can record review decisions and correct typed observations. Corrections preserve the original source, invalidate affected predictions and create a refreshed report. Read-only audiences cannot invoke those actions. Cross-origin changes, expired tokens, cross-entity evidence reads and stale forms are denied. Accepted facts, latest recorded outcomes, future predictions, missing observations, staleness and uncertainty are shown separately. Actual model execution status is displayed rather than inferred from an adapter name. + +Generated Python now runs in the actual host sandbox against an approved audience snapshot and returns a constrained layout. Values are rendered from accepted evidence; invented source references and failed runs cannot be accepted. Source code, bounded logs, snapshot identity, pinned container image, candidate preview and local review decisions remain durable. Development code generation and operator approvals are explicitly simulated. The final scope check passed 59 checks in `artifacts/validation/Audiences/e64d6a3c2be4.json`. A real Chromium journey passed 84 checks with 24 desktop/mobile captures in `artifacts/validation/AudienceUI/8b06b63f-612c-4f36-87f0-ea92c8652785/checks.json`; rendered critique and the resolved mobile-table finding are in `review.json` beside it. The initial browser failure exposed a same-origin form issue caused by the referrer policy; the policy was corrected while foreign origins remain denied. Thirty-four existing regression checks also passed. Temporary audience HTTP processes were stopped after verification; test records and receipts remain available. No paid provider calls, commits, push, public publication or Olist changes occurred. + +These are working-tree subsystem checks on model fixtures. The running isolated development API image still predates the audience changes. Actual Jev interpretation, the complete real-model comparison and predictive attention flow, refreshed runtime packaging, full operational/recovery checks and exact-commit acceptance remain unfinished. The existing paid one-request probe question is still pending; do not infer approval from this checkpoint. + +**2026-09-27, integrated development and recovery checkpoint:** One development command now runs both unrelated scenarios through the isolated Aegra service, automatic follow-ups, forecast analysis, attention, audience artifacts and actual host sandbox execution. Each invocation preserves its own logs and receipts. `--verify-recovery` restarts only the isolated runtime after bootstrap while follow-ups remain pending, then waits for durable completion. Replay uses the real generic API and must leave accepted evidence, attempts and delivery history unchanged. `--serve` can start the scoped local viewer after completion. Generated-code responses and local operator reviews are explicitly simulated; they do not imply external publication. + +The `integrated-v2` stream completed 14 jobs per scenario after restarting with 26 pending triggers. Its 738 accepted records survived that restart and API replay was unchanged. The development package contains 26 scoped report/export/source/review files. Receipt: `artifacts/validation/CapabilityDemo/integrated-v2/Attempta413833c830f/Integration.json`. The image was then rebuilt directly from the repository's canonical `Dockerfile.runtime`, without inheriting the preserved Olist image; the obsolete goal-owned `Dockerfile.capabilities` was removed. Resuming `integrated-v2` passed replay and produced another retained package in `Attemptde75e0dc8381`. The original Olist container ID, image and start timestamp were unchanged; its receipt is `PreservedOlist.json` in that attempt. + +Forecasts now feed a persisted analysis record and the attention decision. This is an explicit fictional policy for synthetic scenarios: normalize classification forecasts by 1 and synthetic equipment outcomes by 5, then take the larger known interpretation/forecast risk. Missing interpretation remains unknown, even with a forecast. Older task versions without a configured prediction scale retain interpretation-only attention. Reports show the two contributing risks. A direct check proves that a high forecast can open an episode despite a false text finding, preserves forecast lineage, and keeps missing interpretation unknown. Model activation now refreshes attention; a presentation-only refresh still does not notify. Thirty-five regression checks passed. The packaged forecast explanation passed desktop/mobile rendering checks in `Attemptde75e0dc8381/Package/VisualCheck/Checks.json`. + +A database backup was restored into a newly created isolated test database. The restored task evidence, audience artifacts and job state matched exactly; all 28 completed jobs replayed without changes. The temporary clone was removed. Receipt: `artifacts/validation/CapabilityRestore/Attempt2465d4a5f71a/Restore.json` (746 records after retained generated candidates/reviews, 624,970-byte backup). This verifies the two fixture task streams, not actual model-file/checkpoint recovery or a second live Aegra deployment. No paid calls, commits, push, public deployment, or Olist data changes occurred. + +The final offline-export correction is retained in `artifacts/validation/CapabilityDemo/integrated-v2/Attempt907d51817dc2`. That replay passed and produced 36 packaged files, including audience-specific source JSON. All eight HTML exports have local links and no live forms. Chromium opened an exported source record and returned from the generated view to its report without a server. Evidence: `Package/OfflineExports.json` and `Package/OfflineBrowser.json`. Interactive corrections and reviews remain available through the authenticated local viewer; exported copies are read-only. + +**Previous checkpoint dependency (updated below):** Run the single Jev probe only after explicit paid approval; inspect actual response identity and cost before proposing any larger batch. Integrate real extraction as a separately committed stage before preparation and scoring, because paid admission cannot depend on uncommitted source rows. Integrate real extraction through committed admission stages, then real predictor preparation/scoring into the now-packaged background workflow. Finish real-model file recovery, remaining operational acceptance and the approved exact-commit checks. The development restore checker currently depends on the separately provisioned isolated validation PostgreSQL service. Preserve the original Olist service and data. Do not complete the goal at this checkpoint. + +**2026-09-27, real-stage and model-container checkpoint:** The optional model-enabled `Dockerfile.runtime` installs the pinned CPU model dependencies. The isolated Compose service mounts the existing approved weights, runs one worker with two CPU threads and disables automatic checkpoint downloads. No provider credential is configured. Actual CatBoost and TabICLv2 classification and regression passed inside that image with 18 training and six holdout records per scenario. Receipt: `artifacts/validation/RealPredictorsLinux/3d97e37762a6415fa5df9408a9270be1.json`. Synthetic metrics do not establish real-world prediction quality. + +`real_pipeline.py` now separates committed source admission, authorized interpretation and real comparison. The preparation stage makes no paid request. The interpretation stage requires a source in its immutable saved plan and a matching named authorization. The comparison stage rejects missing, simulated, late or untraceable observations and unknown actual billing before using real predictors and refreshing analysis, attention and reports. The paid stages have boundary checks but have not executed real Jev. Follow-up arrivals/corrections and scheduled updates still require integration into this real path. + +The new `scripts.real_capabilities` command submits those stages through the actual generic API. Both scenarios admitted 24 sources and 24 outcomes; replay left their domain records and job attempts unchanged. Receipt: `artifacts/validation/RealPipeline/Attempt5753ef17653e/Preparation.json`. Job attempts now record newly admitted provider requests and measured charges; cache reuse adds zero charge, and uncertain execution or missing prices stay unknown. A missing runtime credential cannot consume an approved request slot. Provider fixtures verify these controls and are not actual Jev evidence. + +Actual model-file recovery retained an 11-file backup, restored it into a temporary directory, and reproduced all 24 holdout predictions across both model families and scenarios. The temporary directory was removed. Receipt: `artifacts/validation/RealModelRecovery/Attempt46687ef81fab/Restore.json`. This check used the existing isolated evidence database. It proves model-package/checkpoint recovery, while combined restoration of the final real-Jev task database and model files remains pending. The original Olist container, image and start time remain unchanged. All new evidence is from an uncommitted working tree. + +Fifty regression checks passed inside the final model-enabled image, using separate isolated databases for the generic and older Olist fixtures. After replacing only the development runtime with that image, both prepared histories replayed unchanged. No paid request was recorded. The final runtime identity, preserved Olist identity, test groups and setup corrections are recorded in `artifacts/validation/RealPipeline/Attempt5753ef17653e/Checkpoint.json`. + +**Prior checkpoint dependency (updated below):** The existing single paid Jev probe question remains pending. Its original exact scope cannot authorize these new history tasks or a full batch. After that approval, run only the matching probe and inspect its actual model identity and charge before seeking broader authority. Complete real follow-up integration, the real facts-only/facts-plus-Jev comparison and operational acceptance; obtain commit authority only for a concrete complete candidate. The goal remains active. + +**2026-09-27, durable real-path follow-up checkpoint:** History and future source admission now complete before paid extraction. Future revisions carry their own synthetic availability times and remain outside earlier feature cutoffs. A single `real_start` request persists the history interpretation sequence, comparison and follow-up start; the latter persists extraction, correction application, outcome arrival, attention actions, bounded retraining and report refresh. Each provider request retains its separate scope and budget checks. Queue admission now defers due triggers when a task already has 20 pending jobs, preserving them for the next dispatch instead of failing the dispatch transaction. + +Both unrelated scenarios completed 45 durable jobs using actual local CatBoost/TabICLv2 and explicitly deterministic Jev fixtures. All four predictor/feature routes executed; corrections invalidated prior predictions and excluded superseded source features from the update's 24 training rows. Accepted task contracts retained their real-provider settings throughout scheduled actions. Replaying every completed job left domain evidence unchanged and added no provider-fixture calls. Receipt: `artifacts/validation/RealFollowups/Attempt333bb285ed2d/Followups.json`. Due timestamps and retry delays were advanced in the isolated test database. This is not actual Jev or native-clock proof of the real journey. Injected provider responses now carry a fixture marker, and result bundles identify fixture semantics rather than presenting them as real Jev. + +The actual generic API prepared both future-source plans without any paid request or paid schedule. Each added three source versions while keeping the original 24 records visible at the history cutoff; replay made no changes. Receipt: `artifacts/validation/RealFollowups/Attemptc0aa7e84b6f8/Preparation.json`. The original Olist container, image and start time remain unchanged. Forty-seven regression checks passed inside the model-enabled image. Failed-request accounting now tolerates missing metadata, and failures before the handler starts cannot count earlier jobs' charges as new spending. + +**2026-09-27, real driver, packaging and combined recovery checkpoint:** The one-command driver now prepares both source scopes, checks both approved authorizations before credential access, owns an expiring task-bound tmpfs credential, dispatches only its two tasks through native cron, preserves pending work across a safe restart, replays completed work and packages explicitly real results. Preparation and deliberately unauthorized execution were checked without provider access. A dummy non-provider credential verified native-clock source preparation, restart, task isolation and cleanup on the current isolated image. Full paid execution remains unverified. + +Packaging rejects mode mismatches and unknown real-provider cost. Actual local predictors with fixture Jev produced scoped ordinary and sandbox-generated offline reports; eight browser checks and representative desktop/mobile visual inspection passed. Fictional fixture charges remain separate from actual spending. Fifty-one regression tests passed in the model image before the later recovery-checker extension. + +Combined recovery restored a separate database and approved model files, reproduced all 50 recorded predictions across 10 model packages, and replayed 90 completed jobs without changing evidence, triggers or provider requests. Temporary database and model restores were removed. The host attempt correctly rejected mismatched predictor-library versions; the successful check used the pinned Linux model image. These streams contain fixture Jev and do not establish actual-Jev or live scheduler/broker restoration. Receipt: `artifacts/validation/RealModelRecovery/Attempt6bd517aa6298/Restore.json`. + +The README documents preparation, approved execution, authorization-file shape, `--no-start`, credential lifetime and combined recovery. Current checkpoint: `artifacts/validation/RealDemo/real-driver-v1/Attempt93d646ff65d1/Checkpoint.json`. The isolated service is healthy on its rebuilt image; the original Olist container, image and start timestamp remain unchanged. No paid Jev calls, commits, push or publication occurred. + +**Current next dependency:** The single paid Jev probe has passed. The remaining two-scenario run covers 54 exact synthetic source versions and requires separate approval. Its scope is retained in `artifacts/validation/RealModelPreparation/FullRunProposal.json`; pricing and a batch cost ceiling remain unresolved. The public router endpoint returned no concrete rates, and one observed charge does not establish a future price ceiling. Current controls block unpriced multi-request execution. Once pricing and scoped execution authority are resolved, run the full comparisons, background workflow, operational/recovery checks and approved exact committed-candidate validation. No full-batch execution, commit or publication is authorized. + +**2026-09-28, paid-probe proposal refreshed:** Renewed the same unused, unapproved one-request authorization after its previous expiry. The exact synthetic input remains `Service working`; the single boolean question asks whether it reports a service failure. Requested model remains `jev-1.13`, with no automatic retry and at most 5,000 input characters (15 actual characters). Pricing remains unverified and no dollar cap is enforceable. Refreshed authorization expires **2026-09-29 at 12:48 PM Eastern**. Read-back confirmed zero requests and `approved=false`; no provider credential was retrieved. Existing local-model approval remains intact. Reviewable proposal and receipt: `artifacts/validation/RealModelPreparation/proposal.json` and `Refresh.json`. This refresh authorizes no paid execution or full batch. + +**2026-09-28, approved actual Jev probe:** The user approved the refreshed one-request scope. Existing Keychain authentication passed using the application's HTTP client; the earlier standard-library network check failed without making an inference request. Exactly one paid request sent `Service working` and asked whether it reported a service failure. Jev returned false with a true-answer score of `0.03`, request `gen-dec-1790626111-8cZAkuk9tSKCvScgFee0`, actual model `typesafe/jev-1.13-20260917`, and measured charge **$0.000011592**. Local compute remains unpriced. + +The request response and billing were preserved before the local model-name check rejected the provider's dated alias resolution. The check now permits this exact observed revision; other dated revisions, different model versions, missing identity and unknown cost remain rejected. Eleven focused regression checks passed. Acceptance then reused the saved response with provider construction explicitly disabled; the request ledger remained unchanged and no second paid call occurred. `ProbeExecution.json` preserves the initial local failure; `ProbeAcceptance.json` and `probe-result.json` retain the successful acceptance and actual finding under `artifacts/validation/RealModelPreparation`. + +The original Olist container, image and start timestamp remain unchanged. The isolated runtime has not been rebuilt for this host-side compatibility fix; the real-run command rebuilds it when a later run is authorized. The single-call approval is consumed and does not approve the separate 54-source batch. This remains working-tree evidence, not final committed-candidate acceptance. + +## Run the simulation + +No Olist dataset, database, model key, package installation, or running server is required for the deterministic simulation: + +```sh +python3 -m scripts.simulate +``` + +Outputs default to `~/Library/Application Support/BackIntel/Evidence/Simulation`. Use `--scenario support` or `--scenario equipment` for one scenario, or `--output /absolute/path` for another artifact directory. Each scenario emits a content-addressed HTML briefing and JSON result with all source records and state transitions. + +The virtual clock advances through arrival, duplicate arrival, injected transient failure, retry, acknowledgment, staleness, duplicate replay, missing observations, and recovery. This exercises behavior deterministically; it does not install a scheduler. + +For an approved local runtime containing the new `capability_simulation` graph: + +```sh +python3 -m scripts.simulate --scenario support \ + --base-url http://127.0.0.1:2026 \ + --receipt /absolute/path/simulation-receipt.json +python3 -m scripts.poc inspect --receipt /absolute/path/simulation-receipt.json --wait +``` + +The existing installed runtime still contains the previous Olist candidate until explicitly updated. Offline simulation works independently of that update. Both paths use the same simulation functions; the runtime path adds actual LangGraph execution and PostgreSQL admission/result protection. + +## Immediate next package + +Resume at the current dependency above. Actual local predictors, real-path follow-ups, report packaging and combined fixture recovery are implemented. The remaining provider-dependent work requires actual Jev under separate paid approval, followed by operational and exact-commit acceptance. All 16 capabilities remain in scope. + +--- + +# Historical Olist release plan — retained reference, superseded sequence + +The following preserves the earlier domain-specific contract and rationale. Its sprint ordering, immediate Sprint 1 heading, must-have release gates, and approval statements describe that historical Olist release. They do not override the active simulation-first capability roadmap above. Existing source-correctness and data-use restrictions still apply when running the Olist example. + +# v0.1.0 Development Roadmap — Background Intelligence Refinery + +**Status:** Release plan; business contract confirmed by the product owner on 2026-09-23; technical execution decisions remain open +**Release type:** Local-first technical product demonstrator +**Planning basis:** One small cross-functional team; sequence is dependency-based, not a calendar commitment +**Implementation authority:** The product owner authorized local Sprint 1 execution with approved Kaggle Olist v2 under the non-commercial local-prototype boundary. Paid inference, additional dataset acquisition, persistent infrastructure provisioning, external notifications, cloud deployment, push/PR, and merge remain separately gated. + +## 1. Release goal + +> Help a Marketplace Seller Performance Analyst prepare a trustworthy recurring seller-performance review—with delivery metrics, customer-feedback themes, and evidence-backed findings—faster and with less repetitive data preparation, so a Seller Operations Manager can decide what merits investigation. + +v0.1.0 demonstrates this process locally using Olist's historical Brazilian multi-seller marketplace data. Jev, predictors, background execution, and generated artifacts are implementation mechanisms serving that business goal, not the goal themselves. Orders are evidence and drill-downs; the business-level review concerns seller/category patterns. The demo does not claim to reproduce Olist's internal organization. + +A successful v0.1.0 proves this chain: + +```text +Kaggle Olist v2 source manifest + -> reconciled orders, items, sellers, products, and reviews + -> seller/category metrics with explicit coverage and attribution rules + -> versioned review-text observations where available + -> dated evidence-backed findings and a manager briefing + -> human-vs-system effort and quality comparison + -> optional, separately evaluated delivery prediction + -> local presentation and review; no external operational action +``` + +This is a historical simulation, not a live commerce system. It has no true source-arrival stream, carrier-exception workflow, or intervention history. + +## 2. Business process and human baseline + +### Human process being improved + +The simulated team is Marketplace Seller Performance. The primary user is a Marketplace Seller Performance Analyst; the recipient is a Seller Operations Manager. For a reporting period, the analyst: + +1. Obtains order, item, seller, product, delivery, and review exports. +2. Checks completeness, identifiers, join grain, and date coverage. +3. Calculates delivery and customer-experience measures with explicit numerators, denominators, period, and eligible population. +4. Reviews feedback themes and keeps missing or ambiguous text distinct from negative feedback. +5. Compares seller/category patterns across comparable periods, noting sample sizes and attribution limits. +6. Selects evidence-backed findings for a concise management review, linking each to source records. +7. Answers follow-up questions or corrects the report when a join, classification, or source record is disputed. + +The recurring manual burden is data reconciliation, repeated calculation, review-text reading/categorization, evidence collection, and briefing preparation. This Kaggle demo models that work; it does not claim that any named real company currently performs this exact process. + +### Process established by v0.1.0 + +For a selected historical reporting cutoff, the system should: + +```text +load approved Olist snapshot + -> validate source rows and join grains + -> calculate seller/category delivery and feedback measures + -> interpret available review text with traceable, bounded questions + -> assemble findings with denominators, coverage, and source evidence + -> render a Seller Performance review for the manager + -> preserve reviewer corrections and the exact data/definition versions +``` + +A generated finding is a seller/category observation to review—not a customer-support ticket or a claim that a seller caused an outcome. People define metrics, verify uncertain findings, investigate causes, and decide any business action. No seller/customer contact or external escalation occurs in v0.1.0. + +### Business hypothesis + +Given the same frozen source snapshot and the same review-package specification, BackIntel can produce an independently accepted Seller Performance review with less repetitive analyst effort than the manual workflow, without reducing metric correctness, useful coverage, or traceability. We will measure elapsed time, hands-on analyst/reviewer time, reconciliation defects, coverage, correction/rework, finding validity, traceability, freshness, and total cost. Numeric improvement targets will be set after the manual baseline and before final evaluation. + +A separate experiment may compare human/rule/model ranking at an equal investigation capacity. That secondary top-K evaluation concerns **seller/category findings**, not a fixed number of orders, and is not required to prove the primary reporting workflow. The demo is not successful merely because it uses AI. + +### Baseline comparison + +BINT-1 defines the assignment and acceptance rubric. The revised BINT-5 first produces a reproducible monthly seller-performance report from reconciled Olist facts, without requiring a timed human exercise. A later release-evaluation task measures the human baseline against the same frozen files, historical cutoff, and required report outline. A human analyst and the automated workflow must receive the same information for that later comparison. The report covers all eligible data in the selected slice; **there is no arbitrary 20-order queue cap**. If we later evaluate investigative prioritization, its seller/category finding capacity is a separate value derived from observed reviewer workload and frozen before holdout scoring. + +| Measure | Human baseline | Automated comparison | +|---|---|---| +| Elapsed cycle time | Input release to independently accepted review | Same boundaries; include queue/wait time | +| Active effort | Analyst and independent-review minutes, separated | Human checking/correction plus runtime/provider processing | +| Data correctness | Join/reconciliation defects and metric recomputation | Same checks against the same source manifest | +| Coverage | Eligible orders, sellers, categories, and available reviews represented; show denominators | Same definitions plus processed/blocked/missing dispositions | +| Finding quality | Independent reviewer rates supported and useful findings using an agreed rubric | Blindly reviewed findings, unsupported-claim and correction rates | +| Feedback quality | Adjudicated review-theme sample and ambiguity disposition | Jev agreement/abstention against that same sample | +| Traceability | Share of checked claims verifiable to source records | Same measure, with source-to-artifact lineage | +| Cost | Loaded human-time assumption and tooling | Human review plus measured compute/provider/runtime cost | + +Set numeric success thresholds before final evaluation, after the data profile and human baseline show what is measurable. Do not tune against the final holdout. + +### Manual-baseline protocol (BINT-1; timed comparison deferred to release evaluation) + +**Assignment.** “Using the frozen Olist reporting slice, prepare a Seller Performance review for the Seller Operations Manager. Reconcile eligible orders and items; report delivery performance with counts and denominators; summarize available customer ratings/review themes; identify seller/category patterns that merit human investigation; and provide source-linked examples, coverage, unknowns, and limitations.” The output is a full-period summary plus evidence-backed findings—not a predetermined number of order cases. + +**Source and freeze.** Use Kaggle's Olist Brazilian E-Commerce Public Dataset v2 under the product-owner-approved non-commercial local prototype/demo boundary (CC BY-NC-SA 4.0; retain attribution; no commercial incorporation/redistribution). BINT-3's source manifest contains archive/file hashes. BINT-5 freezes the report period, eligible population, source-file manifest, and historical cutoff for the initial descriptive report. Before any later timed human or model evaluation, also freeze the comparison protocol and chronological development/holdout split. Humans and automation receive the same information. Do not let post-cutoff reviews or outcomes support claims about what was knowable at the cutoff. + +**Attribution rules.** Order delivery can be associated with a seller only when all items in that order identify exactly one seller; report multi-seller orders separately at marketplace level. Associate review text with a category only when the order's item-category set is unambiguous; otherwise use marketplace-level feedback or mark attribution unknown. Describe associations, not causes. Always show denominators, missing coverage, and the eligibility rule. Do not interpret missing comments as no complaint. + +**Human exercise.** A Marketplace Seller Performance Analyst uses ordinary spreadsheet/SQL tools, but no Jev labels, generated findings, or predictor scores. Start elapsed time when the frozen input package is released; record active analyst minutes separately. The analyst produces the assignment above and submits it for independent acceptance. A second reviewer checks metric recomputation, joins, evidence links, unsupported attribution/causal claims, and usefulness of findings; record reviewer time, corrections, and acceptance disposition. Preserve the exact sample, instructions, report, and timing log. + +**Comparison.** Run the automated workflow over the same frozen inputs and report specification. Compare elapsed time, hands-on human effort, reconciliation correctness, eligible-record/feedback coverage, finding validity/usefulness, correction burden, traceability, freshness, and measured total cost. Reviewers should assess human and automated findings without seeing which produced them where practical. This primary report comparison has no fixed findings cap. If a later predictor experiment needs equal-capacity ranking, set capacity from measured analyst workload and freeze it before the held-out evaluation; the unit is seller/category findings, never “20 orders.” + +**Confirmed demo cadence (product-owner approval: 2026-09-23).** Use a calendar-month Seller Performance review, comparing the selected month with the immediately preceding comparable month. The data replay can then reveal orders/reviews as historical dates advance. This is a demo convention, not a claim about Olist's internal cadence; eBay documents monthly seller-performance evaluation, while Walmart uses rolling 30/60-day windows. After the source profile, select periods by a fixed chronological rule and confirm sufficient eligible volume before examining holdout findings. Sources: [eBay seller-performance monitoring](https://www.ebay.co.uk/help/selling/selling/monitor-service-metrics?id=4785); [Walmart Marketplace performance standards](https://marketplacelearn.walmart.com/guides/Policies%20&%20standards/Performance/Seller-performance-standards). + +**Draft independent acceptance rubric.** Mark each check pass/correction/reject: (1) source totals and joins reconcile; (2) every KPI has correct formula, unit, period, eligible denominator, and missingness; (3) seller/category attribution obeys the single-seller/single-category rules; (4) each finding has verifiable evidence and makes no unsupported causal claim; (5) coverage, ambiguity, and limitations are explicit; (6) the manager can identify a justified follow-up from the report. Overall disposition is accepted, accepted after correction, or rejected, with reviewer time and reason recorded. Name the independent reviewer before the timed baseline. + +**BINT-1 decision record.** The product owner confirmed the Marketplace Seller Performance Analyst, Seller Operations Manager recipient, seller/category report, monthly demo cadence, no fixed finding cap for the primary report, and conditional prediction boundary. The baseline protocol and acceptance rubric are specified. BINT-5 implements the first descriptive report; name an independent reviewer and freeze the human exercise and chronological holdout rule before the later timed comparison. Numeric win thresholds are set only after the human baseline and before holdout evaluation. Provider spend, model licensing beyond approved Olist use, cloud deployment, and external notifications remain separately gated; none is authorized by this protocol. + +### Business output + +The v0.1.0 output is a **Seller Performance review package**: + +1. Period-level delivery and customer-experience metrics with definitions, denominators, and coverage. +2. Seller/category findings that merit review, without unsupported causal claims. +3. Source-linked order and review evidence, with ambiguous multi-seller/category attribution visible. +4. A concise manager briefing that distinguishes actuals, interpreted feedback, hypotheses, and optional predictions. +5. A reproducible record of source versions, calculations, questions, and reviewer corrections. + +Delivery-risk prediction is a separate experiment. If it does not improve an agreed prioritization baseline, disclose that result; the descriptive review package remains the primary business deliverable. + +## 3. Principal brief + +### Outcome + +A Marketplace Seller Performance Analyst can submit a historical Olist reporting slice to a durable local workflow, return after processing, and receive an evidence-linked seller/category review for the Seller Operations Manager. The analyst can verify calculations, inspect review-text interpretations, correct uncertain findings, and trace displayed claims back to source rows and definition versions. + +### Primary user + +Marketplace Seller Performance Analyst at an Olist-like multi-seller marketplace. The analyst prepares a recurring review of seller/category delivery performance and customer-feedback patterns, then investigates supported changes with evidence. The Seller Operations Manager is the primary recipient of the briefing. This emulates a marketplace operations function; it does not claim to reproduce Olist's internal organization or workflow. + +### Secondary users + +- Data/model analyst reviewing data quality and predictor evidence. +- Seller Operations Manager receiving a point-in-time summary with drill-down links. + +### Acceptance sources + +- Olist source manifests and audited table contracts. +- Approved Jev question/rubric definitions and independently reviewed examples. +- Versioned target and feature definitions. +- Chronological predictor evaluation manifests. +- Deterministic replay fixtures and expected outcomes. +- Exact-candidate product-validation evidence. + +### Owned surface + +- Local source ingestion and replay. +- Durable job admission and execution. +- Application relational data and artifact manifests. +- Jev adapter and semantic observation contracts. +- Feature and target construction. +- CatBoost and TabICLv2 preparation, evaluation, and scoring. +- Local seller/category finding views, review records, and downloadable snapshots. +- Restricted artifact-code execution and review. + +### Invariants + +1. Source facts, model observations, actual outcomes, predictions, and narratives remain distinguishable. +2. Every result identifies its exact source, schema, question, feature, model, and policy versions. +3. Future evidence and labels cannot enter earlier feature rows or model context. +4. A completed workflow is not automatically an accepted output. +5. Retries cannot duplicate accepted rows, episodes, publications, or simulated deliveries. +6. Stale or missing data is unknown, not false, zero, on-time, or resolved. +7. Predictions are not presented as facts or causal explanations. +8. Generated code receives no production credentials or unrestricted host access. +9. External notifications and business actions are not authorized in v0.1.0. +10. Required evidence ends as `passed`, `failed`, or `blocked`. + +### Forbidden outcomes + +- Using an order's eventual review to predict that same order at approval time. +- Treating missing delivery as on-time delivery. +- Inflating revenue or counts through unsafe one-to-many joins. +- Treating Jev confidence as delivery-risk probability. +- Attributing a multi-seller order review to every seller. +- Publishing incomplete refreshes as complete. +- Allowing generated code to alter protected application code, dependencies, policies, or recipients. +- Presenting mocked classifications as Jev output. +- Silently substituting one predictor for another. +- Sending real email, Slack, Teams, or seller/customer communications. + +### Primary risks + +- Olist's historical availability timestamps may not support every intended feature. +- Portuguese review interpretation may not meet quality requirements. +- Aegra is beta and its recovery semantics are not yet locally validated. +- Jev service availability, cost, and terms may block a real adapter test. +- TabICLv2 checkpoint/dependency licensing or local resource use may be unsuitable. +- Delivery-risk prediction may not improve over a simple baseline. +- Generated-code execution can expand security and validation scope rapidly. + +### Stop conditions + +Stop and return to design when: + +- Source profiling invalidates the target or point-in-time assumptions. +- Required provider/model licensing is unresolved. +- Reviewed Jev quality is below the threshold agreed before the experiment. +- Durable admission or replay-safe publication cannot be demonstrated. +- Required cost telemetry is unknown. +- Sandbox isolation tests expose host secrets, credentials, or unauthorized networking. +- Product validation is not bound to the exact candidate commit. + +### External-action authority + +v0.1.0 may autonomously create and update **local seller/category review records** under approved demo policies. These are not customer-support tickets and do not imply seller fault. New policies, generated executable artifacts, predictor promotion, recipients, cloud deployment, and every outward notification require explicit approval. + +## 4. Confirmed business contract and proposed technical decisions + +The product owner confirmed the business contract and core user on 2026-09-23: + +- **Primary user:** Marketplace Seller Performance Analyst; Seller Operations Manager receives the briefing. +- **Process to improve:** Manually reconciling marketplace order/delivery data, reviewing customer feedback, identifying seller/category trends, and preparing a supported performance briefing. +- **v0.1 output:** An evidence-linked seller/category review and management briefing, compared with a measured human baseline. Orders are supporting drill-down evidence, not assumed to be pre-existing support cases. +- **Prediction boundary:** Test late delivery at order approval only if Olist supports a valid point-in-time target; otherwise deliver descriptive triage and report prediction as blocked, not as a claimed capability. +- **Baseline comparison:** Same frozen sample and report specification; compare elapsed time, human effort, data/claim quality, coverage, correction burden, traceability, and total cost. A capped ranking comparison is a separate optional experiment. + +The following implementation choices remain proposals subject to source profiling, license review, and validation. + +| Decision | Proposed v0.1.0 choice | +|---|---| +| Dataset | Olist Brazilian E-Commerce Public Dataset on Kaggle: https://www.kaggle.com/datasets/olistbr/brazilian-ecommerce. Kaggle's public metadata API checked 2026-09-23 reports version 2, 126,186,995 bytes, and CC BY-NC-SA 4.0. Product owner approved use strictly for a non-commercial local prototype/demo on 2026-09-23; no commercial incorporation or redistribution is authorized. Record attribution and observe share-alike terms for any covered adaptations; revisit rights before public/commercial use. | +| Replay mode | Historical simulation using explicit business and wall clocks | +| Primary prediction | Conditional experiment: late delivery among eligible orders, only if point-in-time validity is established | +| Prediction time | Order approval, subject to source timestamp audit | +| Primary stakeholder | Marketplace Seller Performance Analyst; briefing recipient: Seller Operations Manager | +| Jev scope | Fixed, reviewed questions over review text; no automatic feature discovery | +| Predictors | CatBoost baseline and TabICLv2 candidate behind one feature contract | +| Runtime | Aegra candidate in worker-backed local mode, subject to proof | +| Application store | PostgreSQL logical application database separate from Aegra-owned tables | +| Bulk/artifact storage | Local content-addressed files initially | +| Review authority | Local attention episodes only | +| Artifact authority | Approved templates plus restricted generated-code surface | +| Sandbox | Hardened local provider behind a provider-neutral interface | +| Delivery | Local authenticated application links and downloadable snapshots | +| Cloud execution | Not required for v0.1.0; evaluation follows local proof | + +### BINT-3 source profile (Kaggle v2; non-commercial local prototype) + +**Acquisition and provenance:** Kaggle public dataset `olistbr/brazilian-ecommerce`, version 2, current as checked 2026-09-23; license `CC BY-NC-SA 4.0`. The product owner approved strictly non-commercial local prototype/demo use. No commercial incorporation or redistribution is authorized; retain attribution and observe applicable share-alike terms, and revisit rights before any public/commercial use. The ZIP is stored outside the Git checkout at `~/Library/Application Support/BackIntel/Datasets/OlistV2/brazilian-ecommerce.zip`; the source-only profile and reproducible local profiler are alongside it. ZIP size is 44,717,580 bytes; SHA-256 is `967e41e04fc306fe604e2a693f488995a8b41e5047418f8a5c8e4abd6deca784`. Dataset files are not in Git. + +**Measured rows and relationships:** + +| CSV | Rows | +|---|---:| +| Customers | 99,441 | +| Geolocation | 1,000,163 | +| Order items | 112,650 | +| Payments | 103,886 | +| Reviews | 99,224 | +| Orders | 99,441 | +| Products | 32,951 | +| Sellers | 3,095 | +| Product-category translations | 71 | + +- `order_id` is unique across 99,441 order rows. The observed order/customer, item/order, item/seller, item/product, and review/order joins have no unmatched foreign IDs under the measured keys. 775 orders have no item rows (603 unavailable, 164 canceled, 5 created, 2 invoiced, 1 shipped); eligibility and reporting must handle these statuses explicitly. +- The item key `(order_id, order_item_id)` is unique. 1,278 orders contain items from multiple sellers; order-level feedback must not be assigned to every seller in such orders. +- Reviews cover 98,673 distinct orders; 547 orders have multiple review rows. `review_id` is not a safe unique key here: 789 review IDs occur across multiple orders. Preserve a source-row identity and audit those anomalies before choosing a canonical review grain. +- There are 40,950 nonblank review comments out of 99,224 review rows; missing free text is common. Two non-null product categories are absent from the English translation mapping. + +**Time and target feasibility:** order purchases range from 2016-09-04 to 2018-10-17; approval timestamps range from 2016-09-15 to 2018-09-03, with 160 missing; delivered-customer timestamps range from 2016-10-11 to 2018-10-17; estimated delivery dates range from 2016-09-30 to 2018-11-12. Review creation ranges from 2016-10-02 to 2018-08-31 and review-answer timestamps through 2018-10-29. `shipping_limit_date` has four rows in 2020 despite the main order period ending in 2018; flag/exclude or explain these before feature use. + +A **retrospective outcome candidate** is computable as delivered orders whose actual customer-delivery timestamp is after estimated delivery: 96,456 delivered orders have approval plus both outcome timestamps; 7,826 meet that late definition (8.11%). This is a descriptive base rate, not evidence of predictive performance or a validated production target. There are 96,478 delivered orders total; eight lack one of the two outcome timestamps. The approval-time experiment remains conditional: this historical extract does not contain source-ingestion/availability history, and later reviews cannot be inputs for the same order's approval-time prediction. Only a timestamp-respecting replay using features demonstrably available by the prediction cutoff can test that hypothesis. + +**Emulation boundary:** this is Olist's historical multi-seller marketplace dataset, not Amazon, Walmart, or a standalone Shopify merchant; it has no per-order marketplace-channel field, live update stream, seller-contact/intervention log, or carrier-exception feed. It can support a simulated seller/category performance review with order/review drill-downs, but cannot prove live incident handling, seller accountability, or intervention impact. + +**Reproducible evidence:** local manifest/profile: `~/Library/Application Support/BackIntel/Datasets/OlistV2/source-manifest-profile.json`; profiler: `~/Library/Application Support/BackIntel/Datasets/OlistV2/profile_olist.py`. Both are outside the repository for now; add repository-managed profiling code after the validation bootstrap. The profiler verifies the exact nine-file archive inventory and expected sizes, extracts safely, computes SHA-256, streams CSV row/schema/null/key/date profiles, and checks joins/target counts. + +Sources: [Kaggle dataset page](https://www.kaggle.com/datasets/olistbr/brazilian-ecommerce); [Kaggle metadata API](https://www.kaggle.com/api/v1/datasets/view/olistbr/brazilian-ecommerce); [Kaggle file-list API](https://www.kaggle.com/api/v1/datasets/list/olistbr/brazilian-ecommerce); [Olist marketplace integrations](https://olist.com/integracao-com-marketplaces/). + +If the delivered-late target proves analytically unsound, the release retains descriptive customer-experience intelligence but cannot claim predictive capability until a replacement target is approved. + +## 5. Release scope + +### Must have + +1. Documented and measured human baseline for producing the defined operations review package from a bounded Olist sample. +2. Reproducible Olist acquisition manifest and data profile. +3. Validated core relational tables with reconciled joins and counts. +4. Durable partition-job lifecycle with restart and duplicate protection. +5. Fixed Jev questions with provenance, distributions, and human correction history. +6. Point-in-time feature snapshots and separately versioned outcomes. +7. CatBoost structured-only and structured-plus-Jev comparison. +8. TabICLv2 structured-only and structured-plus-Jev comparison, unless blocked by a recorded license/resource gate. +9. Versioned predictor evaluation and explicit activation decision. +10. Seller/category findings view and evidence drill-down for the Seller Performance review. +11. Executive point-in-time snapshot separating actuals and predictions. +12. One policy-governed local attention episode with enter, update, acknowledgment, and clear behavior. +13. One restricted generated-artifact candidate executed and reviewed in a local sandbox. +14. Exact-candidate validation evidence covering behavior, recovery, data, model, UI, cost, and safety. + +### Should have + +- Analyst/model-health view. +- PDF or workbook export. +- Portuguese-direct versus translated Jev comparison. +- Shadow predictions from the non-active model. +- Demonstration of source correction and targeted downstream invalidation. + +### Explicitly out of scope + +- Amazon Reviews scale processing. +- Live enterprise connectors. +- Excel, Word, Slack, or Teams production integrations. +- Real external notifications. +- Cloud deployment or cloud sandbox selection. +- General-purpose schema discovery. +- Automatic Jev question generation. +- TabICL weight fine-tuning. +- Predictor ensembles. +- Kubernetes, distributed compute, vector databases, or a generic ontology platform. +- Multi-tenant public execution of arbitrary code. + +## 6. Sub-objectives and epics + +## Epic 0 — Release contracts and validation foundation + +**Objective:** Turn the research into approved, testable product contracts before implementation begins. + +Deliverables: + +- Confirm primary user, target, prediction time, and historical replay limitations. +- Define the manual operations-intelligence baseline protocol and acceptance points. +- Define v0.1.0 success measures and threshold-setting process. +- Create the repository validation bootstrap required by workspace policy. +- Define durable evidence output bound to the exact commit. +- Pin decision owners and external-action boundaries. +- Establish initial threat model and data/license register. + +Exit criteria: + +- Principal brief approved. +- Required acceptance outcomes are unambiguous. +- Product-validation configuration and CI proof mechanism exist. +- No unresolved decision prevents the first vertical slice. + +## Epic 1 — Olist source and relational foundation + +**Objective:** Establish trustworthy source records and core business facts. + +Deliverables: + +- Acquire the approved Olist files and record hashes, rights, schemas, and row counts. +- Profile key uniqueness, duplicates, nulls, review language, and timestamps. +- Produce a minimal reproducible monthly seller/category calculation and source-linked readout from a frozen source slice, as a correctness fixture for the later pipeline—not a finished manager dashboard. Defer the timed human exercise to release evaluation. +- Implement immutable source batches and records. +- Build canonical orders, items, sellers, products, payments, customers, and reviews. +- Add reconciliation checks preventing row multiplication and monetary inflation. +- Document historical availability assumptions and exclusions. + +Exit criteria: + +- Source and curated counts reconcile. +- Join cardinalities are measured, not assumed. +- One displayed source row traces to its immutable input. +- One minimal seller/category readout states its period, eligibility, counts, denominators, missingness, attribution rules, and source-linked examples. Its calculations and limitations are checked; presentation quality and manager usefulness belong to the later stakeholder-experience work. +- Target feasibility is confirmed or explicitly rejected. + +## Epic 2 — Durable local background execution + +**Objective:** Prove that accepted work survives disconnection and recoverable failures. + +Deliverables: + +- Pin and inspect an Aegra candidate and dependencies. +- Run worker-backed local topology with PostgreSQL and broker infrastructure. +- Define stable partition and operation identities. +- Implement job admission, status, cancellation, retry, and final disposition. +- Keep bulk data outside graph checkpoints. +- Build a deterministic mock partition workflow before connecting models. + +Required tests: + +- Submit and disconnect. +- Duplicate and concurrent submission. +- Lost HTTP response after admission. +- Worker termination before checkpoint and after application write. +- Redis/broker and full-stack restart. +- Interrupt/resume without repeated effects. + +Exit criteria: + +- Acknowledged work is durable. +- Accepted domain rows and artifacts are not duplicated. +- Failed, blocked, partial, and published states remain distinguishable. +- Aegra is selected, conditionally accepted, or rejected with evidence. + +## Epic 3 — Jev semantic evidence pipeline + +**Objective:** Convert review language into bounded, traceable semantic observations. + +Initial question set: + +- Delivery complaint expressed — Noul. +- Packaging damage mentioned — Noul. +- Product functionality mentioned — Noul. +- Primary issue — Choice with explicit none/other. +- Described impact severity — Score with approved rubric. + +Deliverables: + +- Versioned question definitions and interpretation notes. +- Jev adapter with pinned package/model behavior. +- Bounded batching, retry, caching, cost, and rate controls. +- Original text preservation and language/translation provenance. +- Immutable model answers plus separate resolution/correction history. +- Independently reviewed benchmark sample including ambiguity and adversarial text. + +Exit criteria: + +- Response shapes and probability constraints validate. +- Unsupported or uncertain cases route to review rather than forced facts. +- Quality and cost results meet pre-agreed gates or are marked blocked. +- Every accepted signal traces to the source text and exact question version. + +## Epic 4 — Point-in-time features and target + +**Objective:** Produce leakage-resistant model-ready rows with separately observed outcomes. + +Deliverables: + +- Versioned delivered-late target and eligibility/censoring rules. +- Approval-time feature recipe. +- Historical seller performance features using only available prior outcomes. +- Prior Jev-derived review features using only previously available reviews. +- Multi-seller attribution and aggregation policy. +- Immutable feature and outcome manifests. +- Automated leakage, missingness, and grain checks. + +Exit criteria: + +- Every feature has formula, grain, units, cutoff, denominator, and null policy. +- No future source or label enters a prediction row or context. +- Missing and ineligible outcomes are not silently converted to negatives. +- Rebuilding from the same manifest produces the same table. + +## Epic 5 — Predictor comparison and activation + +**Objective:** Determine whether Jev-derived features and TabICLv2 add value beyond conventional structured prediction. + +Required experiment matrix: + +| Predictor | Structured features | Jev features | +|---|---:|---:| +| CatBoost | Yes | No | +| CatBoost | Yes | Yes | +| TabICLv2 | Yes | No | +| TabICLv2 | Yes | Yes | + +Deliverables: + +- Simple base-rate reference. +- Chronological train/development/test manifests. +- CatBoost training and bounded tuning. +- TabICLv2 context preparation and inference. +- Shared evaluation cases and disclosed resource differences. +- Calibration, review-capacity, latency, memory, and cost assessment. +- Versioned predictor registry and activation record. + +Exit criteria: + +- Final holdout did not guide question, feature, or threshold revisions. +- The value of Jev features is separable from predictor choice. +- One predictor is explicitly activated, or a no-predictor decision is recorded. +- Poor performance cannot be hidden by the artifact layer. + +## Epic 6 — Stakeholder artifacts and local attention episodes + +**Objective:** Turn accepted results into differentiated, useful stakeholder views. + +Required artifacts: + +1. **Seller Performance review:** period metrics, eligible denominators, coverage, supported seller/category findings, and linked order/review evidence. +2. **Manager snapshot:** actual delivery/customer-experience measures, separately labeled optional forecast, material changes, and drill-down links. +3. **Analyst evidence view:** source, feature, outcome, model, cost, and quality lineage. + +Required local episode behavior: + +```text +normal -> watch -> active -> unknown/stale -> cleared + | + +-> acknowledged -> investigating -> resolved +``` + +Deliverables: + +- Versioned artifact definitions and immutable artifact versions. +- Deterministic metric and chart generation from accepted result bundles. +- Fixed demo escalation policy with evidence gates, persistence, hysteresis, cooldown, and clear rules. +- Local inbox only; simulated delivery audit. +- Scheduled, event-driven, and deadline-driven replay triggers. + +Exit criteria: + +- Displayed values reconcile exactly to accepted results. +- Actuals, predictions, and hypotheses are visibly distinct. +- Duplicate events do not create duplicate episodes. +- Acknowledgment does not clear the underlying condition. +- Stale data changes the case to unknown rather than resolved. + +## Epic 7 — Restricted artifact-generation execution plane + +**Objective:** Demonstrate that an agent can create a new presentation safely without obtaining production authority. + +Deliverables: + +- Versioned starter project and owned design-system components. +- Protected and writable source boundaries. +- Provider-neutral sandbox broker contract. +- Hardened local sandbox provider. +- Pinned dependencies and dependency allowlist. +- Build, type, lint, test, browser, accessibility, and screenshot checks. +- Temporary localhost preview and review page. +- Candidate source bundle, build manifest, logs, screenshots, and review decision. + +Exit criteria: + +- Generated code cannot read host secrets, broad host files, or production data. +- Network, resource, process, filesystem, and lifetime limits are demonstrated. +- New dependencies and protected-file changes are rejected. +- Sandbox disappearance can be reconstructed from the durable job record. +- Publication requires explicit approval and a clean rebuild. + +## Epic 8 — Integrated validation and release + +**Objective:** Prove the whole system on one exact v0.1.0 candidate. + +Deliverables: + +- End-to-end Olist replay scenario. +- Fault-injection and replay test report. +- Jev quality and cost evidence. +- Data reconciliation and leakage evidence. +- Predictor comparison report. +- Rendered UI and artifact review evidence. +- Sandbox safety evidence. +- Known limitations and blocked outcomes. +- Reproducible local operator instructions. + +Exit criteria: + +- Every must-have acceptance outcome is passed or the release is blocked. +- Evidence identifies the exact candidate commit, data manifest, model versions, question set, feature recipe, policy, and sandbox image. +- A fresh local run reproduces the published demonstration. +- No cloud, external notification, or production action is required to demonstrate v0.1.0. + +## 7. Suggested sprint sequence + +The durations below are planning increments, not delivery commitments. Re-estimate after Epic 1 profiles the actual data and Epic 2 proves the runtime. + +| Sprint | Sprint goal | Primary epics | Demonstrable increment | +|---|---|---|---| +| 0 | Confirm release contracts and validation foundation | Epic 0 | Approved brief, acceptance map, validation bootstrap | +| 1 | Make Olist facts trustworthy and preserve a checked output | Epic 1 | Reconciled source-to-core data plus one minimal monthly seller/category correctness fixture | +| 2 | Make background work durable | Epic 2 | Submit/disconnect/restart mock partition demo | +| 3 | Turn reviews into traceable observations | Epic 3 | Reviewed Jev signals and correction trace | +| 4 | Build one leakage-safe prediction dataset | Epic 4 | Immutable approval-time feature/outcome snapshot | +| 5 | Compare predictors honestly | Epic 5 | Four-way model evidence and activation decision | +| 6 | Deliver the Seller Performance review | Epic 6 | Local seller/category findings view, manager snapshot, and review-record lifecycle | +| 7 | Generate and review executable artifacts safely | Epic 7 | Sandboxed candidate with preview and validation evidence | +| 8 | Prove the integrated release | Epic 8 | Exact-candidate replay, failure recovery, and release review | + +A single small team should expect roughly nine planning increments. Parallel work is appropriate only after contracts stabilize—for example, artifact component design can proceed alongside model evaluation once result schemas are frozen. + +### Immediate execution goal — finish Sprint 1 + +**Outcome:** A checked, source-linked historical calculation is available as an input/output fixture. Sprint 1 does not deliver an accepted manager experience, a background agent, or AI interpretation. + +1. **Review BINT-4's facts contract.** Accept only after reviewing the migration, source lineage, synthetic join tests, and full Olist reconciliation; tests alone are not business acceptance. +2. **Keep BINT-5 as a narrow correctness fixture.** Retain the committed February–March seller/category calculation, source links, and independent checks. Reviewer acceptance of a manager-facing report is deferred to the stakeholder-experience stage. A locally overwritten HTML/JSON preview from uncommitted code must not be described as an exact-commit artifact. +3. **Move to the defining demo loop.** Sprint 2 proves durable admission, processing, restart, and replay of a bounded new Olist batch; Sprint 3 produces versioned Jev observations from review text. The first integrated demo shows which checked finding changed because of the new batch and links back to source evidence. No model or inference spend without separate authorization. +4. **Return to presentation when the loop works.** Define the manager's question, verified change-result contract, and audience views before building the permanent dashboard or generated artifacts. The experimental Reflex screen is not a Sprint 1 exit gate. No human time saving is claimed without a measured baseline. + +**Stop conditions:** source terms or reconciliation invalid; period eligibility cannot be defined without inventing business rules; unsupported seller/category attribution; missing reviewer acceptance; or missing/failed exact-candidate evidence. No paid calls, persistent infrastructure provisioning, push/PR, external messages, or deployment are authorized by this goal. + +## 8. Critical path and dependencies + +```text +Epic 0 + -> Epic 1 + -> Epic 3 + -> Epic 4 + -> Epic 5 + -> Epic 6 + -> Epic 8 + +Epic 0 -> Epic 2 -------------------------> Epic 8 +Epic 0 -> design-system contract -> Epic 7 -> Epic 8 +Epic 6 result contracts -----------------> Epic 7 +``` + +Critical dependency rules: + +- Real Jev work waits for source profiling, rubric approval, budget, and provider authorization. +- Predictor work waits for accepted point-in-time features and labels. +- Risk-based escalation waits for evaluated model outputs; deterministic data-health cases do not. +- Executable artifact generation waits for stable analytical-result and design-system contracts. +- External integrations wait until after local delivery is validated. +- Amazon scale work waits until unit economics and partition recovery are measured on Olist. + +## 9. Scrum execution model + +### Backlog hierarchy + +```text +Release goal + -> Epic + -> Thin vertical story + -> Acceptance checks + -> Durable evidence +``` + +Stories should produce observable behavior rather than horizontal infrastructure alone. Example: + +> As a Marketplace Seller Performance Analyst, I can inspect a seller/category finding with its period, denominator, coverage, and source-linked order/review evidence, so that I can verify whether it merits follow-up before briefing the Seller Operations Manager. + +### Definition of Ready + +A story is ready only when it has: + +- User or operator outcome. +- Acceptance source and observable criteria. +- Owned data/code surface. +- Dependencies and blockers. +- Forbidden outcomes. +- Required evidence. +- External-action authority. + +### Definition of Done + +A story is done only when: + +- Acceptance behavior is demonstrated. +- Automated and required product validation passes. +- Data and model lineage is recorded where applicable. +- Failure and retry behavior is covered for durable workflows. +- Security and permissions are verified for changed surfaces. +- Evidence is non-empty and bound to the exact candidate commit. +- Documentation and operational contracts are updated. +- No unresolved required evidence remains unknown. + +### Sprint review questions + +1. What can a stakeholder do now that they could not do before? +2. Which evidence proves it? +3. What remains simulated or mocked? +4. What failed or is blocked? +5. Did cost, latency, or reviewer burden change? +6. Which assumption should be retired or converted into a deterministic test? + +## 10. Release success measures + +Numeric thresholds should be set after the Olist profile and before evaluating final holdouts. The categories are fixed now. + +### Data integrity + +- Source-to-curated count reconciliation. +- Duplicate and join-inflation defects. +- Feature completeness and historical availability coverage. +- Provenance trace success rate. + +### Semantic interpretation + +- Per-question reviewed quality. +- Ambiguity/abstention handling. +- Portuguese-direct versus translation difference where tested. +- Cost, latency, retry rate, and provider failure rate. + +### Prediction + +- Base-rate, CatBoost, and TabICL comparison. +- PR-AUC/ROC-AUC as appropriate. +- Log loss/Brier score and calibration. +- For the optional prioritization experiment only: precision/recall among seller/category findings at a predeclared reviewer capacity. +- Coverage, warning lead time, latency, memory, and cost. + +### Workflow reliability + +- Durable admission success. +- Recovery without duplicate accepted effects. +- Queue lag, retries, cancellation, and blocked dispositions. +- Reproducible artifact publication. + +### Stakeholder usefulness + +- Artifact values matching authoritative results. +- Time to trace a finding to evidence. +- Seller/category finding usefulness, correction burden, and simulated attention volume in replay. +- Correct handling of stale data, acknowledgment, and resolution. + +### Execution-plane safety + +- Unauthorized file, secret, and network access blocked. +- Resource and lifetime limits enforced. +- Dependency and protected-file policy enforced. +- Preview and cleanup lifecycle reconciled. + +## 11. Risk register and mitigations + +| Risk | Impact | Early mitigation | +|---|---|---| +| Olist timing cannot support target | Predictive slice invalid | Profile timestamps and availability in Sprint 1; permit explicit no-go | +| Jev Portuguese quality is weak | Semantic features unreliable | Review benchmark; compare direct and translation paths; preserve unknown | +| Jev access or spend blocked | Core named capability unavailable | Deterministic adapter/mocks for plumbing; release remains blocked for real Jev claim | +| Aegra recovery differs from claims | Background durability unreliable | Failure injection before model integration; preserve fallback runtime boundary | +| TabICL resource or license issue | Four-way comparison incomplete | Verify exact checkpoint first; keep CatBoost production-capable baseline | +| Predictor adds no value | Predictive prioritization is not defensible | Treat no-predictor outcome as valid; the descriptive Seller Performance review remains the primary deliverable | +| Alert fatigue | Stakeholder rejects capability | Local replay, explicit capacity, hysteresis, cooldown, and measured false-alert burden | +| Generated code escapes intended surface | Host/data security risk | Restricted starter, isolated execution, deny-by-default access, and safety tests | +| Artifact scope delays release | No integrated demo | Require three fixed artifacts; new formats and integrations remain out of scope | +| Cost telemetry incomplete | Scale claim cannot be trusted | Cost/status required at every provider and execution attempt; unknown means blocked | + +## 12. Release review + +v0.1.0 is ready only if the team can demonstrate, from a clean local environment: + +1. Produce the defined review package through the documented human baseline and record its effort, coverage, quality, freshness, and cost. +2. Submit the same bounded Olist replay to the automated workflow and disconnect. +3. Observe durable, restart-safe progress. +4. Inspect accepted source facts and Jev observations. +5. Reproduce a point-in-time feature row and outcome. +6. Compare CatBoost and TabICL evidence without temporal leakage. +7. Open an operations artifact and trace every displayed claim. +8. Compare automated cycle time, effort, coverage, quality, freshness, and total cost with the frozen human baseline. +9. Watch an approved local policy create, update, and clear one attention episode. +10. Review one agent-generated executable artifact inside a sandbox. +11. Re-run duplicate and failure scenarios without duplicate accepted effects. +12. Retrieve complete exact-candidate validation evidence. + +If any required step lacks evidence, the release disposition is `blocked`, not "mostly done." + +## 13. Post-v0.1.0 direction + +Only after the release gate: + +- Select and validate a cloud execution provider by workload. +- Add an authenticated remote preview path. +- Evaluate one Amazon Reviews category for partition scale and unit economics. +- Define an Amazon-specific prediction or descriptive target. +- Add one workplace delivery adapter after permission and revocation design. +- Consider automatic question discovery, rolling-risk models, or TabICL fine-tuning only when measured limitations justify them. diff --git a/docs/roadmap/poc-recovery-plan.md b/docs/roadmap/poc-recovery-plan.md index 9ed8501..4fc5de6 100644 --- a/docs/roadmap/poc-recovery-plan.md +++ b/docs/roadmap/poc-recovery-plan.md @@ -22,7 +22,7 @@ The inventory and diagnosis below record the initial state, not the current fold Give a Marketplace Seller Performance Analyst one maintained local checkout and one reproducible workflow: load approved Olist facts, submit a bounded review batch, finish processing in the background, compare accepted semantic observations, and inspect a source-linked seller/category review with explicit coverage, unknowns, and costs. -Use the existing [roadmap's immediate execution goal](v0.1.0-development-roadmap.md#immediate-execution-goal--finish-sprint-1). This is the first integrated PoC milestone, not completion of the roadmap's full v0.1.0 release. Prediction experiments, generated-code sandboxes, and a permanent dashboard retain their later gates. +Use the existing [historical immediate execution goal](capability-reference.md#immediate-execution-goal--finish-sprint-1). This is the first integrated PoC milestone, not completion of the roadmap's full v0.1.0 release. Prediction experiments, generated-code sandboxes, and a permanent dashboard retain their later gates. ## Pre-recovery repository inventory diff --git a/docs/roadmap/v0.1.0-development-roadmap.md b/docs/roadmap/v0.1.0-development-roadmap.md index 8367a8e..26b89d6 100644 --- a/docs/roadmap/v0.1.0-development-roadmap.md +++ b/docs/roadmap/v0.1.0-development-roadmap.md @@ -1,734 +1,52 @@ -# v0.1.0 Development Roadmap — Background Intelligence Refinery +# BackIntel roadmap — issue-review demo -**Status:** Release plan; business contract confirmed by the product owner on 2026-09-23; technical execution decisions remain open -**Release type:** Local-first technical product demonstrator -**Planning basis:** One small cross-functional team; sequence is dependency-based, not a calendar commitment -**Implementation authority:** The product owner authorized local Sprint 1 execution with approved Kaggle Olist v2 under the non-commercial local-prototype boundary. Paid inference, additional dataset acquisition, persistent infrastructure provisioning, external notifications, cloud deployment, push/PR, and merge remain separately gated. +## Current focus -## 1. Release goal +Help a repository reviewer turn unread issues into an evidence-linked review queue, then keep it current as responses arrive. **Deliver one clear local demonstration before expanding the platform.** -> Help a Marketplace Seller Performance Analyst prepare a trustworthy recurring seller-performance review—with delivery metrics, customer-feedback themes, and evidence-backed findings—faster and with less repetitive data preparation, so a Seller Operations Manager can decide what merits investigation. +A small real-data prediction test now runs. The complete reviewer demo and broader evaluation are still unfinished. Existing local reports, background jobs, and model integrations are the starting point. -v0.1.0 demonstrates this process locally using Olist's historical Brazilian multi-seller marketplace data. Jev, predictors, background execution, and generated artifacts are implementation mechanisms serving that business goal, not the goal themselves. Orders are evidence and drill-downs; the business-level review concerns seller/category patterns. The demo does not claim to reproduce Olist's internal organization. +## Problem we are trying to solve -A successful v0.1.0 proves this chain: +Reviewers repeatedly read issue text, check missing details, reconstruct comments, and decide what needs attention. We want to reduce that preparation without hiding uncertainty or adding more checking work. Time saved remains unproven. -```text -Kaggle Olist v2 source manifest - -> reconciled orders, items, sellers, products, and reviews - -> seller/category metrics with explicit coverage and attribution rules - -> versioned review-text observations where available - -> dated evidence-backed findings and a manager briefing - -> human-vs-system effort and quality comparison - -> optional, separately evaluated delivery prediction - -> local presentation and review; no external operational action -``` +## Workflow and process we automate -This is a historical simulation, not a live commerce system. It has no true source-arrival stream, carrier-exception workflow, or intervention history. +Original issue → structured findings → response-gap prediction → human review → updated packet when events arrive. -## 2. Business process and human baseline +Code controls processing and follow-up. Models interpret text and estimate outcomes. People decide what action to take. The demo makes no changes to live GitHub issues. -### Human process being improved +## Dataset and prediction -The simulated team is Marketplace Seller Performance. The primary user is a Marketplace Seller Performance Analyst; the recipient is a Seller Operations Manager. For a reporting period, the analyst: +Use historical `microsoft/vscode` issues from [GH Archive](https://www.gharchive.org/), cross-checked against GitHub histories. Start with **20 reconstructable cases**. If usable, freeze the proposed **112-case cohort: 64 training, 16 calibration, 32 held out**, with chronological splits across January–March 2024. -1. Obtains order, item, seller, product, delivery, and review exports. -2. Checks completeness, identifiers, join grain, and date coverage. -3. Calculates delivery and customer-experience measures with explicit numerators, denominators, period, and eligible population. -4. Reviews feedback themes and keeps missing or ambiguous text distinct from negative feedback. -5. Compares seller/category patterns across comparable periods, noting sample sizes and attribution limits. -6. Selects evidence-backed findings for a concise management review, linking each to source records. -7. Answers follow-up questions or corrects the report when a join, classification, or source record is disputed. +Predict at creation: **will this issue still be open seven days later without a recorded qualifying maintainer comment?** This measures a response gap, not severity. Missing history stays unknown. Later comments and outcomes cannot enter initial prediction inputs. -The recurring manual burden is data reconciliation, repeated calculation, review-text reading/categorization, evidence collection, and briefing preparation. This Kaggle demo models that work; it does not claim that any named real company currently performs this exact process. +## Action plan: testing and visible demonstration -### Process established by v0.1.0 +| Step | Deliverable | Done when | +| --- | --- | --- | +| **1. Verify the data** | Original text and seven-day histories for 20 issues; then a frozen cohort and exclusion list. | Sources reproduce, outcome labels follow the agreed rule, and incomplete histories are excluded. Stop or revise the dataset if reconstruction fails. | +| **2. Build the review packet** | Original text beside five findings: failure, reproduction steps, expected/actual behavior, environment, and regression claim. | A reviewer can check each finding against its source; absent or ambiguous information stays explicit. Start with labelled fixtures. | +| **3. Compare predictions** | Baseline plus CatBoost and TabICLv2, each using facts alone and facts plus Jev findings. | All routes use the same held-out cases. Show probability quality and useful cases found at a fixed review capacity, including when the baseline wins. Real-data/model execution requires separate approval. | +| **4. Show the complete journey** | Local queue, case detail, prediction comparison, and event timeline. | A reviewer can inspect evidence, record a local decision, and see a later response update the packet without rewriting the original prediction. | +| **5. Verify and demonstrate** | Repeatable ten-minute walkthrough plus a compact test-results view. | Replay reproduces results; duplicate events and interruption do not duplicate accepted work or local notifications. Required checks pass on the exact demo candidate. | +| **6. Test usefulness** | Reviewer comparison of source-only and assisted preparation. | Report total preparation/checking time and material mistakes. Agree the success target before the pilot; do not claim savings from the demo alone. | -For a selected historical reporting cutoff, the system should: +**Current test:** [actual predictions evaluated](../research/issue-prediction-results.md) on seven later issues after training on 19; neither model beat the baseline. A [local review workspace](../research/issue-review-artifact.md) now supports evidence inspection, human decisions, and export. **Next:** review that artifact and expand evaluation before choosing a predictor. Text interpretation and background integration remain unfinished. -```text -load approved Olist snapshot - -> validate source rows and join grains - -> calculate seller/category delivery and feedback measures - -> interpret available review text with traceable, bounded questions - -> assemble findings with denominators, coverage, and source evidence - -> render a Seller Performance review for the manager - -> preserve reviewer corrections and the exact data/definition versions -``` +## What the demo shows -A generated finding is a seller/category observation to review—not a customer-support ticket or a claim that a seller caused an outcome. People define metrics, verify uncertain findings, investigate causes, and decide any business action. No seller/customer contact or external escalation occurs in v0.1.0. +1. Open an issue and compare original text with structured findings. +2. Show its predicted response gap and why it enters the review queue. +3. Record a human review decision locally. +4. Replay a later response or closure; show the updated timeline and packet. +5. Inject a clearly labelled duplicate/restart test; show recovery and test results. -### Business hypothesis +Label fixture output, recorded actual model output, and new execution distinctly. Use stored actual outputs for repeatable presentations. Show missing evidence, model disagreements, and failures as well as successful cases. -Given the same frozen source snapshot and the same review-package specification, BackIntel can produce an independently accepted Seller Performance review with less repetitive analyst effort than the manual workflow, without reducing metric correctness, useful coverage, or traceability. We will measure elapsed time, hands-on analyst/reviewer time, reconciliation defects, coverage, correction/rework, finding validity, traceability, freshness, and total cost. Numeric improvement targets will be set after the manual baseline and before final evaluation. +## Completion boundary -A separate experiment may compare human/rule/model ranking at an equal investigation capacity. That secondary top-K evaluation concerns **seller/category findings**, not a fixed number of orders, and is not required to prove the primary reporting workflow. The demo is not successful merely because it uses AI. +The demo is ready when the full journey is visible, repeatable, and backed by passing required source, interpretation, prediction, workflow, UI, scope, and cost checks. Unknown required evidence means blocked. This plan does not authorize new provider spending, real-data model use, publication, or live GitHub changes. -### Baseline comparison - -BINT-1 defines the assignment and acceptance rubric. The revised BINT-5 first produces a reproducible monthly seller-performance report from reconciled Olist facts, without requiring a timed human exercise. A later release-evaluation task measures the human baseline against the same frozen files, historical cutoff, and required report outline. A human analyst and the automated workflow must receive the same information for that later comparison. The report covers all eligible data in the selected slice; **there is no arbitrary 20-order queue cap**. If we later evaluate investigative prioritization, its seller/category finding capacity is a separate value derived from observed reviewer workload and frozen before holdout scoring. - -| Measure | Human baseline | Automated comparison | -|---|---|---| -| Elapsed cycle time | Input release to independently accepted review | Same boundaries; include queue/wait time | -| Active effort | Analyst and independent-review minutes, separated | Human checking/correction plus runtime/provider processing | -| Data correctness | Join/reconciliation defects and metric recomputation | Same checks against the same source manifest | -| Coverage | Eligible orders, sellers, categories, and available reviews represented; show denominators | Same definitions plus processed/blocked/missing dispositions | -| Finding quality | Independent reviewer rates supported and useful findings using an agreed rubric | Blindly reviewed findings, unsupported-claim and correction rates | -| Feedback quality | Adjudicated review-theme sample and ambiguity disposition | Jev agreement/abstention against that same sample | -| Traceability | Share of checked claims verifiable to source records | Same measure, with source-to-artifact lineage | -| Cost | Loaded human-time assumption and tooling | Human review plus measured compute/provider/runtime cost | - -Set numeric success thresholds before final evaluation, after the data profile and human baseline show what is measurable. Do not tune against the final holdout. - -### Manual-baseline protocol (BINT-1; timed comparison deferred to release evaluation) - -**Assignment.** “Using the frozen Olist reporting slice, prepare a Seller Performance review for the Seller Operations Manager. Reconcile eligible orders and items; report delivery performance with counts and denominators; summarize available customer ratings/review themes; identify seller/category patterns that merit human investigation; and provide source-linked examples, coverage, unknowns, and limitations.” The output is a full-period summary plus evidence-backed findings—not a predetermined number of order cases. - -**Source and freeze.** Use Kaggle's Olist Brazilian E-Commerce Public Dataset v2 under the product-owner-approved non-commercial local prototype/demo boundary (CC BY-NC-SA 4.0; retain attribution; no commercial incorporation/redistribution). BINT-3's source manifest contains archive/file hashes. BINT-5 freezes the report period, eligible population, source-file manifest, and historical cutoff for the initial descriptive report. Before any later timed human or model evaluation, also freeze the comparison protocol and chronological development/holdout split. Humans and automation receive the same information. Do not let post-cutoff reviews or outcomes support claims about what was knowable at the cutoff. - -**Attribution rules.** Order delivery can be associated with a seller only when all items in that order identify exactly one seller; report multi-seller orders separately at marketplace level. Associate review text with a category only when the order's item-category set is unambiguous; otherwise use marketplace-level feedback or mark attribution unknown. Describe associations, not causes. Always show denominators, missing coverage, and the eligibility rule. Do not interpret missing comments as no complaint. - -**Human exercise.** A Marketplace Seller Performance Analyst uses ordinary spreadsheet/SQL tools, but no Jev labels, generated findings, or predictor scores. Start elapsed time when the frozen input package is released; record active analyst minutes separately. The analyst produces the assignment above and submits it for independent acceptance. A second reviewer checks metric recomputation, joins, evidence links, unsupported attribution/causal claims, and usefulness of findings; record reviewer time, corrections, and acceptance disposition. Preserve the exact sample, instructions, report, and timing log. - -**Comparison.** Run the automated workflow over the same frozen inputs and report specification. Compare elapsed time, hands-on human effort, reconciliation correctness, eligible-record/feedback coverage, finding validity/usefulness, correction burden, traceability, freshness, and measured total cost. Reviewers should assess human and automated findings without seeing which produced them where practical. This primary report comparison has no fixed findings cap. If a later predictor experiment needs equal-capacity ranking, set capacity from measured analyst workload and freeze it before the held-out evaluation; the unit is seller/category findings, never “20 orders.” - -**Confirmed demo cadence (product-owner approval: 2026-09-23).** Use a calendar-month Seller Performance review, comparing the selected month with the immediately preceding comparable month. The data replay can then reveal orders/reviews as historical dates advance. This is a demo convention, not a claim about Olist's internal cadence; eBay documents monthly seller-performance evaluation, while Walmart uses rolling 30/60-day windows. After the source profile, select periods by a fixed chronological rule and confirm sufficient eligible volume before examining holdout findings. Sources: [eBay seller-performance monitoring](https://www.ebay.co.uk/help/selling/selling/monitor-service-metrics?id=4785); [Walmart Marketplace performance standards](https://marketplacelearn.walmart.com/guides/Policies%20&%20standards/Performance/Seller-performance-standards). - -**Draft independent acceptance rubric.** Mark each check pass/correction/reject: (1) source totals and joins reconcile; (2) every KPI has correct formula, unit, period, eligible denominator, and missingness; (3) seller/category attribution obeys the single-seller/single-category rules; (4) each finding has verifiable evidence and makes no unsupported causal claim; (5) coverage, ambiguity, and limitations are explicit; (6) the manager can identify a justified follow-up from the report. Overall disposition is accepted, accepted after correction, or rejected, with reviewer time and reason recorded. Name the independent reviewer before the timed baseline. - -**BINT-1 decision record.** The product owner confirmed the Marketplace Seller Performance Analyst, Seller Operations Manager recipient, seller/category report, monthly demo cadence, no fixed finding cap for the primary report, and conditional prediction boundary. The baseline protocol and acceptance rubric are specified. BINT-5 implements the first descriptive report; name an independent reviewer and freeze the human exercise and chronological holdout rule before the later timed comparison. Numeric win thresholds are set only after the human baseline and before holdout evaluation. Provider spend, model licensing beyond approved Olist use, cloud deployment, and external notifications remain separately gated; none is authorized by this protocol. - -### Business output - -The v0.1.0 output is a **Seller Performance review package**: - -1. Period-level delivery and customer-experience metrics with definitions, denominators, and coverage. -2. Seller/category findings that merit review, without unsupported causal claims. -3. Source-linked order and review evidence, with ambiguous multi-seller/category attribution visible. -4. A concise manager briefing that distinguishes actuals, interpreted feedback, hypotheses, and optional predictions. -5. A reproducible record of source versions, calculations, questions, and reviewer corrections. - -Delivery-risk prediction is a separate experiment. If it does not improve an agreed prioritization baseline, disclose that result; the descriptive review package remains the primary business deliverable. - -## 3. Principal brief - -### Outcome - -A Marketplace Seller Performance Analyst can submit a historical Olist reporting slice to a durable local workflow, return after processing, and receive an evidence-linked seller/category review for the Seller Operations Manager. The analyst can verify calculations, inspect review-text interpretations, correct uncertain findings, and trace displayed claims back to source rows and definition versions. - -### Primary user - -Marketplace Seller Performance Analyst at an Olist-like multi-seller marketplace. The analyst prepares a recurring review of seller/category delivery performance and customer-feedback patterns, then investigates supported changes with evidence. The Seller Operations Manager is the primary recipient of the briefing. This emulates a marketplace operations function; it does not claim to reproduce Olist's internal organization or workflow. - -### Secondary users - -- Data/model analyst reviewing data quality and predictor evidence. -- Seller Operations Manager receiving a point-in-time summary with drill-down links. - -### Acceptance sources - -- Olist source manifests and audited table contracts. -- Approved Jev question/rubric definitions and independently reviewed examples. -- Versioned target and feature definitions. -- Chronological predictor evaluation manifests. -- Deterministic replay fixtures and expected outcomes. -- Exact-candidate product-validation evidence. - -### Owned surface - -- Local source ingestion and replay. -- Durable job admission and execution. -- Application relational data and artifact manifests. -- Jev adapter and semantic observation contracts. -- Feature and target construction. -- CatBoost and TabICLv2 preparation, evaluation, and scoring. -- Local seller/category finding views, review records, and downloadable snapshots. -- Restricted artifact-code execution and review. - -### Invariants - -1. Source facts, model observations, actual outcomes, predictions, and narratives remain distinguishable. -2. Every result identifies its exact source, schema, question, feature, model, and policy versions. -3. Future evidence and labels cannot enter earlier feature rows or model context. -4. A completed workflow is not automatically an accepted output. -5. Retries cannot duplicate accepted rows, episodes, publications, or simulated deliveries. -6. Stale or missing data is unknown, not false, zero, on-time, or resolved. -7. Predictions are not presented as facts or causal explanations. -8. Generated code receives no production credentials or unrestricted host access. -9. External notifications and business actions are not authorized in v0.1.0. -10. Required evidence ends as `passed`, `failed`, or `blocked`. - -### Forbidden outcomes - -- Using an order's eventual review to predict that same order at approval time. -- Treating missing delivery as on-time delivery. -- Inflating revenue or counts through unsafe one-to-many joins. -- Treating Jev confidence as delivery-risk probability. -- Attributing a multi-seller order review to every seller. -- Publishing incomplete refreshes as complete. -- Allowing generated code to alter protected application code, dependencies, policies, or recipients. -- Presenting mocked classifications as Jev output. -- Silently substituting one predictor for another. -- Sending real email, Slack, Teams, or seller/customer communications. - -### Primary risks - -- Olist's historical availability timestamps may not support every intended feature. -- Portuguese review interpretation may not meet quality requirements. -- Aegra is beta and its recovery semantics are not yet locally validated. -- Jev service availability, cost, and terms may block a real adapter test. -- TabICLv2 checkpoint/dependency licensing or local resource use may be unsuitable. -- Delivery-risk prediction may not improve over a simple baseline. -- Generated-code execution can expand security and validation scope rapidly. - -### Stop conditions - -Stop and return to design when: - -- Source profiling invalidates the target or point-in-time assumptions. -- Required provider/model licensing is unresolved. -- Reviewed Jev quality is below the threshold agreed before the experiment. -- Durable admission or replay-safe publication cannot be demonstrated. -- Required cost telemetry is unknown. -- Sandbox isolation tests expose host secrets, credentials, or unauthorized networking. -- Product validation is not bound to the exact candidate commit. - -### External-action authority - -v0.1.0 may autonomously create and update **local seller/category review records** under approved demo policies. These are not customer-support tickets and do not imply seller fault. New policies, generated executable artifacts, predictor promotion, recipients, cloud deployment, and every outward notification require explicit approval. - -## 4. Confirmed business contract and proposed technical decisions - -The product owner confirmed the business contract and core user on 2026-09-23: - -- **Primary user:** Marketplace Seller Performance Analyst; Seller Operations Manager receives the briefing. -- **Process to improve:** Manually reconciling marketplace order/delivery data, reviewing customer feedback, identifying seller/category trends, and preparing a supported performance briefing. -- **v0.1 output:** An evidence-linked seller/category review and management briefing, compared with a measured human baseline. Orders are supporting drill-down evidence, not assumed to be pre-existing support cases. -- **Prediction boundary:** Test late delivery at order approval only if Olist supports a valid point-in-time target; otherwise deliver descriptive triage and report prediction as blocked, not as a claimed capability. -- **Baseline comparison:** Same frozen sample and report specification; compare elapsed time, human effort, data/claim quality, coverage, correction burden, traceability, and total cost. A capped ranking comparison is a separate optional experiment. - -The following implementation choices remain proposals subject to source profiling, license review, and validation. - -| Decision | Proposed v0.1.0 choice | -|---|---| -| Dataset | Olist Brazilian E-Commerce Public Dataset on Kaggle: https://www.kaggle.com/datasets/olistbr/brazilian-ecommerce. Kaggle's public metadata API checked 2026-09-23 reports version 2, 126,186,995 bytes, and CC BY-NC-SA 4.0. Product owner approved use strictly for a non-commercial local prototype/demo on 2026-09-23; no commercial incorporation or redistribution is authorized. Record attribution and observe share-alike terms for any covered adaptations; revisit rights before public/commercial use. | -| Replay mode | Historical simulation using explicit business and wall clocks | -| Primary prediction | Conditional experiment: late delivery among eligible orders, only if point-in-time validity is established | -| Prediction time | Order approval, subject to source timestamp audit | -| Primary stakeholder | Marketplace Seller Performance Analyst; briefing recipient: Seller Operations Manager | -| Jev scope | Fixed, reviewed questions over review text; no automatic feature discovery | -| Predictors | CatBoost baseline and TabICLv2 candidate behind one feature contract | -| Runtime | Aegra candidate in worker-backed local mode, subject to proof | -| Application store | PostgreSQL logical application database separate from Aegra-owned tables | -| Bulk/artifact storage | Local content-addressed files initially | -| Review authority | Local attention episodes only | -| Artifact authority | Approved templates plus restricted generated-code surface | -| Sandbox | Hardened local provider behind a provider-neutral interface | -| Delivery | Local authenticated application links and downloadable snapshots | -| Cloud execution | Not required for v0.1.0; evaluation follows local proof | - -### BINT-3 source profile (Kaggle v2; non-commercial local prototype) - -**Acquisition and provenance:** Kaggle public dataset `olistbr/brazilian-ecommerce`, version 2, current as checked 2026-09-23; license `CC BY-NC-SA 4.0`. The product owner approved strictly non-commercial local prototype/demo use. No commercial incorporation or redistribution is authorized; retain attribution and observe applicable share-alike terms, and revisit rights before any public/commercial use. The ZIP is stored outside the Git checkout at `~/Library/Application Support/BackIntel/Datasets/OlistV2/brazilian-ecommerce.zip`; the source-only profile and reproducible local profiler are alongside it. ZIP size is 44,717,580 bytes; SHA-256 is `967e41e04fc306fe604e2a693f488995a8b41e5047418f8a5c8e4abd6deca784`. Dataset files are not in Git. - -**Measured rows and relationships:** - -| CSV | Rows | -|---|---:| -| Customers | 99,441 | -| Geolocation | 1,000,163 | -| Order items | 112,650 | -| Payments | 103,886 | -| Reviews | 99,224 | -| Orders | 99,441 | -| Products | 32,951 | -| Sellers | 3,095 | -| Product-category translations | 71 | - -- `order_id` is unique across 99,441 order rows. The observed order/customer, item/order, item/seller, item/product, and review/order joins have no unmatched foreign IDs under the measured keys. 775 orders have no item rows (603 unavailable, 164 canceled, 5 created, 2 invoiced, 1 shipped); eligibility and reporting must handle these statuses explicitly. -- The item key `(order_id, order_item_id)` is unique. 1,278 orders contain items from multiple sellers; order-level feedback must not be assigned to every seller in such orders. -- Reviews cover 98,673 distinct orders; 547 orders have multiple review rows. `review_id` is not a safe unique key here: 789 review IDs occur across multiple orders. Preserve a source-row identity and audit those anomalies before choosing a canonical review grain. -- There are 40,950 nonblank review comments out of 99,224 review rows; missing free text is common. Two non-null product categories are absent from the English translation mapping. - -**Time and target feasibility:** order purchases range from 2016-09-04 to 2018-10-17; approval timestamps range from 2016-09-15 to 2018-09-03, with 160 missing; delivered-customer timestamps range from 2016-10-11 to 2018-10-17; estimated delivery dates range from 2016-09-30 to 2018-11-12. Review creation ranges from 2016-10-02 to 2018-08-31 and review-answer timestamps through 2018-10-29. `shipping_limit_date` has four rows in 2020 despite the main order period ending in 2018; flag/exclude or explain these before feature use. - -A **retrospective outcome candidate** is computable as delivered orders whose actual customer-delivery timestamp is after estimated delivery: 96,456 delivered orders have approval plus both outcome timestamps; 7,826 meet that late definition (8.11%). This is a descriptive base rate, not evidence of predictive performance or a validated production target. There are 96,478 delivered orders total; eight lack one of the two outcome timestamps. The approval-time experiment remains conditional: this historical extract does not contain source-ingestion/availability history, and later reviews cannot be inputs for the same order's approval-time prediction. Only a timestamp-respecting replay using features demonstrably available by the prediction cutoff can test that hypothesis. - -**Emulation boundary:** this is Olist's historical multi-seller marketplace dataset, not Amazon, Walmart, or a standalone Shopify merchant; it has no per-order marketplace-channel field, live update stream, seller-contact/intervention log, or carrier-exception feed. It can support a simulated seller/category performance review with order/review drill-downs, but cannot prove live incident handling, seller accountability, or intervention impact. - -**Reproducible evidence:** local manifest/profile: `~/Library/Application Support/BackIntel/Datasets/OlistV2/source-manifest-profile.json`; profiler: `~/Library/Application Support/BackIntel/Datasets/OlistV2/profile_olist.py`. Both are outside the repository for now; add repository-managed profiling code after the validation bootstrap. The profiler verifies the exact nine-file archive inventory and expected sizes, extracts safely, computes SHA-256, streams CSV row/schema/null/key/date profiles, and checks joins/target counts. - -Sources: [Kaggle dataset page](https://www.kaggle.com/datasets/olistbr/brazilian-ecommerce); [Kaggle metadata API](https://www.kaggle.com/api/v1/datasets/view/olistbr/brazilian-ecommerce); [Kaggle file-list API](https://www.kaggle.com/api/v1/datasets/list/olistbr/brazilian-ecommerce); [Olist marketplace integrations](https://olist.com/integracao-com-marketplaces/). - -If the delivered-late target proves analytically unsound, the release retains descriptive customer-experience intelligence but cannot claim predictive capability until a replacement target is approved. - -## 5. Release scope - -### Must have - -1. Documented and measured human baseline for producing the defined operations review package from a bounded Olist sample. -2. Reproducible Olist acquisition manifest and data profile. -3. Validated core relational tables with reconciled joins and counts. -4. Durable partition-job lifecycle with restart and duplicate protection. -5. Fixed Jev questions with provenance, distributions, and human correction history. -6. Point-in-time feature snapshots and separately versioned outcomes. -7. CatBoost structured-only and structured-plus-Jev comparison. -8. TabICLv2 structured-only and structured-plus-Jev comparison, unless blocked by a recorded license/resource gate. -9. Versioned predictor evaluation and explicit activation decision. -10. Seller/category findings view and evidence drill-down for the Seller Performance review. -11. Executive point-in-time snapshot separating actuals and predictions. -12. One policy-governed local attention episode with enter, update, acknowledgment, and clear behavior. -13. One restricted generated-artifact candidate executed and reviewed in a local sandbox. -14. Exact-candidate validation evidence covering behavior, recovery, data, model, UI, cost, and safety. - -### Should have - -- Analyst/model-health view. -- PDF or workbook export. -- Portuguese-direct versus translated Jev comparison. -- Shadow predictions from the non-active model. -- Demonstration of source correction and targeted downstream invalidation. - -### Explicitly out of scope - -- Amazon Reviews scale processing. -- Live enterprise connectors. -- Excel, Word, Slack, or Teams production integrations. -- Real external notifications. -- Cloud deployment or cloud sandbox selection. -- General-purpose schema discovery. -- Automatic Jev question generation. -- TabICL weight fine-tuning. -- Predictor ensembles. -- Kubernetes, distributed compute, vector databases, or a generic ontology platform. -- Multi-tenant public execution of arbitrary code. - -## 6. Sub-objectives and epics - -## Epic 0 — Release contracts and validation foundation - -**Objective:** Turn the research into approved, testable product contracts before implementation begins. - -Deliverables: - -- Confirm primary user, target, prediction time, and historical replay limitations. -- Define the manual operations-intelligence baseline protocol and acceptance points. -- Define v0.1.0 success measures and threshold-setting process. -- Create the repository validation bootstrap required by workspace policy. -- Define durable evidence output bound to the exact commit. -- Pin decision owners and external-action boundaries. -- Establish initial threat model and data/license register. - -Exit criteria: - -- Principal brief approved. -- Required acceptance outcomes are unambiguous. -- Product-validation configuration and CI proof mechanism exist. -- No unresolved decision prevents the first vertical slice. - -## Epic 1 — Olist source and relational foundation - -**Objective:** Establish trustworthy source records and core business facts. - -Deliverables: - -- Acquire the approved Olist files and record hashes, rights, schemas, and row counts. -- Profile key uniqueness, duplicates, nulls, review language, and timestamps. -- Produce a minimal reproducible monthly seller/category calculation and source-linked readout from a frozen source slice, as a correctness fixture for the later pipeline—not a finished manager dashboard. Defer the timed human exercise to release evaluation. -- Implement immutable source batches and records. -- Build canonical orders, items, sellers, products, payments, customers, and reviews. -- Add reconciliation checks preventing row multiplication and monetary inflation. -- Document historical availability assumptions and exclusions. - -Exit criteria: - -- Source and curated counts reconcile. -- Join cardinalities are measured, not assumed. -- One displayed source row traces to its immutable input. -- One minimal seller/category readout states its period, eligibility, counts, denominators, missingness, attribution rules, and source-linked examples. Its calculations and limitations are checked; presentation quality and manager usefulness belong to the later stakeholder-experience work. -- Target feasibility is confirmed or explicitly rejected. - -## Epic 2 — Durable local background execution - -**Objective:** Prove that accepted work survives disconnection and recoverable failures. - -Deliverables: - -- Pin and inspect an Aegra candidate and dependencies. -- Run worker-backed local topology with PostgreSQL and broker infrastructure. -- Define stable partition and operation identities. -- Implement job admission, status, cancellation, retry, and final disposition. -- Keep bulk data outside graph checkpoints. -- Build a deterministic mock partition workflow before connecting models. - -Required tests: - -- Submit and disconnect. -- Duplicate and concurrent submission. -- Lost HTTP response after admission. -- Worker termination before checkpoint and after application write. -- Redis/broker and full-stack restart. -- Interrupt/resume without repeated effects. - -Exit criteria: - -- Acknowledged work is durable. -- Accepted domain rows and artifacts are not duplicated. -- Failed, blocked, partial, and published states remain distinguishable. -- Aegra is selected, conditionally accepted, or rejected with evidence. - -## Epic 3 — Jev semantic evidence pipeline - -**Objective:** Convert review language into bounded, traceable semantic observations. - -Initial question set: - -- Delivery complaint expressed — Noul. -- Packaging damage mentioned — Noul. -- Product functionality mentioned — Noul. -- Primary issue — Choice with explicit none/other. -- Described impact severity — Score with approved rubric. - -Deliverables: - -- Versioned question definitions and interpretation notes. -- Jev adapter with pinned package/model behavior. -- Bounded batching, retry, caching, cost, and rate controls. -- Original text preservation and language/translation provenance. -- Immutable model answers plus separate resolution/correction history. -- Independently reviewed benchmark sample including ambiguity and adversarial text. - -Exit criteria: - -- Response shapes and probability constraints validate. -- Unsupported or uncertain cases route to review rather than forced facts. -- Quality and cost results meet pre-agreed gates or are marked blocked. -- Every accepted signal traces to the source text and exact question version. - -## Epic 4 — Point-in-time features and target - -**Objective:** Produce leakage-resistant model-ready rows with separately observed outcomes. - -Deliverables: - -- Versioned delivered-late target and eligibility/censoring rules. -- Approval-time feature recipe. -- Historical seller performance features using only available prior outcomes. -- Prior Jev-derived review features using only previously available reviews. -- Multi-seller attribution and aggregation policy. -- Immutable feature and outcome manifests. -- Automated leakage, missingness, and grain checks. - -Exit criteria: - -- Every feature has formula, grain, units, cutoff, denominator, and null policy. -- No future source or label enters a prediction row or context. -- Missing and ineligible outcomes are not silently converted to negatives. -- Rebuilding from the same manifest produces the same table. - -## Epic 5 — Predictor comparison and activation - -**Objective:** Determine whether Jev-derived features and TabICLv2 add value beyond conventional structured prediction. - -Required experiment matrix: - -| Predictor | Structured features | Jev features | -|---|---:|---:| -| CatBoost | Yes | No | -| CatBoost | Yes | Yes | -| TabICLv2 | Yes | No | -| TabICLv2 | Yes | Yes | - -Deliverables: - -- Simple base-rate reference. -- Chronological train/development/test manifests. -- CatBoost training and bounded tuning. -- TabICLv2 context preparation and inference. -- Shared evaluation cases and disclosed resource differences. -- Calibration, review-capacity, latency, memory, and cost assessment. -- Versioned predictor registry and activation record. - -Exit criteria: - -- Final holdout did not guide question, feature, or threshold revisions. -- The value of Jev features is separable from predictor choice. -- One predictor is explicitly activated, or a no-predictor decision is recorded. -- Poor performance cannot be hidden by the artifact layer. - -## Epic 6 — Stakeholder artifacts and local attention episodes - -**Objective:** Turn accepted results into differentiated, useful stakeholder views. - -Required artifacts: - -1. **Seller Performance review:** period metrics, eligible denominators, coverage, supported seller/category findings, and linked order/review evidence. -2. **Manager snapshot:** actual delivery/customer-experience measures, separately labeled optional forecast, material changes, and drill-down links. -3. **Analyst evidence view:** source, feature, outcome, model, cost, and quality lineage. - -Required local episode behavior: - -```text -normal -> watch -> active -> unknown/stale -> cleared - | - +-> acknowledged -> investigating -> resolved -``` - -Deliverables: - -- Versioned artifact definitions and immutable artifact versions. -- Deterministic metric and chart generation from accepted result bundles. -- Fixed demo escalation policy with evidence gates, persistence, hysteresis, cooldown, and clear rules. -- Local inbox only; simulated delivery audit. -- Scheduled, event-driven, and deadline-driven replay triggers. - -Exit criteria: - -- Displayed values reconcile exactly to accepted results. -- Actuals, predictions, and hypotheses are visibly distinct. -- Duplicate events do not create duplicate episodes. -- Acknowledgment does not clear the underlying condition. -- Stale data changes the case to unknown rather than resolved. - -## Epic 7 — Restricted artifact-generation execution plane - -**Objective:** Demonstrate that an agent can create a new presentation safely without obtaining production authority. - -Deliverables: - -- Versioned starter project and owned design-system components. -- Protected and writable source boundaries. -- Provider-neutral sandbox broker contract. -- Hardened local sandbox provider. -- Pinned dependencies and dependency allowlist. -- Build, type, lint, test, browser, accessibility, and screenshot checks. -- Temporary localhost preview and review page. -- Candidate source bundle, build manifest, logs, screenshots, and review decision. - -Exit criteria: - -- Generated code cannot read host secrets, broad host files, or production data. -- Network, resource, process, filesystem, and lifetime limits are demonstrated. -- New dependencies and protected-file changes are rejected. -- Sandbox disappearance can be reconstructed from the durable job record. -- Publication requires explicit approval and a clean rebuild. - -## Epic 8 — Integrated validation and release - -**Objective:** Prove the whole system on one exact v0.1.0 candidate. - -Deliverables: - -- End-to-end Olist replay scenario. -- Fault-injection and replay test report. -- Jev quality and cost evidence. -- Data reconciliation and leakage evidence. -- Predictor comparison report. -- Rendered UI and artifact review evidence. -- Sandbox safety evidence. -- Known limitations and blocked outcomes. -- Reproducible local operator instructions. - -Exit criteria: - -- Every must-have acceptance outcome is passed or the release is blocked. -- Evidence identifies the exact candidate commit, data manifest, model versions, question set, feature recipe, policy, and sandbox image. -- A fresh local run reproduces the published demonstration. -- No cloud, external notification, or production action is required to demonstrate v0.1.0. - -## 7. Suggested sprint sequence - -The durations below are planning increments, not delivery commitments. Re-estimate after Epic 1 profiles the actual data and Epic 2 proves the runtime. - -| Sprint | Sprint goal | Primary epics | Demonstrable increment | -|---|---|---|---| -| 0 | Confirm release contracts and validation foundation | Epic 0 | Approved brief, acceptance map, validation bootstrap | -| 1 | Make Olist facts trustworthy and preserve a checked output | Epic 1 | Reconciled source-to-core data plus one minimal monthly seller/category correctness fixture | -| 2 | Make background work durable | Epic 2 | Submit/disconnect/restart mock partition demo | -| 3 | Turn reviews into traceable observations | Epic 3 | Reviewed Jev signals and correction trace | -| 4 | Build one leakage-safe prediction dataset | Epic 4 | Immutable approval-time feature/outcome snapshot | -| 5 | Compare predictors honestly | Epic 5 | Four-way model evidence and activation decision | -| 6 | Deliver the Seller Performance review | Epic 6 | Local seller/category findings view, manager snapshot, and review-record lifecycle | -| 7 | Generate and review executable artifacts safely | Epic 7 | Sandboxed candidate with preview and validation evidence | -| 8 | Prove the integrated release | Epic 8 | Exact-candidate replay, failure recovery, and release review | - -A single small team should expect roughly nine planning increments. Parallel work is appropriate only after contracts stabilize—for example, artifact component design can proceed alongside model evaluation once result schemas are frozen. - -### Immediate execution goal — finish Sprint 1 - -**Outcome:** A checked, source-linked historical calculation is available as an input/output fixture. Sprint 1 does not deliver an accepted manager experience, a background agent, or AI interpretation. - -1. **Review BINT-4's facts contract.** Accept only after reviewing the migration, source lineage, synthetic join tests, and full Olist reconciliation; tests alone are not business acceptance. -2. **Keep BINT-5 as a narrow correctness fixture.** Retain the committed February–March seller/category calculation, source links, and independent checks. Reviewer acceptance of a manager-facing report is deferred to the stakeholder-experience stage. A locally overwritten HTML/JSON preview from uncommitted code must not be described as an exact-commit artifact. -3. **Move to the defining demo loop.** Sprint 2 proves durable admission, processing, restart, and replay of a bounded new Olist batch; Sprint 3 produces versioned Jev observations from review text. The first integrated demo shows which checked finding changed because of the new batch and links back to source evidence. No model or inference spend without separate authorization. -4. **Return to presentation when the loop works.** Define the manager's question, verified change-result contract, and audience views before building the permanent dashboard or generated artifacts. The experimental Reflex screen is not a Sprint 1 exit gate. No human time saving is claimed without a measured baseline. - -**Stop conditions:** source terms or reconciliation invalid; period eligibility cannot be defined without inventing business rules; unsupported seller/category attribution; missing reviewer acceptance; or missing/failed exact-candidate evidence. No paid calls, persistent infrastructure provisioning, push/PR, external messages, or deployment are authorized by this goal. - -## 8. Critical path and dependencies - -```text -Epic 0 - -> Epic 1 - -> Epic 3 - -> Epic 4 - -> Epic 5 - -> Epic 6 - -> Epic 8 - -Epic 0 -> Epic 2 -------------------------> Epic 8 -Epic 0 -> design-system contract -> Epic 7 -> Epic 8 -Epic 6 result contracts -----------------> Epic 7 -``` - -Critical dependency rules: - -- Real Jev work waits for source profiling, rubric approval, budget, and provider authorization. -- Predictor work waits for accepted point-in-time features and labels. -- Risk-based escalation waits for evaluated model outputs; deterministic data-health cases do not. -- Executable artifact generation waits for stable analytical-result and design-system contracts. -- External integrations wait until after local delivery is validated. -- Amazon scale work waits until unit economics and partition recovery are measured on Olist. - -## 9. Scrum execution model - -### Backlog hierarchy - -```text -Release goal - -> Epic - -> Thin vertical story - -> Acceptance checks - -> Durable evidence -``` - -Stories should produce observable behavior rather than horizontal infrastructure alone. Example: - -> As a Marketplace Seller Performance Analyst, I can inspect a seller/category finding with its period, denominator, coverage, and source-linked order/review evidence, so that I can verify whether it merits follow-up before briefing the Seller Operations Manager. - -### Definition of Ready - -A story is ready only when it has: - -- User or operator outcome. -- Acceptance source and observable criteria. -- Owned data/code surface. -- Dependencies and blockers. -- Forbidden outcomes. -- Required evidence. -- External-action authority. - -### Definition of Done - -A story is done only when: - -- Acceptance behavior is demonstrated. -- Automated and required product validation passes. -- Data and model lineage is recorded where applicable. -- Failure and retry behavior is covered for durable workflows. -- Security and permissions are verified for changed surfaces. -- Evidence is non-empty and bound to the exact candidate commit. -- Documentation and operational contracts are updated. -- No unresolved required evidence remains unknown. - -### Sprint review questions - -1. What can a stakeholder do now that they could not do before? -2. Which evidence proves it? -3. What remains simulated or mocked? -4. What failed or is blocked? -5. Did cost, latency, or reviewer burden change? -6. Which assumption should be retired or converted into a deterministic test? - -## 10. Release success measures - -Numeric thresholds should be set after the Olist profile and before evaluating final holdouts. The categories are fixed now. - -### Data integrity - -- Source-to-curated count reconciliation. -- Duplicate and join-inflation defects. -- Feature completeness and historical availability coverage. -- Provenance trace success rate. - -### Semantic interpretation - -- Per-question reviewed quality. -- Ambiguity/abstention handling. -- Portuguese-direct versus translation difference where tested. -- Cost, latency, retry rate, and provider failure rate. - -### Prediction - -- Base-rate, CatBoost, and TabICL comparison. -- PR-AUC/ROC-AUC as appropriate. -- Log loss/Brier score and calibration. -- For the optional prioritization experiment only: precision/recall among seller/category findings at a predeclared reviewer capacity. -- Coverage, warning lead time, latency, memory, and cost. - -### Workflow reliability - -- Durable admission success. -- Recovery without duplicate accepted effects. -- Queue lag, retries, cancellation, and blocked dispositions. -- Reproducible artifact publication. - -### Stakeholder usefulness - -- Artifact values matching authoritative results. -- Time to trace a finding to evidence. -- Seller/category finding usefulness, correction burden, and simulated attention volume in replay. -- Correct handling of stale data, acknowledgment, and resolution. - -### Execution-plane safety - -- Unauthorized file, secret, and network access blocked. -- Resource and lifetime limits enforced. -- Dependency and protected-file policy enforced. -- Preview and cleanup lifecycle reconciled. - -## 11. Risk register and mitigations - -| Risk | Impact | Early mitigation | -|---|---|---| -| Olist timing cannot support target | Predictive slice invalid | Profile timestamps and availability in Sprint 1; permit explicit no-go | -| Jev Portuguese quality is weak | Semantic features unreliable | Review benchmark; compare direct and translation paths; preserve unknown | -| Jev access or spend blocked | Core named capability unavailable | Deterministic adapter/mocks for plumbing; release remains blocked for real Jev claim | -| Aegra recovery differs from claims | Background durability unreliable | Failure injection before model integration; preserve fallback runtime boundary | -| TabICL resource or license issue | Four-way comparison incomplete | Verify exact checkpoint first; keep CatBoost production-capable baseline | -| Predictor adds no value | Predictive prioritization is not defensible | Treat no-predictor outcome as valid; the descriptive Seller Performance review remains the primary deliverable | -| Alert fatigue | Stakeholder rejects capability | Local replay, explicit capacity, hysteresis, cooldown, and measured false-alert burden | -| Generated code escapes intended surface | Host/data security risk | Restricted starter, isolated execution, deny-by-default access, and safety tests | -| Artifact scope delays release | No integrated demo | Require three fixed artifacts; new formats and integrations remain out of scope | -| Cost telemetry incomplete | Scale claim cannot be trusted | Cost/status required at every provider and execution attempt; unknown means blocked | - -## 12. Release review - -v0.1.0 is ready only if the team can demonstrate, from a clean local environment: - -1. Produce the defined review package through the documented human baseline and record its effort, coverage, quality, freshness, and cost. -2. Submit the same bounded Olist replay to the automated workflow and disconnect. -3. Observe durable, restart-safe progress. -4. Inspect accepted source facts and Jev observations. -5. Reproduce a point-in-time feature row and outcome. -6. Compare CatBoost and TabICL evidence without temporal leakage. -7. Open an operations artifact and trace every displayed claim. -8. Compare automated cycle time, effort, coverage, quality, freshness, and total cost with the frozen human baseline. -9. Watch an approved local policy create, update, and clear one attention episode. -10. Review one agent-generated executable artifact inside a sandbox. -11. Re-run duplicate and failure scenarios without duplicate accepted effects. -12. Retrieve complete exact-candidate validation evidence. - -If any required step lacks evidence, the release disposition is `blocked`, not "mostly done." - -## 13. Post-v0.1.0 direction - -Only after the release gate: - -- Select and validate a cloud execution provider by workload. -- Add an authenticated remote preview path. -- Evaluate one Amazon Reviews category for partition scale and unit economics. -- Define an Amazon-specific prediction or descriptive target. -- Add one workplace delivery adapter after permission and revocation design. -- Consider automatic question discovery, rolling-risk models, or TabICL fine-tuning only when measured limitations justify them. +The broader 16-capability acceptance requirements remain intact. Detailed test cases, source definitions, previous implementation notes, and historical plans live in the [capability reference](capability-reference.md). They are supporting material, not additional steps in this demo work queue. diff --git a/migrations/0007_capability_evidence.sql b/migrations/0007_capability_evidence.sql new file mode 100644 index 0000000..e8306bc --- /dev/null +++ b/migrations/0007_capability_evidence.sql @@ -0,0 +1,24 @@ +CREATE SCHEMA IF NOT EXISTS backintel; + +CREATE TABLE IF NOT EXISTS backintel.capability_evidence ( + sha256 text PRIMARY KEY CHECK (length(sha256) = 64), + task_id text NOT NULL, + kind text NOT NULL, + identity text NOT NULL, + available_at bigint NOT NULL CHECK (available_at >= 0), + body jsonb NOT NULL, + parents text[] NOT NULL DEFAULT '{}', + UNIQUE (task_id, kind, identity) +); +CREATE INDEX IF NOT EXISTS capability_evidence_lookup + ON backintel.capability_evidence (task_id, kind, available_at); + +-- Accepted evidence is append-only, including for the application table owner. +CREATE OR REPLACE FUNCTION backintel.reject_evidence_change() RETURNS trigger +LANGUAGE plpgsql AS $$ BEGIN + RAISE EXCEPTION 'Accepted capability evidence is immutable'; +END $$; +DROP TRIGGER IF EXISTS capability_evidence_immutable ON backintel.capability_evidence; +CREATE TRIGGER capability_evidence_immutable BEFORE UPDATE OR DELETE + ON backintel.capability_evidence FOR EACH ROW + EXECUTE FUNCTION backintel.reject_evidence_change(); diff --git a/migrations/0008_capability_background.sql b/migrations/0008_capability_background.sql new file mode 100644 index 0000000..9f87b5e --- /dev/null +++ b/migrations/0008_capability_background.sql @@ -0,0 +1,35 @@ +CREATE TABLE IF NOT EXISTS backintel.capability_triggers ( + trigger_id text PRIMARY KEY, + task_id text NOT NULL, + kind text NOT NULL CHECK (kind IN ('event','schedule','deadline','staleness','on_demand')), + payload jsonb NOT NULL, + due_at timestamptz NOT NULL, + state text NOT NULL DEFAULT 'pending' CHECK (state IN ('pending','fired','cancelled')), + repeat_seconds integer CHECK (repeat_seconds > 0), + remaining integer NOT NULL DEFAULT 1 CHECK (remaining BETWEEN 0 AND 100), + occurrence integer NOT NULL DEFAULT 0 +); +CREATE INDEX IF NOT EXISTS capability_triggers_due ON backintel.capability_triggers (due_at) WHERE state='pending'; + +CREATE TABLE IF NOT EXISTS backintel.capability_jobs ( + job_id text PRIMARY KEY, + task_id text NOT NULL, + trigger_id text REFERENCES backintel.capability_triggers(trigger_id), + payload jsonb NOT NULL, + input_sha256 text NOT NULL, + state text NOT NULL DEFAULT 'queued' CHECK (state IN ('queued','running','retry','completed','failed','cancelled')), + attempts integer NOT NULL DEFAULT 0, + max_attempts integer NOT NULL DEFAULT 2 CHECK (max_attempts BETWEEN 1 AND 5), + due_at timestamptz NOT NULL DEFAULT now(), + lease_until timestamptz, + cancel_requested boolean NOT NULL DEFAULT false, + result_sha256 text REFERENCES backintel.capability_evidence(sha256), + error text, + wall_ms double precision, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + CHECK ((state='completed') = (result_sha256 IS NOT NULL)) +); +CREATE INDEX IF NOT EXISTS capability_jobs_due ON backintel.capability_jobs (due_at,state); +ALTER TABLE backintel.capability_jobs ADD COLUMN IF NOT EXISTS sequence bigserial; +CREATE UNIQUE INDEX IF NOT EXISTS capability_jobs_sequence ON backintel.capability_jobs(sequence); diff --git a/migrations/0009_model_requests.sql b/migrations/0009_model_requests.sql new file mode 100644 index 0000000..dbafe44 --- /dev/null +++ b/migrations/0009_model_requests.sql @@ -0,0 +1,32 @@ +-- Commit request admission before a paid call. An uncertain request is never retried automatically. +CREATE TABLE IF NOT EXISTS backintel.capability_model_requests ( + request_key text PRIMARY KEY, + task_id text NOT NULL, + source_sha256 text NOT NULL REFERENCES backintel.capability_evidence(sha256), + model text NOT NULL, + request jsonb NOT NULL, + state text NOT NULL CHECK (state IN ('admitted','completed','blocked')), + response jsonb, + metadata jsonb, + error text, + created_at timestamptz NOT NULL DEFAULT now(), + finished_at timestamptz, + CHECK ((state='completed') = (response IS NOT NULL)) +); + +CREATE TABLE IF NOT EXISTS backintel.capability_provider_authorizations ( + authorization_id text PRIMARY KEY, + provider text NOT NULL CHECK (provider='openrouter'), + model text NOT NULL, + max_requests integer NOT NULL CHECK (max_requests BETWEEN 1 AND 100), + max_input_characters integer NOT NULL CHECK (max_input_characters BETWEEN 1 AND 5000), + max_measured_usd numeric CHECK (max_measured_usd > 0), + price_ceiling_known boolean NOT NULL DEFAULT false, + approved boolean NOT NULL DEFAULT false, + scope_sha256 text NOT NULL, + scope jsonb NOT NULL, + expires_at timestamptz NOT NULL, + created_at timestamptz NOT NULL DEFAULT now() +); +ALTER TABLE backintel.capability_model_requests ADD COLUMN IF NOT EXISTS authorization_id text + REFERENCES backintel.capability_provider_authorizations(authorization_id); diff --git a/requirements.models.txt b/requirements.models.txt new file mode 100644 index 0000000..016361f --- /dev/null +++ b/requirements.models.txt @@ -0,0 +1,6 @@ +# Real-model development environment. Model checkpoints are approved/downloaded separately. +catboost==1.2.10 +tabicl==2.2.0 +scikit-learn==1.9.1 +torch==2.14.0 +langchain-typesafe==0.0.1a3 diff --git a/runtime/artifacts.py b/runtime/artifacts.py new file mode 100644 index 0000000..525759e --- /dev/null +++ b/runtime/artifacts.py @@ -0,0 +1,212 @@ +"""Audience projections of accepted evidence. Never expose a whole training lineage.""" + +from __future__ import annotations + +import csv +import html +import io +import json + +from runtime.simulation import digest + + +def audience_for(task: dict, actor: str) -> dict: + audience = next((a for a in task["audiences"] if a["id"] == actor), None) + if audience is None: + raise PermissionError("Audience is not configured for this task") + return audience + + +def create_artifacts(store, task_record: dict, result: dict) -> list[dict]: + task, at = task_record["body"], result["body"]["at"] + if result["kind"] != "result" or result["body"]["task"] != task_record["sha256"]: + raise ValueError("Artifacts require a matching accepted result bundle") + predictions = [store.get(sha) for sha in result["body"]["predictions"]] + episodes = [store.get(sha) for sha in result["body"]["episodes"]] + labels = [r for r in store.list("outcome", at) if r["body"]["task"] == task_record["sha256"]] + analysis = store.get(result["body"]["analysis"])["body"] if result["body"].get("analysis") else None + artifacts = [] + for audience in task["audiences"]: + rows, evidence = [], {} + for sha in result["body"]["features"]: + feature = store.get(sha) + entity = feature["body"]["entity"] + if audience["entities"] and entity not in audience["entities"]: + continue + source = store.get(feature["body"]["source"]) + observed = [] + for parent in feature["parents"]: + record = store.get(parent) + if record["kind"] not in ("observation", "correction"): + continue + original = store.get(record["body"]["observation"]) if record["kind"] == "correction" else record + evidence[record["sha256"]] = record + evidence[original["sha256"]] = original + observed.append({"sha256": original["sha256"], "correction": record["sha256"] if record["kind"] == "correction" else None, + "question": original["body"]["question"], "response": record["body"]["response"], + "provider": original["body"]["provider"], "request_id": original["body"].get("request_id")}) + prediction = next((r for r in predictions if r["body"]["feature"] == sha), None) + episode = next((r for r in episodes if r["body"]["entity"] == entity), None) + actual = max((r for r in labels if r["body"]["entity"] == entity), + key=lambda r: (r["body"]["event_at"], r["body"]["revision"]), default=None) + for record in (feature, source, prediction, episode, actual): + if record: + evidence[record["sha256"]] = record + rows.append({"entity": entity, "source": source["sha256"], "source_id": source["body"]["id"], + "source_available_at": source["available_at"], + "stale": at - source["available_at"] >= task["policy"]["stale_after"], + "facts": {k: v for k, v in feature["body"]["values"].items() if k.startswith("structured:")}, + "observations": observed, "expected_observations": len(task["questions"]), + "prediction": prediction, "actual": actual, "attention": episode}) + rows[-1]["analysis"] = next((r for r in analysis["rows"] if r["entity"] == entity), None) if analysis else None + body = {"schema": "backintel-audience-artifact/v1", "task": task_record["sha256"], + "result": result["sha256"], "at": at, "audience": audience, "entity_kind": task["entity"], + "target": task["target"], "rows": rows, "evidence": evidence, + "data": "synthetic", "clock": "synthetic event time", "hypotheses": [], + "limits": ["Synthetic examples do not establish real-world accuracy or business value.", + "Attention uses a fictional demo policy: known interpretation plus scaled forecast; missing interpretation stays unknown.", + "Predictions are estimates; no calibrated prediction interval is available.", + "Latest recorded outcomes and future predictions concern different time windows.", + "Local compute cost is unpriced. Provider cost is separate from prediction value."]} + artifacts.append(store.put("artifact", digest([result["sha256"], audience]), body, at, + [task_record["sha256"], result["sha256"]])) + return artifacts + + +def get_artifact(store, actor: str, sha: str | None = None) -> dict: + if sha: + artifact = store.get(sha) + if artifact["kind"] != "artifact" or artifact["body"]["audience"]["id"] != actor: + raise PermissionError("Artifact is outside this audience") + return artifact + eligible = [r for r in store.list("artifact") if r["body"]["audience"]["id"] == actor] + if not eligible: + raise LookupError("No accepted report is available for this audience") + return max(eligible, key=lambda r: (r["available_at"], r["sha256"])) + + +def visible_evidence(artifact: dict, sha: str) -> dict: + record = artifact["body"]["evidence"].get(sha) + if record is None: + raise PermissionError("Evidence is outside this audience's report") + # Parent hashes preserve traceability; the reader must authorize every dereference. + return record + + +def review_artifact(store, artifact: dict, actor: str, decision: str, reason: str, at: int) -> dict: + task = store.get(artifact["body"]["task"])["body"] + audience = audience_for(task, actor) + if not audience["can_correct"] or audience["view"] != "analysis" or artifact["body"]["audience"]["id"] != actor: + raise PermissionError("This audience cannot review this artifact") + if decision not in ("accepted", "rejected") or not isinstance(reason, str) or not reason.strip() or len(reason) > 1000: + raise ValueError("Review requires a decision and a bounded reason") + body = {"artifact": artifact["sha256"], "actor": actor, "decision": decision, "reason": reason} + with store.connection.transaction(): + store.lock() + return store.find("artifact_review", digest(body)) or store.put("artifact_review", digest(body), body, at, [artifact["sha256"]]) + + +def export_csv(artifact: dict) -> str: + output = io.StringIO() + writer = csv.writer(output) + writer.writerow(["entity", "source_id", "source_available_at", "stale", "latest_actual", "actual_event_at", + "prediction", "prediction_target_at", "predictor_mode", "attention", "source_sha256"]) + for row in artifact["body"]["rows"]: + prediction, actual, attention = (row[k]["body"] if row[k] else {} for k in ("prediction", "actual", "attention")) + values = [row["entity"], row["source_id"], row["source_available_at"], row["stale"], actual.get("value"), + actual.get("event_at"), prediction.get("value"), prediction.get("target_at"), + prediction.get("implementation_mode"), attention.get("condition"), row["source"]] + writer.writerow(["'" + v if isinstance(v, str) and v.lstrip().startswith(("=", "+", "-", "@")) else v for v in values]) + return output.getvalue() + + +STYLE = """body{font:16px/1.6 system-ui,sans-serif;color:#172b4d;background:#f5f7fa;margin:0} +main{max-width:1060px;margin:auto;padding:24px}section{background:white;padding:20px;margin:20px 0;border-radius:8px} +h1{font-size:2rem;line-height:1.2}h2{font-size:1.3rem}a{color:#075985}nav{display:flex;gap:18px;flex-wrap:wrap} +table{border-collapse:collapse;width:100%}th,td{text-align:left;padding:10px;border-bottom:1px solid #ddd;vertical-align:top} +.scroll{overflow-x:auto}.scroll>table{min-width:600px}.scroll-note{font-size:.9rem}pre{white-space:pre-wrap;overflow-wrap:anywhere}code,.wrap{overflow-wrap:anywhere} +summary{cursor:pointer}details{margin:12px 0}a:focus-visible,summary:focus-visible,input:focus-visible,textarea:focus-visible,button:focus-visible,select:focus-visible{outline:3px solid #075985;outline-offset:3px} +label{display:block;margin:10px 0 4px}input,textarea,select,button{font:inherit;max-width:100%;box-sizing:border-box}textarea{width:100%} +button{background:#075985;color:white;border:0;border-radius:4px;padding:9px 14px;cursor:pointer}small{display:block;color:#45566c} +.notice{border-left:4px solid #b45309;padding-left:12px}.tag{font-size:.85rem;font-weight:650}form{margin:12px 0} +@media(max-width:600px){main{padding:14px}section{padding:14px}h1{font-size:1.65rem}} +""" + + +def page(title: str, content: str) -> str: + return ("" + f"{html.escape(title)} · BackIntel

{content}
") + + +def render(artifact: dict, reviews=(), interactive=True, semantic_note=None) -> str: + body, esc = artifact["body"], html.escape + audience, rows = body["audience"], body["rows"] + versions = {r["prediction"]["body"]["implementation_mode"] for r in rows if r["prediction"]} + model_note = "Predictors: " + (", ".join(sorted(versions)) if versions else "no predictions available") + if semantic_note: + model_note += " · "+semantic_note + known = sum(o["response"]["status"] == "known" for r in rows for o in r["observations"]) + expected = sum(r["expected_observations"] for r in rows) + flagged = [r["entity"] for r in rows if r["attention"] and r["attention"]["body"]["condition"] != "cleared"] + headline = "Review: " + ", ".join(flagged) if flagged else "No active attention condition in this report." + navigation = (f"" if interactive else + "

Read-only export. Use the local viewer to correct observations or record a review.

") + content = (f"

SYNTHETIC DATA · {esc(model_note)}

{esc(audience['view'].title())}: {esc(body['entity_kind'])}

" + f"

Audience: {esc(audience['id'])}. Report at t={body['at']} ({esc(body['clock'])}).

" + f"{navigation}" + f"

What needs attention

{esc(headline)}

{len(rows)} visible entities; {known}/{expected} observations known; " + f"{sum(r['stale'] for r in rows)} inputs stale.

Predictions estimate a future outcome. Latest recorded outcomes describe earlier events.

") + if not rows: + content += "

No visible records

There are no accepted records within this audience's scope yet.

" + else: + ranked = sorted((r for r in rows if r["prediction"]), key=lambda r: r["prediction"]["body"]["value"], reverse=True) + content += "

Facts and predictions

" + if ranked: + content += f"

Highest predicted outcome among visible entities: {esc(ranked[0]['entity'])}. This ranking does not establish a cause.

" + content += "

On a narrow screen, swipe the table or focus it and use the arrow keys to see every column.

" + for row in rows: + prediction, actual, attention = (row[k]["body"] if row[k] else {} for k in ("prediction", "actual", "attention")) + actual_text = f"{actual['value']:.3g} at t={actual['event_at']}" if actual else "Not yet observed" + prediction_text = f"{prediction['value']:.3g} {body['target']['unit']} for t={prediction['target_at']}" if prediction else "Unavailable" + finding = row.get("analysis") + risk = "" + if finding: + show = lambda value: "unknown" if value is None else f"{value:.3g}" + risk = f"Demo risk: {show(finding['semantic_risk'])} from text; {show(finding['prediction_risk'])} from forecast." + facts = ", ".join(f"{k.removeprefix('structured:')}: {v}" for k, v in row["facts"].items()) + content += (f"") + content += "
Visible entities and their distinct outcome windows
EntityCurrent factsLatest recorded outcomeFuture predictionAttentionInput freshness
{esc(row['entity'])}{esc(facts)}{esc(actual_text)}{esc(prediction_text)}" + f"{esc(prediction.get('implementation_mode', 'unknown'))}{esc(attention.get('condition','none'))}" + f"{esc(attention.get('response',''))}{risk}{'Stale' if row['stale'] else 'Current'} · t={row['source_available_at']}

Source evidence and observations

" + for row in rows: + source = visible_evidence(artifact, row["source"]) + source_url = f"/evidence/{row['source']}?artifact={artifact['sha256']}" if interactive else f"Evidence/{row['source']}.json" + content += (f"
{esc(row['entity'])} · {esc(row['source_id'])}

" + f"Open source record · SHA-256: {row['source']}

" + f"
{esc(json.dumps(source['body'], indent=2))}
") + for observation in row["observations"]: + answer = observation["response"] + content += (f"

{esc(observation['question']['prompt'])}

{esc(answer['status'])}: {esc(str(answer['value']))}. " + f"{esc(answer['reason'])}

Interpretation: {esc(observation['provider']['implementation_mode'])}" + f"{' · human correction' if observation['correction'] else ''}
{esc(json.dumps(answer['distribution']))}
") + if interactive and audience["can_correct"]: + selected = str(answer["value"]).lower() if answer["status"] == "known" else "unknown" + value_input = (f"" + if observation["question"]["type"] == "boolean" else f"") + content += (f"
" + f"" + f"{value_input}" + f"" + "
") + content += "
" + content += "
" + content += "

Uncertainty and limits

No causal hypotheses have been established.

    " + "".join(f"
  • {esc(v)}
  • " for v in body["limits"]) + "
" + content += "

Report review

" + relevant = [r for r in reviews if r["body"]["artifact"] == artifact["sha256"]] + content += "".join(f"

{esc(r['body']['decision'])}: {esc(r['body']['reason'])}

" for r in relevant) or "

Not reviewed.

" + if interactive and audience["can_correct"] and audience["view"] == "analysis": + content += (f"
" + "" + "
") + content += f"

Report version: {artifact['sha256']}

" + return page("Audience report", content) diff --git a/runtime/attention.py b/runtime/attention.py new file mode 100644 index 0000000..95018bc --- /dev/null +++ b/runtime/attention.py @@ -0,0 +1,68 @@ +"""Durable attention episodes and a local-only, deduplicated delivery ledger.""" +from runtime.simulation import digest + + +def latest_episode(store, entity: str) -> dict | None: + history = [r for r in store.list("episode") if r["body"]["entity"] == entity] + return max(history,key=lambda r:r["body"]["sequence"],default=None) + + +def attend(store, task_record: dict, entity: str, reason: dict, at: int, value: float | None = None, + action="observe", actor="simulated-operator") -> dict: + if action not in ("observe","acknowledge","investigate","resolve","staleness","deadline"): + raise ValueError("Unsupported attention action") + policy = task_record["body"]["policy"] + key = digest([task_record["sha256"],entity,reason["sha256"],action]) + with store.connection.transaction(): + store.lock() + previous = store.find("episode",key) + if previous: + return previous + latest = latest_episode(store,entity) + old = latest["body"] if latest else {} + if latest and at < latest["available_at"]: + raise ValueError("Cannot backdate attention actions") + state = {"entity":entity,"episode_id":old.get("episode_id"),"condition":old.get("condition","cleared"), + "response":old.get("response","open"),"last_observed_at":old.get("last_observed_at"), + "last_delivery_at":old.get("last_delivery_at"),"opened_at":old.get("opened_at"), + "sequence":old.get("sequence",0)+1,"value":old.get("value"),"action":action,"actor":actor, + "policy_sha256":digest(policy),"reason":reason["sha256"],"task":task_record["sha256"]} + notify = None + if action == "observe": + state.update(last_observed_at=at,value=value) + if value is None: + state["condition"] = "unknown" + elif value >= policy["entry"]: + state["condition"] = "active" + if old.get("condition","cleared") == "cleared" or not state["episode_id"]: + state.update(episode_id=digest([entity,reason["sha256"]]),response="open",opened_at=at) + notify = "opened" + elif old.get("condition") == "unknown" and (state["last_delivery_at"] is None or at-state["last_delivery_at"] >= policy["cooldown"]): + notify = "updated" + elif value <= policy["clear"]: + state["condition"] = "cleared" + # Between thresholds retains the previous condition (hysteresis). + elif action in ("acknowledge","investigate","resolve"): + if state["condition"] == "cleared" or not state["episode_id"]: + raise ValueError("Response action requires an open episode") + if actor != "simulated-operator": + audience = next((a for a in task_record["body"]["audiences"] if a["id"] == actor),None) + if not audience or not audience["can_correct"] or (audience["entities"] and entity not in audience["entities"]): + raise PermissionError("Actor cannot respond to this episode") + state["response"] = {"acknowledge":"acknowledged","investigate":"investigating","resolve":"resolved"}[action] + elif action == "staleness": + if state["last_observed_at"] is not None and at-state["last_observed_at"] >= policy["stale_after"]: + state["condition"] = "unknown" + elif action == "deadline" and state["episode_id"] and state["condition"] != "cleared" and state["response"] == "open": + if at-state["opened_at"] >= policy["response_deadline"] and (state["last_delivery_at"] is None or at-state["last_delivery_at"] >= policy["cooldown"]): + notify = "response_overdue" + if notify: + state["last_delivery_at"] = at + parents = [task_record["sha256"],reason["sha256"]] + ([latest["sha256"]] if latest else []) + saved = store.put("episode",key,state,at,parents) + if notify: + delivery_key = digest([state["episode_id"],notify, reason["sha256"] if notify == "updated" else None]) + if not store.find("delivery",delivery_key): + store.put("delivery",delivery_key,{"episode":saved["sha256"],"episode_id":state["episode_id"], + "entity":entity,"reason":notify,"channel":"local_inbox","status":"delivered", "external_messages":0},at,[saved["sha256"]]) + return saved diff --git a/runtime/audience_server.py b/runtime/audience_server.py new file mode 100644 index 0000000..3f21d47 --- /dev/null +++ b/runtime/audience_server.py @@ -0,0 +1,217 @@ +"""Loopback-only stakeholder access. Tokens bind a task version and audience.""" + +from __future__ import annotations + +import hashlib +from http.cookies import SimpleCookie +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +import json +import secrets +import time +from urllib.parse import parse_qs, urlsplit + +import psycopg + +from runtime.artifacts import audience_for, export_csv, get_artifact, page, render, review_artifact, visible_evidence +from runtime.evidence import Evidence +from runtime.observations import correct_observation +from runtime.simulation import digest, encoded + + +def issue_grants(connection, task_ids: list[str], lifetime=3600) -> list[dict]: + grants = [] + for task_id in task_ids: + store = Evidence(connection, task_id) + tasks = store.list("task") + if not tasks: + raise ValueError("Task has no registered contract") + task = max(tasks, key=lambda r: r["available_at"]) + for audience in task["body"]["audiences"]: + grants.append({"task_id": task_id, "task_sha256": task["sha256"], "audience": audience["id"], + "token": secrets.token_urlsafe(32), "expires_at": time.time() + lifetime}) + return grants + + +class AudienceServer(ThreadingHTTPServer): + daemon_threads = True + + def __init__(self, database: str, grants: list[dict], port=2028): + self.database = database + self.grants = {hashlib.sha256(g["token"].encode()).hexdigest(): {k: v for k, v in g.items() if k != "token"} for g in grants} + super().__init__(("127.0.0.1", port), AudienceHandler) + self.origin = f"http://127.0.0.1:{self.server_address[1]}" + + +class AudienceHandler(BaseHTTPRequestHandler): + def log_message(self, *_): + # Do not persist authorization values, URL input or source data in access logs. + pass + + def setup(self): + super().setup() + self.connection.settimeout(5) + + def reply(self, status: int, body: bytes, content_type="text/html; charset=utf-8", headers=()): + self.send_response(status) + self.send_header("Content-Type", content_type) + self.send_header("Content-Length", str(len(body))) + self.send_header("Cache-Control", "no-store") + self.send_header("X-Content-Type-Options", "nosniff") + self.send_header("Referrer-Policy", "same-origin") + self.send_header("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'") + for key, value in headers: + self.send_header(key, value) + self.end_headers() + self.wfile.write(body) + + def principal(self, token=None): + if token is None: + authorization = self.headers.get("Authorization", "") + token = authorization[7:] if authorization.startswith("Bearer ") else None + if token is None: + cookies = SimpleCookie() + cookies.load(self.headers.get("Cookie", "")) + token = cookies["backintel_audience"].value if "backintel_audience" in cookies else "" + grant = self.server.grants.get(hashlib.sha256(token.encode()).hexdigest()) + if grant is None or grant["expires_at"] <= time.time(): + raise PermissionError("A valid, unexpired audience access token is required") + return grant + + def guard_host(self): + if self.headers.get("Host") != self.server.origin.removeprefix("http://"): + raise PermissionError("Only the configured loopback origin is allowed") + + def scoped_artifact(self, store, grant, sha=None): + artifact = get_artifact(store, grant["audience"], sha) + if artifact["body"]["task"] != grant["task_sha256"]: + raise PermissionError("Access token belongs to another task version") + return artifact + + def do_GET(self): + try: + self.guard_host() + path = urlsplit(self.path).path + query = parse_qs(urlsplit(self.path).query, max_num_fields=1) + if any(key != "artifact" or len(values) != 1 for key, values in query.items()): + raise ValueError("Unsupported report query") + if path == "/login": + content = ("

Open your report

Use the local access token issued for your audience. It grants access to one task and audience.

" + "
" + "

") + return self.reply(200, page("Sign in", content).encode()) + grant = self.principal() + with psycopg.connect(self.server.database, autocommit=True) as connection: + store = Evidence(connection, grant["task_id"]) + artifact = self.scoped_artifact(store, grant, query.get("artifact", [None])[0]) + if path == "/": + document = render(artifact, store.list("artifact_review")) + candidates = [r for r in store.list("artifact_candidate") if r["body"]["artifact"] == artifact["sha256"]] + if candidates: + links = "

Generated view candidates

" + "".join(f"

Inspect candidate {i + 1}

" for i, r in enumerate(candidates)) + "
" + document = document.replace("", links + "") + return self.reply(200, document.encode()) + if path == "/artifact.json": + return self.reply(200, encoded(artifact), "application/json") + if path == "/export.csv": + return self.reply(200, export_csv(artifact).encode(), "text/csv; charset=utf-8", + [("Content-Disposition", 'attachment; filename="backintel-report.csv"')]) + if path.startswith("/evidence/"): + return self.reply(200, encoded(visible_evidence(artifact, path.removeprefix("/evidence/"))), "application/json") + if path.startswith("/versions/"): + version = self.scoped_artifact(store, grant, path.removeprefix("/versions/")) + return self.reply(200, render(version, store.list("artifact_review")).encode()) + if path.startswith("/candidate/"): + from runtime.generated_artifacts import render_candidate, scoped_candidate + record = store.get(path.removeprefix("/candidate/")) + if record["kind"] != "artifact_candidate": + raise PermissionError("Not a generated view candidate") + artifact = self.scoped_artifact(store, grant, record["body"]["artifact"]) + candidate = scoped_candidate(store, artifact, path.removeprefix("/candidate/")) + return self.reply(200, render_candidate(candidate, store.list("artifact_candidate_review")).encode()) + raise LookupError("Page not found") + except PermissionError as exc: + self.error_page(403, str(exc)) + except LookupError as exc: + self.error_page(404, str(exc)) + except ValueError: + self.error_page(400, "Invalid report request") + except psycopg.Error: + self.error_page(503, "Report storage is temporarily unavailable. Retry later.") + + def do_POST(self): + try: + self.guard_host() + origin = self.headers.get("Origin") + if origin and origin != self.server.origin: + raise PermissionError("Cross-origin changes are not allowed") + if not origin and not self.headers.get("Authorization", "").startswith("Bearer "): + raise PermissionError("Browser changes require the local report origin") + size = int(self.headers.get("Content-Length", "0")) + if not 0 < size <= 16384 or self.headers.get_content_type() != "application/x-www-form-urlencoded": + raise ValueError("Invalid form body") + fields = parse_qs(self.rfile.read(size).decode(), keep_blank_values=True, max_num_fields=10) + if any(len(v) != 1 for v in fields.values()): + raise ValueError("Duplicate fields are not allowed") + form = {k: v[0] for k, v in fields.items()} + path = urlsplit(self.path).path + if path == "/session": + if set(form) != {"token"}: + raise ValueError("Invalid login") + self.principal(form["token"]) + return self.reply(303, b"", headers=[("Location", "/"), ("Set-Cookie", f"backintel_audience={form['token']}; Path=/; HttpOnly; SameSite=Strict; Max-Age=3600")]) + grant = self.principal() + with psycopg.connect(self.server.database, autocommit=True) as connection: + store = Evidence(connection, grant["task_id"]) + with connection.transaction(): + store.lock() + artifact = self.scoped_artifact(store, grant, form.get("artifact")) + latest = self.scoped_artifact(store, grant) + if artifact["sha256"] != latest["sha256"]: + raise ValueError("Report changed. Reload before making a change.") + at = latest["available_at"] + 1 + if path == "/review" and set(form) == {"artifact", "decision", "reason"}: + review_artifact(store, artifact, grant["audience"], form["decision"], form["reason"], at) + elif path == "/candidate-review" and set(form) == {"artifact", "candidate", "decision", "reason"}: + from runtime.generated_artifacts import review_candidate, scoped_candidate + candidate = scoped_candidate(store, artifact, form["candidate"]) + review_candidate(store, candidate, grant["audience"], form["decision"], form["reason"], at) + elif path == "/correct" and set(form) == {"artifact", "observation", "supersedes", "value", "reason"}: + task_record = store.get(grant["task_sha256"]) + audience = audience_for(task_record["body"], grant["audience"]) + if not audience["can_correct"]: + raise PermissionError("This audience cannot correct observations") + observation = visible_evidence(artifact, form["observation"]) + if observation["kind"] != "observation": + raise ValueError("Choose an original observation") + value = form["value"] + if value == "unknown": + response = {"status": "unknown", "value": None, "distribution": None, "reason": form["reason"]} + elif observation["body"]["question"]["type"] == "boolean": + if value not in ("true", "false"): + raise ValueError("Boolean correction requires true, false or unknown") + response = {"status": "known", "value": value == "true", "distribution": {"true": int(value == "true"), "false": int(value == "false")}, "reason": form["reason"]} + else: + response = {"status": "known", "value": float(value), "distribution": None, "reason": form["reason"]} + correction = correct_observation(store, observation["sha256"], response, grant["audience"], form["reason"], at, form["supersedes"] or None) + from runtime.capability_pipeline import refresh + from runtime.prediction import invalidate_predictions + invalidate_predictions(store, observation["body"]["source"], correction, at) + event = store.put("event", digest(["audience-correction", correction["sha256"]]), + {"operation": "audience_correction", "actor": grant["audience"]}, at, [correction["sha256"]]) + refresh(store, task_record, event, at, observe=True, + entities=[store.get(observation["body"]["source"])["body"]["entity"]]) + else: + raise ValueError("Invalid review or correction request") + return self.reply(303, b"", headers=[("Location", "/")]) + except PermissionError as exc: + self.error_page(403, str(exc)) + except (ValueError, KeyError, UnicodeError) as exc: + self.error_page(400, str(exc)) + except LookupError as exc: + self.error_page(404, str(exc)) + except psycopg.Error: + self.error_page(503, "Report storage is temporarily unavailable. Retry later.") + + def error_page(self, status, message): + import html + self.reply(status, page("Report unavailable", f"

Report unavailable

{html.escape(message)}

Sign in · Return to report

").encode()) diff --git a/runtime/capability_graph.py b/runtime/capability_graph.py new file mode 100644 index 0000000..7784c46 --- /dev/null +++ b/runtime/capability_graph.py @@ -0,0 +1,76 @@ +"""Generic Aegra/LangGraph entry points for durable capability jobs and native cron ticks.""" +import asyncio +import re +from typing import TypedDict + +import psycopg +from langgraph.graph import END, START, StateGraph + +from runtime.evidence import Evidence +from runtime.jobs import cancel, dispatch, enqueue, execute +from runtime.ledger import dsn +from runtime.simulation import load_scenario + + +class CapabilityState(TypedDict, total=False): + operation: str + scenario: str + request_id: str + job_id: str + task_ids: list[str] + demo_id: str + source_sha256: str + plan_sha256: str + provider_authorization_id: str + result: dict + + +async def process(state: CapabilityState) -> dict: + if state.get("operation") == "dispatch": + task_ids = state.get("task_ids") + if task_ids is not None and (not isinstance(task_ids,list) or not 1 <= len(task_ids) <= 2 or + any(not isinstance(t,str) or not re.fullmatch(r"(?:support|equipment)-real-[a-z][a-z0-9-]{0,24}",t) for t in task_ids)): + raise ValueError("Scoped real dispatch requires one or two exact real task identities") + return {"result":await asyncio.to_thread(dispatch,task_ids)} + if state.get("operation") == "cancel": + with psycopg.connect(dsn(),autocommit=True) as connection: + return {"result":{"state":cancel(connection,state["job_id"])}} + operation = state.get("operation") + if operation not in ("bootstrap", "real_prepare", "real_interpret", "real_compare", "real_prepare_followups", "real_start_followups", "real_start"): + raise ValueError("Unsupported generic pipeline operation") + config = load_scenario(state["scenario"]) + demo_id = state.get("demo_id","development-v1") + if not isinstance(demo_id,str) or not re.fullmatch(r"[a-z][a-z0-9-]{0,24}",demo_id): + raise ValueError("Invalid demonstration identity") + request_id = state["request_id"] + if not isinstance(request_id,str) or not 1 <= len(request_id) <= 100: + raise ValueError("Bounded request identity required") + with psycopg.connect(dsn(),autocommit=True) as connection: + payload = {"scenario": config["id"], "operation": operation} + if operation == "real_interpret": + if not re.fullmatch(r"[a-f0-9]{64}", state.get("source_sha256", "")) or not isinstance(state.get("provider_authorization_id"), str): + raise ValueError("Interpretation requires a source hash and named authorization") + payload.update(source_sha256=state["source_sha256"], provider_authorization_id=state["provider_authorization_id"]) + if state.get("plan_sha256"): + if not re.fullmatch(r"[a-f0-9]{64}",state["plan_sha256"]): + raise ValueError("Interpretation plan requires an exact evidence hash") + payload["plan_sha256"] = state["plan_sha256"] + if operation in ("real_start_followups","real_start"): + if not isinstance(state.get("provider_authorization_id"),str) or not 1 <= len(state["provider_authorization_id"]) <= 128: + raise ValueError("Follow-up execution requires a named authorization") + payload["provider_authorization_id"] = state["provider_authorization_id"] + prefix = "-real-" if operation.startswith("real_") else "-" + job_id = enqueue(Evidence(connection,f"{config['id']}{prefix}{demo_id}"),payload,request_id) + try: + return {"job_id":job_id,"result":await asyncio.to_thread(execute,job_id)} + except asyncio.CancelledError: + with psycopg.connect(dsn(),autocommit=True) as connection: + cancel(connection,job_id) + raise + + +builder = StateGraph(CapabilityState) +builder.add_node("durable_capabilities",process) +builder.add_edge(START,"durable_capabilities") +builder.add_edge("durable_capabilities",END) +graph = builder.compile() diff --git a/runtime/capability_pipeline.py b/runtime/capability_pipeline.py new file mode 100644 index 0000000..680ef7f --- /dev/null +++ b/runtime/capability_pipeline.py @@ -0,0 +1,169 @@ +"""One reusable synthetic pipeline for service operations and equipment monitoring.""" +from __future__ import annotations + +import time + +from runtime.attention import attend, latest_episode +from runtime.artifacts import create_artifacts +from runtime.contracts import admit_source, current_sources, register_task +from runtime.jobs import schedule +from runtime.observations import extract +from runtime.prediction import (cases, compare, evaluate, features, invalidate_predictions, outcome, + prepare, registry, score, transition, update_plan) +from runtime.simulation import digest, load_scenario +from runtime.synthetic import history + + +def bootstrap(store, task_record: dict, name: str) -> dict: + _, rows, labels = history(name) + snapshots = [] + for row,label in zip(rows,labels): + at = label["event_at"] + admit_source(store,task_record,{"format":"json","data":[row]},at) + source = next(r for r in current_sources(store,at,task_record["sha256"]) if r["body"]["id"] == label["source_id"]) + extract(store,task_record,source,at) + snapshots.extend(r for r in features(store,task_record,at) if r["body"]["source_id"] == label["source_id"]) + outcome(store,task_record,label) + comparison = compare(store,task_record,snapshots,71) + selected = comparison["body"]["selected"] + transition(store,task_record,selected,"approve",71,"initial-approval") + transition(store,task_record,selected,"activate",71,"initial-activation") + event = store.put("event","bootstrap",{"operation":"bootstrap","scenario":name},71,[comparison["sha256"]]) + result = refresh(store,task_record,event,71,observe=True) + seed_followups(store,task_record,name) + return result + + +def followup_events(name: str) -> list: + task, rows, labels = history(name,31) + changed = dict(rows[24],revision=2,arrived_at=75) + changed[task["fields"]["content"]] = "Login failed" if task["questions"][0]["type"] == "boolean" else "9" + recovered = dict(rows[30]) + recovered[task["fields"]["content"]] = "Service working" if task["questions"][0]["type"] == "boolean" else "0" + entity = rows[24][task["fields"]["entity"]] + return [ + (2,"event",{"operation":"arrival","at":72,"row":rows[24],"fail_once":True}), + (4,"event",{"operation":"outcome","at":74,"label":labels[24]}), + (6,"event",{"operation":"arrival","at":75,"row":changed}), + (8,"deadline",{"operation":"deadline","at":76}), + (10,"on_demand",{"operation":"acknowledge","at":77,"entity":entity}), + (12,"on_demand",{"operation":"investigate","at":78,"entity":entity}), + (14,"staleness",{"operation":"staleness","at":84}), + (16,"schedule",{"operation":"model_update_prepare","at":85}), + (18,"on_demand",{"operation":"resolve","at":86,"entity":entity}), + (20,"event",{"operation":"arrival","at":90,"row":recovered}), + (22,"event",{"operation":"outcome","at":92,"label":labels[30]}), + (24,"schedule",{"operation":"model_update_complete","at":93}), + (26,"schedule",{"operation":"artifact_refresh","at":94}), + ] + +def seed_followups(store, task_record: dict, name: str) -> None: + now = time.time() + for offset,kind,payload in followup_events(name): + schedule(store,kind,{**payload,"scenario":name},now+offset,f"demo-{payload['operation']}-{payload['at']}") + + +def analyze(store, task_record, snapshots, predictions, event, at): + if len(snapshots) != len(predictions) or any(p["body"]["feature"] != f["sha256"] for f, p in zip(snapshots, predictions)): + raise ValueError("Analysis requires one matching prediction for each feature snapshot") + policy = task_record["body"]["policy"] + rows = [] + for feature, prediction in zip(snapshots, predictions): + semantic = feature["body"]["values"].get("semantic:" + policy["signal_question"]) + semantic_risk = min(1, max(0, semantic / policy["signal_scale"])) if semantic is not None else None + prediction_risk = min(1, max(0, prediction["body"]["value"] / policy["prediction_scale"])) if "prediction_scale" in policy else None + # Fictional policy for synthetic demonstrations. Missing interpretation stays unknown. + attention_value = max(semantic_risk, prediction_risk or 0) if semantic_risk is not None else None + rows.append({"entity": feature["body"]["entity"], "feature": feature["sha256"], "prediction": prediction["sha256"], + "semantic_risk": semantic_risk, "prediction_risk": prediction_risk, "attention_value": attention_value}) + body = {"task": task_record["sha256"], "event": event["sha256"], "at": at, "rows": rows, + "policy_sha256": digest(policy), "rule": "maximum_known_interpretation_and_scaled_forecast_else_unknown", + "synthetic_policy": True, "prediction_scale": policy.get("prediction_scale")} + return store.put("analysis", digest(body), body, at, [task_record["sha256"], event["sha256"], *[r["sha256"] for r in snapshots + predictions]]) + + +def refresh(store, task_record: dict, event: dict, at: int, observe=False, entities=None) -> dict: + snapshots = features(store,task_record,at) + active = registry(store,at)["active"] + predictions = [score(store,task_record,f,at) for f in snapshots] + analysis = analyze(store, task_record, snapshots, predictions, event, at) + episodes = [] + for feature, finding in zip(snapshots, analysis["body"]["rows"]): + entity = feature["body"]["entity"] + if observe and (entities is None or entity in entities): + episodes.append(attend(store,task_record,entity,analysis,at,finding["attention_value"])) + else: + latest = latest_episode(store,entity) + if latest: + episodes.append(latest) + real = task_record["body"]["observation_provider"]["implementation_mode"] == "real" + from runtime.real_semantics import usage_for + usage = usage_for(store) if real else {"provider_calls":0,"provider_usd":0,"local_compute_usd":None} + mode = "synthetic_sources_real_models" if real else "synthetic_simulation" + if usage.get("provider_fixture_requests"): + mode = "synthetic_sources_real_predictors_fixture_semantics" + body = {"task":task_record["sha256"],"event":event["sha256"],"analysis":analysis["sha256"],"at":at,"model":active, + "features":[r["sha256"] for r in snapshots],"predictions":[r["sha256"] for r in predictions], + "episodes":[r["sha256"] for r in episodes],"mode":mode, + **usage} + result = store.put("result",digest(body),body,at,[task_record["sha256"],event["sha256"],analysis["sha256"],*[r["sha256"] for r in snapshots+predictions+episodes]]) + create_artifacts(store, task_record, result) + return result + + +def handle(store, payload: dict, task_record=None) -> dict: + name, operation = payload["scenario"], payload["operation"] + if operation.startswith("real_"): + from runtime.real_pipeline import handle as handle_real + return handle_real(store, payload) + if task_record is None: + task = load_scenario(name)["task"] + task["id"] = store.task_id + task_record = register_task(store,task) + if operation == "bootstrap": + return bootstrap(store,task_record,name) + at = payload["at"] + event = store.put("event",digest(payload),payload,at,[task_record["sha256"]]) + if operation == "arrival": + row = payload["row"] + old = next((r for r in current_sources(store,at,task_record["sha256"]) + if r["body"]["id"] == row[task_record["body"]["fields"]["id"]]),None) + receipt = admit_source(store,task_record,{"format":"json","data":[row]},at) + dispositions = receipt["body"]["dispositions"] + if any(r["status"] == "quarantined" for r in dispositions): + return receipt + source = store.get(dispositions[0]["source"]) + extract(store,task_record,source,at) + if old and old["sha256"] != source["sha256"]: + invalidate_predictions(store,old["sha256"],receipt,at) + result = refresh(store,task_record,event,at,observe=True,entities=[source["body"]["entity"]]) + update_plan(store,store.get(result["body"]["model"]),[store.get(s) for s in result["body"]["features"]],receipt,at) + return result + if operation == "outcome": + return outcome(store,task_record,payload["label"]) + if operation in ("acknowledge","investigate","resolve","deadline","staleness"): + entities = [payload["entity"]] if "entity" in payload else sorted({r["body"]["entity"] for r in store.list("episode")}) + for entity in entities: + attend(store,task_record,entity,event,at,action=operation) + return refresh(store,task_record,event,at) + if operation == "model_update_prepare": + active = store.get(registry(store,at)["active"]) + fresh = features(store,task_record,at) + update_plan(store,active,fresh,event,at,prepare_requested=True) + training = cases(store,task_record,store.list("feature",at),at) + current = {r["sha256"] for r in current_sources(store,at,task_record["sha256"])} + training = [r for r in training if r["feature"]["body"]["cutoff"] < at and r["feature"]["body"]["source"] in current] + mode = "real" if task_record["body"]["observation_provider"]["implementation_mode"] == "real" else "simulated" + prepared = prepare(store,task_record,training,active["body"]["route"],active["body"]["feature_set"],at,implementation_mode=mode) + return store.put("model_update","bounded-update",{"model":prepared["sha256"]},at,[event["sha256"],prepared["sha256"]]) + if operation == "model_update_complete": + model = store.get(store.find("model_update","bounded-update")["body"]["model"]) + holdout = [r for r in cases(store,task_record,store.list("feature",at),at) + if r["feature"]["body"]["cutoff"] >= model["body"]["prepared_at"]] + evaluate(store,model,holdout,at) + transition(store,task_record,model["sha256"],"approve",at,"update-approval") + transition(store,task_record,model["sha256"],"activate",at,"update-activation") + return refresh(store,task_record,event,at,observe=True) + if operation == "artifact_refresh": + return refresh(store,task_record,event,at) + raise ValueError("Unsupported capability operation") diff --git a/runtime/contracts.py b/runtime/contracts.py new file mode 100644 index 0000000..d463b08 --- /dev/null +++ b/runtime/contracts.py @@ -0,0 +1,202 @@ +"""Portable synthetic task contracts and revision-aware CSV/JSON/text admission.""" +from __future__ import annotations + +import csv +import io +import json +import math +import re + +from runtime.simulation import digest, encoded + + +def number(value) -> bool: + return type(value) in (int, float) and math.isfinite(value) + + +def validate_task(task: dict) -> dict: + required = {"schema", "id", "entity", "fields", "measures", "questions", "target", "audiences", "policy", "observation_provider"} + if set(task) != required or task["schema"] != "backintel-task/v1": + raise ValueError("Invalid task schema or fields") + if not isinstance(task["id"], str) or not re.fullmatch(r"[a-z][a-z0-9-]{0,40}", task["id"]): + raise ValueError("Invalid task ID") + if not isinstance(task["entity"], str) or not task["entity"].strip(): + raise ValueError("Task entity is required") + provider = task["observation_provider"] + if set(provider) != {"name","version","implementation_mode"} or provider["implementation_mode"] not in ("simulated","real") or any( + not isinstance(provider[k],str) or not provider[k] for k in ("name","version")): + raise ValueError("Task requires an explicit observation provider identity and execution mode") + fields = task["fields"] + if set(fields) != {"id", "entity", "event_at", "available_at", "revision", "content"}: + raise ValueError("Source mapping requires identity, event/availability times, revision and content") + if any(not isinstance(v, str) or not v for v in fields.values()) or len(set(fields.values())) != len(fields): + raise ValueError("Source mapping must use distinct fields") + measures = task["measures"] + if not measures or any(set(m) != {"id", "field", "unit", "nullable"} or type(m["nullable"]) is not bool + or any(not isinstance(m[k], str) or not m[k] for k in ("id", "field", "unit")) for m in measures): + raise ValueError("Measures require IDs, source fields, units and null policies") + if len({m["id"] for m in measures}) != len(measures): + raise ValueError("Duplicate measure ID") + questions = task["questions"] + if not questions or len({q["id"] for q in questions}) != len(questions): + raise ValueError("Questions require unique IDs") + for question in questions: + if set(question) != {"id", "prompt", "type", "rule"} or question["type"] not in ("boolean", "number"): + raise ValueError("Invalid typed question") + if any(not isinstance(question[k], str) or not question[k].strip() for k in ("id", "prompt")): + raise ValueError("Question requires ID and prompt") + rule = question["rule"] + if rule.get("kind") == "keywords": + if question["type"] != "boolean" or set(rule) != {"kind", "terms"} or not rule["terms"] or any( + not isinstance(term, str) or not term.strip() for term in rule["terms"]): + raise ValueError("Keyword rule requires boolean type and terms") + elif rule.get("kind") == "number": + if question["type"] != "number" or set(rule) != {"kind"}: + raise ValueError("Number rule requires number type") + else: + raise ValueError("Unsupported simulated extraction rule") + target = task["target"] + if set(target) != {"id", "kind", "unit", "horizon", "minimum_train", "holdout_fraction"} or target["kind"] not in ("classification", "regression"): + raise ValueError("Invalid target contract") + if any(not isinstance(target[k], str) or not target[k] for k in ("id", "unit")) or type(target["horizon"]) is not int or target["horizon"] < 1: + raise ValueError("Target requires ID, unit and positive horizon") + if type(target["minimum_train"]) is not int or target["minimum_train"] < 2 or not number(target["holdout_fraction"]) or not 0 < target["holdout_fraction"] < 1: + raise ValueError("Target requires bounded chronological split") + if not task["audiences"] or any(set(a) != {"id", "view", "entities", "can_correct"} or a["view"] not in ("briefing", "analysis", "export") + or not isinstance(a["id"], str) or not re.fullmatch(r"[a-z][a-z0-9-]{0,40}", a["id"]) + or type(a["can_correct"]) is not bool or not isinstance(a["entities"], list) + or any(not isinstance(e, str) or not e for e in a["entities"]) for a in task["audiences"]): + raise ValueError("Invalid audience scope") + if len({a["id"] for a in task["audiences"]}) != len(task["audiences"]): + raise ValueError("Duplicate audience ID") + policy = task["policy"] + required_policy = {"max_rows", "max_attempts", "max_provider_calls", "entry", "clear", "cooldown", "stale_after", "response_deadline", "signal_question", "signal_scale"} + if set(policy) not in (required_policy, required_policy | {"prediction_scale"}): + raise ValueError("Invalid policy fields") + if "prediction_scale" in policy and (not number(policy["prediction_scale"]) or policy["prediction_scale"] <= 0): + raise ValueError("Prediction attention scale must be positive") + if any(type(policy[k]) is not int or policy[k] < 1 for k in ("max_rows", "max_attempts", "max_provider_calls", "cooldown", "stale_after", "response_deadline")): + raise ValueError("Policy limits must be positive integers") + if policy["max_rows"] > 1000 or policy["max_attempts"] > 5 or policy["max_provider_calls"] > 5000: + raise ValueError("Synthetic policy exceeds execution bounds") + if not number(policy["entry"]) or not number(policy["clear"]) or not 0 <= policy["clear"] < policy["entry"] <= 1: + raise ValueError("Attention requires distinct entry and clear thresholds") + if policy["signal_question"] not in {q["id"] for q in questions} or not number(policy["signal_scale"]) or policy["signal_scale"] <= 0: + raise ValueError("Attention requires a known question and positive normalization scale") + return task + + +def register_task(store, task: dict, available_at=0) -> dict: + validate_task(task) + if task["id"] != store.task_id: + raise ValueError("Task/store mismatch") + return store.put("task", digest(task), task, available_at) + + +def parse_source(source: dict, max_rows: int) -> list[dict]: + if set(source) != {"format", "data"} or len(encoded(source)) > 1_000_000: + raise ValueError("Invalid or oversized source envelope") + if source["format"] == "json": + rows = json.loads(source["data"]) if isinstance(source["data"], str) else source["data"] + elif source["format"] == "csv": + reader = csv.DictReader(io.StringIO(source["data"])) + if not reader.fieldnames or len(reader.fieldnames) != len(set(reader.fieldnames)): + raise ValueError("Missing or duplicate CSV headers") + rows = list(reader) + elif source["format"] == "text": + # Text batches are line-delimited JSON envelopes with untrusted free text in content. + rows = [json.loads(line) for line in source["data"].splitlines() if line.strip()] + else: + raise ValueError("Supported sources are JSON, CSV and text envelopes") + if not isinstance(rows, list) or not 1 <= len(rows) <= max_rows: + raise ValueError("Source row budget exceeded or batch empty") + return rows + + +def mapped_fact(task: dict, row: dict, source_format: str) -> dict: + if not isinstance(row, dict): + raise ValueError("Source row must be an object") + try: + fact = {key: row[value] for key, value in task["fields"].items()} + measures = {m["id"]: row[m["field"]] for m in task["measures"]} + except KeyError as error: + raise ValueError("Source row missing mapped field") from error + if any(not isinstance(fact[key], str) or not fact[key].strip() or len(fact[key]) > 200 for key in ("id", "entity")): + raise ValueError("Source identity/entity must be nonempty strings") + for key in ("event_at", "available_at", "revision"): + value = fact[key] + if source_format == "csv" and isinstance(value, str) and value.isdecimal(): + value = int(value) + if type(value) is not int or not (0 <= value <= 2**53): + raise ValueError("Source times and revision must be nonnegative integers") + fact[key] = value + if fact["revision"] < 1 or fact["available_at"] < fact["event_at"]: + raise ValueError("Invalid source revision/availability") + if fact["content"] is not None and (not isinstance(fact["content"], str) or len(fact["content"]) > 5000): + raise ValueError("Content must be bounded text or null") + for measure in task["measures"]: + value = measures[measure["id"]] + if source_format == "csv": + value = None if value == "" else float(value) + if value is None and measure["nullable"]: + measures[measure["id"]] = None + elif not number(value): + raise ValueError("Measure violates finite numeric/null contract") + else: + measures[measure["id"]] = value + return {**fact, "measures": measures, "source": row, "source_format": source_format} + + +def admit_source(store, task_record: dict, source: dict, received_at: int) -> dict: + task = task_record["body"] + batch_identity = digest({"task": task_record["sha256"], "source": source, "received_at": received_at}) + with store.connection.transaction(): + store.lock() + previous = store.find("admission", batch_identity) + if previous: + return previous + dispositions, parents = [], [task_record["sha256"]] + try: + rows = parse_source(source, task["policy"]["max_rows"]) + except (ValueError, TypeError, KeyError, csv.Error) as error: + rows = [] + dispositions.append({"row": None, "status": "quarantined", "reason": str(error)}) + for index, raw in enumerate(rows): + try: + fact = mapped_fact(task, raw, source["format"]) + if fact["available_at"] > received_at: + raise ValueError("Source claims future availability") + fact["available_at"] = received_at + fact["task"] = task_record["sha256"] + identity = digest([task_record["sha256"], fact["id"], fact["revision"]]) + previous = store.find("source", identity) + if previous: + if previous["body"]["source"] != raw or previous["body"]["source_format"] != source["format"]: + raise ValueError("Conflicting source identity/revision") + disposition = {"row": index, "status": "duplicate", "source": previous["sha256"]} + else: + revisions = [r for r in store.list("source") if r["body"]["id"] == fact["id"] and r["body"]["task"] == task_record["sha256"]] + if any(r["body"]["entity"] != fact["entity"] or r["body"]["event_at"] != fact["event_at"] for r in revisions): + raise ValueError("Correction cannot change entity or event grain") + latest = max(revisions, key=lambda r: r["body"]["revision"], default=None) + source_parents = [task_record["sha256"]] + ([latest["sha256"]] if latest else []) + saved = store.put("source", identity, fact, received_at, source_parents) + disposition = {"row": index, "status": "accepted", "source": saved["sha256"], + "revision_kind": "correction" if latest and fact["revision"] > latest["body"]["revision"] else "late_revision" if latest else "initial", + "late": received_at > fact["event_at"]} + parents.append(disposition["source"]) + dispositions.append(disposition) + except (ValueError, TypeError, KeyError) as error: + dispositions.append({"row": index, "status": "quarantined", "reason": str(error), "source_sha256": digest(raw)}) + return store.put("admission", batch_identity, {"source_sha256": digest(source), "dispositions": dispositions}, received_at, parents) + + +def current_sources(store, cutoff: int, task_sha: str | None = None) -> list[dict]: + current = {} + for record in store.list("source", cutoff): + body = record["body"] + if task_sha is not None and body["task"] != task_sha: + continue + if body["event_at"] <= cutoff and (body["id"] not in current or body["revision"] > current[body["id"]]["body"]["revision"]): + current[body["id"]] = record + return sorted(current.values(), key=lambda r: (r["body"]["event_at"], r["body"]["entity"], r["body"]["id"])) diff --git a/runtime/evidence.py b/runtime/evidence.py new file mode 100644 index 0000000..9bdc181 --- /dev/null +++ b/runtime/evidence.py @@ -0,0 +1,90 @@ +"""Append-only, content-addressed evidence on the existing PostgreSQL stack.""" +from __future__ import annotations + +from psycopg.types.json import Jsonb + +from runtime.simulation import digest + + +def canonical_body(value): + """PostgreSQL JSONB normalizes negative zero; hash the same representation.""" + if isinstance(value,float) and value == 0: + return 0.0 + if isinstance(value,list): + return [canonical_body(item) for item in value] + if isinstance(value,dict): + return {key:canonical_body(item) for key,item in value.items()} + return value + + +class Evidence: + def __init__(self, connection, task_id: str): + self.connection = connection + self.task_id = task_id + + def put(self, kind: str, identity: str, body: dict, available_at: int, parents=()) -> dict: + if type(available_at) is not int or available_at < 0: + raise ValueError("Evidence availability must be a nonnegative integer") + parents = sorted(set(parents)) + body = canonical_body(body) + record = {"task_id": self.task_id, "kind": kind, "identity": identity, + "body": body, "available_at": available_at, "parents": parents} + sha = digest(record) + with self.connection.transaction(): + for parent in parents: + if self.get(parent)["available_at"] > available_at: + raise ValueError("Evidence cannot predate its parents") + self.connection.execute(""" + INSERT INTO backintel.capability_evidence + (sha256, task_id, kind, identity, available_at, body, parents) + VALUES (%s,%s,%s,%s,%s,%s,%s) + ON CONFLICT (task_id, kind, identity) DO NOTHING + """, (sha, self.task_id, kind, identity, available_at, Jsonb(body), parents)) + saved = self.find(kind, identity) + if saved["sha256"] != sha: + raise ValueError("Evidence identity reused with conflicting content") + return saved + + @staticmethod + def _record(row) -> dict: + if row is None: + raise ValueError("Evidence does not exist in this task") + keys = ("sha256", "task_id", "kind", "identity", "available_at", "body", "parents") + return dict(zip(keys, row)) + + def get(self, sha: str) -> dict: + record = self._record(self.connection.execute(""" + SELECT sha256,task_id,kind,identity,available_at,body,parents + FROM backintel.capability_evidence WHERE sha256=%s AND task_id=%s + """, (sha, self.task_id)).fetchone()) + if digest({k: v for k, v in record.items() if k != "sha256"}) != sha: + raise ValueError("Evidence digest mismatch") + return record + + def find(self, kind: str, identity: str) -> dict | None: + row = self.connection.execute(""" + SELECT sha256,task_id,kind,identity,available_at,body,parents + FROM backintel.capability_evidence WHERE task_id=%s AND kind=%s AND identity=%s + """, (self.task_id, kind, identity)).fetchone() + return self.get(row[0]) if row else None + + def list(self, kind: str, cutoff: int | None = None) -> list[dict]: + rows = self.connection.execute(""" + SELECT sha256 FROM backintel.capability_evidence + WHERE task_id=%s AND kind=%s AND (%s::bigint IS NULL OR available_at<=%s) + ORDER BY available_at,identity + """, (self.task_id, kind, cutoff, cutoff)).fetchall() + return [self.get(row[0]) for row in rows] + + def lock(self) -> None: + """Serialize task admission/transitions until the surrounding transaction ends.""" + self.connection.execute("SELECT pg_advisory_xact_lock(hashtextextended(%s,0))", (self.task_id,)) + + def lineage(self, sha: str) -> list[dict]: + pending, records = [sha], {} + while pending: + current = pending.pop() + if current not in records: + records[current] = self.get(current) + pending.extend(records[current]["parents"]) + return list(records.values()) diff --git a/runtime/generated_artifacts.py b/runtime/generated_artifacts.py new file mode 100644 index 0000000..32dbaf0 --- /dev/null +++ b/runtime/generated_artifacts.py @@ -0,0 +1,121 @@ +"""Generated layouts may select approved evidence; they cannot invent its values.""" + +import html + +from runtime.artifacts import audience_for, page +from runtime.sandbox import resolve_image, run_candidate +from runtime.simulation import digest + +COLUMNS = {"entity": "Entity", "prediction": "Predicted outcome", "target_at": "Prediction time", + "attention": "Attention", "source": "Source evidence"} + +# Development generator response. Execution and evidence validation remain real. +DEVELOPMENT_SOURCE = '''import json +snapshot = json.load(open("/input/snapshot.json")) +rows = sorted(snapshot["rows"], key=lambda row: row["prediction"] if row["prediction"] is not None else -1, reverse=True) +print(json.dumps({"schema": "backintel-generated-layout/v1", "title": "Predicted outcomes in descending order", + "columns": ["entity", "prediction", "target_at", "attention"], "sources": [row["source"] for row in rows]})) +''' + + +def snapshot_for(artifact): + return {"schema": "backintel-generated-input/v1", "artifact": artifact["sha256"], "data": "synthetic", + "rows": [{"entity": row["entity"], "source": row["source"], + "prediction": row["prediction"]["body"]["value"] if row["prediction"] else None, + "target_at": row["prediction"]["body"]["target_at"] if row["prediction"] else None, + "implementation_mode": row["prediction"]["body"]["implementation_mode"] if row["prediction"] else "unavailable", + "attention": row["attention"]["body"]["condition"] if row["attention"] else "none"} + for row in artifact["body"]["rows"]]} + + +def validate_layout(layout, snapshot): + if not isinstance(layout, dict) or set(layout) != {"schema", "title", "columns", "sources"} or layout["schema"] != "backintel-generated-layout/v1": + raise ValueError("Generated view must use the supported layout contract") + if not isinstance(layout["title"], str) or not 1 <= len(layout["title"]) <= 120: + raise ValueError("Generated title must be bounded") + if not isinstance(layout["columns"], list) or not layout["columns"] or any(not isinstance(v, str) or v not in COLUMNS for v in layout["columns"]) or len(set(layout["columns"])) != len(layout["columns"]): + raise ValueError("Generated view contains unsupported columns") + allowed = {r["source"] for r in snapshot["rows"]} + if not isinstance(layout["sources"], list) or any(not isinstance(v, str) or v not in allowed for v in layout["sources"]) or len(set(layout["sources"])) != len(layout["sources"]): + raise ValueError("Generated view references unapproved or duplicate source rows") + + +def create_candidate(store, artifact, actor, source, image="backintel-capability-demo-runtime:latest"): + audience = audience_for(store.get(artifact["body"]["task"])["body"], actor) + if not audience["can_correct"] or audience["view"] != "analysis" or artifact["body"]["audience"]["id"] != actor: + raise PermissionError("This audience cannot generate a candidate view") + snapshot = snapshot_for(artifact) + image = resolve_image(image) + key = digest([artifact["sha256"], source, image, snapshot]) + previous = store.find("artifact_candidate", key) + if previous: + return previous + run = run_candidate(source, snapshot, image) + if run["status"] == "candidate": + try: + validate_layout(run["result"], snapshot) + except ValueError as exc: + run.update(status="rejected", stop_reason=str(exc)) + body = {"artifact": artifact["sha256"], "actor": actor, "snapshot": snapshot, "run": run, + "code_generation": "simulated", "review": "pending", "external_publications": 0} + return store.put("artifact_candidate", key, body, artifact["available_at"], [artifact["sha256"]]) + + +def scoped_candidate(store, artifact, sha): + candidate = store.get(sha) + if candidate["kind"] != "artifact_candidate" or candidate["body"]["artifact"] != artifact["sha256"]: + raise PermissionError("Generated candidate is outside this report") + return candidate + + +def review_candidate(store, candidate, actor, decision, reason, at): + artifact = store.get(candidate["body"]["artifact"]) + audience = audience_for(store.get(artifact["body"]["task"])["body"], actor) + if candidate["body"]["actor"] != actor or not audience["can_correct"] or audience["view"] != "analysis": + raise PermissionError("This audience cannot review this candidate") + if decision not in ("accepted", "rejected") or not isinstance(reason, str) or not reason.strip() or len(reason) > 1000: + raise ValueError("Candidate review needs a decision and a bounded reason") + if decision == "accepted": + if candidate["body"]["run"]["status"] != "candidate": + raise ValueError("A failed sandbox candidate cannot be accepted") + validate_layout(candidate["body"]["run"]["result"], candidate["body"]["snapshot"]) + body = {"candidate": candidate["sha256"], "actor": actor, "decision": decision, "reason": reason} + with store.connection.transaction(): + store.lock() + return store.find("artifact_candidate_review", digest(body)) or store.put("artifact_candidate_review", digest(body), body, at, [candidate["sha256"]]) + + +def render_candidate(candidate, reviews=(), interactive=True, semantic_note=None): + body, esc = candidate["body"], html.escape + modes = ", ".join(sorted({r.get("implementation_mode", "unverified") for r in body["snapshot"]["rows"]})) or "unavailable" + if semantic_note: + modes += " · "+semantic_note + destination = "/" if interactive else "Report.html" + content = f"

SYNTHETIC DATA · GENERATED VIEW CANDIDATE
Predictors: {esc(modes)}

Inspect a generated view

Return to report

" + matching = [r for r in reviews if r["body"]["candidate"] == candidate["sha256"]] + content += "

Review status

" + ("".join(f"

{esc(r['body']['decision'])}: {esc(r['body']['reason'])}

" for r in matching) or "

Pending local review. No external publication.

") + "
" + if body["run"]["status"] == "candidate": + layout = body["run"]["result"] + validate_layout(layout, body["snapshot"]) + rows = {r["source"]: r for r in body["snapshot"]["rows"]} + content += f"

{esc(layout['title'])}

Generated title and row selection require human review. Displayed values come directly from approved evidence.

On a narrow screen, swipe the table or focus it and use the arrow keys to see every column.

" + content += "".join(f"" for c in layout["columns"]) + "" + for source in layout["sources"]: + content += "" + for column in layout["columns"]: + value = rows[source][column] + shown = f"{value:.3g}" if isinstance(value, float) else str(value) + content += f"{esc(shown)}" + content += "" + content += "
Generated selection of accepted report rows
{esc(COLUMNS[c])}
" + else: + content += f"

Candidate rejected

{esc(body['run']['stop_reason'] or 'Sandbox failed')}

" + content += f"

Source and execution

Inspect generated Python and bounded logs
{esc(body['run']['source'])}
{esc(body['run']['stdout'])}
{esc(body['run']['stderr'])}
" + if not interactive: + return page("Generated view export", content + "

Read-only export. Local review records are retained above.

") + content += (f"

Review this candidate

" + f"" + "
") + return page("Generated view review", content) diff --git a/runtime/jev.py b/runtime/jev.py index 92355d7..98ef59f 100644 --- a/runtime/jev.py +++ b/runtime/jev.py @@ -26,11 +26,13 @@ class _OpenRouterCaptureClient(httpx2.Client): def __init__(self, **client_kwargs): super().__init__(**client_kwargs) self.last_metadata: dict = {} + self.last_response: dict = {} def post(self, *args, **kwargs): response = super().post(*args, **kwargs) try: body = response.json() + self.last_response = body usage = body.get("usage") or {} self.last_metadata = { "request_id": body.get("id"), @@ -46,7 +48,7 @@ def post(self, *args, **kwargs): class OpenRouterJevClassifier: """LangChain TypeSafeClassifier routed through OpenRouter's System One API.""" provider_name = "openrouter" - def __init__(self, api_key: str, transport: Any | None = None): + def __init__(self, api_key: str, transport: Any | None = None, model: str = "jev-1.13"): from langchain_typesafe import Choice, Noul, Score, TypeSafeClassifier if transport is not None: self._client = _OpenRouterCaptureClient(transport=transport) @@ -56,18 +58,22 @@ def __init__(self, api_key: str, transport: Any | None = None): self._classifier = TypeSafeClassifier( api_key=api_key, base_url="https://openrouter.ai/api", - model="jev-1.13", + model=model, client=self._client, async_client=self._async_client, ) self.question_types = type("QuestionTypes", (), {"Choice": Choice, "Noul": Noul, "Score": Score}) self.last_metadata: dict = {} + self.last_response: dict = {} def invoke(self, request: dict) -> Any: self._client.last_metadata = {} - response = self._classifier.invoke(request) - self.last_metadata = dict(self._client.last_metadata) - return response + self._client.last_response = {} + try: + return self._classifier.invoke(request) + finally: + self.last_metadata = dict(self._client.last_metadata) + self.last_response = dict(self._client.last_response) async def aclose(self) -> None: self._client.close() diff --git a/runtime/jobs.py b/runtime/jobs.py new file mode 100644 index 0000000..4079dbc --- /dev/null +++ b/runtime/jobs.py @@ -0,0 +1,193 @@ +"""Durable bounded jobs. Aegra's native cron scheduler is the only timer owner.""" +from __future__ import annotations + +import time + +import psycopg +from psycopg.types.json import Jsonb + +from runtime.evidence import Evidence +from runtime.ledger import dsn +from runtime.real_semantics import usage_for +from runtime.simulation import digest, encoded + + +def schedule(store, kind: str, payload: dict, due_at: float, request_id: str, + repeat_seconds: int | None = None, occurrences=1) -> str: + if kind not in ("event","schedule","deadline","staleness","on_demand") or len(encoded(payload)) > 100_000: + raise ValueError("Invalid or oversized trigger") + if type(occurrences) is not int or not 1 <= occurrences <= 100 or (occurrences > 1 and (type(repeat_seconds) is not int or repeat_seconds < 1)): + raise ValueError("Recurring trigger requires a bounded interval/count") + trigger_id = digest([store.task_id,request_id]) + with store.connection.transaction(): + store.lock() + existing = store.connection.execute("SELECT kind,payload,repeat_seconds FROM backintel.capability_triggers WHERE trigger_id=%s",(trigger_id,)).fetchone() + if existing: + if existing != (kind,payload,repeat_seconds): + raise ValueError("Conflicting trigger identity") + return trigger_id + pending = store.connection.execute("SELECT count(*) FROM backintel.capability_triggers WHERE task_id=%s AND state='pending'",(store.task_id,)).fetchone()[0] + if pending >= 100: + raise ValueError("Trigger backpressure limit reached") + store.connection.execute("""INSERT INTO backintel.capability_triggers + (trigger_id,task_id,kind,payload,due_at,repeat_seconds,remaining) VALUES (%s,%s,%s,%s,to_timestamp(%s),%s,%s)""", + (trigger_id,store.task_id,kind,Jsonb(payload),due_at,repeat_seconds,occurrences)) + return trigger_id + + +def enqueue(store, payload: dict, request_id: str, trigger_id=None) -> str: + if len(encoded(payload)) > 100_000: + raise ValueError("Job payload budget exceeded") + key, fingerprint = digest([store.task_id,request_id]), digest(payload) + with store.connection.transaction(): + store.lock() + existing = store.connection.execute("SELECT input_sha256 FROM backintel.capability_jobs WHERE job_id=%s",(key,)).fetchone() + if existing: + if existing[0] != fingerprint: + raise ValueError("Job identity reused with conflicting input") + return key + pending = store.connection.execute("SELECT count(*) FROM backintel.capability_jobs WHERE task_id=%s AND state IN ('queued','running','retry')",(store.task_id,)).fetchone()[0] + if pending >= 20: + raise ValueError("Job backpressure limit reached") + store.connection.execute("""INSERT INTO backintel.capability_jobs(job_id,task_id,trigger_id,payload,input_sha256) + VALUES (%s,%s,%s,%s,%s)""",(key,store.task_id,trigger_id,Jsonb(payload),fingerprint)) + return key + + +def release_due(connection, task_ids=None) -> list[str]: + jobs = [] + with connection.transaction(): + # The native scheduler can overlap dispatch runs; only one releases each occurrence. + due = connection.execute("""SELECT trigger_id,task_id,payload,occurrence,remaining,repeat_seconds + FROM backintel.capability_triggers WHERE state='pending' AND due_at<=now() + AND (%s::text[] IS NULL OR task_id=ANY(%s::text[])) + ORDER BY due_at,trigger_id FOR UPDATE SKIP LOCKED LIMIT 20""",(task_ids,task_ids)).fetchall() + for trigger, task_id, payload, occurrence, remaining, interval in due: + store = Evidence(connection,task_id) + store.lock() + pending = connection.execute("""SELECT count(*) FROM backintel.capability_jobs + WHERE task_id=%s AND state IN ('queued','running','retry')""",(task_id,)).fetchone()[0] + if pending >= 20: + continue + if occurrence and "at" in payload: + payload = {**payload,"at":payload["at"]+occurrence*(interval or 0)} + jobs.append(enqueue(store,payload,f"{trigger}:{occurrence}",trigger)) + connection.execute("""UPDATE backintel.capability_triggers SET remaining=remaining-1,occurrence=occurrence+1, + state=CASE WHEN remaining=1 THEN 'fired' ELSE 'pending' END, + due_at=CASE WHEN remaining>1 THEN due_at+make_interval(secs=>%s) ELSE due_at END + WHERE trigger_id=%s""",(interval or 0,trigger)) + return jobs + + +def cancel(connection, job_id: str) -> str: + with connection.transaction(): + row = connection.execute("""UPDATE backintel.capability_jobs SET cancel_requested=true, + state=CASE WHEN state IN ('queued','retry') THEN 'cancelled' ELSE state END,updated_at=now() + WHERE job_id=%s AND state NOT IN ('completed','failed','cancelled') RETURNING state""",(job_id,)).fetchone() + return row[0] if row else "unchanged" + + +def claim(connection, job_id: str) -> dict | None: + with connection.transaction(): + row = connection.execute("""UPDATE backintel.capability_jobs SET state='running',attempts=attempts+1, + lease_until=now()+interval '30 seconds',updated_at=now() + WHERE job_id=%s AND cancel_requested=false AND attempts None: + if not reason.strip(): + raise ValueError("Repair reason required") + with connection.transaction(): + row = connection.execute("""UPDATE backintel.capability_jobs SET state='retry',max_attempts=attempts+1, + due_at=now(),updated_at=now() WHERE job_id=%s AND state='failed' AND attempts<5 + RETURNING task_id,attempts""",(job_id,)).fetchone() + if row is None: + raise ValueError("Only failed jobs below the repair limit may resume") + Evidence(connection,row[0]).put("job_repair",f"{job_id}:{row[1]}", + {"job_id":job_id,"after_attempt":row[1],"reason":reason},int(time.time())) + + +class JobCancelled(Exception): + pass + + +def execute(job_id: str, handler=None) -> dict: + if handler is None: + from runtime.capability_pipeline import handle + handler = handle + with psycopg.connect(dsn(),autocommit=True) as connection: + connection.execute("SET statement_timeout='30s'") + job = claim(connection,job_id) + if job is None: + row = connection.execute("SELECT state,result_sha256 FROM backintel.capability_jobs WHERE job_id=%s",(job_id,)).fetchone() + if row is None: + raise ValueError("Unknown job") + return {"job_id":job_id,"state":row[0],"result":row[1],"reused":True} + started = time.perf_counter() + previous_requests = None + try: + with connection.transaction(): + store = Evidence(connection,job["task_id"]) + store.lock() + previous_requests = [r[0] for r in connection.execute( + "SELECT request_key FROM backintel.capability_model_requests WHERE task_id=%s", (job["task_id"],)).fetchall()] + if job["payload"].get("fail_once") and job["attempt"] == 1: + raise TimeoutError("Injected synthetic transient failure") + result = handler(store,job["payload"]) + wall_ms = (time.perf_counter()-started)*1000 + if wall_ms > 25000: + raise TimeoutError("Job wall-time budget exceeded") + current = connection.execute("SELECT cancel_requested,attempts FROM backintel.capability_jobs WHERE job_id=%s FOR UPDATE",(job_id,)).fetchone() + if current[0] or current[1] != job["attempt"]: + raise JobCancelled("Cancelled or lease ownership changed before acceptance") + connection.execute("""UPDATE backintel.capability_jobs SET state='completed',result_sha256=%s, + lease_until=NULL,error=NULL,wall_ms=%s,updated_at=now() WHERE job_id=%s""",(result["sha256"],wall_ms,job_id)) + store.put("job_attempt_result",f"{job_id}:{job['attempt']}", + {"job_id":job_id,"attempt":job["attempt"],"status":"completed","wall_ms":wall_ms, + **usage_for(store, excluding=previous_requests)},int(time.time()),[result["sha256"]]) + connection.execute("SELECT pg_notify('backintel_capability_jobs',%s)",(job_id,)) + return {"job_id":job_id,"state":"completed","result":result["sha256"],"reused":False} + except Exception as error: + state = "cancelled" if isinstance(error,JobCancelled) else "retry" if job["attempt"] < job["max_attempts"] else "failed" + with connection.transaction(): + usage = usage_for(Evidence(connection,job["task_id"]), excluding=previous_requests, strict=False) if previous_requests is not None else { + "provider_calls":0,"provider_usd":0,"local_compute_usd":None,"provider_fixture_requests":0, + "provider_requests_admitted":0,"provider_request_keys":[]} + connection.execute("""UPDATE backintel.capability_jobs SET state=%s,error=%s,lease_until=NULL, + due_at=now()+interval '1 second',wall_ms=%s,updated_at=now() WHERE job_id=%s AND attempts=%s""", + (state,str(error)[:2000],(time.perf_counter()-started)*1000,job_id,job["attempt"])) + Evidence(connection,job["task_id"]).put("job_attempt_result",f"{job_id}:{job['attempt']}", + {"job_id":job_id,"attempt":job["attempt"],"status":state,"error":str(error)[:2000], + **usage, + "wall_ms":(time.perf_counter()-started)*1000},int(time.time())) + connection.execute("SELECT pg_notify('backintel_capability_jobs',%s)",(job_id,)) + return {"job_id":job_id,"state":state,"error":str(error)} + + +def runnable(connection, task_ids=None) -> list[str]: + return [r[0] for r in connection.execute("""SELECT j.job_id FROM backintel.capability_jobs j + WHERE j.due_at<=now() AND (j.state IN ('queued','retry') OR (j.state='running' AND j.lease_until dict: + with psycopg.connect(dsn(),autocommit=True) as connection: + if not connection.execute("SELECT pg_try_advisory_lock(81827026)").fetchone()[0]: + return {"status":"dispatcher_already_running","jobs":[]} + release_due(connection,task_ids) + # A killed last attempt becomes failed, not permanently running. + connection.execute("""UPDATE backintel.capability_jobs SET state=CASE WHEN cancel_requested THEN 'cancelled' ELSE 'failed' END, + error='Worker lease expired at attempt limit',lease_until=NULL,updated_at=now() + WHERE state='running' AND lease_until=max_attempts OR cancel_requested) + AND (%s::text[] IS NULL OR task_id=ANY(%s::text[]))""",(task_ids,task_ids)) + results = [execute(job_id) for job_id in runnable(connection,task_ids)] + return {"status":"completed","jobs":results} diff --git a/runtime/observations.py b/runtime/observations.py new file mode 100644 index 0000000..c7453a3 --- /dev/null +++ b/runtime/observations.py @@ -0,0 +1,141 @@ +"""Typed Jev-style boundary with deterministic simulated responses; no real Jev execution.""" +from __future__ import annotations + +from runtime.contracts import number +from runtime.simulation import digest + +PROVIDER = {"name": "synthetic-jev-contract", "version": "1", "implementation_mode": "simulated"} + + +def simulated_response(question: dict, content: str | None) -> dict: + if content is None or not content.strip(): + return {"status": "unknown", "value": None, "distribution": None, "reason": "missing_content"} + if content.startswith("[abstain]"): + return {"status": "abstained", "value": None, "distribution": None, "reason": "simulated_ambiguity"} + if question["type"] == "boolean": + value = any(term.casefold() in content.casefold() for term in question["rule"]["terms"]) + probability = .9 if value else .1 + distribution = {"false": 1 - probability, "true": probability} + else: + try: + value = float(content) + except ValueError: + return {"status": "abstained", "value": None, "distribution": None, "reason": "not_numeric_text"} + distribution = None + return {"status": "known", "value": value, "distribution": distribution, "reason": "simulated_response"} + + +def validate_response(question: dict, response: dict) -> None: + if set(response) != {"status", "value", "distribution", "reason"} or response["status"] not in ("known", "unknown", "abstained"): + raise ValueError("Provider returned invalid typed response") + if not isinstance(response["reason"], str) or not response["reason"] or len(response["reason"]) > 1000: + raise ValueError("Response requires bounded reason") + value, distribution = response["value"], response["distribution"] + if response["status"] != "known": + if value is not None or distribution is not None: + raise ValueError("Unknown/abstained cannot carry a value or distribution") + elif question["type"] == "boolean": + if type(value) is not bool or not isinstance(distribution, dict) or set(distribution) != {"true", "false"}: + raise ValueError("Boolean response requires boolean value and binary distribution") + if any(not number(v) or not 0 <= v <= 1 for v in distribution.values()) or abs(sum(distribution.values()) - 1) > 1e-9: + raise ValueError("Invalid probability distribution") + if value != (distribution["true"] >= .5): + raise ValueError("Boolean value conflicts with its distribution") + elif not number(value): + raise ValueError("Numeric response requires finite value") + elif distribution is not None: + if not isinstance(distribution,dict) or set(distribution) != {"values","probabilities"}: + raise ValueError("Numeric distribution requires values and probabilities") + values, probabilities = distribution["values"], distribution["probabilities"] + if not isinstance(values,list) or not isinstance(probabilities,list) or not values or len(values) != len(probabilities): + raise ValueError("Numeric distribution has invalid dimensions") + if any(not number(v) for v in values) or any(not number(p) or not 0 <= p <= 1 for p in probabilities) or abs(sum(probabilities)-1) > 1e-6: + raise ValueError("Invalid numeric probability distribution") + if abs(sum(v*p for v,p in zip(values,probabilities))-value) > 1e-4: + raise ValueError("Numeric answer conflicts with its distribution") + + +def extract(store, task_record: dict, source: dict, at: int, response_provider=simulated_response, + provider=None) -> list[dict]: + provider = provider or task_record["body"]["observation_provider"] + if provider["implementation_mode"] == "real": + from runtime.real_semantics import extract_real + return extract_real(store,task_record,source,at) + if provider.get("implementation_mode") != "simulated" or set(provider) != {"name", "version", "implementation_mode"}: + raise ValueError("Only explicitly identified simulated providers are permitted") + task, observations = task_record["body"], [] + with store.connection.transaction(): + store.lock() + for question in task["questions"]: + cache_key = digest({"source": source["sha256"], "task": task_record["sha256"], "question": question, "provider": provider}) + cached = store.find("observation", cache_key) + if cached: + observations.append(cached) + continue + attempts = store.list("extraction_attempt") + prior = [r for r in attempts if r["body"]["cache_key"] == cache_key] + for attempt in range(len(prior) + 1, task["policy"]["max_attempts"] + 1): + if len(attempts) >= task["policy"]["max_provider_calls"]: + break + response, error = None, None + try: + response = response_provider(question, source["body"]["content"]) + validate_response(question, response) + except (ValueError, TypeError, KeyError, TimeoutError) as failure: + error = str(failure) + response = None + attempt_record = store.put("extraction_attempt", f"{cache_key}:{attempt}", { + "cache_key": cache_key, "attempt": attempt, "provider": provider, + "status": "failed" if error else "passed", "error": error, + "response": response, "provider_calls": 0, "simulated_provider_calls": 1, + "simulated_charge_usd": .0001, "measured_provider_usd": 0, "local_compute_usd": None, + }, at, [source["sha256"], task_record["sha256"]]) + prior.append(attempt_record) + attempts.append(attempt_record) + if not error: + break + success = next((r for r in reversed(prior) if r["body"]["status"] == "passed"), None) + response = success["body"]["response"] if success else { + "status": "unknown", "value": None, "distribution": None, + "reason": "attempts_exhausted" if len(prior) >= task["policy"]["max_attempts"] else "provider_budget_exhausted"} + observations.append(store.put("observation", cache_key, { + "source": source["sha256"], "question_id": question["id"], "question": question, + "provider": provider, "response": response, "task": task_record["sha256"], + }, at, [source["sha256"], task_record["sha256"], *[r["sha256"] for r in prior]])) + return observations + + +def correct_observation(store, observation_sha: str, response: dict, actor: str, reason: str, + at: int, supersedes: str | None = None) -> dict: + original = store.get(observation_sha) + if original["kind"] != "observation": + raise ValueError("Correction requires original observation") + task = store.get(original["body"]["task"])["body"] + audience = next((a for a in task["audiences"] if a["id"] == actor), None) + source = store.get(original["body"]["source"])["body"] + if not audience or not audience["can_correct"] or (audience["entities"] and source["entity"] not in audience["entities"]): + raise PermissionError("Actor cannot correct this entity") + if not isinstance(reason, str) or not reason.strip() or len(reason) > 1000: + raise ValueError("Correction requires bounded reason") + validate_response(original["body"]["question"], response) + body = {"observation": observation_sha, "response": response, "actor": actor, "reason": reason, "supersedes": supersedes} + identity = digest(body) + with store.connection.transaction(): + store.lock() + replay = store.find("correction", identity) + if replay: + return replay + history = [r for r in store.list("correction") if r["body"]["observation"] == observation_sha] + latest = max(history, key=lambda r: (r["available_at"], r["body"]["sequence"]), default=None) + if supersedes != (latest["sha256"] if latest else None): + raise ValueError("Correction must supersede the current correction") + body["sequence"] = len(history) + 1 + return store.put("correction", identity, body, at, [observation_sha] + ([supersedes] if supersedes else [])) + + +def effective_observation(store, observation_sha: str, cutoff: int) -> dict: + original = store.get(observation_sha) + if original["available_at"] > cutoff: + raise ValueError("Observation unavailable at cutoff") + history = [r for r in store.list("correction", cutoff) if r["body"]["observation"] == observation_sha] + return max(history, key=lambda r: (r["available_at"], r["body"]["sequence"]), default=original) diff --git a/runtime/prediction.py b/runtime/prediction.py new file mode 100644 index 0000000..ab2c1ca --- /dev/null +++ b/runtime/prediction.py @@ -0,0 +1,387 @@ +"""Time-safe features, evaluation and explicitly real or simulated predictors. + +Real routes load the approved CatBoost/TabICLv2 adapters. Simulated routes remain +development fixtures and cannot satisfy real-model acceptance. +""" +from __future__ import annotations + +import math +import statistics +import time + +from runtime.contracts import current_sources, number +from runtime.observations import effective_observation +from runtime.simulation import digest, encoded + +ROUTES = (("baseline", "structured"), ("catboost", "structured"), + ("catboost", "semantic"), ("tabiclv2", "structured"), ("tabiclv2", "semantic")) + + +def features(store, task_record: dict, cutoff: int) -> list[dict]: + """Latest entity-grain facts/observations actually available at this cutoff.""" + task = task_record["body"] + latest, grains = {}, set() + for source in current_sources(store, cutoff, task_record["sha256"]): + row = source["body"] + grain = (row["entity"], row["event_at"]) + if grain in grains: + raise ValueError("Multiple source identities at the same entity/event grain") + grains.add(grain) + latest[row["entity"]] = source + available = store.list("observation", cutoff) + result = [] + for entity, source in sorted(latest.items()): + values = {"structured:" + key: value for key, value in source["body"]["measures"].items()} + parents = [task_record["sha256"], source["sha256"]] + for question in task["questions"]: + matches = [r for r in available if r["body"]["source"] == source["sha256"] + and r["body"]["question_id"] == question["id"] and r["body"]["task"] == task_record["sha256"]] + if len(matches) > 1: + raise ValueError("Ambiguous observation provider; choose one task version/provider") + observation = effective_observation(store, matches[0]["sha256"], cutoff) if matches else None + value = observation["body"]["response"]["value"] if observation else None + values["semantic:" + question["id"]] = int(value) if type(value) is bool else value + if observation: + parents.append(observation["sha256"]) + body = {"task": task_record["sha256"], "entity": entity, "source_id": source["body"]["id"], + "source": source["sha256"], "event_at": source["body"]["event_at"], "cutoff": cutoff, + "target_at": cutoff + task["target"]["horizon"], "values": values, + "nulls": [key for key, value in values.items() if value is None]} + result.append(store.put("feature", digest(body), body, cutoff, parents)) + return result + + +def outcome(store, task_record: dict, label: dict) -> dict: + if set(label) != {"source_id", "entity", "event_at", "available_at", "revision", "value"}: + raise ValueError("Invalid observed outcome contract") + task = task_record["body"] + if any(type(label[k]) is not int or label[k] < 0 for k in ("event_at", "available_at", "revision")) or label["revision"] < 1: + raise ValueError("Invalid outcome revision/time") + if any(not isinstance(label[k], str) or not label[k] for k in ("source_id", "entity")): + raise ValueError("Outcome requires entity/source identity") + if label["available_at"] < label["event_at"] + task["target"]["horizon"]: + raise ValueError("Outcome cannot be available before target horizon") + if not number(label["value"]) or (task["target"]["kind"] == "classification" and label["value"] not in (0, 1)): + raise ValueError("Outcome violates target contract") + body = {**label, "task": task_record["sha256"], "target": task["target"]["id"]} + identity = digest([task_record["sha256"], label["source_id"], label["event_at"], label["revision"]]) + with store.connection.transaction(): + store.lock() + history = [r for r in store.list("outcome") if r["body"]["source_id"] == label["source_id"] + and r["body"]["task"] == task_record["sha256"] and r["body"]["event_at"] == label["event_at"]] + if any(r["body"]["entity"] != label["entity"] for r in history): + raise ValueError("Outcome revision cannot change entity") + previous = store.find("outcome", identity) + if previous: + if previous["body"] != body: + raise ValueError("Conflicting outcome revision") + return previous + return store.put("outcome", identity, body, label["available_at"], [task_record["sha256"]] + [r["sha256"] for r in history]) + + +def cases(store, task_record: dict, snapshots: list[dict], cutoff: int) -> list[dict]: + labels = {} + for label in store.list("outcome", cutoff): + row = label["body"] + if row["task"] != task_record["sha256"]: + continue + key = (row["source_id"], row["entity"], row["event_at"]) + if key not in labels or row["revision"] > labels[key]["body"]["revision"]: + labels[key] = label + result, seen = [], set() + for feature in sorted(snapshots, key=lambda r: (r["body"]["cutoff"], r["body"]["entity"])): + row = feature["body"] + if row["task"] != task_record["sha256"] or feature["available_at"] > cutoff: + raise ValueError("Incompatible or future feature snapshot") + grain = (row["entity"], row["cutoff"]) + if grain in seen: + raise ValueError("Duplicate feature grain in evaluation cases") + seen.add(grain) + # A stale source carried into a later snapshot has no matching future label. + label = labels.get((row["source_id"], row["entity"], row["cutoff"])) + if label and label["available_at"] >= row["target_at"]: + result.append({"feature": feature, "outcome": label}) + return result + + +def chronological_split(task: dict, records: list[dict]) -> tuple[list[dict], list[dict], int]: + times = sorted({r["feature"]["body"]["cutoff"] for r in records}) + if len(times) < 2: + raise ValueError("Insufficient chronological cases") + split = max(1, min(len(times) - 1, int(len(times) * (1 - task["target"]["holdout_fraction"])))) + prepared_at = times[split] + training = [r for r in records if r["feature"]["body"]["cutoff"] < prepared_at and r["outcome"]["available_at"] <= prepared_at] + holdout = [r for r in records if r["feature"]["body"]["cutoff"] >= prepared_at] + if len(training) < task["target"]["minimum_train"] or not holdout: + raise ValueError("Insufficient eligible train/context or holdout cases") + return training, holdout, prepared_at + + +def validate_case(record: dict) -> None: + feature, label = record["feature"], record["outcome"] + f, y = feature["body"], label["body"] + if (f["task"], f["source_id"], f["entity"], f["cutoff"]) != (y["task"], y["source_id"], y["entity"], y["event_at"]) or label["available_at"] < f["target_at"]: + raise ValueError("Outcome does not match feature grain/horizon") + + +def prepare(store, task_record: dict, training: list[dict], route: str, feature_set: str, at: int, implementation_mode="simulated") -> dict: + if (route, feature_set) not in ROUTES: + raise ValueError("Unsupported predictor route") + if len(training) < task_record["body"]["target"]["minimum_train"]: + raise ValueError("Insufficient preparation cases") + for record in training: + validate_case(record) + f, label = record["feature"], record["outcome"] + if f["body"]["task"] != task_record["sha256"] or label["body"]["task"] != task_record["sha256"]: + raise ValueError("Incompatible preparation contract") + if f["body"]["cutoff"] >= at or label["available_at"] > at: + raise ValueError("Future feature or unavailable label in preparation") + if implementation_mode not in ("simulated","real"): + raise ValueError("Predictor execution mode must be explicit") + if implementation_mode == "real" and route != "baseline": + from runtime.real_models import prepare_real + return prepare_real(store,task_record,training,route,feature_set,at) + columns = sorted(k for k in training[0]["feature"]["body"]["values"] if feature_set == "semantic" or k.startswith("structured:")) + key = digest({"task": task_record["sha256"], "training": [[r["feature"]["sha256"], r["outcome"]["sha256"]] for r in training], + "route": route, "feature_set": feature_set, "at": at, "adapter_version": "1"}) + with store.connection.transaction(): + store.lock() + existing = store.find("model", key) + if existing: + return existing + started = time.perf_counter() + scales = {} + for column in columns: + values = [r["feature"]["body"]["values"][column] for r in training if r["feature"]["body"]["values"][column] is not None] + scales[column] = {"mean": statistics.mean(values) if values else 0., "std": statistics.pstdev(values) if values else 0., + "min": min(values) if values else 0., "max": max(values) if values else 0.} + labels = [r["outcome"]["body"]["value"] for r in training] + context = training[-12:] if route == "tabiclv2" else [] + body = {"task": task_record["sha256"], "route": route, "feature_set": feature_set, "adapter_version": "1", + "implementation_mode": "native_baseline" if route == "baseline" else "simulated", + "preparation": "empirical_mean" if route == "baseline" else "training_style" if route == "catboost" else "context_style", + "columns": columns, "scales": scales, "target": task_record["body"]["target"], "baseline": statistics.mean(labels), + "label_min": min(labels), "label_max": max(labels), "training_count": len(training), + "training_matrix_sha256": digest([[r["feature"]["body"]["values"], r["outcome"]["body"]["value"]] for r in training]), + "context": [{"values": {c: r["feature"]["body"]["values"][c] for c in columns}, "label": r["outcome"]["body"]["value"]} for r in context], + "prepared_at": at, "wall_ms": (time.perf_counter() - started) * 1000, + "provider_calls": 0, "measured_provider_usd": 0, "local_compute_usd": None} + body["prepared_bytes"] = len(encoded(body)) + return store.put("model", key, body, at, [task_record["sha256"], *[r[k]["sha256"] for r in training for k in ("feature", "outcome")]]) + + +def predict(model: dict, feature: dict) -> float: + body, row = model["body"], feature["body"] + if body["task"] != row["task"] or any(c not in row["values"] for c in body["columns"]): + raise ValueError("Model/feature contract mismatch") + if body["implementation_mode"] == "real": + from runtime.real_models import predict_real + return predict_real(model,feature) + if body["route"] == "baseline": + return body["baseline"] + def normalize(values, column): + scale = body["scales"][column] + value = values[column] if values[column] is not None else scale["mean"] + return (value - scale["min"]) / (scale["max"] - scale["min"] or 1.) + if body["route"] == "catboost": + # Synthetic provider response, not tree fitting or CatBoost inference. + value = statistics.mean(normalize(row["values"], c) for c in body["columns"]) + if body["target"]["kind"] == "classification": + return 1 / (1 + math.exp(-max(-30, min(30, (value - .5) * 4)))) + return body["label_min"] + value * (body["label_max"] - body["label_min"]) + # Synthetic context lookup, not TabICLv2 inference or an installed model. + nearest = sorted(body["context"], key=lambda r: sum((normalize(row["values"], c) - normalize(r["values"], c)) ** 2 for c in body["columns"]))[:3] + return statistics.mean(r["label"] for r in nearest) + + +def metrics(kind: str, labels: list[float], predictions: list[float]) -> dict: + if not labels or len(labels) != len(predictions) or any(not number(v) for v in labels + predictions): + raise ValueError("Metrics require paired finite observations and predictions") + mse = statistics.mean((p - y) ** 2 for y, p in zip(labels, predictions)) + if kind == "regression": + mean = statistics.mean(labels) + total = sum((y - mean) ** 2 for y in labels) + return {"count":len(labels), "mae":statistics.mean(abs(p-y) for y,p in zip(labels,predictions)), "rmse":math.sqrt(mse), + "r2":1 - mse * len(labels) / total if total else None} + if kind != "classification" or any(y not in (0,1) for y in labels) or any(not 0 <= p <= 1 for p in predictions): + raise ValueError("Classification requires binary labels and probabilities") + decisions = [int(p >= .5) for p in predictions] + tp = sum(y == 1 and p == 1 for y,p in zip(labels, decisions)) + fp = sum(y == 0 and p == 1 for y,p in zip(labels, decisions)) + fn = sum(y == 1 and p == 0 for y,p in zip(labels, decisions)) + precision, recall = tp / (tp + fp) if tp + fp else 0., tp / (tp + fn) if tp + fn else 0. + bins = [] + for index in range(5): + pairs = [(y,p) for y,p in zip(labels,predictions) if min(4,int(p * 5)) == index] + if pairs: + bins.append({"lower":index/5, "upper":(index+1)/5, "count":len(pairs), + "mean_probability":statistics.mean(p for _,p in pairs), "observed_rate":statistics.mean(y for y,_ in pairs)}) + positive = [p for y,p in zip(labels,predictions) if y == 1] + negative = [p for y,p in zip(labels,predictions) if y == 0] + auc = statistics.mean(float(p > n) + .5 * (p == n) for p in positive for n in negative) if positive and negative else None + return {"count":len(labels), "accuracy":statistics.mean(y == p for y,p in zip(labels,decisions)), + "precision":precision, "recall":recall, "f1":2*precision*recall/(precision+recall) if precision+recall else 0., + "brier":mse, "log_loss":-statistics.mean(y*math.log(max(1e-12,p))+(1-y)*math.log(max(1e-12,1-p)) for y,p in zip(labels,predictions)), + "roc_auc":auc, "calibration_bins":bins, + "ece":sum(b["count"] * abs(b["mean_probability"]-b["observed_rate"]) for b in bins)/len(labels)} + + +def evaluate(store, model: dict, holdout: list[dict], at: int) -> dict: + with store.connection.transaction(): + store.lock() + return _evaluate(store,model,holdout,at) + + +def _evaluate(store, model: dict, holdout: list[dict], at: int) -> dict: + key = digest([model["sha256"], [[r["feature"]["sha256"],r["outcome"]["sha256"]] for r in holdout]]) + existing = store.find("evaluation", key) + if existing: + return existing + started = time.perf_counter() + for case in holdout: + validate_case(case) + if case["feature"]["body"]["cutoff"] < model["body"]["prepared_at"] or case["outcome"]["available_at"] > at: + raise ValueError("Evaluation leakage or unavailable outcome") + if case["outcome"]["body"]["task"] != model["body"]["task"]: + raise ValueError("Evaluation target contract mismatch") + predictions = [predict(model,r["feature"]) for r in holdout] + scores = metrics(model["body"]["target"]["kind"], [r["outcome"]["body"]["value"] for r in holdout], predictions) + body = {"model":model["sha256"], "status":"passed", "metrics":scores, "predictions":predictions, + "cases":[[r["feature"]["sha256"],r["outcome"]["sha256"]] for r in holdout], + "cutoffs":[r["feature"]["body"]["cutoff"] for r in holdout], + "wall_ms":(time.perf_counter()-started)*1000, "prediction_bytes":len(encoded(predictions)), + "provider_calls":0, "simulated_provider_calls":len(holdout) if model["body"]["implementation_mode"] == "simulated" else 0, + "measured_provider_usd":0, "local_compute_usd":None, + "quality_claim":("Actual predictor ran on synthetic data; this does not establish real-world quality." + if model["body"]["implementation_mode"] == "real" else + "Development fixture exercises machinery; no actual CatBoost/TabICLv2 execution claim.")} + return store.put("evaluation", key, body, at, [model["sha256"], *[r[k]["sha256"] for r in holdout for k in ("feature","outcome")]]) + + +def compare(store, task_record: dict, snapshots: list[dict], at: int, implementation_mode="simulated") -> dict: + with store.connection.transaction(): + store.lock() + return _compare(store,task_record,snapshots,at,implementation_mode) + + +def _compare(store, task_record: dict, snapshots: list[dict], at: int, implementation_mode="simulated") -> dict: + training, holdout, prepared_at = chronological_split(task_record["body"], cases(store,task_record,snapshots,at)) + models = [prepare(store,task_record,training,route,feature_set,prepared_at,implementation_mode) for route,feature_set in ROUTES] + evaluations = [evaluate(store,model,holdout,at) for model in models] + primary = "brier" if task_record["body"]["target"]["kind"] == "classification" else "rmse" + selected = min(evaluations, key=lambda e:e["body"]["metrics"][primary]) + body = {"task":task_record["sha256"], "models":[m["sha256"] for m in models], + "evaluations":[e["sha256"] for e in evaluations], "selected":selected["body"]["model"], + "primary_metric":primary, "selection_rule":"Lowest synthetic holdout loss; baseline wins ties by stable order.", + "holdout_cases":evaluations[0]["body"]["cases"], "train_count":len(training), "holdout_count":len(holdout)} + return store.find("comparison",digest(body)) or store.put("comparison", digest(body), body, at, [e["sha256"] for e in evaluations]) + + +def registry(store, cutoff: int | None = None) -> dict: + states, active = {}, None + for model in store.list("model",cutoff): + states[model["sha256"]] = "prepared" + for evaluation in store.list("evaluation",cutoff): + if evaluation["body"]["status"] == "passed": + states[evaluation["body"]["model"]] = "evaluated" + for event in sorted(store.list("model_transition",cutoff), key=lambda r:r["body"]["sequence"]): + body = event["body"] + if body["action"] == "approve": + states[body["model"]] = "approved" + else: + if active: + states[active] = "retired" + active = body["model"] + states[active] = "active" + return {"active":active, "states":states} + + +def transition(store, task_record: dict, model_sha: str, action: str, at: int, request_id: str) -> dict: + if action not in ("approve", "activate", "rollback"): + raise ValueError("Unsupported model lifecycle transition") + with store.connection.transaction(): + store.lock() + previous = store.find("model_transition", request_id) + if previous: + if previous["body"]["model"] != model_sha or previous["body"]["action"] != action: + raise ValueError("Conflicting model transition request") + return previous + model = store.get(model_sha) + if model["kind"] != "model" or model["body"]["task"] != task_record["sha256"]: + raise ValueError("Incompatible model/task version") + evaluations = [r for r in store.list("evaluation",at) if r["body"]["model"] == model_sha] + if not evaluations or any(e["body"]["status"] != "passed" for e in evaluations): + raise ValueError("Failed or blocked evidence prevents model approval/activation") + history = store.list("model_transition") + if history and at < max(r["available_at"] for r in history): + raise ValueError("Cannot backdate a registry transition") + status = registry(store,at) + state = status["states"][model_sha] + if (action == "approve" and state != "evaluated") or (action == "activate" and state != "approved") or (action == "rollback" and state != "retired"): + raise ValueError("Invalid model lifecycle state") + body = {"model":model_sha, "action":action, "previous_active":status["active"], + "sequence":len(store.list("model_transition"))+1, "actor":"simulated-operator", "approval_mode":"simulated"} + return store.put("model_transition", request_id, body, at, [task_record["sha256"], model_sha, *[e["sha256"] for e in evaluations], + *[r["sha256"] for r in history[-1:]]]) + + +def score(store, task_record: dict, feature: dict, at: int, fallback: str | None = None, shadow: str | None = None) -> dict: + if feature["body"]["task"] != task_record["sha256"] or not feature["available_at"] <= at < feature["body"]["target_at"]: + raise ValueError("Scoring requires compatible features before target horizon") + with store.connection.transaction(): + store.lock() + active = registry(store,at)["active"] + selected, mode = (shadow,"shadow") if shadow else (active,"active") + if not selected or store.get(selected)["body"]["task"] != task_record["sha256"]: + selected, mode = fallback, "fallback" + if selected is None: + raise ValueError("No compatible active model or explicit baseline fallback") + model = store.get(selected) + if model["kind"] != "model" or model["body"]["prepared_at"] > feature["body"]["cutoff"]: + raise ValueError("Model unavailable at feature cutoff") + if mode == "fallback" and model["body"]["route"] != "baseline": + raise ValueError("Fallback must use the explicit baseline") + key = digest([feature["sha256"], selected, mode, task_record["sha256"]]) + previous = store.find("prediction", key) + if previous: + return previous + body = {"task":task_record["sha256"], "feature":feature["sha256"], "model":selected, "mode":mode, + "entity":feature["body"]["entity"], "cutoff":feature["body"]["cutoff"], "target_at":feature["body"]["target_at"], + "value":predict(model,feature), "implementation_mode":model["body"]["implementation_mode"], + "policy_sha256":digest(task_record["body"]["policy"])} + transitions = store.list("model_transition",at) if mode == "active" else [] + return store.put("prediction", key, body, at, [task_record["sha256"], feature["sha256"], selected, + *[r["sha256"] for r in transitions[-1:]]]) + + +def update_plan(store, model: dict, fresh: list[dict], reason_record: dict, at: int, prepare_requested=False) -> dict: + if not fresh or any(f["body"]["task"] != model["body"]["task"] for f in fresh): + raise ValueError("Update requires compatible feature rows") + drift = {} + for column, scale in model["body"]["scales"].items(): + values = [f["body"]["values"][column] for f in fresh if f["body"]["values"][column] is not None] + drift[column] = {"mean_shift_std":abs(statistics.mean(values)-scale["mean"])/(scale["std"] or 1) if values else None, + "missing_rate":1-len(values)/len(fresh)} + with store.connection.transaction(): + store.lock() + key = digest([model["sha256"], [f["sha256"] for f in fresh], reason_record["sha256"], prepare_requested]) + replay = store.find("update_plan",key) + if replay: + return replay + prior_preparations = [r for r in store.list("update_plan") if "model_preparation" in r["body"]["actions"]] + if prepare_requested and prior_preparations: + raise ValueError("Synthetic model-update budget exhausted") + actions = ["feature_refresh","scoring","artifact_refresh"] + (["model_preparation"] if prepare_requested else []) + body = {"model":model["sha256"], "drift":drift, "actions":actions, "notify":False, + "reason":reason_record["sha256"], "preparation_requires_evaluation_and_approval":True} + return store.put("update_plan",key,body,at,[model["sha256"],reason_record["sha256"],*[f["sha256"] for f in fresh]]) + + +def invalidate_predictions(store, source_sha: str, reason_record: dict, at: int) -> list[dict]: + affected = [] + for prediction in store.list("prediction",at): + if source_sha in {r["sha256"] for r in store.lineage(prediction["body"]["feature"])}: + body = {"prediction":prediction["sha256"], "reason":reason_record["sha256"], "disposition":"superseded_after_correction"} + key = digest(body) + affected.append(store.find("invalidation",key) or store.put("invalidation",key,body,at,[prediction["sha256"],reason_record["sha256"]])) + return affected diff --git a/runtime/real_models.py b/runtime/real_models.py new file mode 100644 index 0000000..4621475 --- /dev/null +++ b/runtime/real_models.py @@ -0,0 +1,181 @@ +"""Actual CatBoost and TabICLv2 implementations. Checkpoints never auto-download.""" +from __future__ import annotations + +from functools import lru_cache +import hashlib +from importlib.metadata import version +import json +import os +from pathlib import Path +import statistics +import tempfile +import time + +from runtime.simulation import digest, encoded + +CONFIG = Path(__file__).resolve().parents[1] / "config" / "real_models.json" + + +def model_root() -> Path: + value = os.environ.get("BACKINTEL_MODEL_DIR") + if not value: + raise RuntimeError("Real models require an explicit local model-artifact directory") + return Path(value).resolve() + + +def file_sha(path: Path) -> str: + checksum = hashlib.sha256() + with path.open("rb") as stream: + for block in iter(lambda:stream.read(1024*1024),b""): + checksum.update(block) + return checksum.hexdigest() + + +def versions() -> dict: + return {name:version(name) for name in ("catboost","tabicl","torch","scikit-learn","numpy")} + + +def checkpoint(kind: str) -> tuple[Path,dict]: + config = json.loads(CONFIG.read_text()) + spec = config["tabiclv2"]["checkpoints"][kind] + path = model_root() / "Weights" / spec["file"] + if not path.is_file(): + raise RuntimeError("Approved TabICLv2 checkpoint has not been downloaded") + if path.resolve().parent != (model_root()/"Weights").resolve() or file_sha(path) != spec["sha256"]: + raise ValueError("TabICLv2 checkpoint identity does not match the pinned model") + return path,spec + + +def matrix(records: list[dict], columns: list[str]): + import numpy as np + return np.array([[float("nan") if r["body"]["values"][c] is None else r["body"]["values"][c] for c in columns] for r in records],dtype=float) + + +def _tabicl(kind: str, path: Path): + import torch + from tabicl import TabICLClassifier,TabICLRegressor + torch.set_num_threads(2) + cls = TabICLClassifier if kind == "classification" else TabICLRegressor + parameters = json.loads(CONFIG.read_text())["tabiclv2"]["parameters"] + return cls(model_path=str(path),allow_auto_download=False,**parameters) + + +def _allow_model_use(route: str) -> dict: + config = json.loads(CONFIG.read_text()) + approval_path = model_root()/"model-use-approval.json" + if not approval_path.is_file(): + raise PermissionError("Actual model execution requires explicit local model-use approval") + approval = json.loads(approval_path.read_text()) + if approval.get("approved") is not True or approval.get("configuration_sha256") != digest(config) or route not in approval.get("routes",[]): + raise PermissionError("Model-use approval does not match the pinned configuration") + return config + + +def prepare_real(store, task_record: dict, training: list[dict], route: str, feature_set: str, at: int) -> dict: + if route not in ("catboost","tabiclv2"): + raise ValueError("Unsupported actual predictor implementation") + config = _allow_model_use(route) + if len(training) > config["limits"]["max_training_rows"]: + raise ValueError("Real model training/context row budget exceeded") + if feature_set == "semantic": + expected = {q["id"] for q in task_record["body"]["questions"]} + if task_record["body"]["observation_provider"]["implementation_mode"] != "real": + raise ValueError("Final semantic-feature model preparation requires actual Jev findings") + for case in training: + observations = [r for r in store.lineage(case["feature"]["sha256"]) if r["kind"] == "observation"] + if {r["body"]["question_id"] for r in observations} != expected or any(r["body"]["provider"]["implementation_mode"] != "real" or not r["body"].get("request_id") for r in observations): + raise ValueError("Final semantic-feature model preparation requires actual Jev findings") + columns = sorted(c for c in training[0]["feature"]["body"]["values"] if feature_set == "semantic" or c.startswith("structured:")) + libraries = versions() + key = digest({"task":task_record["sha256"],"training":[[r["feature"]["sha256"],r["outcome"]["sha256"]] for r in training], + "route":route,"feature_set":feature_set,"at":at,"implementation_mode":"real","libraries":libraries,"config":config}) + with store.connection.transaction(): + store.lock() + existing = store.find("model",key) + if existing: + return existing + root = model_root() + root.mkdir(parents=True,exist_ok=True) + package = root/key + if package.exists(): + body = json.loads((package/"manifest.json").read_text()) + if file_sha(package/body["artifact"]["file"]) != body["artifact"]["sha256"]: + raise ValueError("Prepared model package was modified") + else: + started = time.perf_counter() + x = matrix([r["feature"] for r in training],columns) + y = [r["outcome"]["body"]["value"] for r in training] + kind = task_record["body"]["target"]["kind"] + with tempfile.TemporaryDirectory(prefix="Preparing",dir=root) as directory: + staging = Path(directory) + weights = None + if route == "catboost": + from catboost import CatBoostClassifier,CatBoostRegressor + cls = CatBoostClassifier if kind == "classification" else CatBoostRegressor + estimator = cls(**config["catboost"]["parameters"]) + estimator.fit(x,y) + artifact = staging/"model.cbm" + estimator.save_model(str(artifact)) + else: + path,weights = checkpoint(kind) + estimator = _tabicl(kind,path) + estimator.fit(x,y) + artifact = staging/"context.json" + artifact.write_bytes(encoded({"columns":columns,"features":[[r["feature"]["body"]["values"][c] for c in columns] for r in training],"outcomes":y})) + scales = {} + for column in columns: + values = [r["feature"]["body"]["values"][column] for r in training if r["feature"]["body"]["values"][column] is not None] + scales[column] = {"mean":statistics.mean(values) if values else 0.,"std":statistics.pstdev(values) if values else 0., + "min":min(values) if values else 0.,"max":max(values) if values else 0.} + body = {"task":task_record["sha256"],"route":route,"feature_set":feature_set,"adapter_version":"real-v1", + "implementation_mode":"real","preparation":"trained_catboost" if route == "catboost" else "tabiclv2_context", + "columns":columns,"scales":scales,"target":task_record["body"]["target"],"prepared_at":at,"training_count":len(training), + "training_matrix_sha256":digest([[r["feature"]["sha256"],r["outcome"]["sha256"]] for r in training]), + "libraries":libraries,"configuration_sha256":digest(config),"checkpoint":weights, + "artifact":{"package":key,"file":artifact.name,"sha256":file_sha(artifact)}, + "wall_ms":(time.perf_counter()-started)*1000,"prepared_bytes":artifact.stat().st_size, + "provider_calls":0,"measured_provider_usd":0,"local_compute_usd":None, + "parameters":config[route]["parameters"]} + (staging/"manifest.json").write_bytes(encoded(body)) + staging.rename(package) + return store.put("model",key,body,at,[task_record["sha256"],*[r[k]["sha256"] for r in training for k in ("feature","outcome")]]) + + +@lru_cache(maxsize=2) +def _load(root_value: str, model_json: str): + model = json.loads(model_json) + _allow_model_use(model["route"]) + if model["libraries"] != versions(): + raise ValueError("Installed predictor libraries differ from the prepared package") + root = Path(root_value) + artifact = root/model["artifact"]["package"]/model["artifact"]["file"] + if not artifact.resolve().is_relative_to(root) or file_sha(artifact) != model["artifact"]["sha256"]: + raise ValueError("Model artifact identity mismatch") + kind = model["target"]["kind"] + if model["route"] == "catboost": + from catboost import CatBoostClassifier,CatBoostRegressor + estimator = (CatBoostClassifier if kind == "classification" else CatBoostRegressor)() + estimator.load_model(str(artifact)) + else: + import numpy as np + path,spec = checkpoint(kind) + if spec != model["checkpoint"]: + raise ValueError("Prepared TabICLv2 checkpoint changed") + context = json.loads(artifact.read_text()) + estimator = _tabicl(kind,path) + estimator.fit(np.array([[float("nan") if v is None else v for v in row] for row in context["features"]]),context["outcomes"]) + return estimator + + +def predict_real(model: dict, feature: dict) -> float: + body = model["body"] + estimator = _load(str(model_root()),json.dumps(body,sort_keys=True)) + values = matrix([feature],body["columns"]) + if body["target"]["kind"] == "classification": + classes = list(estimator.classes_) + if 1 not in classes: + return 0. + kwargs = {"thread_count":2} if body["route"] == "catboost" else {} + return float(estimator.predict_proba(values,**kwargs)[0][classes.index(1)]) + kwargs = {"thread_count":2} if body["route"] == "catboost" else {} + return float(estimator.predict(values,**kwargs)[0]) diff --git a/runtime/real_pipeline.py b/runtime/real_pipeline.py new file mode 100644 index 0000000..5f1ea99 --- /dev/null +++ b/runtime/real_pipeline.py @@ -0,0 +1,249 @@ +"""Real-model stages with committed source admission before any paid request.""" + +from __future__ import annotations + +import time +from runtime.contracts import admit_source, current_sources, register_task +from runtime.observations import effective_observation +from runtime.prediction import compare, features, invalidate_predictions, outcome, transition, update_plan +from runtime.real_semantics import extract_real, scope_for, usage_for +from runtime.simulation import digest +from runtime.synthetic import history + + +def prepare_history(store, scenario: str) -> dict: + existing = store.find("real_plan", "history-v1") + if existing: + if existing["body"]["scenario"] != scenario: + raise ValueError("Real-model preparation identity conflicts with its scenario") + return existing + task, rows, labels = history(scenario) + task["id"] = store.task_id + task["observation_provider"] = {"name": "openrouter-jev", "version": "jev-1.13", "implementation_mode": "real"} + task_record = register_task(store, task) + sources, outcomes = [], [] + for row, label in zip(rows, labels): + admission = admit_source(store, task_record, {"format": "json", "data": [row]}, label["event_at"]) + disposition = admission["body"]["dispositions"][0] + if disposition["status"] == "quarantined": + raise ValueError("Synthetic real-model history failed source admission") + sources.append(store.get(disposition["source"])) + outcomes.append(outcome(store, task_record, label)) + at = max(r["available_at"] for r in outcomes) + body = {"scenario": scenario, "task": task_record["sha256"], "sources": [r["sha256"] for r in sources], + "outcomes": [r["sha256"] for r in outcomes], "scope": scope_for(task_record, sources), + "at": at, "model_execution": "not_started", "provider_approval": "required", "provider_calls": 0, + "unique_texts": len({r["body"]["content"] for r in sources}), "source_records": len(sources), + "followup": "Separate approved interpretation jobs must finish before real comparison."} + return store.put("real_plan", "history-v1", body, at, + [task_record["sha256"], *[r["sha256"] for r in sources + outcomes]]) + + +def interpret_source(store, plan, source_sha, authorization_id, classifier_factory=None): + if source_sha not in plan["body"]["sources"]: + raise PermissionError("Interpretation source is outside the prepared history") + if not isinstance(authorization_id, str) or not 1 <= len(authorization_id) <= 128: + raise PermissionError("A named approved provider authorization is required") + task_record, source = store.get(plan["body"]["task"]), store.get(source_sha) + # prepare_history is a different completed job; the foreign-key parents are committed. + observations = extract_real(store, task_record, source, source["available_at"], + authorization_id=authorization_id, classifier_factory=classifier_factory) + body = {"plan": plan["sha256"], "source": source_sha, "observations": [r["sha256"] for r in observations], + "implementation_mode": "real", "request_ids": sorted({r["body"]["request_id"] for r in observations})} + return store.find("real_interpretation", digest(body)) or store.put("real_interpretation", digest(body), body, + plan["available_at"], [plan["sha256"], *[r["sha256"] for r in observations]]) + + +def require_observations(store, plan, *, historical=False): + task_record = store.get(plan["body"]["task"]) + task, at = task_record["body"], plan["body"]["at"] + observations = store.list("observation", at) + for source_sha in plan["body"]["sources"]: + source = store.get(source_sha) + cutoff = source["body"]["event_at"] if historical else at + for question in task["questions"]: + matches = [r for r in observations if r["body"]["source"] == source_sha + and r["body"]["task"] == task_record["sha256"] and r["body"]["question_id"] == question["id"]] + if len(matches) != 1: + raise PermissionError("Real comparison requires an actual Jev finding for every history question") + finding = matches[0] + if finding["available_at"] > cutoff or finding["body"]["provider"]["implementation_mode"] != "real" or not finding["body"].get("request_id"): + raise ValueError("Observation is simulated, untraceable or unavailable at the feature cutoff") + effective_observation(store, finding["sha256"], cutoff) + usage_for(store) # Unknown actual billing or uncertain requests block acceptance. + return task_record + + +def compare_history(store, plan): + previous = store.find("real_stage_result", "history-v1") + if previous: + return store.get(previous["body"]["result"]) + task_record = require_observations(store, plan, historical=True) + at = plan["body"]["at"] + snapshots = [] + for source_sha in plan["body"]["sources"]: + source = store.get(source_sha) + snapshots.extend(r for r in features(store, task_record, source["body"]["event_at"]) + if r["body"]["source"] == source_sha) + comparison = compare(store, task_record, snapshots, at, implementation_mode="real") + selected = comparison["body"]["selected"] + transition(store, task_record, selected, "approve", at, "real-demo-policy-approval") + transition(store, task_record, selected, "activate", at, "real-demo-policy-activation") + event = store.put("event", "real-initial-comparison", {"operation": "real_compare", "scenario": plan["body"]["scenario"], + "operator_approval": "simulated_demo_policy"}, at, [comparison["sha256"], plan["sha256"]]) + from runtime.capability_pipeline import refresh + result = refresh(store, task_record, event, at, observe=True) + store.put("real_stage_result", "history-v1", {"result":result["sha256"]}, at, [result["sha256"]]) + return result + + +def prepare_arrival(store, task_record, scenario, payload): + at, row = payload["at"], payload["row"] + key = "arrival-"+digest([task_record["sha256"],row,at]) + previous = store.find("real_plan",key) + if previous: + return previous + old = next((r for r in current_sources(store,at,task_record["sha256"]) + if r["body"]["id"] == row[task_record["body"]["fields"]["id"]]),None) + admission = admit_source(store,task_record,{"format":"json","data":[row]},at) + disposition = admission["body"]["dispositions"][0] + if disposition["status"] == "quarantined": + raise ValueError("Synthetic real follow-up failed source admission") + source = store.get(disposition["source"]) + body = {"scenario":scenario,"task":task_record["sha256"],"sources":[source["sha256"]],"at":at, + "admission":admission["sha256"],"scope":scope_for(task_record,[source]), + "previous_source":old["sha256"] if old and old["sha256"] != source["sha256"] else None} + return store.put("real_plan",key,body,at,[task_record["sha256"],admission["sha256"]]) + + +def prepare_followups(store, history_plan): + previous = store.find("real_plan","followups-v1") + if previous: + return previous + from runtime.capability_pipeline import followup_events + task_record = store.get(history_plan["body"]["task"]) + scenario = history_plan["body"]["scenario"] + events, plans, sources = [], [], [] + for _,kind,payload in followup_events(scenario): + if payload["operation"] == "arrival": + plan = prepare_arrival(store,task_record,scenario,payload) + plans.append(plan) + sources.extend(store.get(sha) for sha in plan["body"]["sources"]) + events.append({"kind":kind,"payload":{"operation":"real_interpret","plan_sha256":plan["sha256"], + "source_sha256":plan["body"]["sources"][0]}}) + payload = {"operation":"real_arrival_apply","plan_sha256":plan["sha256"],"at":payload["at"], + "fail_once":payload.get("fail_once",False)} + else: + payload = {**payload,"operation":"real_event","action":payload["operation"]} + events.append({"kind":kind,"payload":payload}) + at = max(p["payload"].get("at",0) for p in events) + body = {"scenario":scenario,"task":task_record["sha256"],"sources":[r["sha256"] for r in sources], + "scope":scope_for(task_record,sources),"at":at,"events":events, + "arrival_plans":[r["sha256"] for r in plans], + "mode":"synthetic_future_sources_with_cutoff_enforcement","provider_calls":0} + return store.put("real_plan","followups-v1",body,at,[history_plan["sha256"],*[r["sha256"] for r in plans]]) + + +def apply_arrival(store, plan): + previous = store.find("real_stage_result",plan["sha256"]) + if previous: + return store.get(previous["body"]["result"]) + task_record = require_observations(store,plan) + from runtime.capability_pipeline import refresh + at = plan["body"]["at"] + source = store.get(plan["body"]["sources"][0]) + admission = store.get(plan["body"]["admission"]) + if plan["body"]["previous_source"]: + invalidate_predictions(store,plan["body"]["previous_source"],admission,at) + result = refresh(store,task_record,admission,at,observe=True,entities=[source["body"]["entity"]]) + update_plan(store,store.get(result["body"]["model"]),[store.get(s) for s in result["body"]["features"]],admission,at) + store.put("real_stage_result",plan["sha256"],{"result":result["sha256"]},at,[result["sha256"]]) + return result + + +def require_approved_scope(store, task, expected, authorization_id): + authorization = store.connection.execute("""SELECT approved,expires_at>now(),scope,scope_sha256,model + FROM backintel.capability_provider_authorizations WHERE authorization_id=%s""",(authorization_id,)).fetchone() + if not authorization or not all(authorization[:2]): + raise PermissionError("Follow-up scheduling requires current explicit provider approval") + scope, scope_sha, model = authorization[2:] + if digest(scope) != scope_sha or model != task["body"]["observation_provider"]["version"] or any( + scope.get(k) != expected[k] for k in ("task_sha256","questions_sha256")) or not set( + expected["source_sha256s"]).issubset(scope.get("source_sha256s",[])): + raise PermissionError("Follow-up sources exceed the approved scope") + + +def start_followups(store, plan, authorization_id): + previous = store.find("real_schedule","followups-v1") + if previous: + if previous["body"]["authorization_id"] != authorization_id: + raise ValueError("Follow-up scheduling identity conflicts with its authorization") + return previous + if not store.find("real_stage_result","history-v1"): + raise ValueError("Real history comparison must complete before follow-up scheduling") + require_approved_scope(store,store.get(plan["body"]["task"]),plan["body"]["scope"],authorization_id) + from runtime.jobs import schedule + triggers = [] + now = time.time() + for index,event in enumerate(plan["body"]["events"]): + payload = {**event["payload"],"scenario":plan["body"]["scenario"]} + if payload["operation"] == "real_interpret": + payload["provider_authorization_id"] = authorization_id + triggers.append(schedule(store,event["kind"],payload,now+2*(index+1),f"real-followup-{index}")) + return store.put("real_schedule","followups-v1",{"plan":plan["sha256"],"authorization_id":authorization_id, + "triggers":triggers},plan["available_at"],[plan["sha256"]]) + + +def start_journey(store, history_plan, followups, authorization_id): + previous = store.find("real_schedule","journey-v1") + if previous: + if previous["body"]["authorization_id"] != authorization_id: + raise ValueError("Journey identity conflicts with its authorization") + return previous + task = store.get(history_plan["body"]["task"]) + sources = [store.get(sha) for sha in history_plan["body"]["sources"]+followups["body"]["sources"]] + require_approved_scope(store,task,scope_for(task,sources),authorization_id) + events = [{"operation":"real_interpret","source_sha256":sha,"provider_authorization_id":authorization_id} + for sha in history_plan["body"]["sources"]] + events.extend(({"operation":"real_compare"}, + {"operation":"real_start_followups","provider_authorization_id":authorization_id})) + from runtime.jobs import schedule + now = time.time() + triggers = [schedule(store,"event",{**payload,"scenario":history_plan["body"]["scenario"]}, + now+2*(index+1),f"real-history-{index}") for index,payload in enumerate(events)] + return store.put("real_schedule","journey-v1",{"history_plan":history_plan["sha256"],"followups":followups["sha256"], + "authorization_id":authorization_id,"triggers":triggers},followups["available_at"], + [history_plan["sha256"],followups["sha256"]]) + + +def handle(store, payload): + operation = payload["operation"] + if operation == "real_prepare": + return prepare_history(store, payload["scenario"]) + plan = store.get(payload["plan_sha256"]) if payload.get("plan_sha256") else store.find("real_plan", "history-v1") + if plan is None or plan["body"]["scenario"] != payload["scenario"]: + raise ValueError("A separate source-admission stage must complete first") + if plan["kind"] != "real_plan": + raise ValueError("An immutable real-model plan is required") + if operation == "real_prepare_followups": + return prepare_followups(store,plan) + if operation == "real_interpret": + return interpret_source(store, plan, payload["source_sha256"], payload["provider_authorization_id"]) + if operation == "real_compare": + return compare_history(store, plan) + if operation == "real_arrival_apply": + return apply_arrival(store,plan) + if operation in ("real_start_followups","real_start"): + followups = store.find("real_plan","followups-v1") + if followups is None: + raise ValueError("Follow-up source preparation must complete first") + if operation == "real_start": + return start_journey(store,plan,followups,payload["provider_authorization_id"]) + return start_followups(store,followups,payload["provider_authorization_id"]) + if operation == "real_event": + if payload["action"] not in ("outcome","acknowledge","investigate","resolve","deadline","staleness", + "model_update_prepare","model_update_complete","artifact_refresh"): + raise ValueError("Unsupported real follow-up action") + from runtime.capability_pipeline import handle as handle_event + return handle_event(store,{**payload,"operation":payload["action"]},task_record=store.get(plan["body"]["task"])) + raise ValueError("Unsupported real-model stage") diff --git a/runtime/real_semantics.py b/runtime/real_semantics.py new file mode 100644 index 0000000..b9cfeff --- /dev/null +++ b/runtime/real_semantics.py @@ -0,0 +1,231 @@ +"""Real Jev boundary with durable paid-request admission and fail-closed replay. + +No credential is loaded and no network call occurs without a matching, approved, +unexpired authorization. Unknown pricing permits only an explicitly approved +single-request probe; a money ceiling is not claimed for an unpriced request. +""" +from __future__ import annotations + +import asyncio +import os +import json +import math +from pathlib import Path +import time +from decimal import Decimal, InvalidOperation + +import psycopg +from psycopg.types.json import Jsonb + +from runtime.evidence import canonical_body +from runtime.jev import OpenRouterJevClassifier, answer_payload, request_id, usage_counts +from runtime.ledger import dsn +from runtime.observations import validate_response +from runtime.simulation import digest, encoded + + +def questions_for(task: dict, types) -> dict: + questions = {} + for question in task["questions"]: + if question["type"] == "boolean": + questions[question["id"]] = types.Noul(instructions=question["prompt"]) + else: + scale = task["policy"]["signal_scale"] + if scale != int(scale) or not 1 <= scale <= 10: + raise ValueError("Real Jev numeric questions require an explicit 1..10 ordinal scale") + questions[question["id"]] = types.Score(instructions=question["prompt"],criteria=list(range(int(scale)+1))) + return questions + + +def typed_answer(question: dict, answer: dict) -> dict: + if question["type"] == "boolean": + probability = answer["noul"] + response = {"status":"known","value":probability >= .5, + "distribution":{"true":probability,"false":1-probability},"reason":"actual_jev_response"} + else: + legend = {int(k):v for k,v in answer["legend"].items()} + probabilities = {int(k):v for k,v in answer["probabilities"].items()} + if set(probabilities) - set(legend): + raise ValueError("Jev score probability has no matching legend") + values = [legend[k] for k in sorted(probabilities)] + weights = [probabilities[k] for k in sorted(probabilities)] + response = {"status":"known","value":sum(v*p for v,p in zip(values,weights)), + "distribution":{"values":values,"probabilities":weights},"reason":"actual_jev_ordinal_expectation"} + validate_response(question,response) + return response + + +def scope_for(task_record: dict, sources: list[dict]) -> dict: + return {"task_sha256":task_record["sha256"],"source_sha256s":sorted(r["sha256"] for r in sources), + "questions_sha256":digest(task_record["body"]["questions"])} + + +def runtime_credential(task_id): + key = os.environ.get("OPENROUTER_API_KEY") + if key: + return key + path = os.environ.get("BACKINTEL_PROVIDER_CREDENTIAL_FILE") + if not path or not Path(path).is_file(): + return None + try: + record = json.loads(Path(path).read_text()) + expiry, tasks = record["expires_at"],record["task_ids"] + if type(expiry) not in (int,float) or not math.isfinite(expiry) or not isinstance(tasks,list) or not all(isinstance(t,str) for t in tasks): + raise ValueError("Invalid credential bounds") + if expiry <= time.time() or task_id not in tasks: + return None + key = record["key"] + return key if isinstance(key,str) and key.strip() else None + except (OSError,KeyError,TypeError,ValueError): + raise RuntimeError("Ephemeral provider credential record is invalid") from None + + +def _default_classifier(model: str, key): + if not key: + raise RuntimeError("Approved provider request requires ephemeral credential injection") + return OpenRouterJevClassifier(key,model=model) + + +def _verified_metadata(metadata: dict, requested_model: str) -> None: + expected_models = {requested_model, "typesafe/" + requested_model} + if requested_model == "jev-1.13": + # Exact alias resolution returned by the approved 2026-09-28 probe. + # Keep other dated revisions blocked until their identity is checked. + expected_models.add("typesafe/jev-1.13-20260917") + if not metadata.get("request_id") or metadata.get("model") not in expected_models: + raise RuntimeError("Actual Jev request/model identity is missing or differs; response retained without retry") + try: + cost = Decimal(str(metadata["cost_usd"])) + if not cost.is_finite() or cost < 0: + raise ValueError("Invalid charge") + except (KeyError,InvalidOperation,ValueError) as error: + raise RuntimeError("Actual provider charge unavailable; response retained and further calls blocked") from error + + +def usage_for(store, *, excluding=(), strict=True) -> dict: + records = store.connection.execute("""SELECT request_key,state,model,metadata FROM backintel.capability_model_requests + WHERE task_id=%s AND NOT (request_key=ANY(%s::text[]))""", (store.task_id, list(excluding))).fetchall() + spent = Decimal(0) + uncertain_calls = unknown_cost = False + for _, state, model, metadata in records: + try: + if state != "completed": + uncertain_calls = True + raise RuntimeError("Uncertain provider request prevents accepting a complete-cost result") + _verified_metadata(metadata, model) + spent += Decimal(str(metadata["cost_usd"])) + except RuntimeError: + if strict: + raise + unknown_cost = True + return {"provider_calls": None if uncertain_calls else len(records), + "provider_fixture_requests": sum(bool((r[3] or {}).get("test_fixture")) for r in records), + "provider_usd": None if unknown_cost else float(spent), "local_compute_usd": None, + "provider_requests_admitted": len(records), "provider_request_keys": [r[0] for r in records]} + + +def request_real(task_record: dict, source: dict, authorization_id: str, classifier_factory=None) -> dict: + task, provider = task_record["body"], task_record["body"]["observation_provider"] + if provider["implementation_mode"] != "real" or provider["name"] != "openrouter-jev": + raise ValueError("Real Jev execution requires the explicit OpenRouter provider contract") + content = source["body"]["content"] + key = digest({"task":task_record["sha256"],"source":source["sha256"],"provider":provider}) + with psycopg.connect(dsn(),autocommit=True) as connection: + # Session lock spans the external request without keeping an SQL transaction open. + connection.execute("SELECT pg_advisory_lock(hashtextextended(%s,71))",(authorization_id,)) + existing = connection.execute("SELECT state,response,metadata FROM backintel.capability_model_requests WHERE request_key=%s",(key,)).fetchone() + if existing: + if existing[0] != "completed": + raise RuntimeError("Previous provider request is uncertain/blocked; automatic paid retry prohibited") + _verified_metadata(existing[2],provider["version"]) + return {"request_key":key,"response":existing[1],"metadata":existing[2],"cached":True} + with connection.transaction(): + authorization = connection.execute("""SELECT model,max_requests,max_input_characters,max_measured_usd, + price_ceiling_known,approved,scope,scope_sha256,expires_at>now() + FROM backintel.capability_provider_authorizations WHERE authorization_id=%s FOR UPDATE""",(authorization_id,)).fetchone() + if not authorization or not authorization[5] or not authorization[8]: + raise PermissionError("Provider execution lacks current explicit approval") + model,limit,max_characters,max_usd,priced,_,scope,scope_sha,_ = authorization + if model != provider["version"] or digest(scope) != scope_sha or scope.get("task_sha256") != task_record["sha256"] or source["sha256"] not in scope.get("source_sha256s",[]) or scope.get("questions_sha256") != digest(task["questions"]): + raise PermissionError("Provider request exceeds approved source/question/model scope") + if not isinstance(content,str) or not content.strip() or len(content)>max_characters: + raise ValueError("Provider input violates approved character bound") + previous = connection.execute("SELECT state,metadata FROM backintel.capability_model_requests WHERE authorization_id=%s",(authorization_id,)).fetchall() + if len(previous)>=limit or (not priced and (limit != 1 or previous)): + raise RuntimeError("Provider request budget exhausted or unpriced multi-request execution prohibited") + spent = Decimal(0) + for state,metadata in previous: + if state != "completed": + raise RuntimeError("Uncertain prior provider request blocks further spend") + _verified_metadata(metadata,model) + spent += Decimal(str(metadata["cost_usd"])) + if max_usd is not None and spent >= max_usd: + raise RuntimeError("Measured provider budget exhausted") + # Record exact primitive definitions; the factory is created only after authorization checks. + from langchain_typesafe import Noul, Score + types = type("Questions",(),{"Noul":Noul,"Score":Score}) + question_objects = questions_for(task,types) + request_doc = {"model":model,"state":content,"questions":{k:v.model_dump(mode="json") for k,v in question_objects.items()}} + if len(encoded(request_doc))>25_000: + raise ValueError("Bounded provider payload exceeded") + credential = runtime_credential(task["id"]) if classifier_factory is None else None + if classifier_factory is None and not credential: + raise RuntimeError("Approved provider request requires ephemeral credential injection") + # The foreign key requires source admission to have committed before a paid request. + connection.execute("SET LOCAL lock_timeout='2s'") + connection.execute("""INSERT INTO backintel.capability_model_requests + (request_key,task_id,source_sha256,model,request,state,authorization_id) + VALUES (%s,%s,%s,%s,%s,'admitted',%s)""",(key,task["id"],source["sha256"],model,Jsonb(request_doc),authorization_id)) + classifier = None + started = time.perf_counter() + try: + classifier = classifier_factory(model) if classifier_factory else _default_classifier(model,credential) + response = classifier.invoke({"state":content,"questions":question_objects}) + answers = answer_payload(response) + metadata = dict(classifier.last_metadata) + metadata["request_id"] = metadata.get("request_id") or request_id(response) + metadata["model"] = metadata.get("model") or getattr(response,"model",None) + metadata["input_tokens"],metadata["output_tokens"] = usage_counts(response) + metadata.update(wall_ms=(time.perf_counter()-started)*1000,provider="openrouter",implementation_mode="real", + requested_model=model,provider_calls=1,local_compute_usd=None) + metadata["test_fixture"] = classifier_factory is not None + saved = canonical_body({"answers":answers,"raw_response":getattr(classifier,"last_response",{})}) + # Preserve a valid response even if charge/model checks subsequently block acceptance. + connection.execute("""UPDATE backintel.capability_model_requests SET state='completed',response=%s,metadata=%s,finished_at=now() + WHERE request_key=%s""",(Jsonb(saved),Jsonb(canonical_body(metadata)),key)) + except Exception as error: + connection.execute("""UPDATE backintel.capability_model_requests SET state='blocked',error=%s,finished_at=now() + WHERE request_key=%s AND state='admitted'""",(type(error).__name__,key)) + raise + finally: + if classifier is not None: + asyncio.run(classifier.aclose()) + _verified_metadata(metadata,model) + return {"request_key":key,"response":saved,"metadata":metadata,"cached":False} + + +def extract_real(store, task_record: dict, source: dict, at: int, *, authorization_id=None,classifier_factory=None) -> list[dict]: + authorization_id = authorization_id or os.environ.get("BACKINTEL_PROVIDER_AUTHORIZATION") + if not authorization_id: + raise PermissionError("Real extraction requires a named approved provider authorization") + reply = request_real(task_record,source,authorization_id,classifier_factory) + provider = task_record["body"]["observation_provider"] + response_record = store.find("provider_response",reply["request_key"]) + if not response_record: + response_record = store.put("provider_response",reply["request_key"],{ + "provider":provider,"request_key":reply["request_key"],"response":reply["response"],"metadata":reply["metadata"], + "source":source["sha256"],"task":task_record["sha256"]},at,[task_record["sha256"],source["sha256"]]) + observations = [] + for question in task_record["body"]["questions"]: + answer = reply["response"]["answers"].get(question["id"]) + if answer is None: + raise ValueError("Real Jev response omitted a required question") + response = typed_answer(question,answer) + key = digest({"source":source["sha256"],"task":task_record["sha256"],"question":question,"provider":provider}) + previous = store.find("observation",key) + observations.append(previous or store.put("observation",key,{ + "source":source["sha256"],"question_id":question["id"],"question":question,"provider":provider, + "response":response,"task":task_record["sha256"],"request_key":reply["request_key"], + "request_id":reply["metadata"]["request_id"],"actual_model":reply["metadata"]["model"]},at, + [source["sha256"],task_record["sha256"],response_record["sha256"]])) + return observations diff --git a/runtime/sandbox.py b/runtime/sandbox.py new file mode 100644 index 0000000..b595742 --- /dev/null +++ b/runtime/sandbox.py @@ -0,0 +1,123 @@ +"""Host-side runner for generated Python; never mount Docker access into workers.""" + +from __future__ import annotations + +import json +import os +from pathlib import Path +import selectors +import subprocess +import tempfile +import time +import uuid + +from runtime.simulation import digest, encoded + +LIMITS = {"seconds": 5, "memory_bytes": 268435456, "cpus": 1, + "processes": 32, "output_bytes": 32768, "input_bytes": 131072, + "source_bytes": 32768, "temporary_bytes": 16777216} + + +def resolve_image(image: str) -> str: + resolved = subprocess.run(["docker", "image", "inspect", image, "--format", "{{.Id}}"], + check=True, capture_output=True, text=True, timeout=10).stdout.strip() + if not resolved.startswith("sha256:") or len(resolved) != 71: + raise ValueError("Sandbox requires an existing immutable local image") + return resolved + + +def run_candidate(source: str, snapshot: dict, image: str) -> dict: + """Return an untrusted JSON candidate and a review receipt. Never publish it.""" + if not isinstance(snapshot, dict) or len(encoded(snapshot)) > LIMITS["input_bytes"]: + raise ValueError("Approved input snapshot exceeds sandbox contract") + if not isinstance(source, str) or len(source.encode()) > LIMITS["source_bytes"]: + raise ValueError("Generated source exceeds sandbox contract") + resolved = resolve_image(image) + name = "backintel-sandbox-" + uuid.uuid4().hex + started = time.monotonic() + receipt = {"schema": "backintel-sandbox-run/v1", "image": resolved, "limits": LIMITS, + "source": source, "source_sha256": digest(source), "input_sha256": digest(snapshot), + "status": "rejected", "review": "not_reviewed", "network": "none", + "host_mounts": "generated source and approved snapshot only", "result": None} + with tempfile.TemporaryDirectory(prefix="BackIntelSandbox") as temporary: + root = Path(temporary) + root.chmod(0o755) + (root / "source").mkdir(mode=0o755) + (root / "input").mkdir(mode=0o755) + script = root / "source/code.py" + inputs = root / "input/snapshot.json" + script.write_text(source) + inputs.write_bytes(encoded(snapshot)) + script.chmod(0o444) + inputs.chmod(0o444) + command = ["docker", "run", "--pull=never", "--name", name, "--rm", + "--network=none", "--read-only", "--user=65534:65534", + "--cap-drop=ALL", "--security-opt=no-new-privileges", + "--memory=256m", "--memory-swap=256m", "--cpus=1", "--pids-limit=32", + "--ulimit=nofile=64:64", "--ulimit=fsize=1048576:1048576", + "--tmpfs=/tmp:rw,nosuid,nodev,noexec,size=16m,mode=1777", + "--tmpfs=/app:ro,nosuid,nodev,noexec,size=1m", + "--workdir=/tmp", "--env=PYTHONDONTWRITEBYTECODE=1", + "--mount", f"type=bind,source={root / 'source'},target=/candidate,readonly", + "--mount", f"type=bind,source={root / 'input'},target=/input,readonly", + "--entrypoint=python", resolved, "-I", "-B", "-u", "/candidate/code.py"] + process = subprocess.Popen(command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + streams = {"stdout": bytearray(), "stderr": bytearray()} + reason = None + try: + with selectors.DefaultSelector() as selector: + for label, stream in (("stdout", process.stdout), ("stderr", process.stderr)): + os.set_blocking(stream.fileno(), False) + selector.register(stream, selectors.EVENT_READ, label) + deadline = time.monotonic() + LIMITS["seconds"] + while selector.get_map(): + remaining = deadline - time.monotonic() + if remaining <= 0: + reason = "time_limit" + break + for key, _ in selector.select(min(remaining, .1)): + chunk = os.read(key.fileobj.fileno(), 4096) + if not chunk: + selector.unregister(key.fileobj) + continue + capacity = LIMITS["output_bytes"] - sum(len(v) for v in streams.values()) + streams[key.data].extend(chunk[:capacity]) + if len(chunk) > capacity: + reason = "output_limit" + break + if reason: + break + if reason is None: + process.wait(timeout=max(.01, deadline - time.monotonic())) + except subprocess.TimeoutExpired: + reason = "time_limit" + finally: + # Kill the named container as well as its CLI. No writable host output exists. + try: + cleanup = subprocess.run(["docker", "rm", "--force", name], capture_output=True, timeout=10) + receipt["cleanup_confirmed"] = cleanup.returncode == 0 or b"No such container" in cleanup.stderr + except (OSError, subprocess.TimeoutExpired): + receipt["cleanup_confirmed"] = False + finally: + if process.poll() is None: + process.kill() + process.wait(timeout=5) + process.stdout.close() + process.stderr.close() + receipt.update(stdout=streams["stdout"].decode("utf-8", errors="replace"), + stderr=streams["stderr"].decode("utf-8", errors="replace"), + returncode=process.returncode, wall_seconds=time.monotonic() - started) + if not receipt["cleanup_confirmed"]: + reason = "cleanup_unconfirmed" + if reason is None and process.returncode != 0: + reason = "process_failed" + if reason is None: + try: + result = json.loads(receipt["stdout"], parse_constant=lambda value: (_ for _ in ()).throw(ValueError(value))) + if not isinstance(result, dict): + raise ValueError("Candidate must return a JSON object") + receipt.update(status="candidate", result=result) + except (ValueError, RecursionError): + reason = "invalid_output" + receipt["stop_reason"] = reason + return receipt diff --git a/runtime/simulation.py b/runtime/simulation.py new file mode 100644 index 0000000..671785b --- /dev/null +++ b/runtime/simulation.py @@ -0,0 +1,239 @@ +"""Dataset-independent capability simulation. No network, provider, or generated code execution.""" +from __future__ import annotations + +import csv +import hashlib +import html +import io +import json +import math +import os +import re +import tempfile +from pathlib import Path + +ENGINE_VERSION = "capability-simulation-v1" +SCENARIOS = Path(__file__).resolve().parents[1] / "config" / "simulation" + + +def encoded(value: object) -> bytes: + return (json.dumps(value, sort_keys=True, ensure_ascii=False, indent=2, allow_nan=False) + "\n").encode() + + +def digest(value: object) -> str: + return hashlib.sha256(encoded(value)).hexdigest() + + +def load_scenario(name: str) -> dict: + if not isinstance(name, str) or not re.fullmatch(r"[a-z][a-z0-9-]{0,40}", name): + raise ValueError("Scenario must be a local configuration name") + config = json.loads((SCENARIOS / f"{name}.json").read_text()) + if config.get("id") != name: + raise ValueError("Scenario ID must match its filename") + return config + + +def normalize(config: dict) -> list[dict]: + """Two real parsers, one mapped observation contract; extraction is a labeled rule stand-in.""" + if config.get("schema") != "backintel-simulation/v1": + raise ValueError("Unsupported simulation schema") + for key in ("id", "title", "signal_label", "audience"): + if not isinstance(config.get(key), str) or not config[key].strip(): + raise ValueError(f"Missing scenario {key}") + source = config["source"] + if source["format"] == "csv": + rows = list(csv.DictReader(io.StringIO(source["data"]))) + elif source["format"] == "json": + rows = source["data"] + else: + raise ValueError("Supported synthetic sources are CSV and JSON") + if not isinstance(rows, list) or not 1 <= len(rows) <= 100: + raise ValueError("A scenario requires 1..100 source rows") + fields, rule, policy = config["fields"], config["extractor"], config["policy"] + if set(fields) != {"id", "entity", "period", "content"} or any(not isinstance(v, str) for v in fields.values()): + raise ValueError("Map id, entity, period, and content source fields") + kind = rule.get("kind") + if kind == "keywords": + terms = rule.get("terms") + if not isinstance(terms, list) or not terms or any(not isinstance(t, str) or not t.strip() for t in terms): + raise ValueError("Keyword extraction requires nonempty terms") + elif kind == "above": + if type(rule.get("threshold")) not in (int, float) or not math.isfinite(rule["threshold"]): + raise ValueError("Numeric extraction requires a finite threshold") + else: + raise ValueError("Supported simulated extractors are keywords and above") + if type(policy.get("attention_rate")) not in (float, int) or not 0 < policy["attention_rate"] <= 1: + raise ValueError("Attention rate must be greater than zero and at most one") + if type(policy.get("stale_after")) is not int or policy["stale_after"] < 1: + raise ValueError("Staleness requires a positive simulated duration") + result, seen = [], set() + for position, row in enumerate(rows, 1): + if not isinstance(row, dict) or any(field not in row for field in fields.values()): + raise ValueError("Source row does not satisfy its field mapping") + mapped = {key: row[value] for key, value in fields.items()} + if any(not isinstance(mapped[key], str) or not mapped[key].strip() for key in ("id", "entity", "period")): + raise ValueError("Source IDs, entities, and periods must be nonempty strings") + identity = (mapped["period"], mapped["id"]) + if identity in seen: + raise ValueError("Duplicate source identity within one period") + seen.add(identity) + content = mapped["content"] + if content is None or (isinstance(content, str) and not content.strip()): + value = None + elif kind == "keywords": + if not isinstance(content, str) or len(content) > 5000: + raise ValueError("Text content must be a string of at most 5000 characters") + value = int(any(term.casefold() in content.casefold() for term in rule["terms"])) + else: + if isinstance(content, bool) or not isinstance(content, (str, int, float)): + raise ValueError("Numeric content must be finite") + number = float(content) + if not math.isfinite(number): + raise ValueError("Numeric content must be finite") + value = int(number > rule["threshold"]) + result.append({"record_id": mapped["id"], "entity": mapped["entity"], "period": mapped["period"], + "value": value, "mode": "simulated_rule_extraction", "source_row": position, + "source_format": source["format"], "source_sha256": digest(row), "source": row, + "definition_sha256": digest({"engine": ENGINE_VERSION, "fields": fields, "extractor": rule})}) + return result + + +def summarize(observations: list[dict]) -> dict: + known = [row for row in observations if row["value"] is not None] + flagged = sum(row["value"] for row in known) + return {"records": len(observations), "known": len(known), "unknown": len(observations) - len(known), + "flagged": flagged, "rate": flagged / len(known) if known else None} + + +def simulate(config: dict) -> dict: + observations = normalize(config) + events = config["events"] + if not isinstance(events, list) or not 1 <= len(events) <= 100: + raise ValueError("A scenario requires 1..100 events") + batches, timeline, episodes, failed_once = {}, [], [], set() + last_at, last_data_at, last_period = -1, None, None + current_episode = None + for event in events: + at, action = event.get("at"), event.get("action") + if type(at) is not int or at < 0 or at < last_at: + raise ValueError("Events must have nonnegative, chronological simulated times") + last_at = at + entry = {"at": at, "action": action} + if action in ("arrival", "schedule", "retry"): + period = event.get("period") + rows = [row for row in observations if row["period"] == period] + if not rows: + raise ValueError("Event references an absent source period") + entry["period"] = period + if period in batches: + entry["outcome"] = "reused" + elif event.get("fail_once", False) and period not in failed_once: + failed_once.add(period) + entry["outcome"] = "simulated_transient_failure" + else: + summary = summarize(rows) + previous = batches[last_period]["summary"] if last_period is not None else None + delta = (summary["rate"] - previous["rate"] + if previous and previous["rate"] is not None and summary["rate"] is not None else None) + batch = {"period": period, "summary": summary, "change": delta, + "entities": {entity: summarize([row for row in rows if row["entity"] == entity]) + for entity in sorted({row["entity"] for row in rows})}, + "observations": rows, + "projection": {"mode": "simulated_linear_extrapolation", "validated": False, + "next_rate": min(1, max(0, summary["rate"] + delta)) if delta is not None else None}} + batches[period] = batch + last_period, last_data_at = period, at + condition = ("unknown" if summary["rate"] is None else + "active" if summary["rate"] >= config["policy"]["attention_rate"] else "cleared") + if condition == "active" and (current_episode is None or current_episode["condition"] == "cleared"): + current_episode = {"id": f"{config['id']}:{period}", "condition": "active", "acknowledged": False, + "history": [], "delivery": "simulated_local_inbox"} + episodes.append(current_episode) + if current_episode is not None: + current_episode["condition"] = condition + current_episode["history"].append({"at": at, "condition": condition, "period": period}) + entry.update(outcome="completed", condition=condition if current_episode else "normal") + elif action == "acknowledge": + if current_episode is None or current_episode["condition"] == "cleared": + raise ValueError("Acknowledgment requires an open attention episode") + current_episode["acknowledged"] = True + current_episode["history"].append({"at": at, "action": "acknowledged", "condition": current_episode["condition"]}) + entry.update(outcome="acknowledged", condition=current_episode["condition"]) + elif action == "tick": + stale = last_data_at is not None and at - last_data_at >= config["policy"]["stale_after"] + if stale and current_episode is not None and current_episode["condition"] != "cleared": + current_episode["condition"] = "unknown" + current_episode["history"].append({"at": at, "condition": "unknown", "reason": "stale_data"}) + entry.update(outcome="stale" if stale else "fresh", condition=current_episode["condition"] if current_episode else "normal") + else: + raise ValueError("Unsupported simulation event") + timeline.append(entry) + if not batches: + raise ValueError("Simulation produced no completed batch") + if set(batches) != {row["period"] for row in observations}: + raise ValueError("Simulation ended with unprocessed periods; retry or schedule the remaining work") + return {"schema": "backintel-simulation-result/v1", "engine": ENGINE_VERSION, "scenario": config["id"], + "title": config["title"], "audience": config["audience"], "signal_label": config["signal_label"], + "mode": "synthetic_simulation", "input_sha256": digest({"engine": ENGINE_VERSION, "config": config}), + "batches": list(batches.values()), "timeline": timeline, "inbox": episodes, + "cost": {"provider_calls": 0, "provider_usd": 0, "local_compute_usd": None}, + "limits": ["All source records are synthetic.", "Extraction uses deterministic rules, not live model inference.", + "Triggers, clock, injected failure, and inbox delivery are simulated.", + "Linear extrapolation demonstrates a prediction output contract, not forecast quality.", + "Artifacts use a fixed renderer; generated-code execution is not implemented."]} + + +def render_html(result: dict) -> str: + esc = lambda value: html.escape(str(value), quote=True) + rows, evidence = [], [] + for batch in result["batches"]: + s = batch["summary"] + rate = "unknown" if s["rate"] is None else f"{s['rate']:.0%}" + change = "—" if batch["change"] is None else f"{batch['change'] * 100:+.0f} pp" + rows.append(f"{esc(batch['period'])}{s['flagged']}/{s['known']}" + f"{rate}{change}{s['unknown']}") + for row in batch["observations"]: + evidence.append(f"
{esc(row['period'])} · {esc(row['record_id'])} · {esc(row['entity'])}" + f"

Signal: {esc(row['value'])}; source row {row['source_row']}; simulated extraction.

" + f"
{esc(json.dumps(row['source'], ensure_ascii=False, indent=2))}
" + f"

Source SHA-256: {row['source_sha256']}

") + events = "".join(f"
  • t={row['at']}: {esc(row['action'])} {esc(row.get('period', ''))} → {esc(row['outcome'])}" + f" {esc(row.get('condition', ''))}
  • " for row in result["timeline"]) + return ("" + f"BackIntel simulation — {esc(result['title'])}

    SYNTHETIC SIMULATION · No live AI calls

    " + f"

    {esc(result['title'])}

    Audience: {esc(result['audience'])}. Signal: {esc(result['signal_label'])}.

    " + "

    Briefing

    " + "" + "" + + "".join(rows) + "
    Simulated periods and known-record denominators
    PeriodFlagged / knownRateChangeUnknown

    Background and attention timeline

      " + + events + "

    Analyst source evidence

    " + "".join(evidence) + + "

    Simulation boundaries

      " + + "".join(f"
    • {esc(limit)}
    • " for limit in result["limits"]) + + "

    Provider calls: 0. Provider charges: $0. Local compute cost: unpriced.

    ") + + +def publish(result: dict, directory: Path) -> dict: + directory.mkdir(parents=True, exist_ok=True) + stem = "simulation-" + digest(result) + artifacts = {} + for suffix, payload in (("json", encoded(result)), ("html", render_html(result).encode())): + destination = directory / f"{stem}.{suffix}" + if destination.exists(): + if destination.read_bytes() != payload: + raise ValueError("Existing simulation artifact differs from expected content") + else: + fd, temporary = tempfile.mkstemp(prefix=".simulation-", dir=directory) + try: + with os.fdopen(fd, "wb") as handle: + handle.write(payload) + os.replace(temporary, destination) + finally: + Path(temporary).unlink(missing_ok=True) + artifacts[suffix] = {"path": str(destination.resolve()), "sha256": hashlib.sha256(payload).hexdigest()} + return artifacts diff --git a/runtime/simulation_graph.py b/runtime/simulation_graph.py new file mode 100644 index 0000000..f56d2d2 --- /dev/null +++ b/runtime/simulation_graph.py @@ -0,0 +1,53 @@ +"""Run the same simulation through the existing LangGraph/application ledger.""" +from __future__ import annotations + +import asyncio +import os +import time +from pathlib import Path +from typing import TypedDict + +from langgraph.graph import END, START, StateGraph + +from runtime.costs import record_usage +from runtime.ledger import accept, admit +from runtime.simulation import ENGINE_VERSION, digest, load_scenario, normalize, publish, simulate + + +class Simulation(TypedDict, total=False): + scenario: str + partition_id: str + result: dict + report: dict + + +async def process(state: Simulation) -> Simulation: + config = load_scenario(state["scenario"]) + identifier = state["partition_id"] + if not isinstance(identifier, str) or not identifier.startswith("simulation-") or len(identifier) > 80: + raise ValueError("Simulation partition must start with simulation- and contain at most 80 characters") + count = len(normalize(config)) + fingerprint = digest({"engine": ENGINE_VERSION, "config": config}) + started, outcome = time.monotonic(), "error" + try: + await admit(identifier, count, fingerprint) + result = await asyncio.to_thread(simulate, config) + await accept(identifier, count, fingerprint, digest(result)) + output = Path(os.environ.get("BACKINTEL_REPORT_DIR", "/reports")) / "Simulation" + report = await asyncio.to_thread(publish, result, output) + outcome = "success" + return {"result": result, "report": report} + except asyncio.CancelledError: + outcome = "cancelled" + raise + finally: + await record_usage(partition_id=identifier, stage="runtime", provider="local", outcome=outcome, + record_count=count, wall_ms=round((time.monotonic() - started) * 1000), + charge_status="not_applicable") + + +builder = StateGraph(Simulation) +builder.add_node("simulate_capabilities", process) +builder.add_edge(START, "simulate_capabilities") +builder.add_edge("simulate_capabilities", END) +graph = builder.compile() diff --git a/runtime/synthetic.py b/runtime/synthetic.py new file mode 100644 index 0000000..a099420 --- /dev/null +++ b/runtime/synthetic.py @@ -0,0 +1,22 @@ +"""Reproducible, explicitly synthetic histories for the two configured domains.""" +from runtime.simulation import load_scenario + + +def history(name: str, count=24) -> tuple[dict, list[dict], list[dict]]: + task = load_scenario(name)["task"] + rows, outcomes = [], [] + support = name == "support" + for i in range(count): + at = i * 3 + structured = (i * 7 % 11) / 10 + semantic = (i * 5 % 7) / 6 + entity = ("Accounts" if i % 2 == 0 else "Access") if support else ("Pump-A" if i % 2 == 0 else "Pump-B") + values = {"id": f"{name}-{i:03}", "entity": entity, "event_at": at, "available_at": at, + "revision": 1, "content": ("Login failed" if semantic >= .5 else "Service working") if support else str(semantic * 10)} + row = {task["fields"][k]: v for k, v in values.items()} + row[task["measures"][0]["field"]] = structured * (100 if support else 80) + (0 if support else 20) + rows.append(row) + outcomes.append({"source_id": values["id"], "entity": entity, "event_at": at, + "available_at": at + task["target"]["horizon"], "revision": 1, + "value": int(structured + semantic >= 1) if support else round(2 * structured + 3 * semantic, 6)}) + return task, rows, outcomes diff --git a/scripts/audience_demo.py b/scripts/audience_demo.py new file mode 100644 index 0000000..32e03cc --- /dev/null +++ b/scripts/audience_demo.py @@ -0,0 +1,35 @@ +"""Serve scoped local reports; private tokens never appear in URLs or logs.""" + +import argparse +import os +from pathlib import Path + +import psycopg + +from runtime.audience_server import AudienceServer, issue_grants +from runtime.ledger import dsn +from runtime.simulation import encoded + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--task", action="append", required=True) + parser.add_argument("--port", type=int, default=2028) + parser.add_argument("--access-file", type=Path, required=True, help="New private file for one-hour local audience tokens") + args = parser.parse_args() + with psycopg.connect(dsn(), autocommit=True) as connection: + grants = issue_grants(connection, args.task) + server = AudienceServer(dsn(), grants, args.port) + args.access_file.parent.mkdir(parents=True, exist_ok=True) + descriptor = os.open(args.access_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + with os.fdopen(descriptor, "wb") as output: + output.write(encoded({"login": server.origin + "/login", "grants": grants})) + print(f"Local reports: {server.origin}/login\nPrivate access file: {args.access_file.resolve()}", flush=True) + try: + server.serve_forever() + finally: + server.server_close() + + +if __name__ == "__main__": + main() diff --git a/scripts/capability_demo.py b/scripts/capability_demo.py new file mode 100644 index 0000000..40c7e8d --- /dev/null +++ b/scripts/capability_demo.py @@ -0,0 +1,196 @@ +"""Start the isolated capability development journey; real-model completion remains separate.""" +from __future__ import annotations + +import argparse +from datetime import datetime, timedelta, timezone +import json +from pathlib import Path +import re +import subprocess +import time +import uuid + +import psycopg + +from scripts.jev.run_review_batch import request +from runtime.simulation import digest, encoded + +ROOT = Path(__file__).resolve().parents[1] +OUTPUT = ROOT / "artifacts" / "validation" / "CapabilityDemo" +BASE = "http://127.0.0.1:2027" +DEMO_DSN = "postgresql://capability_demo@127.0.0.1:55437/test_backintel_demo" + + +def status(connection, demo_id="development-v1") -> dict: + task_ids = [f"{name}-{demo_id}" for name in ("support","equipment")] + jobs = connection.execute("""SELECT task_id,state,count(*) FROM backintel.capability_jobs + WHERE task_id=ANY(%s) GROUP BY task_id,state ORDER BY task_id,state""",(task_ids,)).fetchall() + pending = connection.execute("SELECT count(*) FROM backintel.capability_triggers WHERE state='pending' AND task_id=ANY(%s)",(task_ids,)).fetchone()[0] + errors = connection.execute("""SELECT task_id,payload->>'operation',error FROM backintel.capability_jobs + WHERE state='failed' AND task_id=ANY(%s)""",(task_ids,)).fetchall() + running = sum(count for _,state,count in jobs if state in ("queued","running","retry")) + counts = connection.execute("""SELECT task_id,kind,count(*) FROM backintel.capability_evidence + WHERE task_id=ANY(%s) AND kind IN ('prediction','comparison','delivery','model_update','artifact','artifact_candidate') + GROUP BY task_id,kind ORDER BY task_id,kind""",(task_ids,)).fetchall() + modes = {r[0] for r in connection.execute("""SELECT DISTINCT body->>'implementation_mode' + FROM backintel.capability_evidence WHERE task_id=ANY(%s) AND kind='model'""",(task_ids,))} + return {"status":"failed" if errors else "passed" if jobs and not pending and not running else "running", + "jobs":[{"scenario":task,"state":state,"count":count} for task,state,count in jobs], + "pending_triggers":pending,"errors":errors, + "counts":[{"scenario":task,"kind":kind,"count":count} for task,kind,count in counts], + "model_execution":"real_local_predictors" if "real" in modes else "simulated_development_only", + "final_real_model_acceptance":"blocked"} + + +def wait_for_completion(timeout=120, demo_id="development-v1") -> dict: + with psycopg.connect(DEMO_DSN,autocommit=True) as connection: + connection.execute("LISTEN backintel_capability_jobs") + deadline = time.monotonic()+timeout + while True: + result = status(connection,demo_id) + if result["status"] != "running": + return result + remaining = deadline-time.monotonic() + if remaining <= 0: + return {**result,"status":"blocked","reason":"Bounded background completion timeout"} + # Database notifications wait for actual work completion; no polling/sleep loop. + next(connection.notifies(timeout=remaining,stop_after=1),None) + + +def domain_snapshot(connection, task_ids): + evidence = [row[0] for row in connection.execute("SELECT sha256 FROM backintel.capability_evidence WHERE task_id=ANY(%s) ORDER BY sha256", (task_ids,)).fetchall()] + jobs = connection.execute("SELECT job_id,state,attempts,result_sha256 FROM backintel.capability_jobs WHERE task_id=ANY(%s) ORDER BY sequence", (task_ids,)).fetchall() + return {"evidence": evidence, "jobs": jobs, "digest": digest([evidence, jobs])} + + +def restart_with_pending_work(task_ids, output): + with psycopg.connect(DEMO_DSN, autocommit=True) as connection: + connection.execute("LISTEN backintel_capability_jobs") + deadline = time.monotonic() + 45 + while True: + completed = connection.execute("SELECT count(*) FROM backintel.capability_jobs WHERE task_id=ANY(%s) AND payload->>'operation'='bootstrap' AND state='completed'", (task_ids,)).fetchone()[0] + if completed == len(task_ids): + break + remaining = deadline - time.monotonic() + if remaining <= 0: + raise TimeoutError("Bootstrap did not finish before the bounded restart check") + next(connection.notifies(timeout=remaining, stop_after=1), None) + pending = connection.execute("SELECT count(*) FROM backintel.capability_triggers WHERE task_id=ANY(%s) AND state='pending'", (task_ids,)).fetchone()[0] + if not pending: + raise ValueError("Restart proof requires a fresh demonstration with pending work") + before = domain_snapshot(connection, task_ids) + (output / "RestartBefore.json").write_bytes(encoded(before)) + with (output / "Restart.log").open("w") as log: + subprocess.run(["docker", "compose", "-f", "compose.capabilities.yml", "restart", "runtime"], cwd=ROOT, + stdout=log, stderr=subprocess.STDOUT, check=True, timeout=60) + subprocess.run(["docker", "compose", "-f", "compose.capabilities.yml", "up", "--detach", "--wait", "--no-build", "--no-recreate", "runtime"], cwd=ROOT, + stdout=log, stderr=subprocess.STDOUT, check=True, timeout=90) + return {"status": "restarted", "pending_triggers_before": pending, "evidence_before": len(before["evidence"]), + "before_sha256": before["digest"], "records": before["evidence"]} + + +def verify_replay(connection, assistant, runs, demo_id): + task_ids = [f"{name}-{demo_id}" for name in ("support", "equipment")] + before = domain_snapshot(connection, task_ids) + outputs = [] + for run in runs: + response = request("POST", f"/threads/{run['thread_id']}/runs/wait", { + "assistant_id": assistant, "input": {"operation": "bootstrap", "scenario": run["scenario"], + "demo_id": demo_id, "request_id": "capability-bootstrap-v1"}}, base=BASE) + outputs.append(response) + after = domain_snapshot(connection, task_ids) + if before["digest"] != after["digest"]: + raise AssertionError("Replay changed accepted evidence, job attempts, or delivery history") + return {"status": "passed", "before_sha256": before["digest"], "after_sha256": after["digest"], + "evidence_records": len(after["evidence"]), "responses": outputs} + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--development",action="store_true",help="Explicitly permit simulated model fixtures") + parser.add_argument("--no-start",action="store_true",help="Use the already-running isolated service") + parser.add_argument("--wait",action="store_true",help="Wait for persisted due work using database notifications") + parser.add_argument("--status",action="store_true",help="Inspect without submitting work") + parser.add_argument("--verify-recovery",action="store_true",help="Restart only the isolated runtime with pending work, then verify replay") + parser.add_argument("--serve",action="store_true",help="After completion and packaging, serve scoped local reports until Ctrl-C") + parser.add_argument("--demo-id",default="development-v1",help="Persistent demonstration identity; reuse resumes the same work") + args = parser.parse_args() + if not re.fullmatch(r"[a-z][a-z0-9-]{0,24}", args.demo_id): + parser.error("Invalid demonstration identity") + if not args.development and not args.status: + parser.error("Real Jev/CatBoost/TabICLv2 integration is unfinished. --development runs fixtures and cannot satisfy final acceptance.") + OUTPUT.mkdir(parents=True,exist_ok=True) + if args.status: + with psycopg.connect(DEMO_DSN,autocommit=True) as connection: + result = status(connection,args.demo_id) + print(json.dumps(result,indent=2)) + return 1 if result["status"] == "failed" else 0 + output = OUTPUT / args.demo_id / ("Attempt" + uuid.uuid4().hex[:12]) + output.mkdir(parents=True, exist_ok=True) + output.chmod(0o700) + if not args.no_start: + with (output / "Startup.log").open("w") as log: + subprocess.run(["docker","compose","-f","compose.capabilities.yml","up","--build","--detach","--wait","--wait-timeout","120"], + cwd=ROOT,stdout=log,stderr=subprocess.STDOUT,check=True,timeout=150) + assistants = request("POST","/assistants/search",{"graph_id":"capability_platform","limit":1},base=BASE) + if not assistants: + raise RuntimeError("Isolated service lacks capability_platform") + assistant = assistants[0]["assistant_id"] + receipts = [] + for name in ("support","equipment"): + thread = request("POST","/threads",{},base=BASE) + run = request("POST",f"/threads/{thread['thread_id']}/runs",{ + "assistant_id":assistant,"input":{"operation":"bootstrap","scenario":name,"demo_id":args.demo_id,"request_id":"capability-bootstrap-v1"}},base=BASE) + receipts.append({"scenario":name,"thread_id":thread["thread_id"],"run_id":run["run_id"]}) + crons = request("POST","/runs/crons/search",{"assistant_id":assistant,"limit":100},base=BASE) + cron = next((c for c in crons if c.get("metadata",{}).get("purpose") == "bounded-capability-demo"),None) + end = (datetime.now(timezone.utc)+timedelta(seconds=120)).isoformat() + if cron is None: + cron = request("POST","/runs/crons",{"assistant_id":assistant,"schedule":"*/2 * * * * *", "enabled":False, + "input":{"operation":"dispatch"},"metadata":{"purpose":"bounded-capability-demo"},"end_time":end},base=BASE) + cron = request("PATCH",f"/runs/crons/{cron['cron_id']}",{"enabled":True,"end_time":end},base=BASE) + receipt = {"base_url":BASE,"runs":receipts,"scheduler":"aegra_native_cron","cron_id":cron["cron_id"],"end_time":end, + "mode":"synthetic_sources_simulated_models","real_model_acceptance":"blocked","demo_id":args.demo_id} + (output / "Submission.json").write_bytes(encoded(receipt)) + print(json.dumps(receipt,indent=2),flush=True) + if args.wait or args.verify_recovery or args.serve: + result = {"status": "running", "candidate": "uncommitted-working-tree", "final_real_model_acceptance": "blocked"} + task_ids = [f"{name}-{args.demo_id}" for name in ("support", "equipment")] + try: + recovery = restart_with_pending_work(task_ids, output) if args.verify_recovery else None + result.update(wait_for_completion(demo_id=args.demo_id)) + if result["status"] != "passed": + raise RuntimeError("Background stream did not complete; inspect retained job errors") + with psycopg.connect(DEMO_DSN, autocommit=True) as connection: + if recovery: + after = domain_snapshot(connection, task_ids) + if not set(recovery.pop("records")).issubset(after["evidence"]): + raise AssertionError("Restart lost accepted evidence") + recovery.update(status="passed", evidence_after=len(after["evidence"]), after_sha256=after["digest"]) + result["restart"] = recovery + result["replay"] = verify_replay(connection, assistant, receipts, args.demo_id) + from scripts.package_capabilities import package + packaged = package(connection, task_ids, output / "Package") + result["package"] = {"status": packaged["status"], "path": str(output / "Package/Package.json"), "files": len(packaged["files"])} + except Exception as exc: + result.update(status="failed", error={"type": type(exc).__name__, "message": str(exc)}) + raise + finally: + (output / "Integration.json").write_bytes(encoded(result)) + request("PATCH",f"/runs/crons/{cron['cron_id']}",{"enabled":False},base=BASE) + print(json.dumps({"status": result["status"], "receipt": str(output / "Integration.json")}, indent=2), flush=True) + if args.serve: + from runtime.audience_server import AudienceServer + grants = json.loads((output / "Package/AudienceAccess.json").read_text())["grants"] + server = AudienceServer(DEMO_DSN, grants) + print(f"Local reports: {server.origin}/login\nPrivate tokens: {output / 'Package/AudienceAccess.json'}", flush=True) + try: + server.serve_forever() + finally: + server.server_close() + return 0 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/package_capabilities.py b/scripts/package_capabilities.py new file mode 100644 index 0000000..51deb1e --- /dev/null +++ b/scripts/package_capabilities.py @@ -0,0 +1,101 @@ +"""Package scoped, source-linked local artifacts from completed background work.""" + +from __future__ import annotations + +import hashlib +import os +from pathlib import Path + +from runtime.artifacts import export_csv, get_artifact, render +from runtime.audience_server import issue_grants +from runtime.evidence import Evidence +from runtime.real_semantics import usage_for +from runtime.generated_artifacts import DEVELOPMENT_SOURCE, create_candidate, render_candidate, review_candidate +from runtime.simulation import encoded + + +def package(connection, task_ids: list[str], output: Path, *, mode="development") -> dict: + if mode not in ("development","real","predictor_fixture"): + raise ValueError("Explicit supported model packaging mode required") + output.mkdir(parents=True, exist_ok=True) + output.chmod(0o700) + receipt = {"schema": "backintel-capability-package/v1", "status": "running", "tasks": [], "files": [], + "data": "synthetic", "models": "simulated_development_only", "code_generation": "simulated", + "local_review": "simulated_operator", "paid_provider_calls": 0, "local_compute_usd": None, + "final_real_model_acceptance": "blocked", "exact_commit_acceptance": "blocked"} + receipt.update(mode=mode,semantic_observations="simulated" if mode == "development" else "fixture" if mode == "predictor_fixture" else "real_jev", + models="simulated_development_only" if mode == "development" else "actual_local_predictors_and_native_baseline", + provider_usd=0,provider_fixture_requests=0,fixture_provider_usd=0) + + def save(path, value): + path.parent.mkdir(parents=True, exist_ok=True) + data = value.encode() if isinstance(value, str) else encoded(value) + path.write_bytes(data) + receipt["files"].append({"path": str(path.relative_to(output)), "sha256": hashlib.sha256(data).hexdigest(), "bytes": len(data)}) + + try: + for task_id in task_ids: + store = Evidence(connection, task_id) + task = max(store.list("task"), key=lambda r: r["available_at"]) + usage = usage_for(store) + fixtures = usage["provider_fixture_requests"] + if mode == "development": + if task["body"]["observation_provider"]["implementation_mode"] != "simulated" or usage["provider_calls"]: + raise ValueError("Development packaging requires simulated semantic extraction") + else: + routes = {(m["body"]["route"],m["body"]["feature_set"]) for m in store.list("model") if m["body"].get("implementation_mode") == "real"} + expected = {(r,f) for r in ("catboost","tabiclv2") for f in ("structured","semantic")} + if task["body"]["observation_provider"]["implementation_mode"] != "real" or not expected.issubset(routes): + raise ValueError("Actual predictor packaging requires all four real model routes") + if not usage["provider_calls"] or (mode == "real" and fixtures) or (mode == "predictor_fixture" and fixtures != usage["provider_calls"]): + raise ValueError("Semantic provider evidence does not match the explicit packaging mode") + receipt["provider_fixture_requests"] += fixtures + if fixtures: + receipt["fixture_provider_usd"] += usage["provider_usd"] + else: + receipt["paid_provider_calls"] += usage["provider_calls"] + receipt["provider_usd"] += usage["provider_usd"] + semantic_note = {"development":"Text findings: simulated", "real":"Text findings: real Jev responses", + "predictor_fixture":"Text findings: deterministic test fixtures; no paid Jev calls"}[mode] + records = [] + for audience in task["body"]["audiences"]: + artifact = get_artifact(store, audience["id"]) + allowed = {"simulated","native_baseline"} if mode == "development" else {"real","native_baseline"} + if any(r["prediction"] and r["prediction"]["body"]["implementation_mode"] not in allowed for r in artifact["body"]["rows"]): + raise ValueError("Packaging mode must match actual predictor execution") + folder = output / "Reports" / task_id / audience["id"] + save(folder / "Report.json", artifact) + save(folder / "Report.html", render(artifact, store.list("artifact_review"), interactive=False, semantic_note=semantic_note)) + save(folder / "Export.csv", export_csv(artifact)) + for row in artifact["body"]["rows"]: + save(folder / "Evidence" / (row["source"] + ".json"), artifact["body"]["evidence"][row["source"]]) + records.append({"audience": audience["id"], "artifact_sha256": artifact["sha256"], "visible_entities": len(artifact["body"]["rows"])}) + artifact = get_artifact(store, "operator") + candidate = create_candidate(store, artifact, "operator", DEVELOPMENT_SOURCE) + if candidate["body"]["run"]["status"] != "candidate": + raise RuntimeError("Generated view failed its actual sandbox or source checks") + review = review_candidate(store, candidate, "operator", "accepted", + "Simulated operator accepts the fixed development generator after source-reference validation", + artifact["available_at"] + 1) + folder = output / "Reports" / task_id / "operator" + save(folder / "GeneratedView.json", candidate) + save(folder / "GeneratedView.html", render_candidate(candidate, [review], interactive=False, semantic_note=semantic_note)) + save(folder / "GeneratedView.py", DEVELOPMENT_SOURCE) + save(folder / "GeneratedReview.json", review) + counts = connection.execute("SELECT kind,count(*) FROM backintel.capability_evidence WHERE task_id=%s GROUP BY kind ORDER BY kind", (task_id,)).fetchall() + jobs = connection.execute("SELECT payload->>'operation',state,attempts,wall_ms,error FROM backintel.capability_jobs WHERE task_id=%s ORDER BY sequence", (task_id,)).fetchall() + receipt["tasks"].append({"task_id": task_id, "artifacts": records, "candidate_sha256": candidate["sha256"], + "evidence_counts": dict(counts), "stages": [dict(zip(("stage", "state", "attempts", "wall_ms", "error"), row)) for row in jobs], + "simulated_observation_attempts": len(store.list("extraction_attempt"))}) + grants = issue_grants(connection, task_ids) + access = output / "AudienceAccess.json" + with os.fdopen(os.open(access, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600), "wb") as handle: + handle.write(encoded({"grants": grants})) + receipt["private_access_file"] = str(access) + receipt["status"] = "passed" + except Exception as exc: + receipt.update(status="failed", error={"type": type(exc).__name__, "message": str(exc)}) + raise + finally: + (output / "Package.json").write_bytes(encoded(receipt)) + return receipt diff --git a/scripts/prepare_real_models.py b/scripts/prepare_real_models.py new file mode 100644 index 0000000..90ab9aa --- /dev/null +++ b/scripts/prepare_real_models.py @@ -0,0 +1,35 @@ +"""Download only the explicitly approved, hash-pinned TabICLv2 checkpoints.""" +import json +import os +from pathlib import Path + +from huggingface_hub import hf_hub_download + +from runtime.real_models import CONFIG,_allow_model_use,file_sha,model_root +from runtime.simulation import encoded + + +def main() -> int: + config = _allow_model_use("tabiclv2") + _allow_model_use("catboost") + directory = model_root()/"Weights" + directory.mkdir(parents=True,exist_ok=True) + receipts = [] + for spec in config["tabiclv2"]["checkpoints"].values(): + path = directory/spec["file"] + if not path.exists(): + downloaded = hf_hub_download(repo_id=config["tabiclv2"]["repository"],filename=spec["file"], + revision=config["tabiclv2"]["revision"],local_dir=directory,token=False) + path = Path(downloaded) + if path.stat().st_size != spec["bytes"] or file_sha(path) != spec["sha256"]: + raise ValueError("Downloaded checkpoint does not match the approved size/hash") + path.chmod(0o444) + receipts.append({**spec,"path":str(path),"license":config["tabiclv2"]["license"],"verified":True}) + result = {"status":"passed","model_execution":"not_yet_run","checkpoints":receipts,"paid_provider_calls":0} + (model_root()/"download-receipt.json").write_bytes(encoded(result)) + print(json.dumps(result,indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/real_capabilities.py b/scripts/real_capabilities.py new file mode 100644 index 0000000..ae3e3d0 --- /dev/null +++ b/scripts/real_capabilities.py @@ -0,0 +1,68 @@ +"""Submit explicit real-model stages to the isolated development API.""" + +from __future__ import annotations + +import argparse +import json +import uuid + +from scripts.capability_demo import BASE, ROOT +from scripts.jev.run_review_batch import request +from runtime.simulation import encoded + + +def submit(stage, scenario, demo_id, request_id, **fields): + assistants = request("POST", "/assistants/search", {"graph_id":"capability_platform","limit":1}, base=BASE) + if not assistants: + raise RuntimeError("The isolated API has no capability platform assistant") + thread = request("POST", "/threads", {}, base=BASE) + return request("POST", f"/threads/{thread['thread_id']}/runs/wait", { + "assistant_id": assistants[0]["assistant_id"], + "input": {"operation": "real_"+stage, "scenario": scenario, "demo_id": demo_id, + "request_id": request_id, **fields}}, base=BASE) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("stage", choices=("prepare", "prepare_followups", "interpret", "compare", "start_followups", "start")) + parser.add_argument("--scenario", choices=("support", "equipment"), required=True) + parser.add_argument("--demo-id", required=True) + parser.add_argument("--request-id", required=True) + parser.add_argument("--source-sha256") + parser.add_argument("--plan-sha256") + parser.add_argument("--authorization") + args = parser.parse_args() + fields = {} + if args.stage == "interpret": + if not args.source_sha256 or not args.authorization: + parser.error("Interpretation requires a source hash and separately approved authorization") + fields = {"source_sha256": args.source_sha256, "provider_authorization_id": args.authorization} + if args.plan_sha256: + fields["plan_sha256"] = args.plan_sha256 + elif args.stage in ("start_followups","start"): + if not args.authorization or args.source_sha256 or args.plan_sha256: + parser.error("Starting follow-ups requires only the matching approved authorization") + fields = {"provider_authorization_id":args.authorization} + elif args.source_sha256 or args.authorization or args.plan_sha256: + parser.error("Source and authorization arguments apply only to interpretation") + output = ROOT / "artifacts/validation/RealPipeline" / ("Attempt"+uuid.uuid4().hex[:12]) + output.mkdir(parents=True) + receipt = {"schema": "backintel-real-stage/v1", "stage": args.stage, "scenario": args.scenario, + "demo_id": args.demo_id, "request_id":args.request_id, "fields":fields, + "candidate": "uncommitted-working-tree", "status": "running"} + try: + reply = submit(args.stage,args.scenario,args.demo_id,args.request_id,**fields) + receipt.update(reply=reply, status="passed" if reply.get("result",{}).get("state") == "completed" else "blocked") + except Exception as exc: + receipt.update(status="blocked",error={"type":type(exc).__name__,"message":str(exc)}, + state="Transport failure does not establish whether the durable job completed; replay this request identity.") + raise + finally: + path = output / "Stage.json" + path.write_bytes(encoded(receipt)) + print(json.dumps({"status":receipt["status"],"receipt":str(path)})) + return 0 if receipt["status"] == "passed" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/real_demo.py b/scripts/real_demo.py new file mode 100644 index 0000000..7a32940 --- /dev/null +++ b/scripts/real_demo.py @@ -0,0 +1,305 @@ +"""Prepare or run both real-model scenarios through the isolated native scheduler.""" + +from __future__ import annotations + +import argparse +from datetime import datetime, timezone +import json +import os +from pathlib import Path +import re +import subprocess +import time +import uuid + +import psycopg + +from runtime.evidence import Evidence +from runtime.jobs import execute +from runtime.real_pipeline import require_approved_scope +from runtime.real_semantics import scope_for, usage_for +from runtime.simulation import encoded +from scripts.capability_demo import BASE, DEMO_DSN, ROOT, domain_snapshot, status +from scripts.jev.run_review_batch import request +from scripts.package_capabilities import package +from scripts.real_capabilities import submit + +SCENARIOS = ("support","equipment") +CREDENTIAL_PATH = "/run/backintel-credentials/openrouter.json" +RUN_LOCK = 81827027 + + +def startup(output): + active = subprocess.run(["docker","compose","-f","compose.capabilities.yml","ps","--status","running","--quiet","runtime"], + cwd=ROOT,capture_output=True,text=True,check=True,timeout=15).stdout.strip() + connection = psycopg.connect(DEMO_DSN,autocommit=True) if active else None + try: + if connection: + if not connection.execute("SELECT pg_try_advisory_lock(%s)",(RUN_LOCK,)).fetchone()[0]: + raise RuntimeError("Another real-run command owns the isolated runtime") + pending = connection.execute("""SELECT (SELECT count(*) FROM backintel.capability_jobs WHERE state IN ('queued','running','retry')) + + (SELECT count(*) FROM backintel.capability_triggers WHERE state='pending')""").fetchone()[0] + if pending: + raise RuntimeError("Development work is pending. Resume the existing runtime with --no-start; startup will not replace it.") + environment = {**os.environ,"BACKINTEL_WITH_MODELS":"1"} + with (output/"Startup.log").open("w") as log: + subprocess.run(["docker","compose","-f","compose.capabilities.yml","up","--build","--detach","--wait","--wait-timeout","120"], + cwd=ROOT,env=environment,stdout=log,stderr=subprocess.STDOUT,check=True,timeout=600) + finally: + if connection: + connection.close() + + +def prepare(connection,demo_id): + proposals = {} + for scenario in SCENARIOS: + for stage in ("prepare","prepare_followups"): + result = submit(stage,scenario,demo_id,"real-run-"+stage+"-v1")["result"] + if result["state"] != "completed": + raise RuntimeError("Source preparation did not complete: "+str(result)) + store = Evidence(connection,f"{scenario}-real-{demo_id}") + history = store.find("real_plan","history-v1") + followups = store.find("real_plan","followups-v1") + task = store.get(history["body"]["task"]) + sources = [store.get(sha) for sha in history["body"]["sources"]+followups["body"]["sources"]] + proposals[scenario] = {"task_id":store.task_id,"model":task["body"]["observation_provider"]["version"], + "scope":scope_for(task,sources),"source_versions":len(sources),"maximum_new_requests":len(sources), + "maximum_input_characters":max(len(r["body"]["content"]) for r in sources), + "questions":task["body"]["questions"], + "inputs":[{"source_sha256":r["sha256"],"content":r["body"]["content"],"available_at":r["available_at"]} for r in sources], + "history_plan":history["sha256"],"followup_plan":followups["sha256"], + "approval":"separate approval required; this proposal creates no authorization"} + return proposals + + +def preflight(connection,proposals,authorizations,deadline): + if set(authorizations) != set(SCENARIOS) or any(not isinstance(v,str) or not 1 <= len(v) <= 128 for v in authorizations.values()): + raise ValueError("Authorization file must map support and equipment to existing approved authorization IDs") + remaining = 0 + for scenario,proposal in proposals.items(): + store = Evidence(connection,proposal["task_id"]) + usage = usage_for(store) + if usage["provider_fixture_requests"]: + raise PermissionError("The real run cannot use fixture semantic history") + completed = {r[0] for r in connection.execute("SELECT source_sha256 FROM backintel.capability_model_requests WHERE task_id=%s AND state='completed'",(store.task_id,))} + missing = set(proposal["scope"]["source_sha256s"])-completed + if not missing: + continue + authorization = authorizations[scenario] + task = store.get(proposal["scope"]["task_sha256"]) + require_approved_scope(store,task,proposal["scope"],authorization) + limit,characters,priced,expires,max_usd = connection.execute("""SELECT max_requests,max_input_characters,price_ceiling_known,expires_at,max_measured_usd + FROM backintel.capability_provider_authorizations WHERE authorization_id=%s""",(authorization,)).fetchone() + used,spent = connection.execute("SELECT count(*),coalesce(sum((metadata->>'cost_usd')::numeric),0) FROM backintel.capability_model_requests WHERE authorization_id=%s",(authorization,)).fetchone() + if limit-used < len(missing) or characters < proposal["maximum_input_characters"] or (not priced and (limit != 1 or len(missing) != 1)): + raise PermissionError("Prepared batch exceeds the approved request, character or known-price limits") + if max_usd is not None and spent >= max_usd: + raise PermissionError("The approved measured provider budget is exhausted") + deadline = min(deadline,expires.timestamp()) + remaining += len(missing) + if remaining and deadline <= time.time(): + raise PermissionError("The approved execution window expired") + return remaining,deadline + + +def install_credential(credential,task_ids,deadline,owner_id): + # The value travels over stdin into container tmpfs, never argv, logs or Docker environment configuration. + program = """import json,os,sys,time +from pathlib import Path +path=Path('/run/backintel-credentials/openrouter.json') +assert any(row.split()[1:3]==['/run/backintel-credentials','tmpfs'] for row in Path('/proc/mounts').read_text().splitlines()), 'Credential mount must be tmpfs' +record=json.load(sys.stdin) +if path.exists(): + previous=json.loads(path.read_text()) + if previous.get('owner_id')!=record['owner_id'] and previous['expires_at']>time.time(): + raise RuntimeError('Another run owns the live runtime credential') +temporary=path.with_name(record['owner_id']+'.tmp') +with os.fdopen(os.open(temporary,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600),'w') as target: + json.dump(record,target) +os.replace(temporary,path) +""" + subprocess.run(["docker","compose","-f","compose.capabilities.yml","exec","-T","runtime","python","-c",program], + cwd=ROOT,input=json.dumps({"key":credential,"task_ids":task_ids,"expires_at":deadline,"owner_id":owner_id}),text=True, + stdout=subprocess.PIPE,stderr=subprocess.PIPE,check=True,timeout=15) + + +def clear_credential(owner_id): + subprocess.run(["docker","compose","-f","compose.capabilities.yml","exec","-T","runtime","python","-c", + "import json,sys; from pathlib import Path; p=Path('/run/backintel-credentials/openrouter.json'); owner=sys.stdin.read(); p.unlink() if p.exists() and json.loads(p.read_text()).get('owner_id')==owner else None; p.with_name(owner+'.tmp').unlink(missing_ok=True)"], + cwd=ROOT,input=owner_id,text=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE,check=True,timeout=15) + + +def scheduler(assistant,task_ids,demo_id,deadline): + purpose = "backintel-real-"+demo_id + crons = request("POST","/runs/crons/search",{"assistant_id":assistant,"limit":100},base=BASE) + cron = next((c for c in crons if c.get("metadata",{}).get("purpose") == purpose),None) + end = datetime.fromtimestamp(deadline,timezone.utc).isoformat() + if cron is None: + cron = request("POST","/runs/crons",{"assistant_id":assistant,"schedule":"*/2 * * * * *","enabled":False, + "input":{"operation":"dispatch","task_ids":task_ids},"metadata":{"purpose":purpose},"end_time":end},base=BASE) + else: + request("PATCH",f"/runs/crons/{cron['cron_id']}",{"enabled":False,"end_time":end, + "input":{"operation":"dispatch","task_ids":task_ids}},base=BASE) + return cron["cron_id"] + + +def set_scheduler(cron_id,enabled): + request("PATCH",f"/runs/crons/{cron_id}",{"enabled":enabled},base=BASE) + + +def restart_pending(connection,task_ids,cron_id,credential,deadline,output): + set_scheduler(cron_id,False) + connection.execute("SET lock_timeout='45s'") + connection.execute("SELECT pg_advisory_lock(81827026)") + try: + pending = connection.execute("SELECT count(*) FROM backintel.capability_triggers WHERE task_id=ANY(%s) AND state='pending'",(task_ids,)).fetchone()[0] + if not pending: + raise ValueError("Recovery verification needs pending work; replay completed work without --verify-recovery") + before = domain_snapshot(connection,task_ids) + with (output/"Restart.log").open("w") as log: + subprocess.run(["docker","compose","-f","compose.capabilities.yml","restart","runtime"],cwd=ROOT,stdout=log,stderr=subprocess.STDOUT,check=True,timeout=60) + subprocess.run(["docker","compose","-f","compose.capabilities.yml","up","--detach","--wait","--no-build","--no-recreate","runtime"],cwd=ROOT,stdout=log,stderr=subprocess.STDOUT,check=True,timeout=120) + if credential: + install_credential(credential,task_ids,deadline,output.name) + assert domain_snapshot(connection,task_ids) == before, "Restart changed accepted evidence or jobs" + assert connection.execute("SELECT count(*) FROM backintel.capability_triggers WHERE task_id=ANY(%s) AND state='pending'",(task_ids,)).fetchone()[0] == pending + receipt = {"status":"passed","pending_triggers":pending,"domain_digest":before["digest"],"restart_boundary":"between dispatch jobs; no in-flight paid request interrupted"} + (output/"Restart.json").write_bytes(encoded(receipt)) + return receipt + finally: + connection.execute("SELECT pg_advisory_unlock(81827026)") + + +def wait_for_run(connection,demo_id,deadline): + connection.execute("LISTEN backintel_capability_jobs") + task_ids = [f"{name}-real-{demo_id}" for name in SCENARIOS] + while True: + progress = status(connection,"real-"+demo_id) + if progress["status"] != "running": + if progress["status"] != "passed": + raise RuntimeError("Real workflow stopped: "+str(progress["errors"])) + completed = {r[0] for r in connection.execute("""SELECT task_id FROM backintel.capability_jobs + WHERE task_id=ANY(%s) AND state='completed' AND payload->>'operation'='real_event' + AND payload->>'action'='artifact_refresh'""",(task_ids,))} + if completed != set(task_ids): + raise RuntimeError("Both final report stages must complete before packaging") + return progress + remaining = deadline-time.time() + if remaining <= 0: + raise TimeoutError("Bounded real-run window ended; persisted jobs may be resumed with matching approval") + next(connection.notifies(timeout=min(remaining,30),stop_after=1),None) + + +def replay(connection,task_ids): + before = domain_snapshot(connection,task_ids) + prior = {key:os.environ.get(key) for key in ("BACKINTEL_APP_DATABASE_URL","BACKINTEL_TEST_DATABASE_URL")} + os.environ.update(BACKINTEL_APP_DATABASE_URL=DEMO_DSN,BACKINTEL_TEST_DATABASE_URL=DEMO_DSN) + try: + jobs = connection.execute("SELECT job_id FROM backintel.capability_jobs WHERE task_id=ANY(%s) ORDER BY sequence",(task_ids,)).fetchall() + assert all(execute(row[0]).get("reused") for row in jobs) + finally: + for key,value in prior.items(): + if value is None: + os.environ.pop(key,None) + else: + os.environ[key] = value + assert domain_snapshot(connection,task_ids) == before + return {"status":"passed","jobs":len(jobs),"domain_digest":before["digest"]} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--demo-id",required=True) + parser.add_argument("--prepare-only",action="store_true") + parser.add_argument("--authorizations",type=Path,help="JSON mapping support/equipment to existing approved authorization IDs") + parser.add_argument("--no-start",action="store_true",help="Reuse the running model-enabled isolated service") + parser.add_argument("--verify-recovery",action="store_true") + parser.add_argument("--timeout",type=int,default=900) + args = parser.parse_args() + if not re.fullmatch(r"[a-z][a-z0-9-]{0,24}",args.demo_id) or not 60 <= args.timeout <= 3600: + parser.error("Use a bounded demo identity and 60..3600 second execution window") + if args.prepare_only and (args.authorizations or args.verify_recovery): + parser.error("Preparation does not use authorizations or restart an active journey") + if not args.prepare_only and not args.authorizations: + parser.error("Real execution requires an existing approved authorization map; use --prepare-only first") + output = ROOT/"artifacts/validation/RealDemo"/args.demo_id/("Attempt"+uuid.uuid4().hex[:12]) + output.mkdir(parents=True,mode=0o700) + receipt = {"schema":"backintel-real-demo/v1","status":"running","candidate":"uncommitted-working-tree", + "demo_id":args.demo_id,"final_real_model_acceptance":"blocked","exact_commit_acceptance":"blocked"} + cron_id = credential = None + credential_installed = False + try: + if not args.no_start: + startup(output) + with psycopg.connect(DEMO_DSN,autocommit=True) as connection: + if not connection.execute("SELECT pg_try_advisory_lock(%s)",(RUN_LOCK,)).fetchone()[0]: + raise RuntimeError("Another real-run command is active; its native workflow remains running") + proposals = prepare(connection,args.demo_id) + (output/"AuthorizationScopes.json").write_bytes(encoded(proposals)) + receipt["authorization_scopes"] = str(output/"AuthorizationScopes.json") + if args.prepare_only: + receipt.update(status="passed",stage="preparation_only",paid_provider_calls=0) + return 0 + authorizations = json.loads(args.authorizations.read_text()) + remaining,deadline = preflight(connection,proposals,authorizations,time.time()+args.timeout) + task_ids = [proposals[name]["task_id"] for name in SCENARIOS] + assistants = request("POST","/assistants/search",{"graph_id":"capability_platform","limit":1},base=BASE) + if not assistants: + raise RuntimeError("The isolated capability assistant is unavailable") + schemas = request("GET",f"/assistants/{assistants[0]['assistant_id']}/schemas",base=BASE) + if "task_ids" not in schemas.get("input_schema",{}).get("properties",{}): + raise RuntimeError("The running image lacks task-scoped dispatch; update it while quiescent before real execution") + if remaining: + credential = subprocess.run(["security","find-generic-password","-s","BackIntel OpenRouter","-a","runtime","-w"], + capture_output=True,text=True,check=True,timeout=15).stdout.rstrip("\n") + if not credential: + raise RuntimeError("The existing Keychain credential is empty") + receipt["credential"] = {"source":"existing macOS Keychain","runtime_storage":"task-scoped expiring tmpfs","expires_at":deadline,"owner_id":output.name} + credential_installed = True + install_credential(credential,task_ids,deadline,output.name) + cron_id = scheduler(assistants[0]["assistant_id"],task_ids,args.demo_id,deadline) + receipt.update(cron_id=cron_id,scheduler="bounded_task_scoped_aegra_native_cron",task_ids=task_ids) + for scenario in SCENARIOS: + result = submit("start",scenario,args.demo_id,"real-run-start-v1",provider_authorization_id=authorizations[scenario])["result"] + if result["state"] != "completed": + raise RuntimeError("Durable start did not complete: "+str(result)) + if args.verify_recovery: + receipt["recovery"] = restart_pending(connection,task_ids,cron_id,credential,deadline,output) + set_scheduler(cron_id,True) + print(json.dumps({"stage":"running","demo_id":args.demo_id,"new_requests_within_approved_scopes":remaining}),flush=True) + receipt["workflow"] = wait_for_run(connection,args.demo_id,deadline) + set_scheduler(cron_id,False) + cron_id = None + if credential_installed: + clear_credential(output.name) + credential_installed = False + credential = None + receipt["credential"]["removed"] = True + receipt["replay"] = replay(connection,task_ids) + packaged = package(connection,task_ids,output/"Package",mode="real") + receipt.update(status="passed",package=str(output/"Package/Package.json"),paid_provider_calls=packaged["paid_provider_calls"], + provider_usd=packaged["provider_usd"],local_compute_usd=None) + except Exception as exc: + message = str(exc).replace(credential,"[REDACTED]") if credential else str(exc) + receipt.update(status="blocked",error={"type":type(exc).__name__,"message":message}) + finally: + if cron_id: + try: + set_scheduler(cron_id,False) + except Exception as exc: + receipt.update(status="blocked",scheduler_cleanup=type(exc).__name__) + if credential_installed: + try: + clear_credential(output.name) + receipt["credential"]["removed"] = True + except Exception as exc: + receipt.update(status="blocked",credential_cleanup=type(exc).__name__) + credential = None + path = output/"Integration.json" + path.write_bytes(encoded(receipt)) + print(json.dumps({"status":receipt["status"],"receipt":str(path)}),flush=True) + return 0 if receipt["status"] == "passed" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/real_model_probe.py b/scripts/real_model_probe.py new file mode 100644 index 0000000..264f2d8 --- /dev/null +++ b/scripts/real_model_probe.py @@ -0,0 +1,116 @@ +"""Prepare a reviewable one-request Jev probe; execution requires recorded approval.""" +from __future__ import annotations + +import argparse +import json +import os +from pathlib import Path +import subprocess +import time + +import psycopg +from psycopg.types.json import Jsonb + +from runtime.bootstrap import initialize +from runtime.contracts import admit_source,current_sources,register_task +from runtime.evidence import Evidence +from runtime.ledger import dsn +from runtime.real_semantics import extract_real,scope_for +from runtime.simulation import digest,encoded +from runtime.synthetic import history + +ROOT = Path(__file__).resolve().parents[1] +OUTPUT = ROOT/"artifacts"/"validation"/"RealModelPreparation" + + +def prepare(scenario: str) -> dict: + if os.environ.get("BACKINTEL_TEST_DATABASE_URL") != dsn() or not psycopg.conninfo.conninfo_to_dict(dsn())["dbname"].startswith("test_"): + raise RuntimeError("Real-model preparation requires the isolated test database") + initialize() + task,rows,_ = history(scenario) + task["id"] = scenario+"-real-probe" + task["observation_provider"] = {"name":"openrouter-jev","version":"jev-1.13","implementation_mode":"real"} + with psycopg.connect(dsn(),autocommit=True) as connection: + store = Evidence(connection,task["id"]) + task_record = register_task(store,task) + admit_source(store,task_record,{"format":"json","data":[rows[0]]},0) + source = current_sources(store,0,task_record["sha256"])[0] + scope = scope_for(task_record,[source]) + authorization = "jev-probe-"+digest(scope)[:24] + connection.execute("""INSERT INTO backintel.capability_provider_authorizations + (authorization_id,provider,model,max_requests,max_input_characters,max_measured_usd, + price_ceiling_known,approved,scope_sha256,scope,expires_at) + VALUES (%s,'openrouter','jev-1.13',1,5000,NULL,false,false,%s,%s,to_timestamp(%s)) + ON CONFLICT (authorization_id) DO NOTHING""",(authorization,digest(scope),Jsonb(scope),time.time()+86400)) + approved = connection.execute("SELECT approved FROM backintel.capability_provider_authorizations WHERE authorization_id=%s",(authorization,)).fetchone()[0] + config = json.loads((ROOT/"config"/"real_models.json").read_text()) + plan = {"schema":"backintel-real-model-proposal/v1","provider_authorization_id":authorization, + "provider_execution_approved":approved,"scenario":scenario,"task_id":task["id"],"task_sha256":task_record["sha256"], + "source_sha256":source["sha256"],"synthetic_text":source["body"]["content"],"questions":task["questions"], + "provider":{"name":"OpenRouter","requested_model":"jev-1.13","max_requests":1,"max_input_characters":5000, + "input_characters":len(source["body"]["content"]),"known_price":None,"enforceable_dollar_cap":False, + "automatic_retry":False,"credential_store":"macOS Keychain: BackIntel OpenRouter / runtime"}, + "model_use":{"approved":False,"configuration_sha256":digest(config),"routes":["catboost","tabiclv2"], + "catboost_license":"Apache-2.0","tabiclv2_license":config["tabiclv2"]["license"], + "checkpoints":config["tabiclv2"]["checkpoints"], + "download_bytes":sum(c["bytes"] for c in config["tabiclv2"]["checkpoints"].values()), + "device":"cpu","threads":2,"training_records_per_scenario":24}, + "limits":["The first Jev probe requires explicit approval because its price is not publicly available.", + "One successful probe does not authorize the full real-model comparison.", + "Synthetic-data model results do not establish real-world quality or savings."]} + OUTPUT.mkdir(parents=True,exist_ok=True) + (OUTPUT/"proposal.json").write_bytes(encoded(plan)) + return plan + + +def execute(plan: dict) -> dict: + if os.environ.get("BACKINTEL_TEST_DATABASE_URL") != dsn() or not psycopg.conninfo.conninfo_to_dict(dsn())["dbname"].startswith("test_"): + raise RuntimeError("Real-model probe requires the isolated test database") + with psycopg.connect(dsn(),autocommit=True) as connection: + authorization = connection.execute("SELECT approved,expires_at>now() FROM backintel.capability_provider_authorizations WHERE authorization_id=%s", + (plan["provider_authorization_id"],)).fetchone() + if authorization != (True,True): + raise PermissionError("Explicit, current approval is required before credential retrieval") + store = Evidence(connection,plan["task_id"]) + task_record,source = store.get(plan["task_sha256"]),store.get(plan["source_sha256"]) + credential = subprocess.run(["security","find-generic-password","-s","BackIntel OpenRouter","-a","runtime","-w"], + capture_output=True,text=True,check=True).stdout.rstrip("\n") + previous = os.environ.get("OPENROUTER_API_KEY") + try: + os.environ["OPENROUTER_API_KEY"] = credential + observations = extract_real(store,task_record,source,0,authorization_id=plan["provider_authorization_id"]) + finally: + if previous is None: + os.environ.pop("OPENROUTER_API_KEY",None) + else: + os.environ["OPENROUTER_API_KEY"] = previous + responses = store.list("provider_response") + result = {"status":"passed","observations":[r["sha256"] for r in observations], + "metadata":[r["body"]["metadata"] for r in responses],"full_model_comparison":"unfinished"} + (OUTPUT/"probe-result.json").write_bytes(encoded(result)) + return result + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("action",choices=("prepare","execute")) + parser.add_argument("--scenario",choices=("support","equipment"),default="support") + args = parser.parse_args() + try: + if args.action == "prepare": + plan = prepare(args.scenario) + result = {"proposal":str(OUTPUT/"proposal.json"),"provider_authorization_id":plan["provider_authorization_id"], + "paid_calls":0,"credential_retrieved":False,"execution_approved":plan["provider_execution_approved"]} + else: + result = execute(json.loads((OUTPUT/"proposal.json").read_text())) + print(json.dumps(result,indent=2)) + return 0 + except Exception as error: + # Provider exceptions may contain arbitrary transport details; never echo credentials. + print(json.dumps({"status":"blocked","error_type":type(error).__name__, + "message":"Execution stopped. Inspect the sanitized request ledger; do not retry an uncertain paid request."})) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/simulate.py b/scripts/simulate.py new file mode 100644 index 0000000..9ad75ee --- /dev/null +++ b/scripts/simulate.py @@ -0,0 +1,60 @@ +"""Run synthetic capability scenarios locally, or submit them to an installed Aegra runtime.""" +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from urllib.parse import urlsplit + +from runtime.simulation import SCENARIOS, load_scenario, publish, simulate + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--scenario", default="all", help="Local scenario name, or all") + parser.add_argument("--output", type=Path, default=Path.home() / "Library/Application Support/BackIntel/Evidence/Simulation") + parser.add_argument("--base-url", help="Submit to an Aegra runtime containing capability_simulation") + parser.add_argument("--receipt", type=Path, help="New receipt for background submission") + args = parser.parse_args() + if args.base_url: + endpoint = urlsplit(args.base_url) + if endpoint.scheme != "http" or endpoint.hostname not in ("127.0.0.1", "localhost", "::1"): + parser.error("Simulation submission is restricted to a local HTTP runtime") + if args.scenario == "all" or args.receipt is None: + parser.error("Background submission requires one scenario and --receipt") + if args.receipt.exists(): + parser.error("Receipt already exists; inspect it instead of overwriting") + from scripts.jev.run_review_batch import request + scenario = load_scenario(args.scenario) + assistants = request("POST", "/assistants/search", {"graph_id": "capability_simulation", "limit": 1}, base=args.base_url) + if not assistants: + raise RuntimeError("Selected runtime does not contain capability_simulation; use offline mode or an approved runtime update") + thread = request("POST", "/threads", {}, base=args.base_url) + run = request("POST", f"/threads/{thread['thread_id']}/runs", { + "assistant_id": assistants[0]["assistant_id"], + "input": {"scenario": scenario["id"], "partition_id": f"simulation-{thread['thread_id']}"}, + }, base=args.base_url) + receipt = {"thread_id": thread["thread_id"], "run_id": run["run_id"], "status": run["status"], + "base_url": args.base_url, "mode": "synthetic_simulation"} + args.receipt.parent.mkdir(parents=True, exist_ok=True) + with args.receipt.open("x") as handle: + json.dump(receipt, handle, indent=2) + print(json.dumps(receipt, indent=2)) + return 0 + if args.receipt: + parser.error("--receipt is only used with --base-url") + names = sorted(path.stem for path in SCENARIOS.glob("*.json")) if args.scenario == "all" else [args.scenario] + if not names: + parser.error("No simulation scenarios configured") + output = [] + for name in names: + result = simulate(load_scenario(name)) + artifacts = publish(result, args.output) + output.append({"scenario": name, "mode": result["mode"], "batches": len(result["batches"]), + "episodes": len(result["inbox"]), "provider_calls": 0, "artifacts": artifacts}) + print(json.dumps(output, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/validation/check_audience_ui.cjs b/scripts/validation/check_audience_ui.cjs new file mode 100644 index 0000000..1191076 --- /dev/null +++ b/scripts/validation/check_audience_ui.cjs @@ -0,0 +1,111 @@ +/* Render and exercise the local audience surface; access tokens are never logged. */ +const { chromium } = require('playwright'); +const fs = require('node:fs'); +const path = require('node:path'); +const crypto = require('node:crypto'); + +async function main() { + const fixture = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')); + const base = process.env.BACKINTEL_AUDIENCE_URL || 'http://127.0.0.1:2028'; + const output = path.resolve('artifacts/validation/AudienceUI', crypto.randomUUID()); + fs.mkdirSync(output, { recursive: true }); + const result = { schema: 'backintel-audience-ui/v1', candidate: 'uncommitted-working-tree', status: 'running', + checks: [], screenshots: [], browser_errors: [], paid_provider_calls: 0, local_compute_usd: null }; + const check = (name, passed) => { + result.checks.push({ name, passed: Boolean(passed) }); + if (!passed) throw new Error(name); + }; + let browser; + try { + browser = await chromium.launch({ headless: true, executablePath: process.env.BACKINTEL_BROWSER_PATH }); + async function capture(page, name) { + const metrics = await page.evaluate(() => ({ + overflow: document.documentElement.scrollWidth > innerWidth, + unlabeled: [...document.querySelectorAll('input:not([type=hidden]),textarea,select')].filter(e => !e.labels.length && !e.getAttribute('aria-label')).length, + unnamed: [...document.querySelectorAll('a,button')].filter(e => !(e.textContent.trim() || e.getAttribute('aria-label'))).length, + main: document.querySelectorAll('main').length, + headings: document.querySelectorAll('h1').length, + })); + check(name + ':page_width', !metrics.overflow); + check(name + ':labels_and_structure', metrics.unlabeled === 0 && metrics.unnamed === 0 && metrics.main === 1 && metrics.headings === 1); + const file = path.join(output, name + '.png'); + await page.screenshot({ path: file, fullPage: true }); + result.screenshots.push({ name, path: file, sha256: crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'), metrics }); + } + for (const task of fixture.tasks) { + for (const audience of ['manager', 'operator', 'analyst', 'empty']) { + const grant = fixture.grants.find(g => g.task_id === task && g.audience === audience); + const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } }); + const page = await context.newPage(); + page.on('pageerror', e => result.browser_errors.push(e.message)); + await page.goto(base + '/login'); + await page.getByLabel('Audience access token').fill(grant.token); + await Promise.all([page.waitForURL(base + '/'), page.getByRole('button', { name: 'Open report', exact: true }).click()]); + check(task + ':' + audience + ':login', await page.locator('h1').count() === 1 && !(await page.locator('h1').innerText()).includes('unavailable')); + const key = task.split('-')[0] + '-' + audience; + await capture(page, key + '-desktop'); + if (audience === 'manager' || audience === 'analyst') { + check(key + ':readonly_controls', await page.locator('form[action="/correct"],form[action="/review"]').count() === 0); + } + if (audience !== 'empty') { + const summary = page.locator('details > summary').first(); + await summary.focus(); + await page.keyboard.press('Enter'); + check(key + ':keyboard_evidence', await page.locator('details').first().getAttribute('open') !== null); + const href = await page.getByRole('link', { name: 'Open source record' }).first().getAttribute('href'); + const response = await context.request.get(base + href); + check(key + ':source_open', response.status() === 200 && (await response.json()).kind === 'source'); + } + await page.setViewportSize({ width: 390, height: 844 }); + if (audience !== 'empty') { + const table = page.getByRole('region', { name: 'Outcome comparison' }); + await table.focus(); + await page.keyboard.press('ArrowRight'); + await page.waitForFunction(() => document.querySelector('.scroll').scrollLeft > 0); + check(key + ':keyboard_table_scroll', await table.evaluate(element => element.scrollLeft > 0)); + await table.evaluate(element => { element.scrollLeft = 0; }); + } + await capture(page, key + '-mobile'); + if (audience === 'operator') { + const candidates = await page.locator('a[href^="/candidate/"]').evaluateAll(elements => elements.map(e => e.getAttribute('href'))); + check(key + ':candidate_links', candidates.length === 2); + for (let index = 0; index < candidates.length; index++) { + await page.goto(base + candidates[index]); + await capture(page, key + '-candidate-' + index); + } + await page.goto(base + '/'); + await page.locator('details > summary').first().click(); + const form = page.locator('form[action="/correct"]').first(); + const select = form.locator('select[name="value"]'); + if (await select.count()) await select.selectOption('false'); + else await form.locator('input[name="value"]').fill('2.5'); + await form.getByLabel('Reason for correction').fill('Browser-checked correction on synthetic data'); + await Promise.all([page.waitForURL(base + '/'), form.getByRole('button').click()]); + await page.waitForLoadState('domcontentloaded'); + check(key + ':browser_correction', (await page.content()).includes('Browser-checked correction on synthetic data')); + await capture(page, key + '-correction-saved'); + } + await context.close(); + } + } + const context = await browser.newContext({ viewport: { width: 390, height: 844 } }); + const page = await context.newPage(); + const denied = await page.goto(base + '/'); + check('unauthenticated_error', denied.status() === 403); + await capture(page, 'access-denied-mobile'); + await page.goto(base + '/login'); + await capture(page, 'login-mobile'); + await context.close(); + check('browser_runtime_errors', result.browser_errors.length === 0); + result.status = 'passed'; + } catch (error) { + result.status = 'failed'; result.error = error.message; + throw error; + } finally { + if (browser) await browser.close(); + const file = path.join(output, 'checks.json'); + fs.writeFileSync(file, JSON.stringify(result, null, 2)); + console.log(JSON.stringify({ status: result.status, checks: result.checks.length, captures: result.screenshots.length, receipt: file })); + } +} +main().catch(error => { console.error(error.message); process.exitCode = 1; }); diff --git a/scripts/validation/check_audiences.py b/scripts/validation/check_audiences.py new file mode 100644 index 0000000..0047328 --- /dev/null +++ b/scripts/validation/check_audiences.py @@ -0,0 +1,163 @@ +"""Exercise actual scoped HTTP reports and human actions on synthetic fixtures.""" + +from __future__ import annotations + +import json +import os +from pathlib import Path +import threading +import time +from urllib.error import HTTPError +from urllib.parse import urlencode +from urllib.request import Request, urlopen +import uuid + +import psycopg + +from runtime.artifacts import create_artifacts, get_artifact +from runtime.audience_server import AudienceServer, issue_grants +from runtime.bootstrap import initialize +from runtime.capability_pipeline import bootstrap +from runtime.contracts import register_task +from runtime.evidence import Evidence +from runtime.generated_artifacts import create_candidate, review_candidate +from runtime.ledger import dsn +from runtime.simulation import encoded +from runtime.synthetic import history + +OUTPUT = Path(__file__).resolve().parents[2] / "artifacts/validation/Audiences" + + +def request(server, path, token=None, form=None, origin=None): + headers = {"Authorization": "Bearer " + token} if token else {} + data = None + if form is not None: + data = urlencode(form).encode() + headers["Content-Type"] = "application/x-www-form-urlencoded" + if origin: + headers["Origin"] = origin + try: + with urlopen(Request(server.origin + path, headers=headers, data=data), timeout=10) as response: + return response.status, response.read() + except HTTPError as exc: + return exc.code, exc.read() + + +def main(): + OUTPUT.mkdir(parents=True, exist_ok=True) + run_id = uuid.uuid4().hex[:12] + receipt = {"schema": "backintel-audience-check/v1", "candidate": "uncommitted-working-tree", + "model_execution": "simulated", "http_execution": "real", "paid_provider_calls": 0, + "status": "running", "checks": [], "tasks": []} + receipt_path = OUTPUT / (run_id + ".json") + server = None + + def check(name, predicate): + receipt["checks"].append({"name": name, "passed": bool(predicate)}) + receipt_path.write_bytes(encoded(receipt)) + if not predicate: + raise AssertionError(name) + + try: + if not os.environ.get("BACKINTEL_TEST_DATABASE_URL") or os.environ["BACKINTEL_TEST_DATABASE_URL"] != dsn(): + raise PermissionError("Audience checks require an isolated test database") + initialize() + with psycopg.connect(dsn(), autocommit=True) as connection: + for scenario in ("support", "equipment"): + task, _, _ = history(scenario) + task["id"] = scenario + "-views-" + run_id + task["audiences"].append({"id": "empty", "view": "briefing", "entities": ["NoRecords"], "can_correct": False}) + store = Evidence(connection, task["id"]) + task_record = register_task(store, task) + result = bootstrap(store, task_record, scenario) + before = len(store.list("artifact")) + create_artifacts(store, task_record, result) + check(scenario + ":replay_deduplicates_artifacts", before == len(store.list("artifact")) == 4) + receipt["tasks"].append(task["id"]) + grants = issue_grants(connection, receipt["tasks"]) + server = AudienceServer(dsn(), grants, 0) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + check("unauthenticated_access_denied", request(server, "/")[0] == 403) + check("invalid_token_denied", request(server, "/artifact.json", "invalid")[0] == 403) + for task_id in receipt["tasks"]: + tokens = {g["audience"]: g["token"] for g in grants if g["task_id"] == task_id} + store = Evidence(connection, task_id) + manager = get_artifact(store, "manager") + operator = get_artifact(store, "operator") + check(task_id + ":manager_scoped_to_one_entity", len(manager["body"]["rows"]) == 1) + check(task_id + ":operator_sees_two_entities", len(operator["body"]["rows"]) == 2) + status, raw = request(server, "/artifact.json", tokens["manager"]) + check(task_id + ":manager_http_projection", status == 200 and json.loads(raw) == manager) + other = next(r for r in operator["body"]["rows"] if r["entity"] != manager["body"]["rows"][0]["entity"]) + check(task_id + ":other_entity_not_in_json", other["entity"].encode() not in raw) + check(task_id + ":evidence_scope_enforced", request(server, "/evidence/" + other["source"], tokens["manager"])[0] == 403) + check(task_id + ":artifact_scope_enforced", request(server, "/versions/" + operator["sha256"], tokens["manager"])[0] == 403) + csv_status, csv_body = request(server, "/export.csv", tokens["manager"]) + check(task_id + ":csv_scope_enforced", csv_status == 200 and other["entity"].encode() not in csv_body) + check(task_id + ":empty_state", b"No visible records" in request(server, "/", tokens["empty"])[1]) + form = {"artifact": manager["sha256"], "decision": "accepted", "reason": "fixture"} + check(task_id + ":manager_review_denied", request(server, "/review", tokens["manager"], form)[0] == 403) + form["artifact"] = operator["sha256"] + check(task_id + ":cross_origin_change_denied", request(server, "/review", tokens["operator"], form, "https://outside.invalid")[0] == 403) + check(task_id + ":operator_review_saved", request(server, "/review", tokens["operator"], form)[0] == 200) + count = len(store.list("artifact_review")) + check(task_id + ":review_replay", request(server, "/review", tokens["operator"], form)[0] == 200 and len(store.list("artifact_review")) == count) + observation = operator["body"]["rows"][0]["observations"][0] + form = {"artifact": operator["sha256"], "observation": observation["sha256"], "supersedes": "", "value": "unknown", "reason": "Source needs human clarification"} + check(task_id + ":manager_correction_denied", request(server, "/correct", tokens["manager"], {**form, "artifact": manager["sha256"]})[0] == 403) + old_sources = [r["sha256"] for r in store.list("source")] + check(task_id + ":operator_correction_refreshes", request(server, "/correct", tokens["operator"], form)[0] == 200) + updated = get_artifact(store, "operator") + check(task_id + ":new_report_version", updated["sha256"] != operator["sha256"]) + check(task_id + ":correction_visible", any(o["response"]["status"] == "unknown" and o["correction"] for r in updated["body"]["rows"] for o in r["observations"])) + check(task_id + ":sources_preserved", old_sources == [r["sha256"] for r in store.list("source")]) + check(task_id + ":old_report_retained", request(server, "/versions/" + operator["sha256"], tokens["operator"])[0] == 200) + old_prediction = operator["body"]["rows"][0]["prediction"]["sha256"] + check(task_id + ":old_evidence_retained", request(server, "/evidence/" + old_prediction + "?artifact=" + operator["sha256"], tokens["operator"])[0] == 200) + check(task_id + ":stale_form_denied", request(server, "/correct", tokens["operator"], form)[0] == 400) + source = '''import json +s=json.load(open("/input/snapshot.json")) +rows=sorted(s["rows"],key=lambda row: row["prediction"] if row["prediction"] is not None else -1,reverse=True) +print(json.dumps({"schema":"backintel-generated-layout/v1","title":"Predicted outcomes in descending order","columns":["entity","prediction","target_at","attention"],"sources":[r["source"] for r in rows]})) +''' + candidate = create_candidate(store, updated, "operator", source) + check(task_id + ":generated_code_really_executed", candidate["body"]["run"]["status"] == "candidate" and candidate["body"]["run"]["cleanup_confirmed"]) + check(task_id + ":candidate_replay", create_candidate(store, updated, "operator", source) == candidate) + check(task_id + ":candidate_preview", request(server, "/candidate/" + candidate["sha256"], tokens["operator"])[0] == 200) + check(task_id + ":candidate_audience_boundary", request(server, "/candidate/" + candidate["sha256"], tokens["manager"])[0] == 403) + candidate_form = {"artifact": updated["sha256"], "candidate": candidate["sha256"], "decision": "accepted", "reason": "Simulated operator checked the layout against its source rows"} + check(task_id + ":candidate_review_persisted", request(server, "/candidate-review", tokens["operator"], candidate_form)[0] == 200) + bad_source = 'import json\nprint(json.dumps({"schema":"backintel-generated-layout/v1","title":"Bad references","columns":["entity"],"sources":["unapproved"]}))' + rejected = create_candidate(store, updated, "operator", bad_source) + check(task_id + ":invented_source_rejected", rejected["body"]["run"]["status"] == "rejected") + denied = False + try: + review_candidate(store, rejected, "operator", "accepted", "fixture", updated["available_at"] + 1) + except ValueError: + denied = True + check(task_id + ":failed_candidate_cannot_be_accepted", denied) + (OUTPUT / (task_id + ".html")).write_bytes(request(server, "/", tokens["operator"])[1]) + expired = issue_grants(connection, receipt["tasks"][:1], lifetime=-1) + from hashlib import sha256 + server.grants[sha256(expired[0]["token"].encode()).hexdigest()] = expired[0] + check("expired_token_denied", request(server, "/", expired[0]["token"])[0] == 403) + # Private local browser fixture, excluded from Git along with other validation artifacts. + private = OUTPUT / (run_id + "-access.json") + with os.fdopen(os.open(private, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600), "wb") as output: + output.write(encoded({"grants": grants, "database": dsn(), "tasks": receipt["tasks"]})) + receipt["browser_fixture"] = str(private) + receipt["status"] = "passed" + except Exception as exc: + receipt.update(status="blocked" if isinstance(exc, PermissionError) else "failed", error={"type": type(exc).__name__, "message": str(exc)}) + raise + finally: + if server: + server.shutdown() + server.server_close() + receipt_path.write_bytes(encoded(receipt)) + print(json.dumps({"status": receipt["status"], "checks": len(receipt["checks"]), "receipt": str(receipt_path)})) + + +if __name__ == "__main__": + main() diff --git a/scripts/validation/check_capabilities.py b/scripts/validation/check_capabilities.py new file mode 100644 index 0000000..e786cc3 --- /dev/null +++ b/scripts/validation/check_capabilities.py @@ -0,0 +1,43 @@ +"""Persist direct working-tree capability-check results; exact-commit acceptance is separate.""" +import hashlib +import io +import json +import subprocess +import time +import unittest +from pathlib import Path + + +def main() -> int: + root = Path(__file__).resolve().parents[2] + output = root / "artifacts" / "validation" / "CapabilityPackages" + output.mkdir(parents=True, exist_ok=True) + suite = unittest.TestSuite() + for pattern in ("test_capabilities.py", "test_simulation.py", "test_real_integrations.py", "test_jev.py"): + suite.addTests(unittest.defaultTestLoader.discover(str(root / "tests"),pattern=pattern)) + started, log = time.perf_counter(), io.StringIO() + result = unittest.TextTestRunner(stream=log,verbosity=2).run(suite) + log_path = output / "checks.log" + log_path.write_text(log.getvalue()) + paths = [root / "runtime" / name for name in ("contracts.py","evidence.py","observations.py","prediction.py","synthetic.py","jobs.py","attention.py","capability_pipeline.py","capability_graph.py","real_models.py","real_semantics.py","jev.py","artifacts.py","audience_server.py","generated_artifacts.py","sandbox.py")] + paths += [root / "config" / "simulation" / f"{name}.json" for name in ("support","equipment")] + paths += [root / "tests" / "test_capabilities.py",root/"tests"/"test_real_integrations.py",root/"config"/"real_models.json", + root/"scripts"/"real_model_probe.py",root/"requirements.models.txt",root/"migrations"/"0009_model_requests.sql", + root / "migrations" / "0007_capability_evidence.sql",root / "migrations" / "0008_capability_background.sql", Path(__file__),log_path] + evidence = {"schema":"backintel-capability-checks/v1", "status":"passed" if result.wasSuccessful() else "failed", + "base_commit":subprocess.check_output(["git","rev-parse","HEAD"],cwd=root,text=True).strip(), + "candidate":"working-tree", "exact_commit_acceptance":"blocked", + "real_model_acceptance":"blocked", "provider_checks":"local fixtures only; no actual Jev or predictor run certified", + "tests":result.testsRun, "failures":len(result.failures), "errors":len(result.errors), "skipped":len(result.skipped), + "duration_ms":(time.perf_counter()-started)*1000, + "cost":{"provider_calls":0,"provider_usd":0,"local_compute_usd":None}, + "artifacts":[{"path":str(p.relative_to(root)),"sha256":hashlib.sha256(p.read_bytes()).hexdigest(),"bytes":p.stat().st_size} for p in paths]} + (output / "checks.json").write_text(json.dumps(evidence,indent=2)+"\n") + print(json.dumps({k:evidence[k] for k in ("status","tests","failures","errors","exact_commit_acceptance")},indent=2)) + if not result.wasSuccessful(): + print(log.getvalue()) + return 0 if result.wasSuccessful() else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/validation/check_capability_restore.py b/scripts/validation/check_capability_restore.py new file mode 100644 index 0000000..f9cfca4 --- /dev/null +++ b/scripts/validation/check_capability_restore.py @@ -0,0 +1,96 @@ +"""Restore a development database into a new isolated DB and replay completed work.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +from pathlib import Path +import subprocess +import uuid + +import psycopg +from psycopg import sql + +from runtime.artifacts import get_artifact +from runtime.evidence import Evidence +from runtime.jobs import execute +from runtime.simulation import encoded +from scripts.capability_demo import DEMO_DSN, ROOT, domain_snapshot, status + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--demo-id", required=True) + args = parser.parse_args() + task_ids = [f"{name}-{args.demo_id}" for name in ("support", "equipment")] + output = ROOT / "artifacts/validation/CapabilityRestore" / ("Attempt" + uuid.uuid4().hex[:12]) + output.mkdir(parents=True, mode=0o700) + receipt = {"schema": "backintel-capability-restore/v1", "status": "running", "tasks": task_ids, + "candidate": "uncommitted-working-tree", "models": "simulated", "paid_provider_calls": 0, + "local_compute_usd": None, "limits": ["Real model package and checkpoint recovery is not established by this fixture check."]} + target = "test_restore_" + uuid.uuid4().hex[:12] + clone_dsn = f"postgresql://capability_test@127.0.0.1:55436/{target}" + created = False + try: + with psycopg.connect(DEMO_DSN, autocommit=True) as source: + if status(source, args.demo_id)["status"] != "passed": + raise RuntimeError("Only a completed, quiescent demonstration can be restored") + before = domain_snapshot(source, task_ids) + backup = output / "DevelopmentDatabase.dump" + with backup.open("wb") as destination, (output / "Backup.log").open("w") as log: + subprocess.run(["docker", "compose", "-f", "compose.capabilities.yml", "exec", "-T", "postgres", + "pg_dump", "-U", "capability_demo", "-d", "test_backintel_demo", "--format=custom", "--no-owner", "--no-privileges"], + cwd=ROOT, stdout=destination, stderr=log, check=True, timeout=60) + receipt["backup"] = {"file": str(backup), "bytes": backup.stat().st_size, "sha256": hashlib.sha256(backup.read_bytes()).hexdigest()} + with psycopg.connect("postgresql://capability_test@127.0.0.1:55436/postgres", autocommit=True) as admin: + admin.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier(target))) + created = True + with backup.open("rb") as data, (output / "Restore.log").open("w") as log: + subprocess.run(["docker", "exec", "-i", "backintel-capability-test", "pg_restore", "-U", "capability_test", + "-d", target, "--no-owner", "--no-privileges", "--exit-on-error"], stdin=data, stdout=log, + stderr=subprocess.STDOUT, check=True, timeout=60) + with psycopg.connect(clone_dsn, autocommit=True) as clone: + restored = domain_snapshot(clone, task_ids) + if before["digest"] != restored["digest"]: + raise AssertionError("Restored records or job state differ from accepted source state") + receipt["evidence_records"] = len(restored["evidence"]) + receipt["source_sha256"], receipt["restored_sha256"] = before["digest"], restored["digest"] + receipt["audience_rows"] = [] + for task_id in task_ids: + store = Evidence(clone, task_id) + for audience in ("operator", "manager", "analyst"): + artifact = get_artifact(store, audience) + receipt["audience_rows"].append({"task_id": task_id, "audience": audience, "artifact": artifact["sha256"], "rows": len(artifact["body"]["rows"])}) + # Execute only already-completed domain jobs; no scheduler or provider runs in the clone. + prior = {name: os.environ.get(name) for name in ("BACKINTEL_APP_DATABASE_URL", "BACKINTEL_TEST_DATABASE_URL")} + try: + os.environ.update(BACKINTEL_APP_DATABASE_URL=clone_dsn, BACKINTEL_TEST_DATABASE_URL=clone_dsn) + replay = [execute(row[0]) for row in restored["jobs"]] + finally: + for name, value in prior.items(): + if value is None: + os.environ.pop(name, None) + else: + os.environ[name] = value + if not all(r.get("reused") and r["state"] == "completed" for r in replay): + raise AssertionError("Restored completed work was not safely reused") + after = domain_snapshot(clone, task_ids) + if after["digest"] != restored["digest"]: + raise AssertionError("Restored replay changed accepted evidence or delivery history") + receipt.update(status="passed", replayed_jobs=len(replay), after_replay_sha256=after["digest"]) + except Exception as exc: + receipt.update(status="failed", error={"type": type(exc).__name__, "message": str(exc)}) + raise + finally: + if created: + with psycopg.connect("postgresql://capability_test@127.0.0.1:55436/postgres", autocommit=True) as admin: + admin.execute(sql.SQL("DROP DATABASE {} WITH (FORCE)").format(sql.Identifier(target))) + receipt["temporary_database_removed"] = True + (output / "Restore.json").write_bytes(encoded(receipt)) + print(json.dumps({"status": receipt["status"], "receipt": str(output / "Restore.json")})) + + +if __name__ == "__main__": + main() diff --git a/scripts/validation/check_real_driver_runtime.py b/scripts/validation/check_real_driver_runtime.py new file mode 100644 index 0000000..0e20307 --- /dev/null +++ b/scripts/validation/check_real_driver_runtime.py @@ -0,0 +1,106 @@ +"""Native-clock scoped scheduling and tmpfs checks using source preparation only.""" + +import json +from pathlib import Path +import subprocess +import time +import uuid + +import psycopg + +from runtime.evidence import Evidence +from runtime.jobs import schedule +from runtime.simulation import encoded +from scripts.capability_demo import BASE, DEMO_DSN, ROOT +from scripts.jev.run_review_batch import request +from scripts.real_demo import RUN_LOCK, clear_credential, install_credential, restart_pending, scheduler, set_scheduler +from scripts.real_capabilities import submit + + +def main(): + suffix = uuid.uuid4().hex[:8] + demo_id = "driver-clock-"+suffix + output = ROOT/"artifacts/validation/RealDriverRuntime"/("Attempt"+suffix) + output.mkdir(parents=True,mode=0o700) + task_ids = [f"{name}-real-{demo_id}" for name in ("support","equipment")] + receipt = {"status":"running","candidate":"uncommitted-working-tree","operations":"source preparation only", + "credential":"dummy non-provider test value; no Keychain retrieval","actual_paid_provider_calls":0} + cron_id = ignored = None + try: + with psycopg.connect(DEMO_DSN,autocommit=True) as connection: + if not connection.execute("SELECT pg_try_advisory_lock(%s)",(RUN_LOCK,)).fetchone()[0]: + raise RuntimeError("A real-run command owns the runtime; validation must wait") + assistants = request("POST","/assistants/search",{"graph_id":"capability_platform","limit":1},base=BASE) + schemas = request("GET",f"/assistants/{assistants[0]['assistant_id']}/schemas",base=BASE) + assert "task_ids" in schemas["input_schema"]["properties"] + for scenario in ("support","equipment"): + assert submit("prepare",scenario,demo_id,"native-history")["result"]["state"] == "completed" + before_calls = connection.execute("SELECT count(*) FROM backintel.capability_model_requests").fetchone()[0] + install_credential("fixture-only-not-a-provider-key",task_ids,time.time()+60,output.name) + program = """import json,os,stat +from pathlib import Path +from runtime.real_semantics import runtime_credential +p=Path('/run/backintel-credentials/openrouter.json') +r=json.loads(p.read_text()) +assert stat.S_IMODE(p.stat().st_mode)==0o600 +assert runtime_credential(r['task_ids'][0]) is not None +assert runtime_credential('unapproved-task') is None +print(json.dumps({'mode':'0600','task_bound':True,'tasks':len(r['task_ids'])})) +""" + checked = subprocess.run(["docker","compose","-f","compose.capabilities.yml","exec","-T","runtime","python","-c",program], + cwd=ROOT,capture_output=True,text=True,check=True,timeout=15) + receipt["tmpfs_credential"] = json.loads(checked.stdout) + due = float(connection.execute("SELECT extract(epoch FROM now())").fetchone()[0])-1 + for scenario,task_id in zip(("support","equipment"),task_ids): + schedule(Evidence(connection,task_id),"event",{"operation":"real_prepare","scenario":scenario},due,"native-prepare") + ignored = schedule(Evidence(connection,"support-real-ignored-"+suffix),"event",{"operation":"real_prepare","scenario":"support"},due,"untouched") + connection.execute("LISTEN backintel_capability_jobs") + cron_id = scheduler(assistants[0]["assistant_id"],task_ids,demo_id,time.time()+45) + receipt["recovery"] = restart_pending(connection,task_ids,cron_id,"fixture-only-not-a-provider-key",time.time()+60,output) + set_scheduler(cron_id,True) + deadline = time.monotonic()+30 + while True: + rows = connection.execute("SELECT task_id,state,result_sha256 FROM backintel.capability_jobs WHERE task_id=ANY(%s) AND trigger_id IS NOT NULL",(task_ids,)).fetchall() + if len(rows)==2 and all(row[1]=="completed" for row in rows): + break + if any(row[1] in ("failed","cancelled") for row in rows) or time.monotonic() >= deadline: + raise AssertionError("Native-clock preparation did not complete") + next(connection.notifies(timeout=max(0,deadline-time.monotonic()),stop_after=1),None) + set_scheduler(cron_id,False) + assert connection.execute("SELECT state FROM backintel.capability_triggers WHERE trigger_id=%s",(ignored,)).fetchone()[0]=="pending" + for task_id,state,sha in rows: + assert Evidence(connection,task_id).get(sha)["body"]["source_records"]==24 + assert connection.execute("SELECT count(*) FROM backintel.capability_model_requests").fetchone()[0]==before_calls + connection.execute("DELETE FROM backintel.capability_triggers WHERE trigger_id=%s AND state='pending'",(ignored,)) + ignored = None + receipt.update(status="passed",scheduler="aegra_native_cron",cron_id=cron_id,completed_tasks=task_ids,unrelated_trigger_unchanged=True) + except Exception as exc: + receipt.update(status="failed",error={"type":type(exc).__name__,"message":str(exc)}) + raise + finally: + if cron_id: + try: + set_scheduler(cron_id,False) + except Exception as exc: + receipt.update(status="failed",scheduler_cleanup=type(exc).__name__) + try: + clear_credential(output.name) + check = subprocess.run(["docker","compose","-f","compose.capabilities.yml","exec","-T","runtime","python","-c", + "from pathlib import Path; assert not Path('/run/backintel-credentials/openrouter.json').exists()"], + cwd=ROOT,capture_output=True,text=True,timeout=15) + receipt["dummy_credential_removed"] = check.returncode==0 + if check.returncode: + receipt["status"] = "failed" + except Exception as exc: + receipt.update(status="failed",credential_cleanup=type(exc).__name__) + if ignored: + with psycopg.connect(DEMO_DSN,autocommit=True) as connection: + connection.execute("DELETE FROM backintel.capability_triggers WHERE trigger_id=%s AND state='pending'",(ignored,)) + path = output/"Checks.json" + path.write_bytes(encoded(receipt)) + print(json.dumps({"status":receipt["status"],"receipt":str(path)})) + return 0 if receipt["status"]=="passed" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/validation/check_real_followups.py b/scripts/validation/check_real_followups.py new file mode 100644 index 0000000..a0758aa --- /dev/null +++ b/scripts/validation/check_real_followups.py @@ -0,0 +1,149 @@ +"""Actual local predictors with deterministic provider fixtures; never final Jev acceptance.""" + +from __future__ import annotations + +import json +import os +from pathlib import Path +from types import SimpleNamespace +import uuid +from unittest.mock import patch + +import psycopg +from psycopg.types.json import Jsonb + +from runtime.bootstrap import initialize +from runtime.contracts import current_sources +from runtime.evidence import Evidence +from runtime.jobs import enqueue, execute, release_due, runnable +from runtime.ledger import dsn +from runtime.prediction import registry +from runtime.real_semantics import extract_real, scope_for +from runtime.simulation import digest, encoded + + +class ProviderFixture: + calls = 0 + + def __init__(self,model): + self.model = model + + def invoke(self,payload): + type(self).calls += 1 + request_id = "fixture-"+uuid.uuid4().hex + content = payload["state"] + if content in ("Login failed","Service working"): + nouls = {key:{"noul":.95 if content == "Login failed" else .05} for key in payload["questions"]} + scores = {} + else: + value = float(content) + low = int(value) + high = min(10,low+1) + scores = {key:{"score":value,"legend":{str(low):low,str(high):high}, + "probabilities":{str(low):1-(value-low),str(high):value-low} if high != low else {str(low):1.}} + for key in payload["questions"]} + nouls = {} + self.last_metadata = {"request_id":request_id,"model":self.model,"cost_usd":.001,"test_fixture":True} + self.last_response = {"test_fixture":True,"nouls":nouls,"scores":scores} + return SimpleNamespace(nouls=nouls,choices={},scores=scores,request_id=request_id, + model=self.model,usage=SimpleNamespace(input_tokens=10,output_tokens=3)) + + async def aclose(self): + pass + + +def fixture_extract(*args,**kwargs): + kwargs["classifier_factory"] = ProviderFixture + return extract_real(*args,**kwargs) + + +def main(): + if not os.environ.get("BACKINTEL_TEST_DATABASE_URL") or dsn() != os.environ["BACKINTEL_TEST_DATABASE_URL"] or not psycopg.conninfo.conninfo_to_dict(dsn())["dbname"].startswith("test_"): + raise PermissionError("Provider-fixture journey requires an explicitly isolated test database") + initialize() + output = Path(__file__).resolve().parents[2]/"artifacts/validation/RealFollowups"/("Attempt"+uuid.uuid4().hex[:12]) + output.mkdir(parents=True) + receipt = {"schema":"backintel-real-followup-fixture/v1","status":"running","candidate":"uncommitted-working-tree", + "provider":"deterministic local fixture; reported charges are fictional","predictors":"actual local CatBoost and TabICLv2", + "clock":"due timestamps and retry delays advanced in the isolated test database", + "actual_paid_provider_calls":0,"actual_provider_usd":0,"local_compute_usd":None,"checks":[]} + try: + with psycopg.connect(dsn(),autocommit=True) as connection, patch("runtime.real_pipeline.extract_real",side_effect=fixture_extract), patch("runtime.real_semantics._default_classifier",side_effect=AssertionError("Paid provider forbidden in this check")): + for scenario in ("support","equipment"): + store = Evidence(connection,"followup-fixture-"+scenario+"-"+uuid.uuid4().hex[:12]) + def run(operation,identity,**fields): + job = enqueue(store,{"scenario":scenario,"operation":operation,**fields},identity) + result = execute(job) + if result["state"] != "completed": + raise AssertionError(result) + return store.get(result["result"]) + history = run("real_prepare","prepare") + followups = run("real_prepare_followups","prepare-followups") + assert len(current_sources(store,71,history["body"]["task"])) == 24 + sources = [store.get(sha) for sha in history["body"]["sources"]+followups["body"]["sources"]] + scope = scope_for(store.get(history["body"]["task"]),sources) + authorization = "fixture-followups-"+uuid.uuid4().hex + connection.execute("""INSERT INTO backintel.capability_provider_authorizations + (authorization_id,provider,model,max_requests,max_input_characters,price_ceiling_known,approved,scope_sha256,scope,expires_at) + VALUES (%s,'openrouter','jev-1.13',27,5000,true,true,%s,%s,now()+interval '1 hour')""",(authorization,digest(scope),Jsonb(scope))) + scheduled = run("real_start","start",provider_authorization_id=authorization) + assert len(scheduled["body"]["triggers"]) == 26 + while True: + connection.execute("""UPDATE backintel.capability_triggers SET due_at=to_timestamp(ordinal.position) + FROM (SELECT trigger_id,row_number() OVER (ORDER BY due_at,trigger_id) AS position + FROM backintel.capability_triggers WHERE task_id=%s AND state='pending') ordinal + WHERE backintel.capability_triggers.trigger_id=ordinal.trigger_id""",(store.task_id,)) + release_due(connection) + pending = connection.execute("SELECT count(*) FROM backintel.capability_jobs WHERE task_id=%s AND state IN ('queued','retry','running')",(store.task_id,)).fetchone()[0] + if not pending: + break + connection.execute("UPDATE backintel.capability_jobs SET due_at=now() WHERE task_id=%s AND state='retry'",(store.task_id,)) + ready = [j for j in runnable(connection) if connection.execute("SELECT task_id FROM backintel.capability_jobs WHERE job_id=%s",(j,)).fetchone()[0] == store.task_id] + if not ready: + raise AssertionError("Fixture journey has pending work but no runnable job") + assert pending <= 20 + for job in ready: + result = execute(job) + if result["state"] not in ("completed","retry"): + raise AssertionError(result) + failed = connection.execute("SELECT job_id,error FROM backintel.capability_jobs WHERE task_id=%s AND state!='completed'",(store.task_id,)).fetchall() + assert not failed, failed + assert connection.execute("SELECT count(*) FROM backintel.capability_triggers WHERE task_id=%s AND state='pending'",(store.task_id,)).fetchone()[0] == 0 + initial = store.get(store.find("real_stage_result","history-v1")["body"]["result"]) + assert initial["body"]["mode"] == "synthetic_sources_real_predictors_fixture_semantics" + assert {(m["body"]["route"],m["body"]["feature_set"]) for m in store.list("model") if m["body"].get("implementation_mode") == "real"} == {(r,f) for r in ("catboost","tabiclv2") for f in ("structured","semantic")} + assert len(store.list("task")) == 1 + assert store.list("invalidation") and store.list("analysis") and store.list("artifact") + active = store.get(registry(store,94)["active"]) + assert active["body"]["implementation_mode"] in ("real","native_baseline") + updated = store.get(store.find("model_update","bounded-update")["body"]["model"]) + assert updated["body"]["implementation_mode"] in ("real","native_baseline") + assert updated["body"]["training_count"] <= 64 + assert updated["body"]["training_count"] == 24 # Corrected future source must not train on its superseded features. + before = [r[0] for r in connection.execute("SELECT sha256 FROM backintel.capability_evidence WHERE task_id=%s ORDER BY sha256",(store.task_id,))] + calls_before = ProviderFixture.calls + jobs = connection.execute("SELECT job_id FROM backintel.capability_jobs WHERE task_id=%s ORDER BY sequence",(store.task_id,)).fetchall() + assert len(jobs) == 45 + assert all(execute(j[0])["reused"] for j in jobs) + assert ProviderFixture.calls == calls_before + assert [r[0] for r in connection.execute("SELECT sha256 FROM backintel.capability_evidence WHERE task_id=%s ORDER BY sha256",(store.task_id,))] == before + requests = connection.execute("SELECT count(*) FROM backintel.capability_model_requests WHERE task_id=%s",(store.task_id,)).fetchone()[0] + assert requests == 27 + receipt["checks"].append({"scenario":scenario,"task_id":store.task_id,"completed_jobs":len(jobs), + "provider_fixture_requests":requests,"initial_result":initial["sha256"],"active_model":active["sha256"], + "models":len(store.list("model")),"replay_unchanged":True,"correction_invalidations":len(store.list("invalidation")), + "update_training_rows":updated["body"]["training_count"]}) + print(json.dumps(receipt["checks"][-1]),flush=True) + receipt["status"] = "passed" + except Exception as exc: + receipt.update(status="failed",error={"type":type(exc).__name__,"message":str(exc)}) + raise + finally: + path = output/"Followups.json" + path.write_bytes(encoded(receipt)) + print(json.dumps({"status":receipt["status"],"receipt":str(path)})) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/validation/check_real_model_restore.py b/scripts/validation/check_real_model_restore.py new file mode 100644 index 0000000..d6dcbe9 --- /dev/null +++ b/scripts/validation/check_real_model_restore.py @@ -0,0 +1,218 @@ +"""Restore actual predictor packages and checkpoints, then reproduce recorded predictions.""" + +from __future__ import annotations + +import argparse +from contextlib import contextmanager +import json +import math +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import uuid + +import psycopg +from psycopg import sql + +from runtime.evidence import Evidence +from runtime.ledger import dsn +from runtime.jobs import execute +from runtime.real_models import _load, checkpoint, file_sha, model_root, predict_real +from runtime.simulation import digest, encoded +from scripts.capability_demo import domain_snapshot + + +@contextmanager +def restored_database(source_dsn, task_ids, output, receipt, enabled): + if not enabled: + yield source_dsn + return + config = psycopg.conninfo.conninfo_to_dict(source_dsn) + servers = { + ("55436", "capability_test"): "backintel-capability-test", + ("55437", "capability_demo"): "backintel-capability-demo-postgres-1", + } + container = servers.get((config.get("port"), config.get("user"))) + if config.get("host") != "127.0.0.1" or not container: + raise PermissionError("Combined recovery requires a known isolated local database") + + def snapshot(connection): + rows = {} + for table in ("capability_evidence", "capability_jobs", "capability_triggers", "capability_model_requests"): + rows[table] = [r[0] for r in connection.execute(sql.SQL( + "SELECT to_jsonb(r) FROM backintel.{} r WHERE task_id=ANY(%s) ORDER BY to_jsonb(r)::text" + ).format(sql.Identifier(table)), (task_ids,))] + return digest(rows) + + target = "test_model_restore_" + uuid.uuid4().hex[:12] + clone_dsn = f"postgresql://capability_test@127.0.0.1:55436/{target}" + created = False + environment = {name: os.environ.get(name) for name in ("BACKINTEL_APP_DATABASE_URL", "BACKINTEL_TEST_DATABASE_URL")} + try: + with psycopg.connect(source_dsn) as source: + pending = source.execute("""SELECT + (SELECT count(*) FROM backintel.capability_jobs WHERE task_id=ANY(%s) AND state!='completed') + + (SELECT count(*) FROM backintel.capability_triggers WHERE task_id=ANY(%s) AND state='pending')""", + (task_ids, task_ids)).fetchone()[0] + if pending: + raise RuntimeError("Combined recovery requires completed, quiescent task streams") + before = snapshot(source) + backup = output / "Database.dump" + with backup.open("wb") as data, (output / "DatabaseBackup.log").open("w") as log: + subprocess.run(["docker", "exec", container, "pg_dump", "-U", config["user"], "-d", config["dbname"], + "--format=custom", "--no-owner", "--no-privileges"], + stdout=data, stderr=log, check=True, timeout=60) + if snapshot(source) != before: + raise RuntimeError("Source changed during backup; retry from a quiescent stream") + receipt["database_backup"] = {"file": str(backup), "bytes": backup.stat().st_size, "sha256": file_sha(backup)} + with psycopg.connect("postgresql://capability_test@127.0.0.1:55436/postgres", autocommit=True) as admin: + admin.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier(target))) + created = True + with backup.open("rb") as data, (output / "DatabaseRestore.log").open("w") as log: + subprocess.run(["docker", "exec", "-i", "backintel-capability-test", "pg_restore", "-U", "capability_test", + "-d", target, "--no-owner", "--no-privileges", "--exit-on-error"], + stdin=data, stdout=log, stderr=subprocess.STDOUT, check=True, timeout=60) + with psycopg.connect(clone_dsn, autocommit=True) as clone: + if snapshot(clone) != before: + raise AssertionError("Restored evidence, jobs, triggers or provider ledger differ") + os.environ.update(BACKINTEL_APP_DATABASE_URL=clone_dsn, BACKINTEL_TEST_DATABASE_URL=clone_dsn) + yield clone_dsn + completed = domain_snapshot(clone, task_ids)["jobs"] + for row in completed: + result = execute(row[0]) + if not result.get("reused") or result["state"] != "completed": + raise AssertionError("Restored completed job did not replay safely") + if snapshot(clone) != before: + raise AssertionError("Restored scoring or replay changed accepted state or provider requests") + receipt["combined_database_recovery"] = {"status": "passed", "source_and_restored_sha256": before, + "replayed_jobs": len(completed), "provider_requests_unchanged": True} + finally: + for name, value in environment.items(): + if value is None: + os.environ.pop(name, None) + else: + os.environ[name] = value + if created: + with psycopg.connect("postgresql://capability_test@127.0.0.1:55436/postgres", autocommit=True) as admin: + admin.execute(sql.SQL("DROP DATABASE {} WITH (FORCE)").format(sql.Identifier(target))) + receipt["temporary_database_removed"] = True + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--predictor-receipt", type=Path, required=True) + parser.add_argument("--restore-database", action="store_true", help="Restore the isolated database and model files together") + parser.add_argument("--predictor-image", help="Use the existing pinned Linux image that prepared the models") + args = parser.parse_args() + prior = json.loads(args.predictor_receipt.read_text()) + if prior["status"] != "passed" or prior["data"] != "synthetic": + raise PermissionError("Recovery requires a passed synthetic predictor receipt") + test_dsn = os.environ.get("BACKINTEL_TEST_DATABASE_URL") + if not test_dsn or test_dsn != dsn() or not psycopg.conninfo.conninfo_to_dict(test_dsn)["dbname"].startswith("test_"): + raise PermissionError("Recovery requires the explicitly isolated test database") + output = Path(__file__).resolve().parents[2] / "artifacts/validation/RealModelRecovery" / ("Attempt"+uuid.uuid4().hex[:12]) + backup = output / "BackupModels" + output.mkdir(parents=True) + receipt = {"schema":"backintel-real-model-restore/v1", "status":"running", "candidate":"uncommitted-working-tree", + "paid_provider_calls":0, "provider_usd":0, "local_compute_usd":None, "checks":[], + "limits":["Model files and checkpoints restored; this check uses the existing isolated evidence database.", + "Real Jev and the complete combined database/model recovery remain separate acceptance."]} + if args.restore_database: + receipt["limits"] = ["Synthetic evidence only; actual Jev, live scheduler/broker restore and exact-commit acceptance remain separate."] + receipt["semantic_provider"] = prior.get("semantic_provider", "structured-only predictor check") + original = model_root() + try: + task_ids = sorted({check["task_id"] for check in prior["checks"]}) + if not task_ids: + raise ValueError("Recovery requires at least one recorded predictor") + if args.predictor_image: + if not args.restore_database or not args.predictor_image.startswith("sha256:"): + raise ValueError("A pinned predictor image requires combined database recovery") + root = Path(__file__).resolve().parents[2] + inputs = output / "Predictors.json" + inputs.write_bytes(encoded(prior)) + with restored_database(test_dsn, task_ids, output, receipt, True) as restore_dsn: + container_dsn = restore_dsn.replace("127.0.0.1", "host.docker.internal") + command = ["docker", "run", "--rm", "--cpus", "2", "--entrypoint", "python", + "-v", f"{root / 'artifacts'}:/app/artifacts", "-v", f"{original}:/models:ro", + "-e", f"BACKINTEL_APP_DATABASE_URL={container_dsn}", "-e", f"BACKINTEL_TEST_DATABASE_URL={container_dsn}", + "-e", "BACKINTEL_MODEL_DIR=/models", "-e", "OMP_NUM_THREADS=2", "-e", "OPENBLAS_NUM_THREADS=2", + "-e", "MKL_NUM_THREADS=2", "-e", "HF_HUB_OFFLINE=1", args.predictor_image, + "-m", "scripts.validation.check_real_model_restore", "--predictor-receipt", + str(Path('/app') / inputs.relative_to(root))] + with (output / "PredictorRestore.log").open("w+") as log: + subprocess.run(command, stdout=log, stderr=subprocess.STDOUT, check=True, timeout=600) + log.seek(0) + result = json.loads(log.read().strip().splitlines()[-1]) + model_receipt = root / Path(result["receipt"]).relative_to("/app") + recovered = json.loads(model_receipt.read_text()) + if recovered["status"] != "passed" or not recovered.get("temporary_restore_removed"): + raise AssertionError("Container model recovery did not complete and clean up") + receipt.update(checks=recovered["checks"], model_recovery_receipt=str(model_receipt), + predictor_image=args.predictor_image, temporary_restore_removed=True) + receipt["status"] = "passed" + return 0 + backup.mkdir() + relative = {Path("model-use-approval.json")} + for check in prior["checks"]: + body = check["model"]["body"] + relative.update((Path(body["artifact"]["package"])/body["artifact"]["file"], + Path(body["artifact"]["package"])/"manifest.json")) + if body["checkpoint"]: + path, spec = checkpoint(body["target"]["kind"]) + if spec != body["checkpoint"]: + raise ValueError("Predictor receipt has a different checkpoint identity") + relative.add(path.relative_to(original)) + identities = {} + for path in sorted(relative): + source = original / path + if not source.resolve().is_relative_to(original): + raise ValueError("Model receipt references an out-of-scope file") + target = backup / path + target.parent.mkdir(parents=True,exist_ok=True) + shutil.copyfile(source,target) + identities[str(path)] = {"sha256":file_sha(target),"bytes":target.stat().st_size} + receipt["backup_files"] = identities + with restored_database(test_dsn, task_ids, output, receipt, args.restore_database) as restore_dsn, \ + tempfile.TemporaryDirectory(prefix="BackIntelModelRestore") as directory: + restored = Path(directory)/"Models" + shutil.copytree(backup,restored) + for path, identity in identities.items(): + if file_sha(restored/path) != identity["sha256"]: + raise AssertionError("Restored model bytes differ") + os.environ["BACKINTEL_MODEL_DIR"] = str(restored) + _load.cache_clear() + with psycopg.connect(restore_dsn) as connection: + for check in prior["checks"]: + store = Evidence(connection,check["task_id"]) + model = store.get(check["model"]["sha256"]) + verified = 0 + evaluation = store.get(check["evaluation"]["sha256"])["body"] + for case, expected in zip(evaluation["cases"],evaluation["predictions"],strict=True): + feature = store.get(case[0]) + value = predict_real(model,feature) + if not math.isclose(value,expected,rel_tol=1e-6,abs_tol=1e-7): + raise AssertionError("Restored model prediction differs") + verified += 1 + if not verified: + raise AssertionError("Recovery receipt has no predictions") + receipt["checks"].append({"scenario":check["scenario"],"route":check["route"], + "model":model["sha256"],"predictions_reproduced":verified}) + _load.cache_clear() + receipt.update(status="passed",temporary_restore_removed=not Path(directory).exists()) + except Exception as exc: + receipt.update(status="failed",error={"type":type(exc).__name__,"message":str(exc)}) + raise + finally: + _load.cache_clear() + os.environ["BACKINTEL_MODEL_DIR"] = str(original) + path = output/"Restore.json" + path.write_bytes(encoded(receipt)) + print(json.dumps({"status":receipt["status"],"receipt":str(path)})) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/validation/check_real_models.py b/scripts/validation/check_real_models.py new file mode 100644 index 0000000..fca3a0d --- /dev/null +++ b/scripts/validation/check_real_models.py @@ -0,0 +1,86 @@ +"""Run approved local predictors on synthetic, structured-only histories.""" + +from __future__ import annotations + +import json +import os +from pathlib import Path +import time +import uuid + +import psycopg + +from runtime.bootstrap import initialize +from runtime.contracts import admit_source, register_task +from runtime.evidence import Evidence +from runtime.ledger import dsn +from runtime.prediction import cases, chronological_split, evaluate, features, outcome, prepare +from runtime.real_models import _allow_model_use +from runtime.simulation import encoded +from runtime.synthetic import history + +OUTPUT = Path(__file__).resolve().parents[2] / "artifacts/validation/RealPredictors" + + +def main() -> int: + OUTPUT.mkdir(parents=True, exist_ok=True) + run_id = uuid.uuid4().hex + receipt = { + "schema": "backintel-real-predictor-check/v1", "run_id": run_id, + "status": "running", "candidate": "uncommitted-working-tree", + "data": "synthetic", "feature_set": "structured", "paid_provider_calls": 0, + "provider_usd": 0, "local_compute_usd": None, "checks": [], + "limits": ["Synthetic results do not establish real-world quality.", + "Real Jev and four-way comparison remain separate acceptance requirements."], + } + path = OUTPUT / (run_id + ".json") + started = time.perf_counter() + try: + test_dsn = os.environ.get("BACKINTEL_TEST_DATABASE_URL") + if not test_dsn or test_dsn != dsn() or not psycopg.conninfo.conninfo_to_dict(test_dsn)["dbname"].startswith("test_"): + raise PermissionError("Actual predictor checks require the isolated test database") + for route in ("catboost", "tabiclv2"): + _allow_model_use(route) + initialize() + with psycopg.connect(dsn(), autocommit=True) as connection: + for scenario in ("support", "equipment"): + task, rows, labels = history(scenario) + task["id"] = scenario + "-real-" + run_id[:12] + store = Evidence(connection, task["id"]) + task_record = register_task(store, task) + snapshots = [] + for row, label in zip(rows, labels): + at = label["event_at"] + admit_source(store, task_record, {"format": "json", "data": [row]}, at) + snapshots.extend(r for r in features(store, task_record, at) + if r["body"]["source_id"] == label["source_id"]) + outcome(store, task_record, label) + at = max(r["available_at"] for r in labels) + training, holdout, prepared_at = chronological_split(task, cases(store, task_record, snapshots, at)) + for route in ("catboost", "tabiclv2"): + print(json.dumps({"scenario": scenario, "route": route, "state": "preparing", + "training_rows": len(training), "holdout_rows": len(holdout)}), flush=True) + model = prepare(store, task_record, training, route, "structured", prepared_at, "real") + evaluation = evaluate(store, model, holdout, at) + assert model["body"]["implementation_mode"] == "real" + assert len(evaluation["body"]["predictions"]) == len(holdout) > 0 + receipt["checks"].append({"scenario": scenario, "task_id": task["id"], + "route": route, "model": model, "evaluation": evaluation}) + path.write_bytes(encoded(receipt)) + print(json.dumps({"scenario": scenario, "route": route, "state": "passed", + "wall_ms": evaluation["body"]["wall_ms"], + "metrics": evaluation["body"]["metrics"]}), flush=True) + receipt["status"] = "passed" + except Exception as exc: + receipt.update(status="blocked" if isinstance(exc, (PermissionError, FileNotFoundError)) else "failed", + error={"type": type(exc).__name__, "message": str(exc)}) + raise + finally: + receipt["wall_seconds"] = time.perf_counter() - started + path.write_bytes(encoded(receipt)) + print(json.dumps({"status": receipt["status"], "receipt": str(path)}), flush=True) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/validation/check_real_package.py b/scripts/validation/check_real_package.py new file mode 100644 index 0000000..748a8f2 --- /dev/null +++ b/scripts/validation/check_real_package.py @@ -0,0 +1,77 @@ +"""Check real-predictor packaging using previously validated, explicitly fixture Jev data.""" + +import argparse +import json +import os +from pathlib import Path +import stat +import uuid + +import psycopg + +from runtime.ledger import dsn +from runtime.simulation import encoded +from scripts.capability_demo import domain_snapshot +from scripts.package_capabilities import package + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--fixture-receipt",type=Path,required=True) + args = parser.parse_args() + if not os.environ.get("BACKINTEL_TEST_DATABASE_URL") or dsn() != os.environ["BACKINTEL_TEST_DATABASE_URL"] or not psycopg.conninfo.conninfo_to_dict(dsn())["dbname"].startswith("test_"): + raise PermissionError("Packaging checks require an explicitly isolated fixture database") + prior = json.loads(args.fixture_receipt.read_text()) + if prior["status"] != "passed" or "fixture" not in prior["provider"]: + raise ValueError("A passed and clearly labelled provider-fixture receipt is required") + tasks = [r["task_id"] for r in prior["checks"]] + output = Path(__file__).resolve().parents[2]/"artifacts/validation/RealPackages"/("Attempt"+uuid.uuid4().hex[:12]) + output.mkdir(parents=True,mode=0o700) + receipt = {"status":"running","candidate":"uncommitted-working-tree","semantic_provider":"fixture", + "actual_paid_provider_calls":0,"checks":[]} + try: + with psycopg.connect(dsn(),autocommit=True) as connection: + before = domain_snapshot(connection,tasks) + for mode in ("development","real"): + try: + package(connection,tasks,output/("Rejected"+mode.title()),mode=mode) + except ValueError: + receipt["checks"].append({"mode":mode,"relabeling_rejected":True}) + else: + raise AssertionError("Fixture semantic results were relabelled as real Jev or simulated predictors") + assert domain_snapshot(connection,tasks) == before + packaged = package(connection,tasks,output/"Package",mode="predictor_fixture") + assert packaged["paid_provider_calls"] == packaged["provider_usd"] == 0 + assert packaged["provider_fixture_requests"] == 54 + assert abs(packaged["fixture_provider_usd"]-.054) < 1e-10 + assert packaged["semantic_observations"] == "fixture" + reports = list((output/"Package/Reports").glob("*/*/Report.html")) + assert reports + for report in reports: + html = report.read_text() + assert "Text findings: deterministic test fixtures" in html + assert " { + const root = path.resolve(process.argv[2]); + const output = path.join(root, 'Browser'); + fs.mkdirSync(output, { recursive: true }); + const receipt = { status: 'running', provider: 'fixture', checks: [], screenshots: [] }; + let browser; + try { + browser = await chromium.launch({ executablePath: process.env.BACKINTEL_CHROMIUM_EXECUTABLE, headless: true }); + for (const task of fs.readdirSync(path.join(root, 'Reports'))) { + for (const file of ['Report.html', 'GeneratedView.html']) { + const source = path.join(root, 'Reports', task, 'operator', file); + for (const width of [1280, 390]) { + const page = await browser.newPage({ viewport: { width, height: 900 } }); + await page.goto(pathToFileURL(source).href); + const label = await page.locator('p.tag').first().innerText(); + if (!label.includes('Text findings: deterministic test fixtures')) throw new Error('Fixture status is not visible'); + if (await page.locator('form').count()) throw new Error('Offline export exposes a live form'); + if (await page.evaluate(() => document.documentElement.scrollWidth > innerWidth)) throw new Error('Page overflows the viewport'); + if (await page.locator('a[href^="/"]').count()) throw new Error('Offline export contains live-app links'); + receipt.checks.push({ task, file, width, fixture_label_visible: true, offline: true, no_page_overflow: true }); + if (file === 'Report.html') { + const image = path.join(output, `${task}-${width}.png`); + await page.screenshot({ path: image, fullPage: true }); + receipt.screenshots.push(image); + } + await page.close(); + } + } + } + receipt.status = 'passed'; + } catch (error) { + receipt.status = 'failed'; + receipt.error = error.message; + process.exitCode = 1; + } finally { + if (browser) await browser.close(); + const target = path.join(output, 'Checks.json'); + fs.writeFileSync(target, JSON.stringify(receipt, null, 2)); + process.stdout.write(JSON.stringify({ status: receipt.status, checks: receipt.checks.length, receipt: target }) + '\n'); + } +})(); diff --git a/scripts/validation/check_runtime.py b/scripts/validation/check_runtime.py index b3684fd..db322ed 100644 --- a/scripts/validation/check_runtime.py +++ b/scripts/validation/check_runtime.py @@ -15,7 +15,7 @@ def main() -> int: os.environ["BACKINTEL_APP_DATABASE_URL"] = test_dsn initialize() suite = unittest.TestSuite() - for pattern in ("test_costs.py", "test_replay_ledger.py", "test_jev.py"): + for pattern in ("test_costs.py", "test_replay_ledger.py", "test_jev.py", "test_simulation_runtime.py"): suite.addTests(unittest.defaultTestLoader.discover("tests", pattern=pattern)) result = unittest.TextTestRunner(verbosity=2).run(suite) return 0 if result.wasSuccessful() else 1 diff --git a/scripts/validation/check_sandbox.py b/scripts/validation/check_sandbox.py new file mode 100644 index 0000000..bb98659 --- /dev/null +++ b/scripts/validation/check_sandbox.py @@ -0,0 +1,58 @@ +"""Actually attempt prohibited operations in the local generated-code sandbox.""" + +from __future__ import annotations + +import json +from pathlib import Path +import tempfile +import uuid + +from runtime.sandbox import run_candidate +from runtime.simulation import encoded + + +def main() -> int: + output = Path(__file__).resolve().parents[2] / "artifacts/validation/Sandbox" + output.mkdir(parents=True, exist_ok=True) + receipt = {"schema": "backintel-sandbox-check/v1", "candidate": "uncommitted-working-tree", + "code_generation": "simulated", "sandbox_execution": "real", "checks": [], "status": "running"} + with tempfile.TemporaryDirectory(prefix="BackIntelHostBoundary") as temporary: + secret = Path(temporary) / "forbidden.txt" + secret.write_text("synthetic host-only marker") + samples = { + "approved_input": ('import json\ns=json.load(open("/input/snapshot.json"))\nprint(json.dumps({"total":sum(s["values"])}))', "candidate"), + "host_read": (f'print(open({str(secret)!r}).read())', "process_failed"), + "protected_input_write": ('open("/input/snapshot.json","w").write("changed")', "process_failed"), + "protected_source_write": ('open("/candidate/code.py","w").write("changed")', "process_failed"), + "protected_dependency_write": ('import json\nopen(json.__file__,"w").write("changed")', "process_failed"), + "network": ('import socket\nsocket.create_connection(("1.1.1.1",443),timeout=1)', "process_failed"), + "cpu": ('while True: pass', "time_limit"), + "memory": ('x=bytearray(1024*1024*1024)\nprint("{}")', "process_failed"), + "output": ('while True: print("x"*4096)', "output_limit"), + "temporary_storage": ('open("/tmp/large","wb").write(b"x"*(2*1024*1024))', "process_failed"), + } + path = output / (uuid.uuid4().hex + ".json") + try: + for name, (source, expected) in samples.items(): + run = run_candidate(source, {"values": [2, 3, 5]}, "backintel-capability-demo-runtime:latest") + actual = run["status"] if run["status"] == "candidate" else run["stop_reason"] + passed = actual == expected and run["cleanup_confirmed"] + if name == "approved_input": + passed = passed and run["result"] == {"total": 10} + receipt["checks"].append({"name": name, "passed": passed, "expected": expected, "run": run}) + path.write_bytes(encoded(receipt)) + print(json.dumps({"check": name, "passed": passed, "actual": actual}), flush=True) + if not passed: + raise AssertionError("Sandbox failed " + name) + receipt["status"] = "passed" + except Exception as exc: + receipt.update(status="failed", error={"type": type(exc).__name__, "message": str(exc)}) + raise + finally: + path.write_bytes(encoded(receipt)) + print(json.dumps({"status": receipt["status"], "receipt": str(path)}), flush=True) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tabellio.validation.json b/tabellio.validation.json index a37d886..96a1ca5 100644 --- a/tabellio.validation.json +++ b/tabellio.validation.json @@ -1,25 +1,26 @@ { "$schema": "tabellio-validation/v0.2", "acceptance": { - "id": "backintel-monthly-seller-review-v0.1", - "source": "BackIntel BINT-5 Sprint 1 seller/category report contract and BINT-4 reconciled facts", + "id": "backintel-capability-platform-v1", + "source": "Product owner correction 2026-09-26: all 16 capabilities using synthetic sources with actual Jev, actual CatBoost and actual TabICLv2; docs/roadmap/v0.1.0-development-roadmap.md is the active acceptance source.", "risk": "high", "outcomes": [ - "Validation runs against the exact pull-request head SHA, not a merge ref or mutable branch name.", - "Every invocation writes non-empty machine-readable evidence and a human-readable report, including failed and blocked outcomes.", - "Bootstrap validation proves only repository contract and static structure; it does not claim application behavior or product readiness.", - "Before v0.1.0 release, the manifest must require evidence for data, Jev semantics, prediction/model quality, user workflows, visual presentation, cost and resource limits, recovery, and security/isolation.", - "The committed Olist profile records reproducible Kaggle v2 file fingerprints, schemas, row/null/key statistics, relationship checks, timestamp checks, language uncertainty, and a conditional target-feasibility decision.", - "A PostgreSQL application schema loads the approved local Olist v2 facts with source-row lineage, preserves observed one-to-many multiplicity, and reconciles counts and monetary totals without join fan-out.", - "A reproducible February versus March 2017 retrospective seller/category report uses frozen purchase-month cohorts, correct grains, missingness and source-linked evidence; independent raw-data checks are required locally before acceptance.", - "Offline Jev adapter checks preserve typed answer distributions, source/question provenance, usage events, replay-safe observation persistence, and a changed-signal comparison without claiming live model quality.", - "The integrated local demo reuses recorded Jev observations without provider calls, preserves unknown attribution, and renders source-linked sampled changes.", - "Offline runtime checks cover cost accounting, replay safety, and the versioned semantic pipeline; integrated runtime, rendered UI, and total-cost readiness require separate exact-candidate receipts." + "Validation binds to the approved exact candidate commit and writes durable evidence for passed, failed and blocked outcomes.", + "Both unrelated synthetic scenarios preserve source revisions, immutable lineage, typed findings and correction history through shared configurable contracts.", + "Real Jev interprets relevant synthetic text; actual outputs and request/model identities are preserved. Simulated responses only validate development controls.", + "Real CatBoost and real TabICLv2 run comparable facts-only and facts-plus-real-Jev evaluations alongside a baseline, using time-safe features and known synthetic outcomes.", + "Actual predictions flow through further analysis, durable attention and useful audience-scoped reports, with controlled selection, updates, fallback and rollback.", + "Generic background API work, persistent schedules, retries, cancellation, restart and concurrent replay preserve one accepted result and local delivery.", + "Generated artifact code runs inside an actual sandbox that rejects prohibited reads, network access and resource use.", + "One documented local command runs the full demonstration; required workflow, rendering, safety, budget and recovery checks pass; obsolete goal-owned scaffolding has a safe disposition.", + "Blocked real provider/model execution prevents completion. Synthetic-data results establish neither real-world accuracy, time savings nor representative business value." ], "invariants": [ "Missing or untrustworthy required evidence is blocked, never passed.", "An uncommitted or dirty candidate cannot receive exact-commit validation status passed.", - "No model calls, network access, paid services, or external side effects are allowed in the bootstrap validator." + "Development validators use deterministic fixtures with zero provider calls. Final real-model evidence is separately bounded and requires applicable paid-inference/model-use approval.", + "No paid inference, external notifications, production writes, publication or live-stack replacement without its separate authorization.", + "Google TabFM and TabICLv2 are distinct model choices; do not claim Google TabFM execution from a TabICLv2 run." ], "forbiddenOutcomes": [ "Do not describe structural checks as complete product validation.", @@ -27,12 +28,17 @@ "Do not report unknown validation cost as zero.", "Do not commit raw Kaggle source files or represent a review as attributable to every seller in a multi-seller order.", "Do not join order items, payments, and reviews at their raw grains to calculate totals.", - "Do not call retrospective final-extract metrics a live or month-end snapshot, attribute multi-seller order feedback to each seller, or claim human time savings without a measured baseline." + "Do not call retrospective final-extract metrics a live or month-end snapshot, attribute multi-seller order feedback to each seller, or claim human time savings without a measured baseline.", + "Do not substitute invented or prerecorded model responses for final Jev/predictor execution, or claim real-world quality or business value from synthetic data." ], "requiredValidatorTypes": [ "static", "schema", - "semantic" + "semantic", + "workflow", + "visual", + "operational", + "security" ] }, "validators": [ @@ -153,6 +159,30 @@ "requiredTelemetry": true, "maxUsd": 0 } + }, + { + "id": "capability-simulation", + "type": "workflow", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tests", + "-p", + "test_simulation.py", + "-v" + ], + "cwd": ".", + "timeout": 60, + "required": true, + "evidencePath": "capability-simulation.json", + "metricThresholds": {}, + "costPolicy": { + "requiredTelemetry": true, + "maxUsd": 0 + } } ] } diff --git a/tests/test_capabilities.py b/tests/test_capabilities.py new file mode 100644 index 0000000..6f349e5 --- /dev/null +++ b/tests/test_capabilities.py @@ -0,0 +1,369 @@ +"""Direct capability checks against an explicitly isolated PostgreSQL database.""" +import copy +import csv +import io +import json +import os +import unittest +import uuid +import time +from concurrent.futures import ThreadPoolExecutor + +import psycopg + +from runtime.bootstrap import initialize +from runtime.contracts import admit_source, current_sources, register_task, validate_task +from runtime.evidence import Evidence +from runtime.ledger import dsn +from runtime.observations import correct_observation, effective_observation, extract +from runtime.synthetic import history +from runtime.attention import attend +from runtime.jobs import cancel, claim, enqueue, execute, release_due, resume_failed, runnable, schedule +from runtime.prediction import (cases, chronological_split, compare, evaluate, features, invalidate_predictions, + metrics, outcome, prepare, registry, score, transition, update_plan) + + +class CapabilityTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + if not os.environ.get("BACKINTEL_TEST_DATABASE_URL"): + raise RuntimeError("Capability checks require an isolated test database") + if os.environ.get("BACKINTEL_APP_DATABASE_URL") != os.environ["BACKINTEL_TEST_DATABASE_URL"]: + raise RuntimeError("App and test URLs must identify the same isolated database") + if not psycopg.conninfo.conninfo_to_dict(dsn())["dbname"].startswith("test_"): + raise RuntimeError("Refusing capability checks outside test_ database") + initialize() + + def setUp(self): + self.conn = psycopg.connect(dsn(), autocommit=True) + self.addCleanup(self.conn.close) + + def scenario(self, name="support"): + task, rows, labels = history(name) + task["id"] = "test-" + uuid.uuid4().hex + store = Evidence(self.conn, task["id"]) + return store, register_task(store, task), rows, labels + + def test_portable_admission_revisions_quarantine_and_immutable_lineage(self): + for name in ("support", "equipment"): + store, task, rows, _ = self.scenario(name) + source = {"format": "json", "data": [rows[0]]} + receipt = admit_source(store, task, source, 0) + self.assertEqual(receipt, admit_source(store, task, source, 0)) + original = current_sources(store, 0)[0] + duplicate = admit_source(store, task, source, 1) + self.assertEqual(duplicate["body"]["dispositions"][0]["status"], "duplicate") + corrected = dict(rows[0], revision=2, arrived_at=2) + corrected[task["body"]["measures"][0]["field"]] = 99 + result = admit_source(store, task, {"format":"json", "data":[corrected]}, 2) + self.assertEqual(result["body"]["dispositions"][0]["revision_kind"], "correction") + self.assertEqual(current_sources(store, 1), [original]) + self.assertEqual(current_sources(store, 2)[0]["body"]["revision"], 2) + self.assertIn(original["sha256"], {r["sha256"] for r in store.lineage(result["sha256"])}) + conflict = dict(corrected) + conflict[task["body"]["measures"][0]["field"]] = 0 + malformed = {"missing": "fields"} + rejected = admit_source(store, task, {"format":"json", "data":[conflict, malformed]}, 3) + self.assertEqual([d["status"] for d in rejected["body"]["dispositions"]], ["quarantined"] * 2) + with self.assertRaises(psycopg.errors.RaiseException): + self.conn.execute("UPDATE backintel.capability_evidence SET body='{}' WHERE sha256=%s", (original["sha256"],)) + self.assertEqual(store.get(original["sha256"]), original) + + def test_csv_text_late_revision_and_schema_failures(self): + store, task, rows, _ = self.scenario() + csv_buffer = io.StringIO() + writer = csv.DictWriter(csv_buffer, fieldnames=rows[0].keys()) + writer.writeheader() + writer.writerow(dict(rows[0], revision=3)) + receipt = admit_source(store, task, {"format":"csv", "data":csv_buffer.getvalue()}, 1) + self.assertEqual(receipt["body"]["dispositions"][0]["status"], "accepted") + late = dict(rows[0], revision=2) + receipt = admit_source(store, task, {"format":"text", "data":json.dumps(late)}, 2) + self.assertEqual(receipt["body"]["dispositions"][0]["revision_kind"], "late_revision") + self.assertEqual(current_sources(store, 3)[0]["body"]["revision"], 3) + receipt = admit_source(store, task, {"format":"text", "data":"invalid json"}, 3) + self.assertEqual(receipt["body"]["dispositions"][0]["status"], "quarantined") + invalid = copy.deepcopy(task["body"]) + invalid["target"]["horizon"] = 0 + with self.assertRaises(ValueError): + validate_task(invalid) + + def test_typed_extraction_retry_cache_correction_and_scope(self): + store, task, rows, _ = self.scenario() + admit_source(store, task, {"format":"json", "data":[rows[0]]}, 0) + source = current_sources(store, 0)[0] + calls = [] + def provider(question, content): + calls.append(content) + if len(calls) == 1: + raise TimeoutError("injected transient failure") + return {"status":"known", "value":False, "distribution":{"false":.9, "true":.1}, "reason":"simulated"} + observation = extract(store, task, source, 0, provider)[0] + self.assertEqual(len(calls), 2) + self.assertEqual(extract(store, task, source, 5, provider), [observation]) + self.assertEqual(len(calls), 2) + response = {"status":"known", "value":True, "distribution":{"false":0., "true":1.}, "reason":"reviewed synthetic text"} + with self.assertRaises(PermissionError): + correct_observation(store, observation["sha256"], response, "manager", "review", 1) + correction = correct_observation(store, observation["sha256"], response, "operator", "review", 1) + self.assertEqual(effective_observation(store, observation["sha256"], 0), observation) + self.assertEqual(effective_observation(store, observation["sha256"], 1), correction) + self.assertEqual(correct_observation(store, observation["sha256"], response, "operator", "review", 2), correction) + with self.assertRaises(ValueError): + correct_observation(store, observation["sha256"], response, "operator", "stale review", 2) + self.assertFalse(store.get(observation["sha256"])["body"]["response"]["value"]) + self.assertEqual(len(store.list("extraction_attempt")), 2) + + def test_unknown_abstention_invalid_distribution_and_budget(self): + store, task, rows, _ = self.scenario() + task_body = copy.deepcopy(task["body"]) + task_body["policy"]["max_provider_calls"] = 3 + task = register_task(store, task_body) + rows[0]["message"], rows[1]["message"] = None, "[abstain] uncertain" + admit_source(store, task, {"format":"json", "data":rows[:4]}, 9) + sources = current_sources(store, 9) + self.assertEqual(extract(store, task, sources[0], 9)[0]["body"]["response"]["status"], "unknown") + self.assertEqual(extract(store, task, sources[1], 9)[0]["body"]["response"]["status"], "abstained") + def invalid(question, content): + return {"status":"known", "value":True, "distribution":{"true":.1,"false":.9}, "reason":"wrong"} + response = extract(store, task, sources[2], 9, invalid)[0]["body"]["response"] + self.assertEqual(response["reason"], "provider_budget_exhausted") + self.assertIsNone(response["value"]) + self.assertEqual(extract(store, task, sources[3], 9)[0]["body"]["response"]["reason"], "provider_budget_exhausted") + self.assertEqual(len(store.list("extraction_attempt")), 3) + + def prepared_history(self, name="support"): + store, task, rows, labels = self.scenario(name) + snapshots = [] + for row, label in zip(rows, labels): + at = row["occurred_at"] + admit_source(store,task,{"format":"json","data":[row]},at) + source = next(r for r in current_sources(store,at) if r["body"]["id"] == label["source_id"]) + extract(store,task,source,at) + snapshots.extend(r for r in features(store,task,at) if r["body"]["source_id"] == label["source_id"]) + outcome(store,task,label) + return store, task, rows, labels, snapshots + + def test_point_in_time_features_outcomes_and_chronological_isolation(self): + store, task, rows, labels, snapshots = self.prepared_history() + self.assertEqual(len(cases(store,task,snapshots[:1],1)),0) + train, holdout, at = chronological_split(task["body"],cases(store,task,snapshots,71)) + self.assertTrue(all(r["outcome"]["available_at"] <= at for r in train)) + self.assertTrue(all(r["feature"]["body"]["cutoff"] >= at for r in holdout)) + with self.assertRaises(ValueError): + prepare(store,task,train + holdout,"catboost","semantic",at) + wrong = copy.deepcopy(train) + wrong[0]["outcome"] = wrong[1]["outcome"] + with self.assertRaises(ValueError): + prepare(store,task,wrong,"catboost","semantic",at) + old = snapshots[0] + correction = dict(rows[0],revision=2,arrived_at=72,volume=999,message="Login failed") + admit_source(store,task,{"format":"json","data":[correction]},72) + self.assertEqual(features(store,task,0),[old]) + source = next(r for r in current_sources(store,72) if r["body"]["id"] == labels[0]["source_id"]) + extract(store,task,source,72) + self.assertEqual(features(store,task,0),[old]) + with self.assertRaises(ValueError): + outcome(store,task,dict(labels[0],available_at=1)) + + def test_actual_metrics_and_five_comparable_routes(self): + classification = metrics("classification",[0,1],[.25,.75]) + self.assertEqual(classification["accuracy"],1) + self.assertEqual(classification["brier"],.0625) + self.assertEqual(classification["roc_auc"],1) + self.assertEqual(classification["ece"],.25) + regression = metrics("regression",[1,3],[2,2]) + self.assertEqual((regression["mae"],regression["rmse"],regression["r2"]),(1,1,0)) + for name in ("support","equipment"): + store, task, _, _, snapshots = self.prepared_history(name) + comparison = compare(store,task,snapshots,71) + self.assertEqual(comparison,compare(store,task,snapshots,72)) + evaluations = [store.get(sha) for sha in comparison["body"]["evaluations"]] + models = [store.get(sha) for sha in comparison["body"]["models"]] + self.assertEqual(len(evaluations),5) + self.assertEqual({m["body"]["preparation"] for m in models},{"empirical_mean","training_style","context_style"}) + self.assertEqual([m["body"]["implementation_mode"] for m in models],["native_baseline"]+["simulated"]*4) + self.assertTrue(all(e["body"]["cases"] == comparison["body"]["holdout_cases"] for e in evaluations)) + metric = comparison["body"]["primary_metric"] + selected = next(e for e in evaluations if e["body"]["model"] == comparison["body"]["selected"]) + self.assertLessEqual(selected["body"]["metrics"][metric],evaluations[0]["body"]["metrics"][metric]) + self.assertTrue(all(e["body"]["provider_calls"] == 0 for e in evaluations)) + + def test_lifecycle_scoring_fallback_shadow_rollback_and_controlled_corrections(self): + store, task, rows, labels, snapshots = self.prepared_history() + comparison = compare(store,task,snapshots,71) + baseline, candidate = comparison["body"]["models"][:2] + with self.assertRaises(ValueError): + transition(store,task,candidate,"activate",71,"not-approved") + transition(store,task,baseline,"approve",71,"approve-baseline") + transition(store,task,baseline,"activate",71,"activate-baseline") + transition(store,task,candidate,"approve",71,"approve-candidate") + transition(store,task,candidate,"activate",71,"activate-candidate") + self.assertEqual(registry(store)["states"][baseline],"retired") + transition(store,task,baseline,"rollback",71,"rollback-baseline") + self.assertEqual(registry(store)["active"],baseline) + self.assertEqual(registry(store,70)["active"],None) + new_row = history("support",25)[1][-1] + admit_source(store,task,{"format":"json","data":[new_row]},72) + source = current_sources(store,72)[-1] + observation = extract(store,task,source,72)[0] + fresh = next(r for r in features(store,task,72) if r["body"]["source_id"] == new_row["ticket"]) + accepted = score(store,task,fresh,72) + self.assertEqual(score(store,task,fresh,73),accepted) + self.assertEqual(score(store,task,fresh,72,shadow=candidate)["body"]["mode"],"shadow") + with self.assertRaises(ValueError): + score(store,task,fresh,74) + changed_response = {"status":"known","value":True,"distribution":{"true":1.,"false":0.},"reason":"synthetic review"} + corrected = correct_observation(store,observation["sha256"],changed_response,"operator","review",73) + revised = next(r for r in features(store,task,73) if r["body"]["source_id"] == new_row["ticket"]) + self.assertNotEqual(revised["sha256"],fresh["sha256"]) + self.assertEqual(len(invalidate_predictions(store,source["sha256"],corrected,73)),2) + self.assertNotEqual(score(store,task,revised,73)["sha256"],accepted["sha256"]) + plan = update_plan(store,store.get(baseline),[revised],corrected,73) + self.assertNotIn("model_preparation",plan["body"]["actions"]) + self.assertFalse(plan["body"]["notify"]) + planned = update_plan(store,store.get(baseline),[revised],corrected,73,prepare_requested=True) + self.assertIn("model_preparation",planned["body"]["actions"]) + with self.assertRaises(ValueError): + update_plan(store,store.get(candidate),[revised],corrected,73,prepare_requested=True) + self.assertEqual(store.get(accepted["sha256"]),accepted) + # Explicit compatible baseline fallback works before any model activation. + second, task2, _, _, snapshots2 = self.prepared_history() + compared = compare(second,task2,snapshots2,71) + future = features(second,task2,72)[0] + fallback = score(second,task2,future,72,fallback=compared["body"]["models"][0]) + self.assertEqual(fallback["body"]["mode"],"fallback") + + def test_failed_evaluation_blocks_activation_and_contract_mismatch(self): + store, task, _, _, snapshots = self.prepared_history() + compared = compare(store,task,snapshots,71) + candidate = store.get(compared["body"]["models"][1]) + store.put("evaluation","failed-contract",{"model":candidate["sha256"],"status":"blocked"},72,[candidate["sha256"]]) + with self.assertRaises(ValueError): + transition(store,task,candidate["sha256"],"approve",72,"reject-blocked") + changed = copy.deepcopy(task["body"]) + changed["target"]["horizon"] = 3 + new_task = register_task(store,changed,72) + with self.assertRaises(ValueError): + transition(store,new_task,compared["body"]["models"][0],"approve",72,"reject-incompatible") + + def test_durable_jobs_retry_concurrent_replay_cancel_conflict_and_expired_lease(self): + store, task, _, _ = self.scenario() + def handler(ledger,payload): + return ledger.put("test_result",payload["identity"],payload,10,[task["sha256"]]) + payload = {"identity":"replay"} + key = enqueue(store,payload,"replay") + with ThreadPoolExecutor(max_workers=2) as pool: + results = list(pool.map(lambda _:execute(key,handler),range(2))) + self.assertTrue(any(r["state"] == "completed" for r in results)) + self.assertEqual(len(store.list("test_result")),1) + self.assertTrue(execute(key,handler)["reused"]) + with self.assertRaises(ValueError): + enqueue(store,{"identity":"conflict"},"replay") + retry = enqueue(store,{"identity":"retry","fail_once":True},"retry") + self.assertEqual(execute(retry,handler)["state"],"retry") + self.conn.execute("UPDATE backintel.capability_jobs SET due_at=now()-interval '1 second' WHERE job_id=%s",(retry,)) + self.assertEqual(execute(retry,handler)["state"],"completed") + cancelled = enqueue(store,{"identity":"cancelled"},"cancelled") + self.assertEqual(cancel(self.conn,cancelled),"cancelled") + self.assertEqual(execute(cancelled,handler)["state"],"cancelled") + interrupted = enqueue(store,{"identity":"interrupted"},"interrupted") + self.assertIsNotNone(claim(self.conn,interrupted)) + self.conn.execute("UPDATE backintel.capability_jobs SET lease_until=now()-interval '1 second' WHERE job_id=%s",(interrupted,)) + self.assertEqual(execute(interrupted,handler)["state"],"completed") + self.assertEqual(self.conn.execute("SELECT attempts FROM backintel.capability_jobs WHERE job_id=%s",(interrupted,)).fetchone()[0],2) + + def test_persistent_due_triggers_repeat_and_backpressure(self): + store, task, _, _ = self.scenario() + trigger = schedule(store,"schedule",{"identity":"scheduled"},time.time()-5,"timer",repeat_seconds=1,occurrences=2) + with psycopg.connect(dsn(),autocommit=True) as reconnected: + first = release_due(reconnected) + second = release_due(reconnected) + self.assertNotEqual(first,second) + self.assertEqual(reconnected.execute("SELECT state,occurrence FROM backintel.capability_triggers WHERE trigger_id=%s",(trigger,)).fetchone(),("fired",2)) + for key in first+second: + execute(key,lambda ledger,payload:ledger.put("test_result","scheduled",payload,0)) + for i in range(20): + enqueue(store,{"n":i},f"backpressure-{i}") + with self.assertRaisesRegex(ValueError,"backpressure"): + enqueue(store,{"n":21},"over-budget") + self.conn.execute("UPDATE backintel.capability_jobs SET state='cancelled' WHERE task_id=%s AND state='queued'",(store.task_id,)) + + def test_forecast_drives_attention_without_turning_unknown_into_false(self): + from runtime.capability_pipeline import analyze + store, task, rows, _ = self.scenario() + admit_source(store, task, {"format": "json", "data": [rows[0]]}, 0) + source = current_sources(store, 0)[0] + observation = extract(store, task, source, 0)[0] + feature = features(store, task, 0)[0] + # Explicit high-forecast fixture isolates the decision policy, not model quality. + forecast = store.put("prediction", "high-forecast-fixture", {"feature": feature["sha256"], "value": .9, "implementation_mode": "simulated"}, 0, [feature["sha256"]]) + event = store.put("event", "forecast-check", {"operation": "fixture"}, 0, [task["sha256"]]) + analysis = analyze(store, task, [feature], [forecast], event, 0) + self.assertEqual(analysis["body"]["rows"][0]["semantic_risk"], 0) + self.assertEqual(analysis["body"]["rows"][0]["attention_value"], .9) + episode = attend(store, task, feature["body"]["entity"], analysis, 0, .9) + self.assertEqual(episode["body"]["condition"], "active") + self.assertIn(forecast["sha256"], {r["sha256"] for r in store.lineage(episode["sha256"])}) + correct_observation(store, observation["sha256"], {"status": "unknown", "value": None, "distribution": None, "reason": "needs review"}, "operator", "fixture", 1) + missing = features(store, task, 1)[0] + forecast = store.put("prediction", "unknown-high-forecast-fixture", {"feature": missing["sha256"], "value": .9, "implementation_mode": "simulated"}, 1, [missing["sha256"]]) + analysis = analyze(store, task, [missing], [forecast], event, 1) + self.assertIsNone(analysis["body"]["rows"][0]["attention_value"]) + self.assertEqual(attend(store, task, missing["body"]["entity"], analysis, 1, None)["body"]["condition"], "unknown") + with self.assertRaisesRegex(ValueError, "matching prediction"): + analyze(store, task, [feature], [forecast], event, 1) + + def test_overdue_jobs_preserve_order_across_retry_and_restart(self): + store, task, _, _ = self.scenario() + with self.conn.transaction(): + first = enqueue(store,{"identity":"first"},"first") + second = enqueue(store,{"identity":"second"},"second") + self.assertNotIn(second,runnable(self.conn)) + self.conn.execute("UPDATE backintel.capability_jobs SET state='retry',due_at=now()+interval '1 hour' WHERE job_id=%s",(first,)) + with psycopg.connect(dsn(),autocommit=True) as restarted: + self.assertNotIn(second,runnable(restarted)) + cancel(restarted,first) + # Other task queues can precede this one, but no task may bypass its own earlier retry. + self.conn.execute("UPDATE backintel.capability_jobs SET state='cancelled' WHERE job_id=%s",(second,)) + + def test_jsonb_negative_zero_and_failed_job_repair_preserve_evidence(self): + store, task, _, _ = self.scenario() + metric = store.put("test_metric","negative-zero",{"loss":-0.0},0) + self.assertEqual(store.get(metric["sha256"])["body"],{"loss":0.0}) + key = enqueue(store,{"identity":"repaired"},"repaired") + def failed(ledger,payload): + raise ValueError("injected failed implementation") + execute(key,failed) + self.conn.execute("UPDATE backintel.capability_jobs SET due_at=now() WHERE job_id=%s",(key,)) + self.assertEqual(execute(key,failed)["state"],"failed") + resume_failed(self.conn,key,"Fixed deterministic implementation failure") + self.assertEqual(execute(key,lambda ledger,payload:metric)["state"],"completed") + self.assertEqual([r["body"]["status"] for r in store.list("job_attempt_result")],["retry","failed","completed"]) + + def test_attention_hysteresis_acknowledgment_staleness_deadlines_and_local_delivery(self): + store, task, _, _ = self.scenario() + def event(at): + return store.put("test_event",str(at),{"at":at},at,[task["sha256"]]) + opened = attend(store,task,"Accounts",event(0),0,.8) + self.assertEqual(opened,attend(store,task,"Accounts",event(0),0,.8)) + self.assertEqual(len(store.list("delivery")),1) + held = attend(store,task,"Accounts",event(1),1,.45) + self.assertEqual(held["body"]["condition"],"active") + overdue = attend(store,task,"Accounts",event(4),4,action="deadline") + self.assertEqual(len(store.list("delivery")),2) + attend(store,task,"Accounts",event(8),8,action="deadline") + self.assertEqual(len(store.list("delivery")),2) + acknowledged = attend(store,task,"Accounts",event(9),9,action="acknowledge") + self.assertEqual(acknowledged["body"]["condition"],"active") + stale = attend(store,task,"Accounts",event(10),10,action="staleness") + self.assertEqual(stale["body"]["condition"],"unknown") + resolved = attend(store,task,"Accounts",event(11),11,action="resolve") + self.assertEqual(resolved["body"]["condition"],"unknown") + cleared = attend(store,task,"Accounts",event(12),12,.1) + self.assertEqual(cleared["body"]["condition"],"cleared") + self.assertEqual({r["body"]["channel"] for r in store.list("delivery")},{"local_inbox"}) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_real_driver.py b/tests/test_real_driver.py new file mode 100644 index 0000000..34fdc5e --- /dev/null +++ b/tests/test_real_driver.py @@ -0,0 +1,114 @@ +"""Driver boundaries use fictional authorizations and keys; no provider calls.""" + +import json +import os +from pathlib import Path +import tempfile +import time +import unittest +import uuid +from unittest.mock import patch + +import psycopg +from psycopg.types.json import Jsonb + +from runtime.bootstrap import initialize +from runtime.evidence import Evidence +from runtime.jobs import dispatch, enqueue, schedule +from runtime.ledger import dsn +from runtime.real_pipeline import prepare_followups, prepare_history +from runtime.real_semantics import runtime_credential, scope_for +from runtime.simulation import digest +from scripts import real_demo + + +class RealDriverTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + if not os.environ.get("BACKINTEL_TEST_DATABASE_URL") or dsn() != os.environ["BACKINTEL_TEST_DATABASE_URL"] or not psycopg.conninfo.conninfo_to_dict(dsn())["dbname"].startswith("test_"): + raise RuntimeError("Driver checks require an explicitly isolated test database") + initialize() + + def setUp(self): + self.connection = psycopg.connect(dsn(),autocommit=True) + self.addCleanup(self.connection.close) + self.proposals, self.stores, self.authorizations = {}, {}, {} + for scenario in real_demo.SCENARIOS: + store = Evidence(self.connection,"driver-"+scenario+"-"+uuid.uuid4().hex[:12]) + history = prepare_history(store,scenario) + future = prepare_followups(store,history) + task = store.get(history["body"]["task"]) + sources = [store.get(sha) for sha in history["body"]["sources"]+future["body"]["sources"]] + self.stores[scenario] = store + self.proposals[scenario] = {"task_id":store.task_id,"scope":scope_for(task,sources),"maximum_input_characters":100} + authorization = "driver-fixture-"+uuid.uuid4().hex + self.authorizations[scenario] = authorization + scope = self.proposals[scenario]["scope"] + self.connection.execute("""INSERT INTO backintel.capability_provider_authorizations + (authorization_id,provider,model,max_requests,max_input_characters,price_ceiling_known,approved,scope_sha256,scope,expires_at) + VALUES (%s,'openrouter','jev-1.13',27,5000,true,true,%s,%s,now()+interval '1 hour')""",(authorization,digest(scope),Jsonb(scope))) + + def test_both_approvals_precede_credential_and_scheduler_access(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory)/"Authorizations.json" + path.write_text(json.dumps({**self.authorizations,"equipment":"not-approved"})) + with patch.object(real_demo,"ROOT",Path(directory)), patch.object(real_demo,"DEMO_DSN",dsn()), \ + patch.object(real_demo,"prepare",return_value=self.proposals), patch.object(real_demo.subprocess,"run") as external, \ + patch.object(real_demo,"request") as api, patch("sys.argv",["real_demo","--demo-id","driver-check","--no-start","--authorizations",str(path)]): + self.assertEqual(real_demo.main(),1) + external.assert_not_called() + api.assert_not_called() + receipt = json.loads(next(Path(directory).glob("artifacts/validation/RealDemo/driver-check/Attempt*/Integration.json")).read_text()) + self.assertEqual(receipt["status"],"blocked") + self.assertNotIn("credential",receipt) + + def test_preflight_enforces_bounded_known_price_approval(self): + self.assertEqual(real_demo.preflight(self.connection,self.proposals,self.authorizations,time.time()+300)[0],54) + self.connection.execute("UPDATE backintel.capability_provider_authorizations SET price_ceiling_known=false WHERE authorization_id=%s",(self.authorizations["support"],)) + with self.assertRaisesRegex(PermissionError,"known-price"): + real_demo.preflight(self.connection,self.proposals,self.authorizations,time.time()+300) + self.connection.execute("UPDATE backintel.capability_provider_authorizations SET price_ceiling_known=true,max_requests=1 WHERE authorization_id=%s",(self.authorizations["support"],)) + with self.assertRaisesRegex(PermissionError,"request"): + real_demo.preflight(self.connection,self.proposals,self.authorizations,time.time()+300) + self.connection.execute("UPDATE backintel.capability_provider_authorizations SET max_requests=27,max_measured_usd=.01 WHERE authorization_id=%s",(self.authorizations["support"],)) + self.connection.execute("""INSERT INTO backintel.capability_model_requests + (request_key,task_id,source_sha256,model,request,state,authorization_id,metadata,response,finished_at) + VALUES (%s,%s,%s,'jev-1.13',%s,'completed',%s,%s,'{"test_fixture":true}'::jsonb,now())""", + (digest(["fixture-budget",self.stores["support"].task_id]),self.stores["support"].task_id, + self.proposals["support"]["scope"]["source_sha256s"][0],Jsonb({"test_fixture":True}),self.authorizations["support"], + Jsonb({"test_fixture":True,"request_id":"fixture-budget","model":"jev-1.13","cost_usd":.01}))) + with self.assertRaisesRegex(PermissionError,"fixture"): + real_demo.preflight(self.connection,self.proposals,self.authorizations,time.time()+300) + # Isolate the dollar-limit decision; fixture rejection above remains the real-run default. + with patch.object(real_demo,"usage_for",return_value={"provider_fixture_requests":0}): + with self.assertRaisesRegex(PermissionError,"budget"): + real_demo.preflight(self.connection,self.proposals,self.authorizations,time.time()+300) + + def test_ephemeral_credential_has_task_and_time_bounds(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory)/"Credential.json" + record = {"key":"fixture-only-key","task_ids":["approved-task"],"expires_at":time.time()+60} + path.write_text(json.dumps(record)) + with patch.dict(os.environ,{"OPENROUTER_API_KEY":"","BACKINTEL_PROVIDER_CREDENTIAL_FILE":str(path)}): + self.assertEqual(runtime_credential("approved-task"),"fixture-only-key") + self.assertIsNone(runtime_credential("other-task")) + path.write_text(json.dumps({**record,"expires_at":0})) + self.assertIsNone(runtime_credential("approved-task")) + path.write_text(json.dumps({**record,"expires_at":float("nan")})) + with self.assertRaisesRegex(RuntimeError,"invalid"): + runtime_credential("approved-task") + + def test_dispatch_preserves_other_tasks_triggers_and_expired_jobs(self): + selected,other = self.stores["support"],self.stores["equipment"] + schedule(selected,"event",{"scenario":"support","operation":"real_prepare"},0,"selected") + untouched = schedule(other,"event",{"scenario":"equipment","operation":"real_prepare"},0,"other") + expired = enqueue(other,{"scenario":"equipment","operation":"real_prepare"},"expired") + self.connection.execute("UPDATE backintel.capability_jobs SET state='running',attempts=max_attempts,lease_until=now()-interval '1 second' WHERE job_id=%s",(expired,)) + result = dispatch([selected.task_id]) + self.assertEqual([job["state"] for job in result["jobs"]],["completed"]) + self.assertEqual(self.connection.execute("SELECT state FROM backintel.capability_triggers WHERE trigger_id=%s",(untouched,)).fetchone()[0],"pending") + self.assertEqual(self.connection.execute("SELECT state FROM backintel.capability_jobs WHERE job_id=%s",(expired,)).fetchone()[0],"running") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_real_integrations.py b/tests/test_real_integrations.py new file mode 100644 index 0000000..3031329 --- /dev/null +++ b/tests/test_real_integrations.py @@ -0,0 +1,273 @@ +"""Provider-boundary checks use local fixtures, never paid inference or downloaded models.""" +import copy +import json +import os +from pathlib import Path +import tempfile +from types import SimpleNamespace +import unittest +import uuid +from unittest.mock import patch + +import psycopg +from psycopg.types.json import Jsonb + +from runtime.bootstrap import initialize +from runtime.contracts import admit_source,current_sources,register_task +from runtime.evidence import Evidence +from runtime.ledger import dsn +from runtime.jobs import enqueue, execute +from runtime.real_pipeline import compare_history, interpret_source, prepare_followups, prepare_history, start_followups +from runtime.real_semantics import _verified_metadata,extract_real,scope_for,typed_answer,usage_for +from runtime.real_models import _allow_model_use,_tabicl,checkpoint,prepare_real +from runtime.simulation import digest +from runtime.synthetic import history + + +class FixtureAnswer: + def __init__(self,value): + self.value = value + def model_dump(self,mode="json"): + return self.value + + +class FixtureClassifier: + calls = 0 + charge = .001 + fail = False + def __init__(self,model): + self.model = model + self.last_metadata = {} + self.last_response = {} + def invoke(self,request): + type(self).calls += 1 + if self.fail: + raise TimeoutError("injected response uncertainty") + self.last_metadata = {"request_id":"fixture-request","model":self.model,"cost_usd":self.charge} + answer = {"type":"noul","noul":.25} + self.last_response = {"fixture":True,"answer":answer} + return SimpleNamespace(nouls={"risk":FixtureAnswer(answer)},choices={},scores={}, + request_id="fixture-request",model=self.model,usage=SimpleNamespace(input_tokens=10,output_tokens=3)) + async def aclose(self): + pass + + +class ProviderIdentityTests(unittest.TestCase): + def test_checked_alias_revision_keeps_identity_and_billing_guards(self): + metadata = {"request_id": "recorded-probe", "model": "typesafe/jev-1.13-20260917", "cost_usd": .000011592} + _verified_metadata(metadata, "jev-1.13") + for changed in ({"model": "typesafe/jev-1.14-20260917"}, + {"model": "typesafe/jev-1.13-20261001"}, + {"request_id": None}, {"cost_usd": None}): + with self.subTest(changed=changed), self.assertRaises(RuntimeError): + _verified_metadata({**metadata, **changed}, "jev-1.13") + with self.assertRaises(RuntimeError): + _verified_metadata(metadata, "jev-1.12") + + +class RealBoundaryTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + if not os.environ.get("BACKINTEL_TEST_DATABASE_URL") or os.environ["BACKINTEL_TEST_DATABASE_URL"] != dsn() or not psycopg.conninfo.conninfo_to_dict(dsn())["dbname"].startswith("test_"): + raise RuntimeError("Real-boundary fixtures require an explicitly isolated test database") + initialize() + + def setUp(self): + self.conn = psycopg.connect(dsn(),autocommit=True) + self.addCleanup(self.conn.close) + task,rows,_ = history("support") + task["id"] = "real-test-"+uuid.uuid4().hex[:20] + task["observation_provider"] = {"name":"openrouter-jev","version":"jev-1.13","implementation_mode":"real"} + self.store = Evidence(self.conn,task["id"]) + self.task = register_task(self.store,task) + admit_source(self.store,self.task,{"format":"json","data":rows[:2]},3) + self.sources = current_sources(self.store,3,self.task["sha256"]) + scope = scope_for(self.task,self.sources) + self.authorization = "fixture-"+uuid.uuid4().hex + self.conn.execute("""INSERT INTO backintel.capability_provider_authorizations + (authorization_id,provider,model,max_requests,max_input_characters,price_ceiling_known,approved,scope_sha256,scope,expires_at) + VALUES (%s,'openrouter','jev-1.13',1,5000,false,false,%s,%s,now()+interval '1 hour')""", + (self.authorization,digest(scope),Jsonb(scope))) + FixtureClassifier.calls,FixtureClassifier.charge,FixtureClassifier.fail = 0,.001,False + + def approve_fixture(self): + self.conn.execute("UPDATE backintel.capability_provider_authorizations SET approved=true WHERE authorization_id=%s",(self.authorization,)) + + def test_semantic_training_requires_actual_findings_for_each_question(self): + feature = {"feature": {"sha256": "fixture-feature"}} + config = {"limits": {"max_training_rows": 64}} + for observations in ([], [{"kind": "observation", "body": { + "question_id": "risk", "provider": {"implementation_mode": "simulated"}, + "request_id": "fixture"}}]): + with self.subTest(observations=observations), patch("runtime.real_models._allow_model_use", return_value=config), patch.object(self.store, "lineage", return_value=observations): + with self.assertRaisesRegex(ValueError, "actual Jev"): + prepare_real(self.store, self.task, [feature], "catboost", "semantic", 10) + + def test_preparation_commits_sources_and_replays_without_provider_calls(self): + store = Evidence(self.conn, "stage-"+uuid.uuid4().hex[:20]) + job = enqueue(store, {"operation":"real_prepare", "scenario":"support"}, "prepare") + result = execute(job) + self.assertEqual(result["state"], "completed") + plan = store.get(result["result"]) + self.assertEqual(plan["body"]["source_records"], 24) + self.assertEqual(len(plan["body"]["outcomes"]), 24) + with psycopg.connect(dsn()) as other: + self.assertEqual(Evidence(other,store.task_id).get(plan["body"]["sources"][0])["kind"], "source") + self.assertTrue(execute(job)["reused"]) + with self.assertRaises(PermissionError): + interpret_source(store, plan, plan["body"]["sources"][0], "not-approved", FixtureClassifier) + self.assertEqual(FixtureClassifier.calls, 0) + self.assertEqual(usage_for(store)["provider_calls"], 0) + + def test_missing_credential_preserves_approved_request_slot(self): + self.approve_fixture() + with patch.dict(os.environ, {}, clear=True): + # Preserve database addressing without allowing any credential into this check. + os.environ["BACKINTEL_APP_DATABASE_URL"] = self.conn.info.dsn + os.environ["BACKINTEL_TEST_DATABASE_URL"] = self.conn.info.dsn + with self.assertRaisesRegex(RuntimeError, "ephemeral credential"): + extract_real(self.store,self.task,self.sources[0],3,authorization_id=self.authorization) + self.assertEqual(usage_for(self.store)["provider_calls"], 0) + self.assertEqual(len(self.extract()), 1) + + def test_future_source_preparation_preserves_cutoffs_and_requires_separate_approval(self): + store = Evidence(self.conn,"followup-"+uuid.uuid4().hex[:20]) + history_plan = prepare_history(store,"support") + plan = prepare_followups(store,history_plan) + self.assertEqual(prepare_followups(store,history_plan),plan) + self.assertEqual(len(plan["body"]["sources"]),3) + self.assertEqual(len(plan["body"]["events"]),16) + task_sha = history_plan["body"]["task"] + self.assertEqual(len(current_sources(store,71,task_sha)),24) + self.assertEqual(len(current_sources(store,72,task_sha)),25) + revised = [r for r in current_sources(store,75,task_sha) if r["body"]["id"] == "support-024"] + self.assertEqual([r["body"]["revision"] for r in revised],[2]) + with self.assertRaisesRegex(ValueError,"comparison"): + start_followups(store,plan,self.authorization) + store.put("real_stage_result","history-v1",{"result":history_plan["sha256"],"test_fixture":True},71,[history_plan["sha256"]]) + with self.assertRaises(PermissionError): + start_followups(store,plan,self.authorization) + self.approve_fixture() + with self.assertRaisesRegex(PermissionError,"scope"): + start_followups(store,plan,self.authorization) + self.assertEqual(self.conn.execute("SELECT count(*) FROM backintel.capability_triggers WHERE task_id=%s",(store.task_id,)).fetchone()[0],0) + self.assertEqual(FixtureClassifier.calls,0) + + def test_incomplete_and_simulated_history_cannot_enter_real_comparison(self): + store = Evidence(self.conn, "stage-"+uuid.uuid4().hex[:20]) + plan = prepare_history(store,"support") + with self.assertRaisesRegex(PermissionError, "actual Jev"): + compare_history(store,plan) + source = store.get(plan["body"]["sources"][0]) + store.put("observation", "simulated-finding", { + "source":source["sha256"], "task":plan["body"]["task"], "question_id":"risk", + "provider":{"implementation_mode":"simulated"}, "request_id":"fixture"}, source["available_at"]) + with self.assertRaisesRegex(ValueError, "simulated"): + compare_history(store,plan) + + def test_job_attempts_record_fixture_charge_once_and_preserve_unknowns(self): + self.approve_fixture() + def handler(store,payload): + return self.extract()[0] + first = enqueue(self.store,{"operation":"provider-fixture"},"fixture-charge") + self.assertEqual(execute(first,handler)["state"],"completed") + usage = self.store.find("job_attempt_result",first+":1")["body"] + self.assertEqual((usage["provider_calls"],usage["provider_usd"]),(1,.001)) + replay = enqueue(self.store,{"operation":"provider-fixture"},"fixture-cached") + self.assertEqual(execute(replay,handler)["state"],"completed") + cached = self.store.find("job_attempt_result",replay+":1")["body"] + self.assertEqual((cached["provider_calls"],cached["provider_usd"]),(0,0)) + self.assertEqual(FixtureClassifier.calls,1) + + locked = enqueue(self.store,{"operation":"provider-fixture"},"fixture-lock-failure") + with patch.object(Evidence,"lock",side_effect=TimeoutError("fixture task lock timeout")): + self.assertEqual(execute(locked,handler)["state"],"retry") + before_handler = self.store.find("job_attempt_result",locked+":1")["body"] + self.assertEqual((before_handler["provider_calls"],before_handler["provider_usd"]),(0,0)) + self.assertEqual(FixtureClassifier.calls,1) + + self.setUp() + self.approve_fixture() + FixtureClassifier.fail = True + failed = enqueue(self.store,{"operation":"provider-fixture"},"fixture-uncertain") + self.assertEqual(execute(failed,handler)["state"],"retry") + uncertain = self.store.find("job_attempt_result",failed+":1")["body"] + self.assertIsNone(uncertain["provider_calls"]) + self.assertIsNone(uncertain["provider_usd"]) + self.assertEqual(uncertain["provider_requests_admitted"],1) + + def extract(self,index=0): + return extract_real(self.store,self.task,self.sources[index],3,authorization_id=self.authorization,classifier_factory=FixtureClassifier) + + def test_approval_gate_exact_scope_cache_and_one_request_cap(self): + with self.assertRaises(PermissionError): + self.extract() + self.assertEqual(FixtureClassifier.calls,0) + self.approve_fixture() + observations = self.extract() + self.assertEqual(FixtureClassifier.calls,1) + self.assertEqual(self.extract(),observations) + self.assertEqual(FixtureClassifier.calls,1) + self.assertEqual(observations[0]["body"]["request_id"],"fixture-request") + with self.assertRaisesRegex(RuntimeError,"budget exhausted"): + self.extract(1) + self.assertEqual(FixtureClassifier.calls,1) + + def test_unknown_outcome_and_unknown_charge_never_trigger_paid_retry(self): + self.approve_fixture() + FixtureClassifier.fail = True + with self.assertRaises(TimeoutError): + self.extract() + FixtureClassifier.fail = False + with self.assertRaisesRegex(RuntimeError,"automatic paid retry prohibited"): + self.extract() + self.assertEqual(FixtureClassifier.calls,1) + # A distinct fixture authorization proves that a returned but unpriced response is retained. + self.setUp() + self.approve_fixture() + FixtureClassifier.charge = None + with self.assertRaisesRegex(RuntimeError,"charge unavailable"): + self.extract() + with self.assertRaisesRegex(RuntimeError,"charge unavailable"): + self.extract() + self.assertEqual(FixtureClassifier.calls,1) + state,response = self.conn.execute("SELECT state,response FROM backintel.capability_model_requests WHERE authorization_id=%s",(self.authorization,)).fetchone() + self.assertEqual(state,"completed") + self.assertTrue(response["raw_response"]["fixture"]) + self.assertEqual(self.store.list("observation"),[]) + + def test_response_survives_domain_rollback_and_scope_change_is_denied(self): + self.approve_fixture() + with self.assertRaisesRegex(ValueError,"domain rollback"): + with self.conn.transaction(): + self.extract() + raise ValueError("domain rollback") + self.assertEqual(self.store.list("observation"),[]) + self.assertEqual(len(self.extract()),1) + self.assertEqual(FixtureClassifier.calls,1) + changed = copy.deepcopy(self.task["body"]) + changed["questions"][0]["prompt"] = "A different question" + other = register_task(self.store,changed) + with self.assertRaises(PermissionError): + extract_real(self.store,other,self.sources[1],3,authorization_id=self.authorization,classifier_factory=FixtureClassifier) + self.assertEqual(FixtureClassifier.calls,1) + + def test_actual_answer_translation_and_download_denial(self): + question = {"type":"number"} + translated = typed_answer(question,{"score":7.,"legend":{"0":0,"1":10},"probabilities":{"0":.3,"1":.7}}) + self.assertEqual(translated["value"],7.) + self.assertEqual(translated["distribution"],{"values":[0,10],"probabilities":[.3,.7]}) + with tempfile.TemporaryDirectory() as directory,patch.dict(os.environ,{"BACKINTEL_MODEL_DIR":directory}): + with self.assertRaises(PermissionError): + _allow_model_use("catboost") + with self.assertRaisesRegex(RuntimeError,"not been downloaded"): + checkpoint("classification") + with patch("tabicl.TabICLClassifier") as constructor: + _tabicl("classification",Path(directory)/"absent.ckpt") + self.assertFalse(constructor.call_args.kwargs["allow_auto_download"]) + self.assertEqual(constructor.call_args.kwargs["device"],"cpu") + self.assertEqual(constructor.call_args.kwargs["n_estimators"],1) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_simulation.py b/tests/test_simulation.py new file mode 100644 index 0000000..9a2dfbb --- /dev/null +++ b/tests/test_simulation.py @@ -0,0 +1,107 @@ +"""Observable cross-domain workflow checks; synthetic sources only, no dependencies or network.""" +import copy +import json +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +from runtime.simulation import digest, load_scenario, publish, render_html, simulate + +ROOT = Path(__file__).resolve().parents[1] + + +class SimulationTests(unittest.TestCase): + def test_distinct_domains_share_workflow_and_preserve_missingness(self): + for name in ("support", "equipment"): + with self.subTest(name=name): + result = simulate(load_scenario(name)) + baseline, change, missing, recovery = result["batches"] + self.assertEqual(baseline["summary"], {"records": 3, "known": 2, "unknown": 1, "flagged": 0, "rate": 0}) + self.assertEqual(change["summary"], {"records": 4, "known": 3, "unknown": 1, "flagged": 2, "rate": 2/3}) + self.assertEqual(change["change"], 2/3) + self.assertIsNone(missing["summary"]["rate"]) + self.assertEqual(recovery["summary"]["rate"], 0) + self.assertFalse(change["projection"]["validated"]) + self.assertEqual(change["projection"]["next_rate"], 1) + for batch in result["batches"]: + for row in batch["observations"]: + self.assertEqual(row["source_sha256"], digest(row["source"])) + + def test_failure_retry_duplicate_acknowledgment_and_staleness(self): + result = simulate(load_scenario("support")) + self.assertEqual([e["outcome"] for e in result["timeline"]], [ + "completed", "reused", "simulated_transient_failure", "completed", "acknowledged", + "stale", "reused", "completed", "completed"]) + self.assertEqual(result["timeline"][4]["condition"], "active") + self.assertEqual(result["timeline"][5]["condition"], "unknown") + self.assertEqual(result["timeline"][7]["condition"], "unknown") + self.assertEqual(result["timeline"][8]["condition"], "cleared") + self.assertEqual(len(result["inbox"]), 1) + self.assertTrue(result["inbox"][0]["acknowledged"]) + + def test_outputs_change_with_input_and_replay_reuses_exact_bytes(self): + source = load_scenario("equipment") + original = simulate(source) + changed = copy.deepcopy(source) + changed["source"]["data"] = changed["source"]["data"].replace("baseline,40", "baseline,90") + altered = simulate(changed) + self.assertNotEqual(original["input_sha256"], altered["input_sha256"]) + self.assertEqual(altered["batches"][0]["summary"]["rate"], 1/2) + with tempfile.TemporaryDirectory() as directory: + destination = Path(directory) + first = publish(original, destination) + timestamps = {p.name: p.stat().st_mtime_ns for p in destination.iterdir()} + self.assertEqual(first, publish(simulate(source), destination)) + self.assertEqual(timestamps, {p.name: p.stat().st_mtime_ns for p in destination.iterdir()}) + Path(first["json"]["path"]).write_text("corrupted") + with self.assertRaises(ValueError): + publish(original, destination) + + def test_source_text_remains_inert_in_html(self): + config = load_scenario("support") + config["source"]["data"][0]["message"] = '' + rendered = render_html(simulate(config)) + self.assertNotIn(" diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..5825c3f --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3842 @@ +{ + "name": "backintel-decision-workspace", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "backintel-decision-workspace", + "version": "0.1.0", + "dependencies": { + "@fontsource/instrument-sans": "5.3.0", + "@phosphor-icons/react": "2.1.10", + "@radix-ui/react-dialog": "1.1.23", + "@radix-ui/react-tabs": "1.1.21", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "react": "19.3.0", + "react-dom": "19.3.0", + "tailwind-merge": "3.7.0" + }, + "devDependencies": { + "@tailwindcss/vite": "4.3.3", + "@testing-library/jest-dom": "7.0.1", + "@testing-library/react": "16.3.3", + "@types/node": "26.6.3", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "@vitejs/plugin-react": "6.1.1", + "fallow": "2.89.0", + "jsdom": "30.1.1", + "tailwindcss": "4.3.3", + "typescript": "7.0.2", + "vite": "8.3.1", + "vitest": "5.0.2" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-7.1.2.tgz", + "integrity": "sha512-99DHAnXDB5z6EEK+9GMpVI7Mw4oxj97dY5bpOzMnjADQWxI8rN6TvTduuFLUhUMlS7/CfVZ06tcZsus6cltnNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.4.1", + "@csstools/css-color-parser": "^4.2.4", + "@csstools/css-parser-algorithms": "^4.0.1", + "@csstools/css-tokenizer": "^4.0.2", + "lru-cache": "^11.5.3" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-9.2.2.tgz", + "integrity": "sha512-lSWTBMjAcmu2xn5yEDU7jh6QDV+C8GEKtdJ4pIQhXh26RkKQ7S3FuQlt+zZkUbTdJ4d3XyV1kPI/G0zWXxqaqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bidi-js": "^1.1.0", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.3" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.2.tgz", + "integrity": "sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.1.tgz", + "integrity": "sha512-EtC7SoN1j6J4E4DCwg5QgbO5TGxgxIA1RXqe+W+qUM+BUcezx9wT+/tiQ/WO2yCX4i5X+Cuf9ciJ22aP4UEwWw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.1", + "@csstools/css-tokenizer": "^4.0.2" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.4.tgz", + "integrity": "sha512-DyefytAZ735mX4Dq/WcDAXFtXhaEFvme0ZS9tVEBAc2whxUthXr0R0L2rmEPm59SrMBkGrFzQviBXMs/UtnABQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.2", + "@csstools/css-calc": "^3.4.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.1", + "@csstools/css-tokenizer": "^4.0.2" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.1.tgz", + "integrity": "sha512-ShL8BqPfbKJrJiKFH0xBbN0i7Nrh9HXYRuF+pzyj93R/BL2YAsUxJeqANErzqM+0JGl7vjHp+3OIgd/DL69YIA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.2" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.14.tgz", + "integrity": "sha512-HpbVXyrofRXpHpgkNIjU/3EWR4WJvOkO3emNK/L6X/mTJU7bGUI3AkkpoTNXznQLp0KRjLHELTGeKI5dIkI9JQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.2.tgz", + "integrity": "sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.16.0.tgz", + "integrity": "sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@fallow-cli/darwin-arm64": { + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@fallow-cli/darwin-arm64/-/darwin-arm64-2.89.0.tgz", + "integrity": "sha512-moS850FVnDP4ZXY+79O7TA54s2ZmUe/bQpwsu+hkyOKAFeOqlHAoMM12IDiA+P/UBr4N2F+mEzMR65HgShdztQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@fallow-cli/darwin-x64": { + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@fallow-cli/darwin-x64/-/darwin-x64-2.89.0.tgz", + "integrity": "sha512-x3P4NEsLE71convKyFHIfuqXwyN9R7fzYCeMVAwHupB+gmIJyZfl+BiGJx4v1VXCS6pbUYn7X1UxHYhk1T/MPg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@fallow-cli/linux-arm64-gnu": { + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@fallow-cli/linux-arm64-gnu/-/linux-arm64-gnu-2.89.0.tgz", + "integrity": "sha512-Nb6anWSulLkB0KDOEiqkbDn3VoSBQqfPyB3fUorPBMF+EJ6fOgwK8B/wenTEJf2Y4gy7ynUNJ1yITep1FJhqRA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@fallow-cli/linux-arm64-musl": { + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@fallow-cli/linux-arm64-musl/-/linux-arm64-musl-2.89.0.tgz", + "integrity": "sha512-jkIwb53aG4OO0sXeqbF+WLKtAd1sAykQ9qhcVICu08/BVuISShkUL69T0jKBvAw0S4mfvaIitHvCmlsaBUQ0Ig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@fallow-cli/linux-x64-gnu": { + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@fallow-cli/linux-x64-gnu/-/linux-x64-gnu-2.89.0.tgz", + "integrity": "sha512-QLHmh0NRnAhELreLOZtcU4PJbHMFUoBq3J1DUSPfqQj6SqXsxAnARolsMGsg6bOEF3erj51ZWN22JUWYNOW76A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@fallow-cli/linux-x64-musl": { + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@fallow-cli/linux-x64-musl/-/linux-x64-musl-2.89.0.tgz", + "integrity": "sha512-wwvq/1/eiAJwLGuOlN4oF84Pp6RVKgIVGlIPBnXm5jyj5E+Iym8Ng1eGBBu2vtbUEZxumT6bexySIYMRSe+53g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@fallow-cli/win32-arm64-msvc": { + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@fallow-cli/win32-arm64-msvc/-/win32-arm64-msvc-2.89.0.tgz", + "integrity": "sha512-QyR0x2TVfkTxObZRC0gNTXXjiRbHeNyM0gBy1w5g/foos33lx2qdm9wrhM+/jmdDJZZME5DmXvGtnUoT4U+hug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@fallow-cli/win32-x64-msvc": { + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@fallow-cli/win32-x64-msvc/-/win32-x64-msvc-2.89.0.tgz", + "integrity": "sha512-+3nT9JrQRSVQf67TlzzSRJWFE1hfxti8Sv719aVHcuzZhA/dcDEr4+fKQ5cBJc9eap75JAiRWUNzc69J2lVong==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@fontsource/instrument-sans": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/instrument-sans/-/instrument-sans-5.3.0.tgz", + "integrity": "sha512-QwXc4hb/px3XvSPS2CPAOgey8nyrQFxgxrmXQQ+pN+P51hKdAcchxpg8rSbjANFfPu6VKXhDqkVDucXMZ9CM5g==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.151.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.151.0.tgz", + "integrity": "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@phosphor-icons/react": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.10.tgz", + "integrity": "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">= 16.8", + "react-dom": ">= 16.8" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.15.tgz", + "integrity": "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.2.tgz", + "integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.23.tgz", + "integrity": "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.4.tgz", + "integrity": "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.19.tgz", + "integrity": "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-effect-event": "0.0.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.6.tgz", + "integrity": "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.16.tgz", + "integrity": "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz", + "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.17.tgz", + "integrity": "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.10.tgz", + "integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", + "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.3.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.19.tgz", + "integrity": "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", + "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.21.tgz", + "integrity": "sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz", + "integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.6.tgz", + "integrity": "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", + "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", + "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz", + "integrity": "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.11.tgz", + "integrity": "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.11.tgz", + "integrity": "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.11.tgz", + "integrity": "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.11.tgz", + "integrity": "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.11.tgz", + "integrity": "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.11.tgz", + "integrity": "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.11.tgz", + "integrity": "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.11.tgz", + "integrity": "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.11.tgz", + "integrity": "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.11.tgz", + "integrity": "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.11.tgz", + "integrity": "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.11.tgz", + "integrity": "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.11.tgz", + "integrity": "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.11.tgz", + "integrity": "sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz", + "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==", + "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": "7.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.1.tgz", + "integrity": "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==", + "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": ">=22", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@testing-library/dom": ">=10 <11", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "vitest": { + "optional": true + } + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz", + "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "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/@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/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/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.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", + "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz", + "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.145.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.2.tgz", + "integrity": "sha512-Z5FS00Q1SJHkB35xATsmWGdQ5WA1/0MV3CDjqyv7GavHv1OfOj145MNfHOlHk7QLes21dKFDHr8EO2zvL+9WGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.2", + "estree-walker": "^3.0.3", + "magic-string": "^1.2.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/magic-string": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz", + "integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.2.tgz", + "integrity": "sha512-Ijc7T1nT9efNb5LxvjaBrEqw3f/QwUv5EE0nKqZxgqsaV/FxAAZ8baGylA8X/Z2oS4Lp+K74Jr6dTJsDKxJDeg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "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/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "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/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/bidi-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz", + "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "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/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "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/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-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "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/enhanced-resolve": { + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", + "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "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/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fallow": { + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/fallow/-/fallow-2.89.0.tgz", + "integrity": "sha512-q5gKY084d6ycCanOMVhdw5wXfWR7+octSD8EMh+UsJdKlt8vr7EaCPX37CPdJ4Rj1SDf2fVThpdmeBgLi4XsrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "2.1.2" + }, + "bin": { + "fallow": "bin/fallow", + "fallow-lsp": "bin/fallow-lsp", + "fallow-mcp": "bin/fallow-mcp" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@fallow-cli/darwin-arm64": "2.89.0", + "@fallow-cli/darwin-x64": "2.89.0", + "@fallow-cli/linux-arm64-gnu": "2.89.0", + "@fallow-cli/linux-arm64-musl": "2.89.0", + "@fallow-cli/linux-x64-gnu": "2.89.0", + "@fallow-cli/linux-x64-musl": "2.89.0", + "@fallow-cli/win32-arm64-msvc": "2.89.0", + "@fallow-cli/win32-x64-msvc": "2.89.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/html-encoding-sniffer": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-7.0.0.tgz", + "integrity": "sha512-UikN5yr7xsCDAq87Or5or0PAlD3HJJOKVzM05az588WnpDJ4Ux7a2A53Qi6gofGg2/EtvF/H4hCi/TXfCW4Y6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.15.1" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "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/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/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/jsdom": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.1.1.tgz", + "integrity": "sha512-FahmoPK5vbPc+jxV1iErMHmAZypCZ942NHF4+qqaWAuvaKKTBZxawnmAtrbGWLU7MtlxfqIP0qw6aSI+aWGtLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^7.0.0", + "@asamuzakjp/dom-selector": "^9.2.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.13", + "@exodus/bytes": "^1.15.1", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^7.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "tough-cookie": "^6.0.2", + "undici": "^8.10.2", + "w3c-xmlserializer": "^6.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^17.1.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "canvas": "^3.2.3" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "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/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "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/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "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/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "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/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.11.tgz", + "integrity": "sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.151.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.11", + "@rolldown/binding-android-arm64": "1.2.11", + "@rolldown/binding-darwin-arm64": "1.2.11", + "@rolldown/binding-darwin-x64": "1.2.11", + "@rolldown/binding-freebsd-x64": "1.2.11", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.11", + "@rolldown/binding-linux-arm64-gnu": "1.2.11", + "@rolldown/binding-linux-arm64-musl": "1.2.11", + "@rolldown/binding-linux-ppc64-gnu": "1.2.11", + "@rolldown/binding-linux-s390x-gnu": "1.2.11", + "@rolldown/binding-linux-x64-gnu": "1.2.11", + "@rolldown/binding-linux-x64-musl": "1.2.11", + "@rolldown/binding-openharmony-arm64": "1.2.11", + "@rolldown/binding-win32-arm64-msvc": "1.2.11", + "@rolldown/binding-win32-x64-msvc": "1.2.11" + } + }, + "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.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "dev": true, + "license": "MIT" + }, + "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/tailwind-merge": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.7.0.tgz", + "integrity": "sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinybench": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.0.tgz", + "integrity": "sha512-78U2TlB2CnVenajOFzf3BKSm0J6oz5L0NV7g32LCPccvYc0lbWvys4d3uUUCS2B1N8PAf2+aekR8i1KbC3HO7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tldts": { + "version": "7.4.16", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.16.tgz", + "integrity": "sha512-QwBER5KMR86IIjpIiO7H/Z3IMJPsZ1A6RKPAqzTTgOyUQUSt9FdnKcqhTaJmkY6HVrgouZHZR0ncK5QxvmnQeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.16" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.16", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.16.tgz", + "integrity": "sha512-MDolfaSJtlSK5Y0A1xl3277ekubZwobpBjugknDizI9O5Rm60a1m8k4ICK+MRsCDzPygT81mp3BBf5RKDlFRfA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/undici": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.11.2.tgz", + "integrity": "sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/vite": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.1.tgz", + "integrity": "sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.9", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/vite/node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vitest": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.2.tgz", + "integrity": "sha512-7MQrx9pDv5aHiUcovIb/70Ys3tgtkUVgCtledvKdCmEO+/1Dicq5ZqoSxOW034m03oqC+oHOKui2dM6qtMLoJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.2", + "chai": "^6.2.2", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.2.3", + "obug": "^2.1.4", + "picomatch": "^4.0.7", + "std-env": "^4.2.0", + "tinybench": "^6.1.4", + "tinyexec": "^1.3.0", + "tinyglobby": "^0.2.17", + "why-is-node-running": "^3.2.1" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.2", + "@vitest/browser-preview": "5.0.2", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.2", + "@vitest/coverage-v8": "5.0.2", + "@vitest/ui": "5.0.2", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/magic-string": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz", + "integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-6.0.0.tgz", + "integrity": "sha512-4Nsy8K5Tr6SPDH9jhKJOHf7ChDrc1zufZTVSF7x72hwuEXBqxqk9G6cK+K2NRUtB3iELRJqjXb4JPDMBjMTl2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "17.1.2", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.2.tgz", + "integrity": "sha512-TEZA+Zqxin7Jjsm2cjRohCmen5awh+hT6Zi3VZdqZlNRk7zvOI/9WpBFg/DWlA56bWnzwm6DuB8NS0EsxQH9uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.15.1", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, + "node_modules/why-is-node-running": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.2.tgz", + "integrity": "sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A==", + "dev": true, + "license": "MIT", + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=20.11" + } + }, + "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" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..5932314 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,39 @@ +{ + "name": "backintel-decision-workspace", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1 --port 5173 --strictPort", + "build": "tsc -b && vite build", + "test": "vitest run", + "typecheck": "tsc -b", + "code:quality": "fallow --format json --quiet --explain" + }, + "dependencies": { + "@fontsource/instrument-sans": "5.3.0", + "@phosphor-icons/react": "2.1.10", + "@radix-ui/react-dialog": "1.1.23", + "@radix-ui/react-tabs": "1.1.21", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "react": "19.3.0", + "react-dom": "19.3.0", + "tailwind-merge": "3.7.0" + }, + "devDependencies": { + "@tailwindcss/vite": "4.3.3", + "@testing-library/jest-dom": "7.0.1", + "@testing-library/react": "16.3.3", + "@types/node": "26.6.3", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "@vitejs/plugin-react": "6.1.1", + "fallow": "2.89.0", + "jsdom": "30.1.1", + "tailwindcss": "4.3.3", + "typescript": "7.0.2", + "vite": "8.3.1", + "vitest": "5.0.2" + } +} diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx new file mode 100644 index 0000000..8e154ec --- /dev/null +++ b/frontend/src/App.test.tsx @@ -0,0 +1,60 @@ +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { App } from './App' +import { ApiError, type Case, type Workspace } from './api' +import * as api from './api' + +vi.mock('./api', async importOriginal=>({ ...await importOriginal(), loadWorkspace:vi.fn(), saveDecision:vi.fn() })) + +const item:Case={id:'GH-1',workflow:'issues',title:'An issue to review',summary:'Original issue summary',created_at:'2024-02-01T00:00:00Z',source_kind:'GitHub issue',simulated:false,facts:[{label:'Opened',value:'2024-02-01'}],finding:{text:'Check the source',status:'Rule-based suggestion'},prediction:{status:'experimental',explanation:'Did not beat baseline',estimates:{baseline:0.4}},evidence:{text:'',url:'https://github.com/example/project/issues/1',sha256:'a'.repeat(64),collected_at:'2024-02-01T00:00:00Z'},review:null,history:[],outcome:null} +const workspace:Workspace={schema:'backintel-decision-workspace/v1',cases:[item,{...item,id:'EQ-1',workflow:'equipment',title:'Equipment example',simulated:true}],source_mode:'retained-public-issue-evidence'} + +describe('review journey',()=>{ + beforeEach(()=>{vi.mocked(api.loadWorkspace).mockResolvedValue(structuredClone(workspace)); vi.mocked(api.saveDecision).mockReset()}) + it('keeps later knowledge hidden and persists a reasoned human decision',async()=>{ + vi.mocked(api.saveDecision).mockResolvedValue({...item,review:{decision:'follow_up',reason:'Ask owner',revision:1,recorded_at:'2026-09-29T12:00:00Z'},outcome:{text:'Later archived outcome',available_at:'2024-02-08T00:00:00Z'}}) + render();await screen.findByRole('heading',{name:'An issue to review'}) + expect(screen.getByRole('button',{name:'Save decision'})).toBeDisabled() + fireEvent.change(screen.getByLabelText('Reason and next step'),{target:{value:'Ask owner'}}) + fireEvent.click(screen.getByRole('button',{name:'Save decision'})) + await waitFor(()=>expect(api.saveDecision).toHaveBeenCalledWith('GH-1','follow_up','Ask owner',0,item.evidence.sha256)) + await screen.findByRole('button',{name:'Reviewed'}) + fireEvent.mouseDown(screen.getByRole('tab',{name:'Later outcome'}),{button:0,ctrlKey:false}) + expect(await screen.findByText('Later archived outcome')).toBeInTheDocument() + }) + it('shows conflict and preserves unsaved reason',async()=>{ + vi.mocked(api.saveDecision).mockRejectedValue(new ApiError(409,'conflict')) + render();await screen.findByRole('heading',{name:'An issue to review'}) + fireEvent.change(screen.getByLabelText('Reason and next step'),{target:{value:'Keep my note'}}) + fireEvent.click(screen.getByRole('button',{name:'Save decision'})) + expect(await screen.findByRole('alert')).toHaveTextContent('changed in another view') + expect(screen.getByLabelText('Reason and next step')).toHaveValue('Keep my note') + }) + it('renders source as text and supports empty search and second workflow',async()=>{ + render();await screen.findByRole('heading',{name:'An issue to review'}) + fireEvent.click(screen.getByRole('button',{name:'Open evidence'})) + expect(screen.getByText('')).toBeInTheDocument() + expect(document.querySelector('script')).toBeNull() + fireEvent.click(screen.getByRole('button',{name:'Close evidence'})) + fireEvent.change(screen.getByLabelText('Search cases'),{target:{value:'missing-case'}}) + expect(screen.getByText('No cases here')).toBeInTheDocument() + fireEvent.click(screen.getByRole('button',{name:'Equipment watch'})) + expect(await screen.findByRole('heading',{name:'Equipment example'})).toBeInTheDocument() + }) + it('supports retry after service failure',async()=>{ + vi.mocked(api.loadWorkspace).mockRejectedValueOnce(new Error('offline')) + render();await screen.findByRole('alert') + fireEvent.click(screen.getByRole('button',{name:'Retry'})) + await screen.findByRole('heading',{name:'An issue to review'}) + }) + it('does not issue duplicate writes while save is pending',async()=>{ + let done!: (value:Case)=>void + vi.mocked(api.saveDecision).mockReturnValue(new Promise(resolve=>{done=resolve})) + render();await screen.findByRole('heading',{name:'An issue to review'}) + fireEvent.change(screen.getByLabelText('Reason and next step'),{target:{value:'Owner check'}}) + fireEvent.click(screen.getByRole('button',{name:'Save decision'})) + expect(screen.getByRole('button',{name:'Saving…'})).toBeDisabled() + expect(api.saveDecision).toHaveBeenCalledTimes(1) + await act(async()=>done({...item,review:{decision:'follow_up',reason:'Owner check',revision:1,recorded_at:'2026-09-29T12:00:00Z'}})) + }) +}) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..44a0ccd --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,113 @@ +import { useEffect, useRef, useState, type ReactNode } from 'react' +import * as Dialog from '@radix-ui/react-dialog' +import * as Tabs from '@radix-ui/react-tabs' +import { ArrowLeft, ArrowSquareOut, CaretDown, CheckCircle, Clock, FileText, Funnel, GearSix, Tray as Inbox, Info, MagnifyingGlass, Plus, SealCheck, SidebarSimple, Sparkle, Tray, X } from '@phosphor-icons/react' +import { ApiError, loadWorkspace, saveDecision, type Case, type Decision, type Workspace } from './api' +import { Button } from './ui' + +const decisions: { value: Decision; label: string }[] = [ + { value: 'follow_up', label: 'Follow up' }, { value: 'no_action', label: 'No action' }, { value: 'need_more_information', label: 'Need more info' }, +] + +function Evidence({ item, children }: { item: Case; children: ReactNode }) { + return {children} + + +
    Source evidenceOriginal record used for this finding.
    +
    {item.simulated ? 'Simulated record' : 'Archived public issue'}

    {item.title}

    +
    Collected {new Date(item.evidence.collected_at).toLocaleDateString('en-US', { dateStyle: 'medium', timeZone: 'UTC' })}
    +
    {item.evidence.text}
    + {item.evidence.url && Open original source } +
    Record fingerprint{item.evidence.sha256}
    +

    Current source content may differ from this archived snapshot.

    +
    +
    +
    +} + +function CaseReader({ item, onSaved, onRefresh, onBack }: { item: Case; onSaved: (item: Case) => void; onRefresh: () => void; onBack: () => void }) { + const [decision, setDecision] = useState(item.review?.decision ?? 'follow_up') + const [reason, setReason] = useState(item.review?.reason ?? '') + const [saving, setSaving] = useState(false) + const [notice, setNotice] = useState('') + const [error, setError] = useState('') + const saveInProgress = useRef(false) + const [tab, setTab] = useState('finding') + + async function submit() { + if (!reason.trim() || saveInProgress.current) return + saveInProgress.current = true; setSaving(true); setError(''); setNotice('') + try { + const updated = await saveDecision(item.id, decision, reason.trim(), item.review?.revision ?? 0, item.evidence.sha256) + onSaved(updated); setNotice('Decision saved. Later outcome is now available.') + } catch (err) { + setError(err instanceof ApiError && err.status === 409 ? 'This case changed in another view. Refresh the case before saving.' : err instanceof Error ? err.message : 'Could not save. Try again.') + } finally { saveInProgress.current = false; setSaving(false) } + } + + return
    +
    {item.id}
    HReviewer
    +

    {item.title}

    {item.source_kind}{item.simulated ? 'Simulated' : 'Historical replay'}
    + + FindingDecision historyLater outcome{!item.review && } +
    + +
    Source record{new Date(item.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' })}

    {item.summary}

    +
    Observed facts
    +
    {item.facts.map(fact=>
    {fact.label}{fact.value}
    )}
    +
    BackIntel{item.finding.status}

    {item.finding.text}

    Suggested review step · Check evidence before acting
    +
    Prediction estimatesExperimental

    {item.prediction.explanation}

    {Object.entries(item.prediction.estimates).map(([name,value])=>
    {name}{Math.round(value*100)}%
    )}

    These estimates do not set queue order.

    +
    +

    Decision history

    {item.review ? 'Saved on the local server. Available across both demo frontends.' : 'No decision has been recorded for this case.'}

    {item.history.map(review=>
    {decisions.find(d=>d.value===review.decision)?.label}

    {review.reason}

    Revision {review.revision} · {new Date(review.recorded_at).toLocaleString()}
    )}
    +

    Later outcome

    {item.outcome ? item.outcome.text : 'Record your first decision before viewing the later outcome. This keeps later knowledge out of the initial review.'}

    {item.outcome && {new Date(item.outcome.available_at).toLocaleDateString()}}
    +
    +
    +
    Your decision{item.review ? `Saved · revision ${item.review.revision}` : 'Not recorded'}
    +
    {decisions.map(option=>)}
    +