diff --git a/BENCHMARKS.md b/BENCHMARKS.md index 6cb1a2c7..15629a99 100644 --- a/BENCHMARKS.md +++ b/BENCHMARKS.md @@ -94,14 +94,14 @@ interpretation and do not count as additional benchmark-quality gains. ### Public numeric evidence registry Every exact public aggregate retained below comes from the checked-in, public-safe -[`offline-fixtures-release-1.7.12-20261102.json`](docs/benchmark-evidence/offline-fixtures-release-1.7.12-20261102.json) artifact. Its +[`offline-fixtures-paid-plans-20261104.json`](docs/benchmark-evidence/offline-fixtures-paid-plans-20261104.json) artifact. Its SHA-256 is -`025183601c5a28a37daf9326a061010244bc9a3f9f7db1e3e6d4f866eaafc660`, also recorded in the +`cc66984fd3dd78c17addeb979aea0e0bf40ff0f533ebcd683da4b7f532da0055`, also recorded in the adjacent `.sha256` file. The artifact contains no raw questions, answers, prompts, customer data, or per-record content fingerprints. The fixture-suite digest is -`54422b65475bb0d3e5c4ad0613bbda87dd5e01cb43e15513469b7306446831c9`. The artifact defines +`93d671089dd360954fce2c97ce1fdd105f87166ba9224e644d61ce15f9857e79`. The artifact defines the digest algorithm and records the SHA-256 of every suite and dataset file. Each evidence ID also binds its exact command through `sha256(UTF-8 exact command)`: @@ -134,7 +134,7 @@ retrieval-quality improvement. It does not qualify browser behavior, hosted serv external models, or provider billing. The registered artifact was produced with: ```bash -python -m scripts.export_offline_evidence --output docs/benchmark-evidence/offline-fixtures-release-1.7.12-20261102.json +python -m scripts.export_offline_evidence --output docs/benchmark-evidence/offline-fixtures-paid-plans-20261104.json ``` The earlier release-review predecessors, @@ -203,13 +203,13 @@ grounded checks in separate panels; provider billing and MCP transport are not m the SVG and matching PNG with: ```bash -python scripts/render_benchmark_report.py --report docs/benchmark-evidence/offline-fixtures-release-1.7.12-20261102.json --output docs/images/context-efficiency.svg --png-output docs/images/context-efficiency.png +python scripts/render_benchmark_report.py --report docs/benchmark-evidence/offline-fixtures-paid-plans-20261104.json --output docs/images/context-efficiency.svg --png-output docs/images/context-efficiency.png ``` The companion examples are also generated from that artifact with: ```bash -python -m scripts.render_benchmark_examples --report docs/benchmark-evidence/offline-fixtures-release-1.7.12-20261102.json --output docs/images/evidence-backed-agent-examples.svg --png-output docs/images/evidence-backed-agent-examples.png +python -m scripts.render_benchmark_examples --report docs/benchmark-evidence/offline-fixtures-paid-plans-20261104.json --output docs/images/evidence-backed-agent-examples.svg --png-output docs/images/evidence-backed-agent-examples.png ``` The historical-to-executable mapping is in [`docs/BENCHMARK_CHANGE_COVERAGE.md`](docs/BENCHMARK_CHANGE_COVERAGE.md). diff --git a/CHANGELOG.md b/CHANGELOG.md index 8acfdad2..e381b8eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to Engraphis are documented here. Format loosely follows ## [Unreleased] +- Add one checked-in entitlement contract as the read-only plan→feature mirror, and fail the + release gate when it drifts from the private hosted contract. Local workspace export stays + free; a server grant this build cannot render is reported instead of silently dropped. + ## [1.7.12] - 2026-11-02 Candidate package version: **1.7.12**. Publication and hosted-service qualification remain separate. diff --git a/Dockerfile b/Dockerfile index 0b5e611e..6f779689 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Engraphis — self-hosted AI memory engine. Local-first; you bring the LLM. -FROM python:3.11-slim@sha256:b7a9e5f6a93d6d4a31f129bbd4bdee69efccb6ded0fb230d78aafaf37af98377 AS base +FROM python:3.11.18-slim@sha256:db20d8506e2f15b6db39b40b6015ca46bae855dec6dd326d462cfe79ae972893 AS base # ENGRAPHIS_HOST is deliberately NOT set here: docker-entrypoint.sh chooses IPv6 for a # Railway deployment (which injects RAILWAY_SERVICE_NAME) and 0.0.0.0 for ordinary Docker. diff --git a/MANIFEST.in b/MANIFEST.in index 7b4a9491..9655b752 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,6 +4,7 @@ global-exclude *.pyo recursive-include engraphis/dashboard_assets *.html *.css *.js *.png *.ico recursive-include engraphis/dashboard_assets/vendor * include engraphis/commercial_manifest.json +include engraphis/entitlement_contract.json recursive-include integrations/commandcode *.py recursive-include scripts/templates *.svg include LICENSE NOTICE README.md CHANGELOG.md BENCHMARKS.md diff --git a/README.md b/README.md index b38847f3..08b38a58 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Connect a coding agent over MCP with the [agent setup guide](https://github.com/ The current registered artifact contains three deterministic offline fixture runs. It separates context size, retrieval quality, and grounded decision checks; these small fixtures do not establish general task performance.

- Three registered offline fixtures: structure-aware chunking reduced mean retrieved context from 740.3 to 214.3 tokens per question (71.1%, 18 questions); the serialized JSON-shape proxy fell from 24,590 to 11,138 tokens across 26 payload samples and 260 recalls. Candidate and packed retrieval quality (Recall@5, Hit@5, and answer-token recall) are shown separately (each 1.000). Grounded checks show 5/5 answerable queries grounded and 6/6 abstention queries rejected, including a 1/1 quarantined-evidence probe; 11/11 decisions were correct. MCP transport and provider billing were not measured. Artifact SHA-256 prefix 025183601c5a; see the benchmark guide for the full checksum. + Three registered offline fixtures: structure-aware chunking reduced mean retrieved context from 740.3 to 214.3 tokens per question (71.1%, 18 questions); the serialized JSON-shape proxy fell from 24,590 to 11,138 tokens across 26 payload samples and 260 recalls. Candidate and packed retrieval quality (Recall@5, Hit@5, and answer-token recall) are shown separately (each 1.000). Grounded checks show 5/5 answerable queries grounded and 6/6 abstention queries rejected, including a 1/1 quarantined-evidence probe; 11/11 decisions were correct. MCP transport and provider billing were not measured. Artifact SHA-256 prefix cc66984fd3dd; see the benchmark guide for the full checksum.
Three offline fixtures separate context reduction, candidate and packed retrieval quality, and grounded behavior. The chart shows the artifact checksum prefix; see the benchmark guide for the full checksum and reproduction steps.

diff --git a/docs/LICENSING.md b/docs/LICENSING.md index 0a41ed0e..f370ef26 100644 --- a/docs/LICENSING.md +++ b/docs/LICENSING.md @@ -52,6 +52,17 @@ a private repository. Public clients communicate with those services through aut versioned protocols. This boundary protects new commercial implementation work; it does not change the Apache license or prevent a fork from modifying code already released here. +## Entitlement presentation mirror + +`engraphis/entitlement_contract.json` is a checked-in, read-only mirror of the private +control plane's plan→feature contract. It tells the dashboard which plan advertises a +capability and which copy to show when one is unavailable. It is not an entitlement: +`engraphis/entitlements.py` cannot authorize an operation, mint a credential, or unlock a +route. Every paid operation is still authorized by the hosted service, and a server grant +this build does not recognize is reported as unrecognized rather than inferred or silently +dropped. The release gate compares the mirror with the private contract, so a capability that +exists only server-side fails CI instead of disappearing from a paying customer's dashboard. + ## Trial, grace, and recovery The server-issued trial lasts **7 active days for Pro or 14 active days for Team**. Grace is a diff --git a/docs/SYNC.md b/docs/SYNC.md index b3432b50..c7247572 100644 --- a/docs/SYNC.md +++ b/docs/SYNC.md @@ -86,27 +86,35 @@ owner-private `~/.engraphis/config.env`, then restart the local dashboard. The S only whether the key is valid and the local encryption dependency is installed, never the key itself. A read-only pull still needs the same key to decrypt received bundles. +The dashboard can also generate the key in place: **Settings → Cloud Sync → Create sync key**. +It is written once to `~/.engraphis/sync.e2ee_key` (owner-only, mode `0600`), shown exactly once +for you to store, and never returned by the status endpoint again. An operator-provisioned +`ENGRAPHIS_SYNC_E2EE_KEY` always takes precedence and cannot be replaced from the dashboard. +Rotation is explicit and warns that data synced under the previous key is unreadable until every +device uses the new one. This path is for a trusted local device; a headless deployment should +still provision the key through its secrets manager. + ```bash -python -c "import base64, secrets; print(base64.urlsafe_b64encode(secrets.token_bytes(32)).decode().rstrip('='))" -``` - -Treat the command output as a secret: use a trusted terminal without transcript or log capture, -store it in your secret manager, and never paste it into the Cloud portal or a support request. -Before the first upload, keep a separate secure backup and confirm you can retrieve it. If the -key is lost, previously synced encrypted bundles cannot be decrypted; Engraphis Cloud cannot -restore it. - -For the first device, add a single `ENGRAPHIS_SYNC_E2EE_KEY=` assignment to its -owner-private `~/.engraphis/config.env` with a trusted editor, retaining any existing settings. -Use the path named by `ENGRAPHIS_ENV_FILE` instead when that override is configured. A secrets -manager may inject the same process variable instead of using the file. Restart the dashboard, -then check **Settings → Cloud Sync**: Connection must show **Connected**, Encryption key -**Valid**, and Sync readiness **Ready** before **Sync now** is enabled. If encryption support is -missing, install `engraphis[cloud-sync]` in the dashboard's Python environment and restart. -On each additional device, provision the *same* backed-up key through your secure channel; -generating a different key will make earlier encrypted bundles unreadable there. - -The dashboard's **Sync now** action invokes the same customer protocol. The public package does +python -c "import base64, secrets; print(base64.urlsafe_b64encode(secrets.token_bytes(32)).decode().rstrip('='))" +``` + +Treat the command output as a secret: use a trusted terminal without transcript or log capture, +store it in your secret manager, and never paste it into the Cloud portal or a support request. +Before the first upload, keep a separate secure backup and confirm you can retrieve it. If the +key is lost, previously synced encrypted bundles cannot be decrypted; Engraphis Cloud cannot +restore it. + +For the first device, add a single `ENGRAPHIS_SYNC_E2EE_KEY=` assignment to its +owner-private `~/.engraphis/config.env` with a trusted editor, retaining any existing settings. +Use the path named by `ENGRAPHIS_ENV_FILE` instead when that override is configured. A secrets +manager may inject the same process variable instead of using the file. Restart the dashboard, +then check **Settings → Cloud Sync**: Connection must show **Connected**, Encryption key +**Valid**, and Sync readiness **Ready** before **Sync now** is enabled. If encryption support is +missing, install `engraphis[cloud-sync]` in the dashboard's Python environment and restart. +On each additional device, provision the *same* backed-up key through your secure channel; +generating a different key will make earlier encrypted bundles unreadable there. + +The dashboard's **Sync now** action invokes the same customer protocol. The public package does not run a local auto-sync loop or ship a cron/Task Scheduler wrapper. Hosted automation belongs to the private service. A round with any incomplete workspace is a failure, even when other peers were applied successfully: the bounded report retains those good-peer totals, labels the result diff --git a/docs/benchmark-evidence/offline-fixtures-paid-plans-20261104.json b/docs/benchmark-evidence/offline-fixtures-paid-plans-20261104.json new file mode 100644 index 00000000..9afc2048 --- /dev/null +++ b/docs/benchmark-evidence/offline-fixtures-paid-plans-20261104.json @@ -0,0 +1,702 @@ +{ + "environment": { + "embedding": "deterministic", + "numpy": "2.4.6", + "platform": "win32", + "python": "3.11.17", + "vector_backend": "numpy" + }, + "generated_on": "2026-11-04", + "privacy": { + "contains_answers": false, + "contains_customer_data": false, + "contains_per_record_fingerprints": false, + "contains_prompts": false, + "contains_raw_questions": false + }, + "runs": [ + { + "boundary": "Deterministic offline retrieval fixture; normalized-character token estimator; not external QA or provider billing.", + "command": "python -m eval.chunking_eval --dataset eval/datasets/longdoc.jsonl --k 5", + "config_digest": "c1c8196aa7e1568ef3844a9fb2d76b87f342c39108e32d6ad144b885a76143b8", + "config_digest_method": "sha256(UTF-8 exact command)", + "id": "offline-chunking", + "result": { + "chunked": { + "max_stored_tokens": 59, + "mean_context_tokens": 214.3, + "mean_evidence_tokens": 42.4, + "memories": 24, + "recall_at_k": 1.0 + }, + "context_reduction_pct": 71.1, + "documents": 6, + "k": 5, + "questions": 18, + "token_counter": "engraphis.chars4.v1", + "whole": { + "max_stored_tokens": 213, + "mean_context_tokens": 740.3, + "mean_evidence_tokens": 162.2, + "memories": 6, + "recall_at_k": 1.0 + } + } + }, + { + "boundary": "Deterministic offline CodeMem fixture; serialized JSON-shape payload proxies, not MCP transport responses, provider billing, or latency claims.", + "command": "python -m eval.performance --dataset eval/datasets/codemem.jsonl --k 5 --iterations 10 --json", + "config_digest": "bbe4aca81e58d4830e50a8fc7729a1d15b71d97a6299bccd79432b7f119677d7", + "config_digest_method": "sha256(UTF-8 exact command)", + "id": "offline-performance", + "result": { + "answer_token_recall": 1.0, + "compact_serialized_payload_tokens": 11138, + "dataset_cases": 14, + "full_serialized_payload_tokens": 24590, + "hit_at_k": 1.0, + "k": 5, + "max_context_tokens": 108, + "mean_context_tokens": 85.38, + "memories": 44, + "packed_quality": { + "answer_token_recall": 1.0, + "hit_at_k": 1.0, + "recall_at_k": 1.0, + "sample_count": 26 + }, + "payload_boundary": { + "kind": "serialized_json_shape_proxy", + "mcp_envelope_serialized": false, + "token_counter": "engraphis.regex.v1", + "transport_measured": false + }, + "quality_scope": { + "packed": "packed_quality fields score only chunks admitted to reader context", + "retrieved": "legacy quality fields score all candidate chunks returned before context packing" + }, + "questions": 26, + "recall_at_k": 1.0, + "saved_serialized_payload_tokens": 13452, + "serialized_payload_savings_ratio": 0.5471, + "timed_recalls": 260, + "token_budget": 1500, + "token_counter": "engraphis.regex.v1" + } + }, + { + "boundary": "Deterministic offline support/abstention fixture; not a frontier-model answer-quality score.", + "command": "python -m eval.grounded", + "config_digest": "590442e51e3642c10489165759919dc86ffac62c182937330c153e7f8d5fc26f", + "config_digest_method": "sha256(UTF-8 exact command)", + "id": "offline-grounded", + "result": { + "abstained": 6, + "answerable": 5, + "decision_accuracy": 1.0, + "grounded": 5, + "off_topic": 6, + "quarantine_hits": 1, + "quarantined": 1 + } + } + ], + "schema": "engraphis-public-offline-fixtures/v1", + "suite": { + "digest": "93d671089dd360954fce2c97ce1fdd105f87166ba9224e644d61ce15f9857e79", + "digest_method": "sha256(canonical compact JSON mapping each sorted path to its file SHA-256)", + "files": { + "engraphis/__init__.py": "1cde9f19e05ddc4f5f28517e00b0a7bd5ebf09d6be8ec2681cebf5b698195d70", + "engraphis/ai_context.py": "4dfd5d39eb95d05c591d1981e9e855eced73232f53efd9fe9966e0e08957d030", + "engraphis/app.py": "44d68ad8c0ff46baed01978c9b04e40b8be5031609d5a69e205b7f05da3777fb", + "engraphis/backends/__init__.py": "a9f22b9278362904166614081f1df78469d453601b298ce4e8afdba8a3722b25", + "engraphis/backends/codegraph.py": "83e723a91068d23694092fbe00157fcb2597061d453eb36f955bf55bc5b4d35e", + "engraphis/backends/embedder_api.py": "56a6bceea4f757325dcea987b0339e41d3875634b1ae5103f0537a358cf5878b", + "engraphis/backends/embedder_deterministic.py": "ee2476c33ba83a9bba5d7caaa4dbb40861ee22cbdb1cf3c1092ca6c018a6c38f", + "engraphis/backends/embedder_st.py": "c497b47038227516a0bc43d4bc0a91fc46d267bbffb0382c0b893e44f016d6b4", + "engraphis/backends/encrypted_db.py": "25f6c1480d296a88f317213700a8b3c81e2732399bfac464b0d893465b25e846", + "engraphis/backends/extractor.py": "f2e3455ab7f14caee1d5b5c4ef071e498e90118f1b0ddcb8510c969583b0fc57", + "engraphis/backends/graph_extractor.py": "88561efa0d3fabc447a0a005b10e36261379d46218cf62d905e6928cd2fda676", + "engraphis/backends/jev_decision.py": "31d4a789a013226f5be3eb47c54d3ca34af09b1c77b7c9ab1f57622035951d08", + "engraphis/backends/jev_query_planner.py": "c64818474c15acf5d42f673f8084f0fbf782e78b23b1f8cb9a32d6d044d5af63", + "engraphis/backends/jev_transport.py": "e65368169d53998d3e02cc78a42ed7c92fd27749cc1c9b4f95aa856e618a0ee9", + "engraphis/backends/model_source.py": "8c3c7681f95214a2bbabd8de222e5ee11f42fe13402d27365654ae75fb363d4e", + "engraphis/backends/postgres_schema.py": "8468578c3add701d30d5eaa36d768ded2375d116e55f1836e09f6107a07a267e", + "engraphis/backends/query_planner.py": "bbdd77afc9b5523421b85b2ae63c8da7f5a7b777265450e0d21708a83e7bb23c", + "engraphis/backends/reranker.py": "747761d6cbfa421388974bcfd98d844f92391d80f4bf6a4feca00b0c7a6908ca", + "engraphis/backends/resources.py": "47cc867c3aecc8bd95fa284bc5bb04715f3339c19a0a11512973ef6171c95944", + "engraphis/backends/retention.py": "381d9371e3951d762f8b55eb54711de5697642acb39de99a714f246c059ecbd0", + "engraphis/backends/sync_folder.py": "e4f70a92a17f6a365910670df041e6e3ca421d44ada2827917cd66b4dc067bfa", + "engraphis/backends/sync_relay.py": "88a24af68641cfca42a4838ded05337dfe01e3364dcfd39ad81ed4a658433f2e", + "engraphis/backends/vector_numpy.py": "c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72", + "engraphis/backends/vector_sqlitevec.py": "6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b", + "engraphis/build_info.py": "624c22471e56d4c4047160808c4245488292af611564d1a63ca437605bbb414f", + "engraphis/cloud_authz.py": "6d86e17c4bdd4d3897ddc4b8bae44059d47c8763860745e37ee94fa10cdae7be", + "engraphis/cloud_features.py": "ffc7dc0fa690b94248c24807497784407a262365f110514e6a193724c7f4f55f", + "engraphis/cloud_session.py": "cc6ab27c6af69b5c676e721ab06c520730e205f5bbffe27818650edfb71d4b63", + "engraphis/commercial.py": "184f312066a9e682e51a0abeff042f1c0e8eed2d47470157b23930b5a17633aa", + "engraphis/config.py": "7a1aa04246bda341f26866c5145854412e8c157d9880212fe1d7579ac986b764", + "engraphis/core/__init__.py": "dd5143729c3939237f04636f437032b1f2d3a5f7d82c91bbc2a5a283c3f0ebaa", + "engraphis/core/adaptive_context.py": "cc5ce48109bb0d5230a5b2b8424b829c853feec5b5d5b82596413f2279b0c9f9", + "engraphis/core/browsing.py": "cfae752d52ef51b17c4ffbe44dde62d5b0e1ed0ca34ec0e1788f3991d94bf05f", + "engraphis/core/codegraph_export.py": "4641074258d7b23498f92dd45053a0fbb111863eaad2001c08e5e3c2dc2fd54f", + "engraphis/core/conflicts.py": "28530be25a4af0bffd8f609b965b33ca7f93199a70887789b2148fda8a61a486", + "engraphis/core/consolidate.py": "f66eedfe08319a64b761261ccf1c99b163eba564d6d4060534bf8c575aecf232", + "engraphis/core/context.py": "8cc15746e4de88bce7d34db8c11dfaa545f17305788e2167a286179171717fb9", + "engraphis/core/diagnostics.py": "5ba449bdf5087d8ccb5808379596e695c8f405da497372e99f2bac509f96e8e4", + "engraphis/core/documents.py": "9b4a5dce557bbd25217c61b51d5838a8b7c693c77f7a700d010ed607cb39b150", + "engraphis/core/engine.py": "c908dea96113a97dcd56679f62c9e587b28130204888b16e4c8e64d68f47d3fe", + "engraphis/core/evidence.py": "97912b52a3d22de909218f83c09c92572ed695d7b3c2117e5330271c6d84a7bd", + "engraphis/core/fsutil.py": "6db770fa8bd3e1a57dfa70eb8e8bc46d48c2dc53ead1b0b43eeecf085ef58cfc", + "engraphis/core/graph_layers.py": "64d74ab01c77119f6343ba6f1d6a84f9653f1a5d34d47ce7966f3ac31b29d2ea", + "engraphis/core/graph_policy.py": "ec5b373d01adb2de87df31d9f543130018e9a73faaaed239a184f14a32646615", + "engraphis/core/graph_scene.py": "b79c53e156b11102d64196ac5082c6549c13fe0cc57b9760d66872459a0f2110", + "engraphis/core/graphrank.py": "1279a58396104d3f906bfd5ec75b32efedfefe52201467bf19d80be3517017a5", + "engraphis/core/grounded.py": "0d6b60ef16f3cd65b7032bdb60ba853dac364d0df58a74c932ecf12aec98e4b7", + "engraphis/core/ids.py": "e47eeadfaf560bc7638e1879fc491fa82d981cd42b9e4b05ef87033b2d7dfa60", + "engraphis/core/interfaces.py": "bd5f7f4751a035348cf99cae3e8be64f205cb2cb8fb6eb3bd7e56f5099d81f0f", + "engraphis/core/mutations.py": "3d32b63f530204ae0394f3ebafbdf667cd86143a863ca808527d1e754d3a246e", + "engraphis/core/obsidian.py": "39fa18707972563581b06fa129e03841200b8bc05a114876cbbb7f352a885bee", + "engraphis/core/poisoning.py": "e91b6b23d2717680b3f25a7a540ba04e19e815e4ce4744bdb70da9bada21e031", + "engraphis/core/query_planner.py": "249062d67392ab7c203cc71e9040e99bee91bf570604e90949149a93cb652120", + "engraphis/core/read_snapshots.py": "be08e63a88bd38ed91d61b28657a65201c38994856db2798b209a73151dd202a", + "engraphis/core/recall.py": "b56dc6736414cab451bb195df7948a6088c7ae5e14f5961d57daffb4a5bafb28", + "engraphis/core/relocation.py": "86bbd292b374539b480dddc1a9ac19292fbc17ff9fa8e1fe6ff69c91ee7c8bb9", + "engraphis/core/resolve.py": "f01a6f55e44320ab04b97e516342f20155668863b2fc4765305e066d87586524", + "engraphis/core/retention_policy.py": "864c03bdb6e743cd0002c706de471e920f1fa1f1a9918ab343ef4c2042b47429", + "engraphis/core/retrieval_policy.py": "d169eb442115bd06c1e1da776fa6c1849e0795e658edd732a8d6821326b46b03", + "engraphis/core/savings.py": "e34a8f4a26c313a1e439dada16b7fd4a0c3fada2038e2f4aed03978b53310bbc", + "engraphis/core/schema.py": "8384672f2d5406abc5969a3e134e14684d71dbeb44685f8198023b8e3f03be77", + "engraphis/core/scoring.py": "bf0cf02521ab3dae6de4a0e7c9892bf3feb02f73de22478c7fb0c64ce55ab0d3", + "engraphis/core/secrets.py": "78ab40870d6c5918c189307028b107df239d08cd918abe08c85fa15b58fa1663", + "engraphis/core/source_paths.py": "2e71de7f19f260b335137602f07a28a80efa5d65e3d2497215356ba5f172b47e", + "engraphis/core/spec_crawl.py": "23359f9e28a2a68a3abe59f1e705b8be1f6c72880d708123bc85391029e769e5", + "engraphis/core/store.py": "b04fc2cfc162539e54d80691f84dda4b4f454f2be1d31027776d5e49655fe8e8", + "engraphis/core/sync.py": "0d28dd697eb8e8acdb7fed3e346fcba8fce23cbf4606eb11de68cb1fb001fcf2", + "engraphis/core/textutil.py": "acd65031729fa5d91d09527b8eb52518b83cfce77a55e6b7ae2d94686e35c1a6", + "engraphis/core/user_model.py": "3147ec8ee7cfd855783f639874b63f331cdc822bd8bd9298cfb326dd18666026", + "engraphis/core/vector_repair.py": "a8c1812de4e3ed288eda3e54a505e136d6ebd3ec296788bcbb8804b11e13cfc8", + "engraphis/core/vector_search.py": "75800052e573e9af01c6fd098eaf8647c3c45cd7eb2601dae05d42359e19b234", + "engraphis/dashboard_app.py": "77546609aa29031dbf935228ee00162b3635f4c3438d2a5922bfe94eb211a084", + "engraphis/dashboard_assets/__init__.py": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "engraphis/device_connect.py": "c8cd0a22e9fd2d92a65bd74047cc3a8b7159bfc9a297cb639c1c7f1699f8802e", + "engraphis/document_import.py": "5378d17355cbf97db304ea8ac2d1978206b244b553c7e0645f8a060d272664b5", + "engraphis/engines/__init__.py": "111232af583889195c5f5a60298e32484348608e81d2bdd822fd3ecd2a33c1e4", + "engraphis/engines/embedder.py": "998b65dd566966bb6581fd9f09cdf46c58a3923ef5df3073ccdaa541153a3581", + "engraphis/engines/ingest.py": "1a5d4b52c13e533864329f9fff11c0f6ebc299ff7093a9d9275e5c9626a39ce2", + "engraphis/engines/intelligence.py": "b561589b98deb98271f104dfec6276aeba13c4815e627259be5f8769f2e7ad82", + "engraphis/engines/recall.py": "f979580d065599c07acbc3add59f71e52d9a186a2104b0e257daa7959ea88d26", + "engraphis/engines/reweight.py": "91ec5815f5d356a7068c36133d24405334450f361f902f99275bed3ccebffd49", + "engraphis/engines/thoughts.py": "4adb9c8a9bcfe736cb42fff9b5ce24631da6ec473d178e83f1c176fde3b8b814", + "engraphis/entitlements.py": "05a4826de62a3b0ad061be4325f502a5928c6db78ea118e32ae5ac2c8f480717", + "engraphis/factory.py": "be26899074d80b2f96fff9a5a70969894e818dc71e2cfdf4f7ee965f1f4cc550", + "engraphis/file_guard.py": "44eccaeca3afef7149bfdfea2321c325472253ee39a9e0bc24c720b3da3d685b", + "engraphis/graphdata.py": "f5dc93395f52203c66deaeec8bc58d5c1f990a35a535151e0b9a84cecf7d8430", + "engraphis/hosted_client.py": "6a81afd9b40fb783cc9809ebb2dfd23539bc8ae614e03ad9a5b98ed5014fefcd", + "engraphis/http_deadline.py": "bb29ac2d1e2fb49829e8484bb17f8abe98c23895ef17c68abe135d13f5df0099", + "engraphis/http_security.py": "5cd96551778d7a964ce1d87c6bce032a5e70c7a1779fb2f6731432b96f5f069b", + "engraphis/inspector/__init__.py": "720cac28b8a6019d0a0c53809d5905b7d6eb9d4ecbac767505904c6ec3c39071", + "engraphis/inspector/app.py": "8aefe8a935397dde3c6466e691a7e6a7203ebe42e07aa00c332b733465e59e04", + "engraphis/licensing.py": "7e73c28b0e1c3536e2080a129af614f838d2cae3ec3a48a3d8ed7e5153c9e935", + "engraphis/llm/__init__.py": "f3096d2ddd652b99e6fc0b4c5a8786d9bbaba41b259df0b44ce93b57bde3a8b1", + "engraphis/llm/client.py": "0c84000033d3f85b0e0161f100c271c35b591e1480701455ec65c7d3f111ec42", + "engraphis/local_auth.py": "8fd6d42594454d68c99bf25054adcfbf2379d4e08f17affd36af994fc7ad78bc", + "engraphis/logging_setup.py": "f7d2edc756458a852e0401453e9c71b785aba08fa7859aacbdc23fb30dc7982a", + "engraphis/managed_processing.py": "6d33cdfd10800d9552fcfae3c2b071d2b39fe10fb69d1a8e5d9ed6026882197e", + "engraphis/mcp_classic_cli.py": "778122b121a1c654b8f85810fad05d9c8d7799cf13903d7dc274938b51008da0", + "engraphis/mcp_cli.py": "f94e63805aa05cb913e48200819473b86b9623b2b958578f41f025adb0f1a21e", + "engraphis/mcp_http_cli.py": "c0605c53ff53ffde8d98d02ca2812e3c2c792a34e260b0a197433fb0ed2455af", + "engraphis/mcp_server.py": "b1a35b28e81ac44c0812d2569e662b1626704b00c7e6dce67cd70428cef25bea", + "engraphis/mcp_stdio_bridge.py": "a2e64883f4737415c633e9a69c1f3bdafe1860e1ee97d579dfc16814dd36c480", + "engraphis/models.py": "6e76e97db0aca3805c6f582ea78cc6e1c0ccd2665e16fd8ab81eb91b94141b51", + "engraphis/netutil.py": "15cd7c9d0792b5a95d0a722d94e433cf6d07182777f26bdf19f047270673b08b", + "engraphis/observability.py": "a3a6945bf33a0d8e216da56ca7efec0031b82dc64a66cff36f0b2161f5db4981", + "engraphis/obsidian_import.py": "cff5d117a592ceb8aa822e60a6142878da2f3aec1fcba44a5d64853c7bb0cee7", + "engraphis/private_state.py": "7485570efcaee8a1dc00b64ebfdc23178517235ae17609aa78db8bdda7e45fe3", + "engraphis/read_only_api.py": "dab1721cf39b7d374462002f5b770d53510a642d955eb4c9a972969e39841bf8", + "engraphis/redirector.py": "5ba964b81f09008c9369180cb49d247519000763274aabc5e046215b12fa641d", + "engraphis/routes/__init__.py": "f0d59080212cfa0d9b50877bca28e5832d1ae917899500d52c68b1c8821231af", + "engraphis/routes/memory.py": "9ca066e1762eeeaafd9790ed57d730efcaceb57742dbe0c830ad8764500ff9e5", + "engraphis/routes/v2_api.py": "647b9ec769127849da0a148c0f520766407e01e2ccfc73952ec332e25fdfa4e2", + "engraphis/routes/vault.py": "503304c177cf4d81bec5a9836731c0c32b62159175dea6d0fb8ce30c97a2bc6b", + "engraphis/service.py": "dec62fa2912c19b13c904e1d7585babc5c55edfd995846f0bab33d62a20dff85", + "engraphis/service_context.py": "0cd84538ca18b07d6440fa4cb0147510eacef2af5cf04b7a0519f2ac15a7ff87", + "engraphis/stores/__init__.py": "48ee4326c8f28eecf46f558b7aea21adb779226a7038299017ab90196d5d84be", + "engraphis/stores/graph.py": "ebf603b54cf8450e7c9a7319bd05db2f39fcda8491f5969d6af3e8da61571bc2", + "engraphis/stores/ledger.py": "df5cbb30d977decc0a9c3a2365c9c115cfbb5fe951bae48446436661c80e5d30", + "engraphis/stores/vaults.py": "2c986129b9d1e7aab33e18a3b9a278eb5ad2236895587f69b5816a5d96796bc3", + "engraphis/stores/vectors.py": "45a1baca381fc647548cc89424eb36d5853f7562c275b191b35b99339a190b7d", + "engraphis/update_check.py": "ffbf5ef682fb15177915073ebc0b5eab4dee9092bbf0f58d36a8a54c6605ccf4", + "eval/__init__.py": "639f0c6d9d6aac8ff6dc605a34a0a301058905cc53bff4eaed5912247f0e7c56", + "eval/ablation.py": "16f159dee75d2f96cc42f230c2403fa19ea0bda7091c4823660da553463a194a", + "eval/adversarial_memory_security.py": "35dd8d981bcbad50e9815465be420b05b62a9dea28a78eb6cc1e09ec51c320c4", + "eval/agent_benchmarks.py": "8e88943e45a1b2083b366cbb0329326ef918b119a2366ed395c404c2b7ef98b0", + "eval/benchmark.py": "b71832affdf87d23bc1db7b522a669a7888571b3989b03a235c70f5da9952bd6", + "eval/benchmark_analysis.py": "1200791425d029695fee0b7da1f188a8962f337aa31eb4217d50e4503641d2b3", + "eval/benchmark_campaign.py": "033dbebfb47df5fcd3a6588b29ea4ab5fd44d4387ae45334379278ab30f3e57f", + "eval/campaign_adapters.py": "2bdb51de0dcdfceb1be2a649d84541780ac4d1fc0f9b7585004d87490073213e", + "eval/campaign_api.py": "323be4e1d9520e8047ab54c9cbb158013785402b06ee4e93e55227971c54bf95", + "eval/campaign_candidate.py": "88541840d16c3b7368586ed0ee3566054214b1f594826b746b28d0e407cb812a", + "eval/campaign_continuation.py": "7f77a20ac8f85cd96729bba0cf45872c98a3421af005bd735434b7b416dd7e1f", + "eval/campaign_ledger.py": "ba3079ba541cb1f70929a76f147d2bc5e8b264103a1cd7faad97685e0ecc7790", + "eval/campaign_oracle.py": "9ea5479786efd9caa2b59e612e891a93dd34b500a2b36aa20bcf749fb6d4d48a", + "eval/campaign_storage.py": "ebcd1f4aeceaa5ff9304494c64dd31fe31637ceb107d8cc39fa292e3b14055d4", + "eval/capacity_matrix.py": "8c25bd97754c1d7a8468a687042a3c87cc9c0299cf01c19b80dacfec2be4e552", + "eval/chunking_eval.py": "db5c39d98ad7272d25e2f33f39779da74a0d558798441561196bedbd38882e49", + "eval/closed_history.py": "beadb9dbfa97af2ca41efc737f41e8758f4c4635f468e6c7a2ac5c0c95f73f3d", + "eval/code_agent_ab.py": "d98bba6b77700ff6bf86ff0d2cf518a67e5a2676ef51bbddabbb2ff26e1f3aaa", + "eval/code_arm.py": "d211166fce1b8a4173848e1617873b7e84aadeff43746effe7483c54bbdb6f1d", + "eval/codex_oauth.py": "f235fca482de4201d1850bbfb583765ac5f1a057b4ad7e2d504d036bfc03f391", + "eval/coding_acceptance.py": "4b39cbcb60d7fba503cca597399cf9d04ad9567a43cdb950015ccf552e6a2773", + "eval/coding_corpus.py": "7b5205e8544578fe99d9d9cf6cfc34e40e5d1d049238d502ff7bc6f3c14948f5", + "eval/consolidation_ranking.py": "917b578d4e0bcb929bf1a1a37611acf7716a520c076abf4ade0d8d12a1c455de", + "eval/context_economy.py": "709ac7cc866855f96d7717ab2bea12e8b0d3a140d15fb978a2a929ad085931f2", + "eval/context_efficiency_guardrails.py": "22afd1a6fe17219e74701dc587ec35f569a5bf22bea270944525f51746723f14", + "eval/datasets/codemem.jsonl": "341313023c22850a2e14f02742b571ad1deca824f886a1654a59541304c01f3c", + "eval/datasets/coding_memory_v1/oracles/atlas-green--code_relationships.py": "969aa71235e0ae3cb764b9ee12b789184cedb5fea6fcc0d86e5dcb2c0498fa56", + "eval/datasets/coding_memory_v1/oracles/atlas-green--condition_values.py": "648702646fb62990168e98fba0dc6a27be128876548fa7035a59d71aee9da02d", + "eval/datasets/coding_memory_v1/oracles/atlas-green--corrections.py": "c7132c7fe39b7afbdf11065037369c0f3c38a22e69d8742e8afddfd5785d2e61", + "eval/datasets/coding_memory_v1/oracles/atlas-green--long_documents.py": "65dffb25944cb612872655f12db16af73f7a095e7abcf6a7cc5d31dc91f083b1", + "eval/datasets/coding_memory_v1/oracles/atlas-green--multilingual.py": "ff49874a691bfc30c082695ee6d3c89b9f2e1e5eecf086026110770d8e1b0452", + "eval/datasets/coding_memory_v1/oracles/atlas-green--paraphrases.py": "dbdcf5d7abf136aa5816df8465bfde842284aef5ef002e955cee05ae69d574ca", + "eval/datasets/coding_memory_v1/oracles/atlas-green--poisoning.py": "58b8ca4a37f55c4d112640414dc82620e32e643ab0aa539260884a5ecc50740f", + "eval/datasets/coding_memory_v1/oracles/atlas-green--scope_boundaries.py": "c1e92b10b40004c95abd3dfbada1a38fe0ce56300ecddca8360b51a1e6671458", + "eval/datasets/coding_memory_v1/oracles/atlas-green--temporal_history.py": "a64e4e5a29ce1040cf53e94571491325347f443d72a93f05bcf87c2878665952", + "eval/datasets/coding_memory_v1/oracles/atlas-green--unsupported_questions.py": "c8b415052ea7320e1df3c5475a676d4dd24602c45dbf1868fba6880846e029ee", + "eval/datasets/coding_memory_v1/oracles/atlas-north--code_relationships.py": "bb4305d15a81b4b4ef80b374acb59fee6fc32b213b101dd4b173ac4144d8570d", + "eval/datasets/coding_memory_v1/oracles/atlas-north--condition_values.py": "4bc5b1aff48ef6ad5ece1bd915f6949f72d365bb4815c4d82515b438bc644112", + "eval/datasets/coding_memory_v1/oracles/atlas-north--corrections.py": "7851502a9bc842f840bee1001e42140ee1df08be2730b563b1b197285d14742c", + "eval/datasets/coding_memory_v1/oracles/atlas-north--long_documents.py": "6204123eded20fc0c7aa59b312f8b5bd29aa88a720f7318e32b3c6a17651b57b", + "eval/datasets/coding_memory_v1/oracles/atlas-north--multilingual.py": "5c989b34fdad1e076b61ffb1f7a1371dc635f44a537abc743b765d5f6009004e", + "eval/datasets/coding_memory_v1/oracles/atlas-north--paraphrases.py": "fd189285d0cde9991dea461af9cd18b4203656a69da1298494fccf972ed8a58b", + "eval/datasets/coding_memory_v1/oracles/atlas-north--poisoning.py": "66305d69419736bdbb11a896543933604d898cd5394d94eedda8b92e632a09ac", + "eval/datasets/coding_memory_v1/oracles/atlas-north--scope_boundaries.py": "d9fbbe3924b5178f32f1937485af3b8db242a0e3a08c7bf41a3d2a57f34a8879", + "eval/datasets/coding_memory_v1/oracles/atlas-north--temporal_history.py": "7e7be0d6e558a71d3baad14a8812a6c998ad25e3f52c997a301333e0711f820e", + "eval/datasets/coding_memory_v1/oracles/atlas-north--unsupported_questions.py": "91f574dd678bf244a1fa6f9708e945259bb8ff8e2eb213426be5c3b187be3c3b", + "eval/datasets/coding_memory_v1/oracles/atlas-violet--code_relationships.py": "d0998e13db38d1fae9be6d254431b4fe592f89eb03ee25705f4c70985cd48c89", + "eval/datasets/coding_memory_v1/oracles/atlas-violet--condition_values.py": "f97610aee190dca03b01aa48ffe4aa3f75e099a59fdd867db98f743f4a376729", + "eval/datasets/coding_memory_v1/oracles/atlas-violet--corrections.py": "b7c31f7a9a1f978321ca5445997e0d284deea770c85f6d81a80204e548ad53ea", + "eval/datasets/coding_memory_v1/oracles/atlas-violet--long_documents.py": "20ec2b41cd3b6691164bce096dff16f7bf5be674f922cb637120c0347e60d0b6", + "eval/datasets/coding_memory_v1/oracles/atlas-violet--multilingual.py": "131e7dc978d3f2f7df395b6f1b4a9150051c0ca93c6dc5c386e58d46c8fdc573", + "eval/datasets/coding_memory_v1/oracles/atlas-violet--paraphrases.py": "1e7d9da36e6634ca2d2b11dc97468bd671df133345dcb76859e1dc3cb77dc664", + "eval/datasets/coding_memory_v1/oracles/atlas-violet--poisoning.py": "48460691ac0086c47102eec85387ac03bee9190ac83e3dafc4babc79b71d9bd7", + "eval/datasets/coding_memory_v1/oracles/atlas-violet--scope_boundaries.py": "b96207e9b1fd09f49218102a5b88fde5b9656604df4cfd314e86426e3b6d6075", + "eval/datasets/coding_memory_v1/oracles/atlas-violet--temporal_history.py": "7a196ff9a1db05b0c8f3a83086ae44cdc580dc58e4acc217d1db2575d9711b88", + "eval/datasets/coding_memory_v1/oracles/atlas-violet--unsupported_questions.py": "724564b5b75b9425b6889faae6d5e650bd5c940016c2b96054e5359d9d8b0887", + "eval/datasets/coding_memory_v1/oracles/atlas-west--code_relationships.py": "14295f735bcac4d4ae738eedfbd8fad4593afc914bc835233ec35e2263d43613", + "eval/datasets/coding_memory_v1/oracles/atlas-west--condition_values.py": "8ac70d9ca0c9122567bb9986d2cc5798104966df20e262b8087920e8fb5d82a2", + "eval/datasets/coding_memory_v1/oracles/atlas-west--corrections.py": "89d6a1a7781465c311830b7e769d2a76df41ab5c703ff662b3df1cc4c9a01668", + "eval/datasets/coding_memory_v1/oracles/atlas-west--long_documents.py": "38e67a26f69707089f852657a810ef1fca0395beb5511fff67c30f21ed63df57", + "eval/datasets/coding_memory_v1/oracles/atlas-west--multilingual.py": "bccf6579681d45940542ccb1f936ea1b225f4cf551a3ef31f3f8b1a026856e51", + "eval/datasets/coding_memory_v1/oracles/atlas-west--paraphrases.py": "89c8ce16c682553b27652e176b67a06f3228ae6209fbc8a7264260680f5651a9", + "eval/datasets/coding_memory_v1/oracles/atlas-west--poisoning.py": "fe9beb97d27eacdd87056fa1c9b04c62e2facae9354e765b912de60b35021621", + "eval/datasets/coding_memory_v1/oracles/atlas-west--scope_boundaries.py": "2eb3b1130c54644fcbade52da8f50cad1376a07b51856a962522913cbcbb7649", + "eval/datasets/coding_memory_v1/oracles/atlas-west--temporal_history.py": "116ee20af475da329cb535b0a02bd19ea1e7518b5b780ec12ba396ec896cc116", + "eval/datasets/coding_memory_v1/oracles/atlas-west--unsupported_questions.py": "080969e026215b9fe4ede6d0b4d02e9143e1293bea5d7383681e478426ddf121", + "eval/datasets/coding_memory_v1/oracles/borealis-green--code_relationships.py": "3965671e5e2432c8236c250f7982d4f478d8f1fe1268ffe57c555cf5fbbcd414", + "eval/datasets/coding_memory_v1/oracles/borealis-green--condition_values.py": "92994990327445258984ee6bf1d95f47359ff4b194e1f36e193980a523e6c5e0", + "eval/datasets/coding_memory_v1/oracles/borealis-green--corrections.py": "c5b473f962deacd6d59ea9967626e029888abbdb48f320704bcb6de876ce3f46", + "eval/datasets/coding_memory_v1/oracles/borealis-green--long_documents.py": "54aee2c1031c17b5678db2b0955eda8fafe5990aa9e52f02936584a9b75b9ab1", + "eval/datasets/coding_memory_v1/oracles/borealis-green--multilingual.py": "c458ac5c63e440e3d9486d096278759920d636f73e1901258679251afd86eb60", + "eval/datasets/coding_memory_v1/oracles/borealis-green--paraphrases.py": "250d027660f268cef1caa745cf3155de6e2f4adfaed02ba675eaa83224ffeee8", + "eval/datasets/coding_memory_v1/oracles/borealis-green--poisoning.py": "978f16eb5033c602ec09ed57ba0667edec9ac3e26accc1ce1456bc893ac9e6a7", + "eval/datasets/coding_memory_v1/oracles/borealis-green--scope_boundaries.py": "4b73a2e5dc5ac815c14e08a34acefd2ad426282cfae379ca2fd3060a3784c9b7", + "eval/datasets/coding_memory_v1/oracles/borealis-green--temporal_history.py": "6636d511bbcca51ca85c4eb0ec5ae956b3baaa5f9af89458ceb86329115743bd", + "eval/datasets/coding_memory_v1/oracles/borealis-green--unsupported_questions.py": "5cd4f70627bc60572c720a797bff122ef9bf219f32d265f694affa33b1f9f4fe", + "eval/datasets/coding_memory_v1/oracles/borealis-north--code_relationships.py": "769903aeac3ce360d845ddf1a6e89192737dbb460e1f2e251230b74d40763dd6", + "eval/datasets/coding_memory_v1/oracles/borealis-north--condition_values.py": "f6e69f525fd240a09152989b9d8a529e9abe076e7fc2c98d10bd84f9c6da2b56", + "eval/datasets/coding_memory_v1/oracles/borealis-north--corrections.py": "987ece1c7507a69a973c1e051b27c1d3b104ecda79df48959e49a62da31aa137", + "eval/datasets/coding_memory_v1/oracles/borealis-north--long_documents.py": "011134140cc486a3c089b407adbfbf5b958e1f8c1bffd572c29b8cd5d7f07f7a", + "eval/datasets/coding_memory_v1/oracles/borealis-north--multilingual.py": "1d29536a4a5836f64f56adcd1a965db85bc3371a12ad5bbb5644520c9d9606c8", + "eval/datasets/coding_memory_v1/oracles/borealis-north--paraphrases.py": "09a5fad90c24f75f2f448854c8778aab896c67854c97f884c2d689ead808583e", + "eval/datasets/coding_memory_v1/oracles/borealis-north--poisoning.py": "c1bec2ed9b31103e39db9dbab2be8db401cb0874bb3ce8573a0841a054e28a46", + "eval/datasets/coding_memory_v1/oracles/borealis-north--scope_boundaries.py": "faf51b368d7ba9406442edde29e290c931e67bff2799783dfd27f2226020e15b", + "eval/datasets/coding_memory_v1/oracles/borealis-north--temporal_history.py": "da52f43530149a4a293065674623fa49f83b37cd84b9871044b3bf32762fd8c1", + "eval/datasets/coding_memory_v1/oracles/borealis-north--unsupported_questions.py": "be0abc698692c9fe82f40f76a4694c472882a53843894dddd631ab1ab77aae45", + "eval/datasets/coding_memory_v1/oracles/borealis-violet--code_relationships.py": "9497b7d2373bb9f858e994f7919cff72e7e3c4c4eaa1efda287524b1a182d1f4", + "eval/datasets/coding_memory_v1/oracles/borealis-violet--condition_values.py": "aea13e4b7721019e7bc1f54878bb25b9f27a8a5044fe444e86ba5460deb13e09", + "eval/datasets/coding_memory_v1/oracles/borealis-violet--corrections.py": "bfd446d431afb6c05ad9d8775b90887947b85981037756e05262f3b101d41e7b", + "eval/datasets/coding_memory_v1/oracles/borealis-violet--long_documents.py": "eae829a374c0cd6237fac4ec52fec0d98d7340ec0b307b9b0b0c90c116d4b472", + "eval/datasets/coding_memory_v1/oracles/borealis-violet--multilingual.py": "e821f91999b5f12cff53fdaa956af4e0bb2f929497f95b4e2ceb57a6621e58f7", + "eval/datasets/coding_memory_v1/oracles/borealis-violet--paraphrases.py": "1a99c15898831a1c9aacab951868101d57e59e91ed90a2777015a26dbcc3043c", + "eval/datasets/coding_memory_v1/oracles/borealis-violet--poisoning.py": "411877fc07b053201a63aa4e45cb499344abccb0a562c236d53a672078b1eb63", + "eval/datasets/coding_memory_v1/oracles/borealis-violet--scope_boundaries.py": "b198dfb636910953ba372edf1cd6db382d4e5725bf7f820171be8c63f9fedf7d", + "eval/datasets/coding_memory_v1/oracles/borealis-violet--temporal_history.py": "3de5efd6ef661572191c6aa426fb10caf9f4bed4aa495b6298815aab3acdce22", + "eval/datasets/coding_memory_v1/oracles/borealis-violet--unsupported_questions.py": "baf5dd85e86a97dedb0b395b5c574ea5ffcdad7a46bbbe9787cfdc33d6c7a06c", + "eval/datasets/coding_memory_v1/oracles/borealis-west--code_relationships.py": "48e53b3a561f91fdf4eedb2d63aad65eece8ae80188bb3d5bdcaf184fb0b6945", + "eval/datasets/coding_memory_v1/oracles/borealis-west--condition_values.py": "2dd7a524bd2a2aa2fbf212246880d4e4ecf03f503b70f25179f3ab6bab28a07a", + "eval/datasets/coding_memory_v1/oracles/borealis-west--corrections.py": "7d3080e05544e10243040fe0a969a8c524e4262f8f5c38abdc3d8e06c67fdabd", + "eval/datasets/coding_memory_v1/oracles/borealis-west--long_documents.py": "fc42e935879e7a81f6eef9fe72f8683244501ae10654008a7863d397ee73058c", + "eval/datasets/coding_memory_v1/oracles/borealis-west--multilingual.py": "19181c442ce5f6941485190ecf050e455914dae988f86efa42665cdef0d835a2", + "eval/datasets/coding_memory_v1/oracles/borealis-west--paraphrases.py": "f7a5cb842aae3d2bc8d62ea40325b332a8bd4f013b76bac8bf154fedb3dbcbe1", + "eval/datasets/coding_memory_v1/oracles/borealis-west--poisoning.py": "285a2526a6be4e556a564899e9a376ae1286fef6f0ad22eb9289fba1905a2b68", + "eval/datasets/coding_memory_v1/oracles/borealis-west--scope_boundaries.py": "fb994823743ff5b4853b142c17779778bdb11296958e033e5c6be598833a4cf5", + "eval/datasets/coding_memory_v1/oracles/borealis-west--temporal_history.py": "f30a74c85ad654a7450e20d07baceb3b4fb2a56b454d7b43b854bef3cdb2cee6", + "eval/datasets/coding_memory_v1/oracles/borealis-west--unsupported_questions.py": "01cbbd0b21a745720514aea6a21a98a3cccdbf0c0518bd5508019433b964776b", + "eval/datasets/coding_memory_v1/oracles/cinder-green--code_relationships.py": "8b08e7c65289515a9eee2ae5f4cd60ce94350e9a561225bef0e8f8815ea28f79", + "eval/datasets/coding_memory_v1/oracles/cinder-green--condition_values.py": "92e7d75d38f5dbcc685075e947005a9ae5a410207e7f17aa770b034b8690cbe5", + "eval/datasets/coding_memory_v1/oracles/cinder-green--corrections.py": "424cbf0924a8cbc1a59a6d7bc82b5527e41f52c8ee9f558ea3e398ab4d913919", + "eval/datasets/coding_memory_v1/oracles/cinder-green--long_documents.py": "2a52df9cef3ee66357847322794a382c927b828c6ab1a2819a799ff55d53657b", + "eval/datasets/coding_memory_v1/oracles/cinder-green--multilingual.py": "e47e791dd15b045a6a958ca114b1359ca8537e160274b015214746a2564363c6", + "eval/datasets/coding_memory_v1/oracles/cinder-green--paraphrases.py": "9e92a79ec00231cb381ce58e389dfdb2520a17ecf1e9c2e39e79501449e9ddd0", + "eval/datasets/coding_memory_v1/oracles/cinder-green--poisoning.py": "33213e6f194332969da88f4748c8f26ef93219b6c4bc710a142082f8147ccfcc", + "eval/datasets/coding_memory_v1/oracles/cinder-green--scope_boundaries.py": "8d2489da1fba137981e45e42c843f9cbd17e851c82ddcdc5904b4a86af0d9692", + "eval/datasets/coding_memory_v1/oracles/cinder-green--temporal_history.py": "29403fd08f0de7da3dfcc6c181e1f448a5c5264e95ff3924f3066508d85468b1", + "eval/datasets/coding_memory_v1/oracles/cinder-green--unsupported_questions.py": "ba21014188f9ae849e0bb79babe163194703c4cf01f0600f4c9f71058e7fed56", + "eval/datasets/coding_memory_v1/oracles/cinder-north--code_relationships.py": "46280659557bd08afff185352438bb63dfd308dabf5c7c1862c2f442bd171d48", + "eval/datasets/coding_memory_v1/oracles/cinder-north--condition_values.py": "6c62d673b1b6cb4b5c89ff2af4ecf57c2c9d73074a01ab92b8fde57815cab783", + "eval/datasets/coding_memory_v1/oracles/cinder-north--corrections.py": "8245b196072389517fad6d76ed7709d7e12eb52e08732e4ddcfc40fc1d986879", + "eval/datasets/coding_memory_v1/oracles/cinder-north--long_documents.py": "6790c7b89d80ab909d9e1a31b59bfa5cd199cddabed2e159daf7c5c049ff9a43", + "eval/datasets/coding_memory_v1/oracles/cinder-north--multilingual.py": "9532bbd14506cac0edcc09f474e7c96a402903de947e0e76f5c8aab4bff37646", + "eval/datasets/coding_memory_v1/oracles/cinder-north--paraphrases.py": "cc904350ae7d40e26d91d482ac15d87ec8022d4c82905021dd38003c5fa2d067", + "eval/datasets/coding_memory_v1/oracles/cinder-north--poisoning.py": "e8e451c49c98744b4a484141f86d337db4ec5326264b1500893a20c3451c5ae4", + "eval/datasets/coding_memory_v1/oracles/cinder-north--scope_boundaries.py": "50d42fc48aa70d55b6a3b39c9bf17437726f37e1e5ae9870d415fccad92c7bae", + "eval/datasets/coding_memory_v1/oracles/cinder-north--temporal_history.py": "14b1336fbaed11cad455f588bbe078effc31f5f357f670e1f189ec3d78b172f5", + "eval/datasets/coding_memory_v1/oracles/cinder-north--unsupported_questions.py": "0a2f740700a990d22b4c3051aa3e4260e3df4ce65931d419052f74e05c750a67", + "eval/datasets/coding_memory_v1/oracles/cinder-violet--code_relationships.py": "0e29053f11fe618c01fa90a274688f18ba8a7f3cfea56757a96d5a5bc95b61a8", + "eval/datasets/coding_memory_v1/oracles/cinder-violet--condition_values.py": "ca57ac4e22e9b4739217020d7792ac642326935b8db9305ead2ec5bea2a6e456", + "eval/datasets/coding_memory_v1/oracles/cinder-violet--corrections.py": "9edbfaa58a99776e835c325e50a77df5b4b302b3ddd0932dc2a349a77b4100b5", + "eval/datasets/coding_memory_v1/oracles/cinder-violet--long_documents.py": "4b3fbe7380201285ac4b4a1ec4d8238c5663b97af01cbdc8ffff8fdf2a9467c5", + "eval/datasets/coding_memory_v1/oracles/cinder-violet--multilingual.py": "b0bba5ecf669f3368ef29373b755dc1296502665584574c771ebf4664debb5ce", + "eval/datasets/coding_memory_v1/oracles/cinder-violet--paraphrases.py": "e146f7aa1ffabf3b313251b59c59e1836c0cd17901d07fc6ef5334eaccf8bf36", + "eval/datasets/coding_memory_v1/oracles/cinder-violet--poisoning.py": "692ee07e750fb08652a1ca6bc6dcbbef77219701585f1336c6037dae5a7c5767", + "eval/datasets/coding_memory_v1/oracles/cinder-violet--scope_boundaries.py": "8ca0b911d38a72e4ecce6871ba3d0c7dbab433b0541cb64b116f7a88d521554c", + "eval/datasets/coding_memory_v1/oracles/cinder-violet--temporal_history.py": "6ba5400c5435af64c80a7366fdf49fea132f1011f9d84f9d0cc5f2f2f30a67ac", + "eval/datasets/coding_memory_v1/oracles/cinder-violet--unsupported_questions.py": "1d3324db0a085be30ce3dff1a2ec3866b8592a5ef6b43f39211a2e3a59fc1cd6", + "eval/datasets/coding_memory_v1/oracles/cinder-west--code_relationships.py": "0149333a3fb0f8d60939bf927921305eae447f7511c74f042ff3c880eadff825", + "eval/datasets/coding_memory_v1/oracles/cinder-west--condition_values.py": "3158243f9f7f0b92aaee4164379159b46143acd11fc3d41a708630c1d33ea22c", + "eval/datasets/coding_memory_v1/oracles/cinder-west--corrections.py": "a9fbb89aa629a23e7a1fddf4fca09da40365bbc7a9a6192d805e498fe17f93ff", + "eval/datasets/coding_memory_v1/oracles/cinder-west--long_documents.py": "3c702c653a8ddff84eb5c24a77567aa63d2069c48ed174bcf29065c0ddb30ec6", + "eval/datasets/coding_memory_v1/oracles/cinder-west--multilingual.py": "b8f5f7e00e6edac3f8e0b7e8bc09ea6c7d661370fa2226eb1795306ce3c634a7", + "eval/datasets/coding_memory_v1/oracles/cinder-west--paraphrases.py": "10361be87e9d9cb98d2ff1bbc18252178d475a5c5f2efb77bc81dea05109df05", + "eval/datasets/coding_memory_v1/oracles/cinder-west--poisoning.py": "889a5a43604f7d1677ee0f28683abfc48a8a597b052f558c1b4d19c5f33444ec", + "eval/datasets/coding_memory_v1/oracles/cinder-west--scope_boundaries.py": "b84e78812baaa92422226c06d78cfe7c1be260e2bfcf9c5bb1ed73f2f3cc136b", + "eval/datasets/coding_memory_v1/oracles/cinder-west--temporal_history.py": "9d99a981fca0c801a0a52e2af24cd4262dad72f4f34cd84894784285d241bd05", + "eval/datasets/coding_memory_v1/oracles/cinder-west--unsupported_questions.py": "43b8f854961d9cca5cb83b53b9e07d22388d0c7db406dc8e4f8cfdaa5f89fffd", + "eval/datasets/coding_memory_v1/oracles/delta-green--code_relationships.py": "97a08f2bf9b1a11f95a062f7df27d69db3e9dbc421837347acfe62111af02885", + "eval/datasets/coding_memory_v1/oracles/delta-green--condition_values.py": "5e53695288b84a526c61b748b170f6d6219f94a942edc0887ea6157535ad8d52", + "eval/datasets/coding_memory_v1/oracles/delta-green--corrections.py": "c6d16232ebe6bdb0b5c94db1ba4f108257b47ee8f8b96cda5f8e17212022b542", + "eval/datasets/coding_memory_v1/oracles/delta-green--long_documents.py": "bedb6f495437beada344ce6cdd92954108aa4c639622fd1cc294d2a1139b26d9", + "eval/datasets/coding_memory_v1/oracles/delta-green--multilingual.py": "dbf84f4e8c016c5e60fdac5fd3ac2faf79fb0534f5c2ac534491acc31d5f3530", + "eval/datasets/coding_memory_v1/oracles/delta-green--paraphrases.py": "ea8f00c86e8fbc2ab10a448f48019146181d0e55438def6f47f41f0ef3181be4", + "eval/datasets/coding_memory_v1/oracles/delta-green--poisoning.py": "4eb4e289e944e7762c39ce85a8fc2582e310de6a86cbc1cc36b44112653140ea", + "eval/datasets/coding_memory_v1/oracles/delta-green--scope_boundaries.py": "2fe9b96deecfb488430abd9657adc6b3ea092b18f32c41c1669d29175ec6e860", + "eval/datasets/coding_memory_v1/oracles/delta-green--temporal_history.py": "531cbb953400584356cb6525bc1a2e559f7ec3867d845c736d8026a046b91e7d", + "eval/datasets/coding_memory_v1/oracles/delta-green--unsupported_questions.py": "edebf25c2168411fdefae1307a71d829224dba3dbc7ea000054416e52123d5a3", + "eval/datasets/coding_memory_v1/oracles/delta-north--code_relationships.py": "9357ad0d5f6a1cc787f980d0d506b07808538179f71ae3e8ce1c3eb655d24e35", + "eval/datasets/coding_memory_v1/oracles/delta-north--condition_values.py": "4199f7409696a260b1531f4ec7cfd15555b4d58d06b0b8a88b1c7621fe7d328b", + "eval/datasets/coding_memory_v1/oracles/delta-north--corrections.py": "b8bd58db7c5ae93d33e6223cc34f0e09d58831361ac013ec08277fe2c102ac17", + "eval/datasets/coding_memory_v1/oracles/delta-north--long_documents.py": "0f08b744446b1c52e8fe86626ab05c171377f17c46e604cb2ea647ed5d0daddd", + "eval/datasets/coding_memory_v1/oracles/delta-north--multilingual.py": "ada9eb404190d05637cea59e3f8ed541aea2c9e0f9be0d166885d6789351f7a3", + "eval/datasets/coding_memory_v1/oracles/delta-north--paraphrases.py": "04ec2e8308f47f46e38c89f4a2fe44deef4a0b20a1c8517395ad00983bf7361c", + "eval/datasets/coding_memory_v1/oracles/delta-north--poisoning.py": "d72ea7b98db47e4de03884e2786206dc3da5cc08e5203c86b1d38a2d48c2fd47", + "eval/datasets/coding_memory_v1/oracles/delta-north--scope_boundaries.py": "cedb3234b22a40a7b889f50cfb5751fbba31e46f541b19feb2c4759c8007bac3", + "eval/datasets/coding_memory_v1/oracles/delta-north--temporal_history.py": "2f7c1bd4ebc051eee411d11251aff2ee5fee602a0800410bc0dbd2e86e99628b", + "eval/datasets/coding_memory_v1/oracles/delta-north--unsupported_questions.py": "f1f0b9858c806956122144c6e769f3baf9f87f07ff92eec98eb58d8019e6fba2", + "eval/datasets/coding_memory_v1/oracles/delta-violet--code_relationships.py": "79579639a60d96365781e853d1319a2bf85d9a2b4d8639410da7e3c1efc364b2", + "eval/datasets/coding_memory_v1/oracles/delta-violet--condition_values.py": "27defbfe7c237f971ae29e8f000f098eae32a11f3afad06f2bf3b27af432d902", + "eval/datasets/coding_memory_v1/oracles/delta-violet--corrections.py": "b3b685a5c33d1086e8ab4258d26c5ec9aa7396b8250a250351f0a6057fcf2075", + "eval/datasets/coding_memory_v1/oracles/delta-violet--long_documents.py": "4a5ce75372c8762bcd7986f8ceef454800c3636124ffd9de3a8315d210ae85f1", + "eval/datasets/coding_memory_v1/oracles/delta-violet--multilingual.py": "7ef19f25e97a24522b79a9d7e7f3d2d9e395a1fc2c60564e2f66081533bdacb5", + "eval/datasets/coding_memory_v1/oracles/delta-violet--paraphrases.py": "b6e00019bead0a5bc0facdac886b48fccfd3cccaa254267fda780e933d57904f", + "eval/datasets/coding_memory_v1/oracles/delta-violet--poisoning.py": "3e3e1e1fe04ae3e0c615ca21e794fead5d06f887d6fe7347fb9122684c8b8017", + "eval/datasets/coding_memory_v1/oracles/delta-violet--scope_boundaries.py": "336ae66910d533344712a2d87126f53df48d43eae9e7d17797938c3f9c560204", + "eval/datasets/coding_memory_v1/oracles/delta-violet--temporal_history.py": "a81cb5d2a5eb521dfd161ffbd21bd366777b1239f2d3412acc77b0a9c7c7d82b", + "eval/datasets/coding_memory_v1/oracles/delta-violet--unsupported_questions.py": "3341895608073374b98e67a88489abb9020fed5bb692231a0a61e15c8de0ddf1", + "eval/datasets/coding_memory_v1/oracles/delta-west--code_relationships.py": "e8f17f3838cb74e1ade0cfd6edf540dc086507b51ca67c2020ed490d75dbffe5", + "eval/datasets/coding_memory_v1/oracles/delta-west--condition_values.py": "a935a6e7a4081737eef76a93d54b34dc885b4699e02d797f1be78286dd642a2b", + "eval/datasets/coding_memory_v1/oracles/delta-west--corrections.py": "ee407e5aa8d57254bb7029d537bb2b59a297958cdff9bf271de3787b6448241e", + "eval/datasets/coding_memory_v1/oracles/delta-west--long_documents.py": "55fa08b75982d0547f0c2f13cdc6522d0fbd33efb4af9ae8c6ada11b2d46f321", + "eval/datasets/coding_memory_v1/oracles/delta-west--multilingual.py": "ce8a3e45ad8417ca1798579a22e12eeb7d170a11ebfb865746388eb0e7dca848", + "eval/datasets/coding_memory_v1/oracles/delta-west--paraphrases.py": "e6458588e20e76596e24be6cc838bd4b0d5a47b05b7bf0bb6017b0cecd0d6a68", + "eval/datasets/coding_memory_v1/oracles/delta-west--poisoning.py": "4c70955a2650d13eba24d6e933f765e0bd40da0a1cd8b920416989a76bdaf2d7", + "eval/datasets/coding_memory_v1/oracles/delta-west--scope_boundaries.py": "cfe9120c49224b6f9ed86ea7d85e496000bef0648c92c7fd474c540e892863cd", + "eval/datasets/coding_memory_v1/oracles/delta-west--temporal_history.py": "3527edc8a85c71f889ff0b2ac8bf9ab005ad5bb6c95bcd917dedae44aa3efc72", + "eval/datasets/coding_memory_v1/oracles/delta-west--unsupported_questions.py": "8c40923b8d4bd6bfc78ff6548ef9a840319b7ee4ea967554f26a8d24a4349935", + "eval/datasets/coding_memory_v1/oracles/ember-green--code_relationships.py": "cc7f82ad85363ee35ccd75744159bf4c4f07965e396bff07989cca7a55071679", + "eval/datasets/coding_memory_v1/oracles/ember-green--condition_values.py": "3fa888c125812f1488de9e021358cd531a9d50bb7d4b6173be3656268474b635", + "eval/datasets/coding_memory_v1/oracles/ember-green--corrections.py": "7cc639d4bd7b287ed0e3f2188491d6e4866e021b21874b34b365c1eb2b2d6d68", + "eval/datasets/coding_memory_v1/oracles/ember-green--long_documents.py": "972e7879a30ab26f86f09bce6dc9853c7f96124d0bbe5062111607496bcc9a95", + "eval/datasets/coding_memory_v1/oracles/ember-green--multilingual.py": "0f0585bd3264de3d5fed5928b54ff4fb87f71b2edb1d35c83a534251121124b4", + "eval/datasets/coding_memory_v1/oracles/ember-green--paraphrases.py": "a3c15541bb87cc862d1c9764cfdc1ffebbc831876b55167470ecb3b66519740a", + "eval/datasets/coding_memory_v1/oracles/ember-green--poisoning.py": "ace5b7b12c4ca068739af4bc5e215af1af7fe153399ff17ffb7a67b8b9aa1f94", + "eval/datasets/coding_memory_v1/oracles/ember-green--scope_boundaries.py": "daf132e130c5743d67129c1564f329fbb2670d1242648920b646a83bc2236e4b", + "eval/datasets/coding_memory_v1/oracles/ember-green--temporal_history.py": "1438f09f0ae41b1f6b42930518203a36c7de31a870b684dbb7f16e71a37c784b", + "eval/datasets/coding_memory_v1/oracles/ember-green--unsupported_questions.py": "da27396afbc1529c61972f739cefb26dcaf97bea529c084518a95080eac4fe78", + "eval/datasets/coding_memory_v1/oracles/ember-north--code_relationships.py": "debaf885151c3f47660053ff50e015338b8aa9bca089d7e901ee8bc34f7085e6", + "eval/datasets/coding_memory_v1/oracles/ember-north--condition_values.py": "109a7ee90c04da83f57a94a9a724dcf98a3e96405c7a456686c198ff5d85a69c", + "eval/datasets/coding_memory_v1/oracles/ember-north--corrections.py": "c246ba011c68d3ad0a4ee1d14231304a7f17f28027f1764e60da93062cf1cd3c", + "eval/datasets/coding_memory_v1/oracles/ember-north--long_documents.py": "176200cf2a011c7231ed4cadacbe86b1eeb695fbbee282bf4c4a96430d98fe84", + "eval/datasets/coding_memory_v1/oracles/ember-north--multilingual.py": "e850a910515e71a27edff26f7bedf39d8cb75c726e48357a2c41d4da051be6e9", + "eval/datasets/coding_memory_v1/oracles/ember-north--paraphrases.py": "0a6f03310e44abcf4ca61d69e22edbd21ce47a17629adf4f5079f7565534b469", + "eval/datasets/coding_memory_v1/oracles/ember-north--poisoning.py": "a81ee758fc57bcc58bf0bf2a38dae8b6eb4aeb14949063206e32cdff6f301dfb", + "eval/datasets/coding_memory_v1/oracles/ember-north--scope_boundaries.py": "fe42df24612c66b175f1c98794ed9f13a1a7819418e4aeb5ff98531515221de1", + "eval/datasets/coding_memory_v1/oracles/ember-north--temporal_history.py": "410f9770097228724a5af1357003ea23caefe1226275d030d46efa535c42d306", + "eval/datasets/coding_memory_v1/oracles/ember-north--unsupported_questions.py": "eebe43443938693a74081661d2ff3733b39c6dac5c7ecd88c4dbab5ad18689ee", + "eval/datasets/coding_memory_v1/oracles/ember-violet--code_relationships.py": "c00a30669fbd1c740b44e17c7c7369582c28a16da3aeec9d27420b8aef7c553a", + "eval/datasets/coding_memory_v1/oracles/ember-violet--condition_values.py": "1645e7c56c870f4367c263ccc9041181b1d187a698252203ca651235db59e684", + "eval/datasets/coding_memory_v1/oracles/ember-violet--corrections.py": "735f1d125d2ade6f3eabbf1082a408fe6afd4328cd954833e23df275df498e37", + "eval/datasets/coding_memory_v1/oracles/ember-violet--long_documents.py": "e1ded8e9641639443a6b14da18b2e0cbcddc6fc6045bf0b57404b59b59327dce", + "eval/datasets/coding_memory_v1/oracles/ember-violet--multilingual.py": "fbf04cda8e1231c7ac4c64d4c1856f7bfafcb6f1d016de0b1bdbbaaafcfe03b6", + "eval/datasets/coding_memory_v1/oracles/ember-violet--paraphrases.py": "2fd495def306778073df30b20dbc7a1500c06c90638acf6d9b9280db78dec53a", + "eval/datasets/coding_memory_v1/oracles/ember-violet--poisoning.py": "335674216a418254a01a7f5505344af20ad499a5fe41cb9c8654a0705574a18e", + "eval/datasets/coding_memory_v1/oracles/ember-violet--scope_boundaries.py": "078d60287888903e90343d144d8f54e08a3751ffcba82fd827ca0c9f8ee75fe7", + "eval/datasets/coding_memory_v1/oracles/ember-violet--temporal_history.py": "f7ad47f3253fe922977cbd0b63679abf00559f1b2dc895246e09840652b2c41b", + "eval/datasets/coding_memory_v1/oracles/ember-violet--unsupported_questions.py": "b91545082a31952351bc4c08d6cc09132432126dfd772952c818396bd7a68af1", + "eval/datasets/coding_memory_v1/oracles/ember-west--code_relationships.py": "74fb3e0d99a5bd4f54b8b57f6040ce7978a81bffa1ae70f9069fb2322e6ea581", + "eval/datasets/coding_memory_v1/oracles/ember-west--condition_values.py": "26d605b42d18cf029dfdc29124eea898e0dfa5642c43c1c775007fb443ba6cda", + "eval/datasets/coding_memory_v1/oracles/ember-west--corrections.py": "65c2ef8544b42ecb6b3ea9ce8bc95ec5f0ea8bb07c2b74d52e7880d99962f563", + "eval/datasets/coding_memory_v1/oracles/ember-west--long_documents.py": "4c8e54c7ec930d6cc782fa1a0519a0f1e0e916e3929902e13199775cefdaf338", + "eval/datasets/coding_memory_v1/oracles/ember-west--multilingual.py": "356e407f47d39e7f53337ffc4f27c68935a2c831301dc3dd006b2939cc61decd", + "eval/datasets/coding_memory_v1/oracles/ember-west--paraphrases.py": "c91cf574cf8b5aadaa6c9331c3f8f4a8fec7ec33c227f20894f463d81d110a7d", + "eval/datasets/coding_memory_v1/oracles/ember-west--poisoning.py": "5f6b8740798a7a093994748ef4b11e0e1de0be562c7f09790e1f0ebb9ed21f7d", + "eval/datasets/coding_memory_v1/oracles/ember-west--scope_boundaries.py": "5e20c5154818cc29b7e71444054d67836eddc882f2f17be8aed78afe31a9037f", + "eval/datasets/coding_memory_v1/oracles/ember-west--temporal_history.py": "fc51d1a39693d1f7fb2db4bf1ffe865d4c44848c159188cdcfda54618c0d8fe2", + "eval/datasets/coding_memory_v1/oracles/ember-west--unsupported_questions.py": "079c421cae974bcccc2107dac74421dd5bc2d1b40f7d91cb79a8014af526d260", + "eval/datasets/coding_memory_v1/oracles/fjord-green--code_relationships.py": "b5e4a284bad1d7ff81d5be9cf1faaaaf5e718cb51624189482624721cb459f1b", + "eval/datasets/coding_memory_v1/oracles/fjord-green--condition_values.py": "63c152b8da5655ae5c701cbb51099340d5f5103fcc47840239098a7366d24151", + "eval/datasets/coding_memory_v1/oracles/fjord-green--corrections.py": "b7b33b8de153c205ff4fdaadd5538eae8a22fd4401e70db9f645dae205c62cba", + "eval/datasets/coding_memory_v1/oracles/fjord-green--long_documents.py": "624cf014bc795a24e2f1f4fa2f9dbf11d2ad1c38f81cbc58ab3948f96c48c8c4", + "eval/datasets/coding_memory_v1/oracles/fjord-green--multilingual.py": "f8d2483fd9266946c980764feba0eda27c95ea2ff11222a4dd4247fb537f1f00", + "eval/datasets/coding_memory_v1/oracles/fjord-green--paraphrases.py": "151be253b730d5c5b4bbcabcce3bd218477241ebf0f1bd5fe9dc8d70798feb6f", + "eval/datasets/coding_memory_v1/oracles/fjord-green--poisoning.py": "e7b0efca3813bbb1d4ea9cefa752b4318942b118bb61ee9dcbbf640daa66644d", + "eval/datasets/coding_memory_v1/oracles/fjord-green--scope_boundaries.py": "56177cfb45f32c548be83cc78197fcf5b53826f01cca69e471535476184ae8f9", + "eval/datasets/coding_memory_v1/oracles/fjord-green--temporal_history.py": "2c79d26467eb8443d743bb8823cc1f2a5d1e5da7c887754505baa130e37f1b2e", + "eval/datasets/coding_memory_v1/oracles/fjord-green--unsupported_questions.py": "fb5f222f4e0715379591f559036c0549f67a7b6e3491e880c6cf8d5b91fa9000", + "eval/datasets/coding_memory_v1/oracles/fjord-north--code_relationships.py": "07402078200db9ab15fc61a72e31d9df0938edfb1e9d5b7e532b422dccab15aa", + "eval/datasets/coding_memory_v1/oracles/fjord-north--condition_values.py": "8c1549b5a444c9cbf1304fcca13b79263bf8d9f0b4f41c16da539b3bf5da3bf4", + "eval/datasets/coding_memory_v1/oracles/fjord-north--corrections.py": "a2634af73134c025de1b6239756ee2051e505e0a535221947babef5ba161fa13", + "eval/datasets/coding_memory_v1/oracles/fjord-north--long_documents.py": "4f80f3673afd1320d16a6bd45fefc85522d6f4bb889ff96f6e99410580feec09", + "eval/datasets/coding_memory_v1/oracles/fjord-north--multilingual.py": "3a9c953fbe76eb7374992a6a81d35b8dc0a635c02b90f328af140bbda2acc180", + "eval/datasets/coding_memory_v1/oracles/fjord-north--paraphrases.py": "ec2d9d7bbd678be73946cba8177184839505cd891df410ae8e676fc254df2ca5", + "eval/datasets/coding_memory_v1/oracles/fjord-north--poisoning.py": "6d9b91b4d53925d58d3c0cea694f591572a53184f23816101af0aa644aa1b86d", + "eval/datasets/coding_memory_v1/oracles/fjord-north--scope_boundaries.py": "2e98afa064e8a295a78bea654014a92734696a235fb0538732bb8d85799c7287", + "eval/datasets/coding_memory_v1/oracles/fjord-north--temporal_history.py": "4059ab8e427c2b6fd68155992404fbf493c7ebdf0a4357aa5c5f4ff0c67808f9", + "eval/datasets/coding_memory_v1/oracles/fjord-north--unsupported_questions.py": "79f14e18e282c1b6bbe21ef2affc9dd8a259a171c2e4975d42363f7c91689c1e", + "eval/datasets/coding_memory_v1/oracles/fjord-violet--code_relationships.py": "9ea8a9ade1001bcd43929782c3410f35fc5056e438342ca848fe0b1b0ff56170", + "eval/datasets/coding_memory_v1/oracles/fjord-violet--condition_values.py": "09e223a1c0bff6c95a5863fdc28dc00ed96b90887cd635d014712058079ccbfe", + "eval/datasets/coding_memory_v1/oracles/fjord-violet--corrections.py": "f822c373fc4a93189fdce09999a9dcbe8797b0c0dc780eddd4e342edf21dc14e", + "eval/datasets/coding_memory_v1/oracles/fjord-violet--long_documents.py": "983aeaae1e14937a740218228778be3e1938cdf1ff9228eba44064336188f297", + "eval/datasets/coding_memory_v1/oracles/fjord-violet--multilingual.py": "6e929f8846cfc756c71bcc21bc0f27ef8fc23270d21292fc35d70e6bbd31c066", + "eval/datasets/coding_memory_v1/oracles/fjord-violet--paraphrases.py": "c644d9f01edbd213447618bc269f56804450901faf4f4660c1f666d4e9b4ada4", + "eval/datasets/coding_memory_v1/oracles/fjord-violet--poisoning.py": "cf3849312f96b0072efdfd4e858abb24d7e6a808bf883bcc9dde5dd5a4594364", + "eval/datasets/coding_memory_v1/oracles/fjord-violet--scope_boundaries.py": "373d10e20713bdc26849706525f765f2be4ab57e87e576e39a1a1dd4e7feed88", + "eval/datasets/coding_memory_v1/oracles/fjord-violet--temporal_history.py": "4d2c36480cc04d43aa8f535bf998968b474f2f596d5349dcd42684c5a667c6a4", + "eval/datasets/coding_memory_v1/oracles/fjord-violet--unsupported_questions.py": "b381d2016dea525eebd8de8d026dc48ad2080b93823a95d6e2bb06d29cc86775", + "eval/datasets/coding_memory_v1/oracles/fjord-west--code_relationships.py": "e1e10ed9ee8a4d5c2e798c2bf04cba5c39eaa3873901bd93724af166ba2479ae", + "eval/datasets/coding_memory_v1/oracles/fjord-west--condition_values.py": "427764a62d2e11f6c9fe4b2cdcd32fa92f61749403dff648601a47187e38787a", + "eval/datasets/coding_memory_v1/oracles/fjord-west--corrections.py": "4aa041aa87f4c8628d113bab855a151c2e38bd6919cd02224827fd07d060fb34", + "eval/datasets/coding_memory_v1/oracles/fjord-west--long_documents.py": "89875548e6952ce810d8983b0eaf2724bf8f329bf44e4d9193cfa3405e1ce80e", + "eval/datasets/coding_memory_v1/oracles/fjord-west--multilingual.py": "8b5c14cdb26941d1ae25701bc99efc5fe4ae6e50fed3c65b193952444f1593d1", + "eval/datasets/coding_memory_v1/oracles/fjord-west--paraphrases.py": "bcdc9a5f1fe45ef63fd5e6b728c83c7a8d05c7b80957e3276c074700b409e924", + "eval/datasets/coding_memory_v1/oracles/fjord-west--poisoning.py": "5504e1bab0865fb3041c88f151938789d79422eff91a3ce72f4a41fc14e2f897", + "eval/datasets/coding_memory_v1/oracles/fjord-west--scope_boundaries.py": "63f226631a663dca0ef62a4f32bb98fbe1e30e3b1f030ee42c3f7f2bf6a09af0", + "eval/datasets/coding_memory_v1/oracles/fjord-west--temporal_history.py": "75c9582a16db2c89f12506bd7a4115fcd871c6da63e80429a8e70e31c1f5f862", + "eval/datasets/coding_memory_v1/oracles/fjord-west--unsupported_questions.py": "05e4ba65be7cb4e6500af5e14721c70f93fd7390abe58a60f6f8e4f8e9b0ae5c", + "eval/datasets/coding_memory_v1/oracles/grove-green--code_relationships.py": "bd5619518e839f84ca03297c4f7842a2944d8b445ec1863a8dfc68e4c2114a03", + "eval/datasets/coding_memory_v1/oracles/grove-green--condition_values.py": "67377847b6ff1ab865f5c25351fcfa1ee13e6ec6a089444cb3eb0709b660148b", + "eval/datasets/coding_memory_v1/oracles/grove-green--corrections.py": "c07e8a03c6c47818f9c574d4f21b27668989711fb5e0af3b5cf1a87b293abcbd", + "eval/datasets/coding_memory_v1/oracles/grove-green--long_documents.py": "3f75fc2158c66a19dbbd19209fc80026339abe93a2282ff55c7c01429ac549f3", + "eval/datasets/coding_memory_v1/oracles/grove-green--multilingual.py": "19b1f800b38a11cf0f50df6da3a011806098561e32a279b78e1806de9b8c11fb", + "eval/datasets/coding_memory_v1/oracles/grove-green--paraphrases.py": "a5996376b9f589283dbe301dd1e0644af92a8b6a0222b51b99ed6fbb83570782", + "eval/datasets/coding_memory_v1/oracles/grove-green--poisoning.py": "c4dee0969e39b52dc9b697e45587c8cd84926a97c9a48f7211c0a00571bfb657", + "eval/datasets/coding_memory_v1/oracles/grove-green--scope_boundaries.py": "8d45422f0a1a2115deb90f560409fdad1105b7cae1f03052f8ebac404ba9517c", + "eval/datasets/coding_memory_v1/oracles/grove-green--temporal_history.py": "abc1b386173c8252ac10f11a36fbcdd6d91c731686108a6d32c62d17955d21b9", + "eval/datasets/coding_memory_v1/oracles/grove-green--unsupported_questions.py": "1ee09741a1c94a1fa83c7edf5b6c07d8da2e45826f748d899ec71917e8369d54", + "eval/datasets/coding_memory_v1/oracles/grove-north--code_relationships.py": "fd75d734a5f45a5ec54b125c1d379df67c5531659f212a2f5c1c5e88169167e0", + "eval/datasets/coding_memory_v1/oracles/grove-north--condition_values.py": "c600c0e5f50e352406b99527b050f4df6dc8ab8920b5b946de190ad7629b5e6d", + "eval/datasets/coding_memory_v1/oracles/grove-north--corrections.py": "4d6fdbe4f5eb9c8e4bfc2a974e6c3cbf1d124d0b20d3f050c84e766f507b4be7", + "eval/datasets/coding_memory_v1/oracles/grove-north--long_documents.py": "5b0bbb9f0a90eb377cbfe45ff6f37077a9ce32c7c6730720f0bfd49b844fdf4b", + "eval/datasets/coding_memory_v1/oracles/grove-north--multilingual.py": "525ea2fdad66d66d7b90892e9424ebae281c1dabfb49f2668784a687b7ec894e", + "eval/datasets/coding_memory_v1/oracles/grove-north--paraphrases.py": "1105e52a8c8d3ff79c64b59ec5f23d66d742b8dcecf91e8e62060895c7371f4a", + "eval/datasets/coding_memory_v1/oracles/grove-north--poisoning.py": "7d32e533f6dac57e5d651868386686d2b78e7db1cc29b1860fede23fc9c49813", + "eval/datasets/coding_memory_v1/oracles/grove-north--scope_boundaries.py": "7bea4129589ea34ba10667a7035b126a32b423ec7f3b1ff2b10f6ffc9aa1c2ee", + "eval/datasets/coding_memory_v1/oracles/grove-north--temporal_history.py": "859b6504250f5746ce197e4a0a7c1006f1c2257b939c53a493f9d36f6d2b7d6a", + "eval/datasets/coding_memory_v1/oracles/grove-north--unsupported_questions.py": "6b893c4c124f071e9ad5d9946448b070c2b2f5f10da6f058e60840c18510fd0f", + "eval/datasets/coding_memory_v1/oracles/grove-violet--code_relationships.py": "09bb7df6b115b83cb98a4274b2d883e81b809643ed4f912b383a9e09a7f7a1f9", + "eval/datasets/coding_memory_v1/oracles/grove-violet--condition_values.py": "a68c126f0d7b2fc0950baa5df095d7d513cdb8359883404bad3c1c84c8de666d", + "eval/datasets/coding_memory_v1/oracles/grove-violet--corrections.py": "46024b5fb187aaf3574d380af1a3bb2aa44028f89377f810ca5e98ee03c3b9aa", + "eval/datasets/coding_memory_v1/oracles/grove-violet--long_documents.py": "ec8d401f6437fbaa3b11ddb96973b31944ff374da677f7aae26c0e79e6a1315b", + "eval/datasets/coding_memory_v1/oracles/grove-violet--multilingual.py": "d14e8bf66593ceab7b691119b8a82f211933e2b3f6ebf8a25a75e61ffedbc1c0", + "eval/datasets/coding_memory_v1/oracles/grove-violet--paraphrases.py": "963b7321a32931f238d12e7cdc3849f64682c7cb704b77f4b849543cc40b6df0", + "eval/datasets/coding_memory_v1/oracles/grove-violet--poisoning.py": "39e5edc76a122d37d90b1af28c2293e2f8b0c38e1bdb6460340c0bc2a71ad87d", + "eval/datasets/coding_memory_v1/oracles/grove-violet--scope_boundaries.py": "472298607415aedfb9a36bad7783e7b7e3bccc339ee44cd83746d782ca99e411", + "eval/datasets/coding_memory_v1/oracles/grove-violet--temporal_history.py": "dcf793210a0a547c25587bba70e01d4d56576604856ed084eb5fa466b8be7b83", + "eval/datasets/coding_memory_v1/oracles/grove-violet--unsupported_questions.py": "bbe1769dc0afbe220e8069ef21b85164c31ecff6c3c7f5bf6d18d1e48f5f3c72", + "eval/datasets/coding_memory_v1/oracles/grove-west--code_relationships.py": "5374fb66cd91be79b5eabd2696ada5fee89605d8a1fc84ce36bd6a8ca82cbe5e", + "eval/datasets/coding_memory_v1/oracles/grove-west--condition_values.py": "2e807846b760b65afe0f5e48947463a66c15b8d89899a2724cddd865311760b0", + "eval/datasets/coding_memory_v1/oracles/grove-west--corrections.py": "bca219aae5f85e3f5504d5b39646efe89ff7dc6c007a4323abd5c5154cb4d5a9", + "eval/datasets/coding_memory_v1/oracles/grove-west--long_documents.py": "19c3ca44f78ef79ca1b907c80214e11a8195e994893f1de683b5d9796eb29d6b", + "eval/datasets/coding_memory_v1/oracles/grove-west--multilingual.py": "7842efb41f88fcd49e7a2350e9b44a3a1946bf73e5cb103e4a91a11e810301a2", + "eval/datasets/coding_memory_v1/oracles/grove-west--paraphrases.py": "fd70e8a20da665073ad172219bd57b24e53575dccc344adc1fa0190770ffc6cd", + "eval/datasets/coding_memory_v1/oracles/grove-west--poisoning.py": "473acfa06852554dee20445dd1a86df4e626e0363a7b72de57d68819314bf938", + "eval/datasets/coding_memory_v1/oracles/grove-west--scope_boundaries.py": "3ba0e93abf07498c570cd81b0c4f753bb75b6d4f4bf6c786461bad1a3b301286", + "eval/datasets/coding_memory_v1/oracles/grove-west--temporal_history.py": "334bc7b057f570efd20896d282e54c986f5b6c642930057c8520f84b721ed5e9", + "eval/datasets/coding_memory_v1/oracles/grove-west--unsupported_questions.py": "5102eb2dadcce0dcc0ba157495d4fa10c4983e4bf6627aff25bbd547a08115ca", + "eval/datasets/coding_memory_v1/oracles/helios-green--code_relationships.py": "d376cda89c44b3282a86afe7cd01583f0b53f2c80cd92b47bc2fa4a19271e9ee", + "eval/datasets/coding_memory_v1/oracles/helios-green--condition_values.py": "7b6eaf2f46a5b2f1ae6e8e6da0bcfe6044fea5639df158e137fb8ba32b929909", + "eval/datasets/coding_memory_v1/oracles/helios-green--corrections.py": "2e849f6c926aa0ce1b2e69ce8cdc5ba9c50feb89b33b257fc71bf3aa3b8461b8", + "eval/datasets/coding_memory_v1/oracles/helios-green--long_documents.py": "884c17b7d20d986708c0eff2821cc7b76c5f7b16afd26c43ca80fb507e2f3ea7", + "eval/datasets/coding_memory_v1/oracles/helios-green--multilingual.py": "1182048aef0dcfbc416a004bc43f49f1d2a5de4132e7243ab7d147f54aab1acc", + "eval/datasets/coding_memory_v1/oracles/helios-green--paraphrases.py": "a73950ae7fceacfaa6c7c7a26c8379f9164a29ebea883cede9222e72c506db5e", + "eval/datasets/coding_memory_v1/oracles/helios-green--poisoning.py": "60c5112afb7b84ee5513f2657f42d0eacb0607ba345c28618af3b3b3eaab753e", + "eval/datasets/coding_memory_v1/oracles/helios-green--scope_boundaries.py": "dab035b811e7e9a60bf447d1ea26f6db915c320b5dc2dd04cda4b6f1023296be", + "eval/datasets/coding_memory_v1/oracles/helios-green--temporal_history.py": "c22dd57bc7cd659863231b164192caf6c4929c5baaf8edd8be8cac00268e5847", + "eval/datasets/coding_memory_v1/oracles/helios-green--unsupported_questions.py": "f78986b8093eab4ecc8c1319d359498d64177db55a4e38cf8d0b32d1e5af4b2a", + "eval/datasets/coding_memory_v1/oracles/helios-north--code_relationships.py": "81721c60bd4e3466600442214147ae6641ebc05ae414965f4a09b89cc74cb8d6", + "eval/datasets/coding_memory_v1/oracles/helios-north--condition_values.py": "8fbcf116c1f0d480d61b2974e7bef73acef053d45bf0ac3b12e6ea48b017e43f", + "eval/datasets/coding_memory_v1/oracles/helios-north--corrections.py": "beae8d6c5828c1f225e5e953236136a217ee48e67cebc82049caf8f1c60cfdcc", + "eval/datasets/coding_memory_v1/oracles/helios-north--long_documents.py": "019e7ff3b9efcb461c1c68acc657b2438cc2d63d4b4165c0caa4523c440f12bc", + "eval/datasets/coding_memory_v1/oracles/helios-north--multilingual.py": "a48324390ab60686c2b1417f0c049f47601737c55fdf000b696ccc5de260ccab", + "eval/datasets/coding_memory_v1/oracles/helios-north--paraphrases.py": "9e2552399b988a6d722ccfaec7cfbea10853164336fadc9e9146dbae788a2d8a", + "eval/datasets/coding_memory_v1/oracles/helios-north--poisoning.py": "75eecaa42c302898581d3bef8aee00fa1dec6c3bc968c0014a522196e18c7681", + "eval/datasets/coding_memory_v1/oracles/helios-north--scope_boundaries.py": "8bd32a30cd1c2e346375bf2b37ef4908c3feeb67a80ec52ed3108bfe9cbd548e", + "eval/datasets/coding_memory_v1/oracles/helios-north--temporal_history.py": "1d1118cb5d438f91bb027acaa3b64f867e93cca7eb0f480bb83073584fafd97a", + "eval/datasets/coding_memory_v1/oracles/helios-north--unsupported_questions.py": "02a854bf89085637e366f801d2be6ab26b9e18296f13531485d08c2a64621c7c", + "eval/datasets/coding_memory_v1/oracles/helios-violet--code_relationships.py": "e32101847ccad5cd18fbc2c667ba4d1b63e3e7f8067dc89de5f485546dfef525", + "eval/datasets/coding_memory_v1/oracles/helios-violet--condition_values.py": "c8ef59dc9dcfe3aecc990d82fe44f1f93b7adc8c65f1fc38e79b934d351635c6", + "eval/datasets/coding_memory_v1/oracles/helios-violet--corrections.py": "a3a36bb554a6f4a83ea612bbbe9c0ee4ee1a08071e490a9a909c46d3049bb379", + "eval/datasets/coding_memory_v1/oracles/helios-violet--long_documents.py": "f9fa707bb28b4d7ea96e3bacfa22f24502b571a76a79defd147fe9d5205963a2", + "eval/datasets/coding_memory_v1/oracles/helios-violet--multilingual.py": "cfbfe0535e905dd58434bc25d84e7746dbdf1fc50d72d0fa96cc78677eaf18b9", + "eval/datasets/coding_memory_v1/oracles/helios-violet--paraphrases.py": "1743c8cbf0564d91dfb4db88b694072fdc9bb5a0b9f26f578649af7f539ee393", + "eval/datasets/coding_memory_v1/oracles/helios-violet--poisoning.py": "02cf390520757473e9d677f5c1f0934ecf50645ad5edcdc64caec23721a5d99e", + "eval/datasets/coding_memory_v1/oracles/helios-violet--scope_boundaries.py": "5ab003dfc23cb00d0a70195541a780a4b80b1d496728b2b88f7f154310e67a6d", + "eval/datasets/coding_memory_v1/oracles/helios-violet--temporal_history.py": "209e95f62c39d3f2223ed00a378788e396d04de392324455cd9d19dedb3a87ad", + "eval/datasets/coding_memory_v1/oracles/helios-violet--unsupported_questions.py": "a3ff0703297e2f743917e74abeffc0c922a6241318f37d63a49242c6bcc18059", + "eval/datasets/coding_memory_v1/oracles/helios-west--code_relationships.py": "4d8543e4c49d59f844830ccee3fcea0643ee17ce43f51863b94a1b4fbc9825ea", + "eval/datasets/coding_memory_v1/oracles/helios-west--condition_values.py": "f2efa7fd803df0e10c440d5ec2d98dedf5e02e23a4cedddcde5ea722f4039e40", + "eval/datasets/coding_memory_v1/oracles/helios-west--corrections.py": "181c42640cee47db2b5c779bdf8fe2e84103a92459d7f6cb0d9587458e421406", + "eval/datasets/coding_memory_v1/oracles/helios-west--long_documents.py": "49337830deb24f9d6b11ba4e01f397c0cf1a40a15524f1ce33354a2c61615072", + "eval/datasets/coding_memory_v1/oracles/helios-west--multilingual.py": "f1e4ef8be6bba13457a2da1c3a2082f81bba765f2cf1f07b93eb20edf5e2debe", + "eval/datasets/coding_memory_v1/oracles/helios-west--paraphrases.py": "29abfff3b323496fb62e739cdc0be2aada1b7e1467a141cc8d19a1d54c1ab130", + "eval/datasets/coding_memory_v1/oracles/helios-west--poisoning.py": "14b7f874c10681ff3829b2659cbe7500250369652d11b26c842e62718093ca0f", + "eval/datasets/coding_memory_v1/oracles/helios-west--scope_boundaries.py": "decbdb3cd7ab0c40c0aadb1ecdeef9e2ee2586dd8802b9bb16235f211ce05ee1", + "eval/datasets/coding_memory_v1/oracles/helios-west--temporal_history.py": "6e7c2369157234374d3f9633b7e772b4a3bff405d418c6e1336e56c87ebf28de", + "eval/datasets/coding_memory_v1/oracles/helios-west--unsupported_questions.py": "379716803a300bbecd3fc8f1661bdac2fcd8a98a2bb18514d8de168bd57fa144", + "eval/datasets/coding_memory_v1/oracles/island-green--code_relationships.py": "833962801d594247d5c5644df0e6aee877d452127ea5c0dbc22c81512132ab21", + "eval/datasets/coding_memory_v1/oracles/island-green--condition_values.py": "cfbb4ea0a4dd12d69686178ba8246bd08b93743dc5395cd0479984a53655d909", + "eval/datasets/coding_memory_v1/oracles/island-green--corrections.py": "4c7ee2538df9988864e24d9b86805d26b64d1a37638b57abdd1761e24c4ab340", + "eval/datasets/coding_memory_v1/oracles/island-green--long_documents.py": "f73cd2b884539680c500af9a5c8cc00d1dc9f1d2ef7bda61f6c8253b2bf799ed", + "eval/datasets/coding_memory_v1/oracles/island-green--multilingual.py": "e954fa56aa819ef9088c8d6da4abd647b689956c4ff4a7b0b8e31078522f2702", + "eval/datasets/coding_memory_v1/oracles/island-green--paraphrases.py": "4cde223eac40d4a2962aa2ace533b8761c5b82784767b0307c884ac5e6ad0576", + "eval/datasets/coding_memory_v1/oracles/island-green--poisoning.py": "08725e295848b2c58c9e6d6a0554a2dd4a6506af75039205c444251d11a62acb", + "eval/datasets/coding_memory_v1/oracles/island-green--scope_boundaries.py": "257cd579af07491748f5de88d04f8af85056b0448d348e59c7b0cbb28754fb1b", + "eval/datasets/coding_memory_v1/oracles/island-green--temporal_history.py": "e93105f0639a83b300a6de3bbae56166eedac2e58122f7e84ce5a40a074db6b6", + "eval/datasets/coding_memory_v1/oracles/island-green--unsupported_questions.py": "a707cee90fb3f0cabd0e692399a361f59da0fc4e9e7bbf0cde702e05579f0b3f", + "eval/datasets/coding_memory_v1/oracles/island-north--code_relationships.py": "f00e022ce900e4ad0fcec93f2dbf016337a4d83afacbcf739f61f71316723173", + "eval/datasets/coding_memory_v1/oracles/island-north--condition_values.py": "c704a03a70253585a67823826dc0a6ebaf6323a8880c00af80c148c150176daf", + "eval/datasets/coding_memory_v1/oracles/island-north--corrections.py": "e2cadf6e4f365f94cf0f440758b173da4d14ba12c03a84351e0608f0c60bccc5", + "eval/datasets/coding_memory_v1/oracles/island-north--long_documents.py": "300246fd3d85d73b63066d133361bc784ae26b46f4197c5c41ac960eb37f9508", + "eval/datasets/coding_memory_v1/oracles/island-north--multilingual.py": "f8aba8e14ed3c420e90e701880ea38e924e1219e3cc3643ba8bde17cb31ddef6", + "eval/datasets/coding_memory_v1/oracles/island-north--paraphrases.py": "503ce2c132c666bb67920e90d4a898f5a73f1d66779cd83c1885812f223e30e5", + "eval/datasets/coding_memory_v1/oracles/island-north--poisoning.py": "3369d956e5af12bc6be80828c4c6ab03ccf80c79e38dea3162b4e2c09771ff30", + "eval/datasets/coding_memory_v1/oracles/island-north--scope_boundaries.py": "9b4f6186f71713175f1eb5aacb0b6258c746b2297ca7c692f54bf5feabd5d261", + "eval/datasets/coding_memory_v1/oracles/island-north--temporal_history.py": "7c22b0edbf8edb7a511a581ffe6f1ce14454ef3feb527eca90d1386ce00b2c6b", + "eval/datasets/coding_memory_v1/oracles/island-north--unsupported_questions.py": "c1e5d526b0c6b88394f19a8b8edca7dae1db718b24be2d57374eb3769bbd1f24", + "eval/datasets/coding_memory_v1/oracles/island-violet--code_relationships.py": "9672d608ea0c8623ff1fe760f943173b2fdac5dc4ea4b7ae0dba33436d860254", + "eval/datasets/coding_memory_v1/oracles/island-violet--condition_values.py": "cfea91adcc9b3f9eb28cc24cf310e10b436f81cff51297f161b9a3b59a21f62c", + "eval/datasets/coding_memory_v1/oracles/island-violet--corrections.py": "075d48a2dfd91af8328982fe475ec915c00b75db38819aff3b4c2343147e2bfc", + "eval/datasets/coding_memory_v1/oracles/island-violet--long_documents.py": "85cc789277e59925b36ea3bd82e23f989e79fb0be2e55083e2656e518641dd3c", + "eval/datasets/coding_memory_v1/oracles/island-violet--multilingual.py": "84615b70eb74eb2180dd8a89d8d34c11e904030bfaa26c57e2e6c968daf64d29", + "eval/datasets/coding_memory_v1/oracles/island-violet--paraphrases.py": "5244d32ffc4127d519e2f5a875ffaecf7bc51eaa1426d2aacbc42490cf291d22", + "eval/datasets/coding_memory_v1/oracles/island-violet--poisoning.py": "9bde478876ea0be60555d5bd26d3790e4f305327ece10bf6c258bc564354fa1d", + "eval/datasets/coding_memory_v1/oracles/island-violet--scope_boundaries.py": "8e8b0a64342963b4dbc000578a72c0e0801fc1ca4af140f09b55d4cdb03b1274", + "eval/datasets/coding_memory_v1/oracles/island-violet--temporal_history.py": "3e187ce259c7f57f6dba4a28c3ecdf31fbe6458f7d908b22cb8b5ce8325437ae", + "eval/datasets/coding_memory_v1/oracles/island-violet--unsupported_questions.py": "0e60f29a47953c5d434c159eece26bbb03cc837842c493111b34310f9cff4693", + "eval/datasets/coding_memory_v1/oracles/island-west--code_relationships.py": "c19c42978fa5f9c6e1f41886eb1cc0fdb7e12304ba58b160b4cbe1975640acfe", + "eval/datasets/coding_memory_v1/oracles/island-west--condition_values.py": "07c4c591e08f5248f3016dc16cecfea7dd75bebff1a61599f258938d7031043d", + "eval/datasets/coding_memory_v1/oracles/island-west--corrections.py": "a7d93dfb579ec4f58cf2f6d29a6d0ab7dbf1a17f6ae49b98902a5ea22525e649", + "eval/datasets/coding_memory_v1/oracles/island-west--long_documents.py": "20a50dd63e04ce751115fca671b9da7ebbf240e26f39d494583cb405a03b750c", + "eval/datasets/coding_memory_v1/oracles/island-west--multilingual.py": "65d82c4d7968e0e597509c692015bdac0b92841c726d87560d9caa32429176f4", + "eval/datasets/coding_memory_v1/oracles/island-west--paraphrases.py": "feb7eb2e8592162d7d1f433610cb02ba04cd9797191d65a9f0e5bc4f4711c4ed", + "eval/datasets/coding_memory_v1/oracles/island-west--poisoning.py": "3d7c2c6a7f247f3e8158d5ba007bbbdf027a74e9a2c52f52953b3ccf2c21f17f", + "eval/datasets/coding_memory_v1/oracles/island-west--scope_boundaries.py": "ff0f36139f6da5428b9538d108aded522e62ca803035ebef78e11255cf55e08b", + "eval/datasets/coding_memory_v1/oracles/island-west--temporal_history.py": "42cb1c552ce01308b47d526c6e6993fb3270239b392cfa9f5e42b8e0fcfb3f82", + "eval/datasets/coding_memory_v1/oracles/island-west--unsupported_questions.py": "911b4257380640d35d6e2fc5292e235eedc113f5e71d8e71e8ccab7c72d4f290", + "eval/datasets/coding_memory_v1/oracles/juniper-green--code_relationships.py": "373395d3b70f64f8102609d27ce10c4aeb937ac73988c2ce2be1e283df94ef82", + "eval/datasets/coding_memory_v1/oracles/juniper-green--condition_values.py": "f04794a96b06d537550227ec308b1f02405cdab42d7d3ae4db4afd2c25bee8cd", + "eval/datasets/coding_memory_v1/oracles/juniper-green--corrections.py": "5a354a8c8efdbc498d25ffbf6095fee7b3d7d638134b452c7c9ecb186b4e1bc7", + "eval/datasets/coding_memory_v1/oracles/juniper-green--long_documents.py": "8610157668d6a90c794a956f6f0b432f03e3559238599ad1cb412f6f1a2f7e46", + "eval/datasets/coding_memory_v1/oracles/juniper-green--multilingual.py": "dcb99335fa18b008197532809ff7127be2cda600848e10c8bfe4677bd896f90e", + "eval/datasets/coding_memory_v1/oracles/juniper-green--paraphrases.py": "7a4cb1bbe3de2e6fe6a33b1f78829f4aeb21d21c2e8d4d3399d3d98958ba0909", + "eval/datasets/coding_memory_v1/oracles/juniper-green--poisoning.py": "ba73c2c6f04590538e16c50be527da60f5949781bc8833a3c18a7c597146eb10", + "eval/datasets/coding_memory_v1/oracles/juniper-green--scope_boundaries.py": "9d09c0851a3906b6281cb1c5811714224161e27a1aa46edd81c5659e7f93cd3d", + "eval/datasets/coding_memory_v1/oracles/juniper-green--temporal_history.py": "fd05595a452952dca2d0089f5701a60145a3b5705b78a5ff2f8f9f13fbf1c259", + "eval/datasets/coding_memory_v1/oracles/juniper-green--unsupported_questions.py": "f58bfcb89db799dc1578fc8791b49e97e7f22b30e720aeaf661f0ad2e6a9660c", + "eval/datasets/coding_memory_v1/oracles/juniper-north--code_relationships.py": "a84f02bd3c2b9c367248dae8aa3580d83de6a1beb6fe464553eb8a2bfb172b75", + "eval/datasets/coding_memory_v1/oracles/juniper-north--condition_values.py": "d70311ae339556f18240f824d0f0bd0024060c2698fcca8fe7cb540e8a8bc727", + "eval/datasets/coding_memory_v1/oracles/juniper-north--corrections.py": "fa290629339007fdd4230a76d49fa294a218c5f0cf6b90c3074678b0f16c539a", + "eval/datasets/coding_memory_v1/oracles/juniper-north--long_documents.py": "e30025d68ae3b48b566bf3fb2aea5e3603f5f7ab658da8fcb2256d3b1964c85a", + "eval/datasets/coding_memory_v1/oracles/juniper-north--multilingual.py": "b8bb0199c4acfe71df8d4e7df75bdd37e038cc72aa82bc6df10715819aeb3686", + "eval/datasets/coding_memory_v1/oracles/juniper-north--paraphrases.py": "aabf70509d3b0602d43c9590f04fa3ad1370a40c266d20f64a5ece15d4c79a51", + "eval/datasets/coding_memory_v1/oracles/juniper-north--poisoning.py": "ebfb9d7bf3c50eed8f44b73e391496ef377942de389b6feb64c7a1c0f9646444", + "eval/datasets/coding_memory_v1/oracles/juniper-north--scope_boundaries.py": "06c2c9d57ade79139c9b0d20568703477bb9e3a934f2f8900ad717ef157e5844", + "eval/datasets/coding_memory_v1/oracles/juniper-north--temporal_history.py": "0d97ba6d7a5d7741bf248b599ed18ae81b1c0053ad53cbc64934f31e9c3461aa", + "eval/datasets/coding_memory_v1/oracles/juniper-north--unsupported_questions.py": "526648ba7a4edc21bc2eb589c1e5c98a20fb07aa692687de4c3845e559be8096", + "eval/datasets/coding_memory_v1/oracles/juniper-violet--code_relationships.py": "c73fd24d0adbeda6c7afeb15ab07296c0438402893e8d442af26865106d6e8ea", + "eval/datasets/coding_memory_v1/oracles/juniper-violet--condition_values.py": "4aadae570ef84f976d49ba1fc92574ef55630b1c3017e90dbec1f85e6fb74c4a", + "eval/datasets/coding_memory_v1/oracles/juniper-violet--corrections.py": "07236b2a88df73c8d0f6afbe7d972fdc18f8c835a852f5c20748a369c77d82e2", + "eval/datasets/coding_memory_v1/oracles/juniper-violet--long_documents.py": "54d3201b621b168ce03963c423942f142cb403f9d90234f063ff33b7add4d2dc", + "eval/datasets/coding_memory_v1/oracles/juniper-violet--multilingual.py": "e0cc26d743cbea8e98261895474879d078fd53941c075606bfc6e52ee098db93", + "eval/datasets/coding_memory_v1/oracles/juniper-violet--paraphrases.py": "a1f836316408052cab1253b56b5caa6b1432fe6eeaabbb5872da97ec9bde633a", + "eval/datasets/coding_memory_v1/oracles/juniper-violet--poisoning.py": "bc09271eadf77f1b6c03fff20ca9c0f0a42a99d1c5600e6b7843f0a0d549ed5d", + "eval/datasets/coding_memory_v1/oracles/juniper-violet--scope_boundaries.py": "17a7258bed9efe1c86a05756c5b5d17926fd72f01de4b2addd5d2fb4550f4da5", + "eval/datasets/coding_memory_v1/oracles/juniper-violet--temporal_history.py": "8bda8599d861857204c48b55729607c8cb0cb3ae2701eb18fb600d398468cafe", + "eval/datasets/coding_memory_v1/oracles/juniper-violet--unsupported_questions.py": "10cdbb013f78f0e54c31a6e76f03d8d15a6063fd7971a2d803a390890c036ad2", + "eval/datasets/coding_memory_v1/oracles/juniper-west--code_relationships.py": "f0a682d413b3d8418c664db2775de4eef35240c508cc71ca6bbc055cd0c8dbe6", + "eval/datasets/coding_memory_v1/oracles/juniper-west--condition_values.py": "200a4b78f201e992ac10112a194c97b88841dc5930e3e7de9bfc076bf4c69fa7", + "eval/datasets/coding_memory_v1/oracles/juniper-west--corrections.py": "41ea9b5a5f40d8703343f4570593c3f27905e5ff70fae57bdbdb5fcccc585d22", + "eval/datasets/coding_memory_v1/oracles/juniper-west--long_documents.py": "7d8cdd01c5d90ad62e9da93a99febead683e3f01c5ca37315382a2fb0f512d76", + "eval/datasets/coding_memory_v1/oracles/juniper-west--multilingual.py": "1815adc5946bf9e7dba16786d8c29f9466537b7a43b1c392f927b61c56da54ee", + "eval/datasets/coding_memory_v1/oracles/juniper-west--paraphrases.py": "5d0110bc2098d2143303476fb5c5714086d0278b848110c63aa626e0b88311cc", + "eval/datasets/coding_memory_v1/oracles/juniper-west--poisoning.py": "4e16d9de9931cffc6915805b4fc375dad3ffb7fad17eb893205ce6d07102bcb3", + "eval/datasets/coding_memory_v1/oracles/juniper-west--scope_boundaries.py": "6c5d875184c890a922ee18ac423598fdcece109a91e9e62d5a7ccc82d2eaaa70", + "eval/datasets/coding_memory_v1/oracles/juniper-west--temporal_history.py": "3404fdbb5cc4693333d48d27da928531279d148ffe1cdadbdd741dfc1a8d38d3", + "eval/datasets/coding_memory_v1/oracles/juniper-west--unsupported_questions.py": "b85a4cf14f9596cd14b480acd323772fc91084ad1413fa27435bdb7ffe5baa6b", + "eval/datasets/longdoc.jsonl": "7f5ade95e1f283d0db8cf78e53ed8995d3534f847e616d2c0005fd8da37ac790", + "eval/desktop_memory.py": "65a3b5fcd248948c209baa115b275a6db40b386be5f0828bf9ca611787fa02a6", + "eval/engine_capacity.py": "1940da1c3a435d81c7b7a1a689e966c23b92e4f94af115b45813dd30e2c04eec", + "eval/evidence_contracts.py": "03969fdf01e78135c66e698d3f523e5312ccca3737dd3036b37a2c6676ba4ea2", + "eval/external.py": "98500a97c18152b1b86a270062033c48176224d7468ac0f703ddc7de862a30e2", + "eval/external_checkpoints.py": "484e298039bfa04eef86bd7145428c312aaeaf059f5bdf8da7ae29d1150008d7", + "eval/extractor_quality.py": "50820fe7f821d111e17e4e77a3d1159e7d6979d110b052c4f254a5b309c2652a", + "eval/fts_insert_scaling.py": "6088997e0f85d430fafb74509e24a960fbd59c06c8b2839285942b97d2116724", + "eval/graph_every_bench.py": "79da573c5edf315f71bfab412d3ea283b8da45d1fdabc78ddd19302ead09e4f0", + "eval/graph_traversal.py": "b094f75c3a1d75ba3cf19e372187d692d3c595a1d9bde19bbe95ae0c79a5175f", + "eval/grounded.py": "053d5193b716a2c3e507fcd44057d392de910a4442b46bd7cc1f30ac0ba68541", + "eval/handoff_quality.py": "7daf635510764e236f48ca7e2537a85d8ebc1ad995513144329c1f0236405937", + "eval/harness.py": "8c96c26a121dfc2d9ea051a05861af8951c93a732dba9cb13de0de178414b016", + "eval/hosted_evidence.py": "7946cd8c1e3aa291268271b2aa11210d5f09c9b05ba635aa0b64bef07bdcee45", + "eval/hosted_ledger.py": "a53036d12ff671371c148910a816fb20c7e7f3346250a303722352f47b06c476", + "eval/hosted_luna.py": "4dbf02a65eec38bcde92d82952a0b372abbac1f68378d11a04528f4a31a835dc", + "eval/jev_recall_quality.py": "3c31080767c2bc0301f1085c837eaa60ac6e1f5ae8eeb6ff908e573a630510cd", + "eval/local_benchmark_queue.py": "d87ace3cf9ed59d65e57917e5387ff226c1b8a4abef8b98719c5de3aa1b8dddc", + "eval/local_capacity_campaign.py": "ab4935263f9d24c4e38bd4f16164b4c4c07eff91c695809b6fc75299d4f03d62", + "eval/longmemeval_v2.py": "defb4d47f453aa4615a8f101b82df3fadf64f0f9eae0ce1021d86d3750dad437", + "eval/longmemeval_v2_evidence.py": "8486bd4dcfdd8b1f7a14c32fe4f427c980ccdcc3aa2eb018d7dfc40305412eff", + "eval/longmemeval_v2_matrix.py": "ca085cd59481813cce5dbdfbc94f40f67173ac3a1bc3dce6f6d3e09eb7b08153", + "eval/metrics.py": "16857e2cf6ed339cb57a26c9bfa1879444b4d279bd972e5a9fa644ed1308afe0", + "eval/native_coverage_scaling.py": "1df274ae8f0a8266282fd5b530167aa749ab67024349bf97c6c0a5a4987bfa77", + "eval/performance.py": "dccc55c26dc396f9fee98defd152900cf8aabeb5d6198bc711e0eb8f472ecc57", + "eval/performance_engine.py": "3d37cf0a5989c8fa6e8b6ab7ae0b2e0d5410a8b922d2f3130c1a7794d93dcbb1", + "eval/planned_recall.py": "f9a87291ecb181045b98ca65fe55db820bf7cee4ae4f4d087ca3458afdd7837e", + "eval/proactive_ranking.py": "8610541f1d547f9c0eb46d078dbcaa670c0a97157acc37f96ec08b482cb7a6ab", + "eval/productivity.py": "6d4644ebdc44472aeb3879963774fab276bfa269b781139c46ded48774a22717", + "eval/public_readiness.py": "5ce8a18d0bfe09e75e88548a589b8fc6d2cbf04c0f8cd1ce51cd9519136a2751", + "eval/redteam_poisoning.py": "fce120cc3adf2ee966b59cd3ea7f20d242a0af49b52492143543130fe14c0cc8", + "eval/reinforcement.py": "72ed766775a2658eaa728afec51c0ac22d97a90e813111954df66a6ec50f2bef", + "eval/repair_discovery.py": "db05496fbbcb0df86c5cdc2f0c85fb6b6605b0cace6cb44ae2add10b72784b5f", + "eval/resolver_reworded_corrections.py": "a9054778a37b2175b46f04b674b4779e358931eee5d2ae52bbc5f953d234fb9e", + "eval/resource_hierarchy.py": "5ab6c989bb143c4386749c45a33c190447e829c1b5657e8c0aca30f34bd69461", + "eval/rework_statistics.py": "e12c14288797c5cf2dd93d51f606244287bc2f4ff8bd401f1d1b072efd1f9529", + "eval/run_longmemeval_v2.py": "866863d9f8f7ce8f7c3c36741ab324faa5fae417827f907c655eace38de0f5ac", + "eval/spec_crawl.py": "da3b079ab5674ce1004b0dafcfd471da9897c951a03456ba704b720b719f3295", + "eval/task_pairs.py": "fddc54804e8837ec0731813297fb55825458317f898e29176e16f3f5a2f527fd", + "eval/user_journeys.py": "a1c8436d4a6871baff21f9aa4f6ac1545def60d946cd3b1454c3d9cd8661a049", + "eval/vector_scale.py": "3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d", + "eval/vector_scale_storage.py": "24040fd1b96b9f9cd43ea37b0b37ea118a02bd096aff77fc67ff8b0df769b1dc", + "eval/vector_scan_plan.py": "34fba3d029bfc78134e8c9c450b019ff56c3bdcbeb921400888077cb44e9b848", + "scripts/export_offline_evidence.py": "4e10c2b3d5f6a2024a7f95f7b87ce47ec2cb4ef31c55611b2e5cf35502ebc0ba" + } + } +} diff --git a/docs/benchmark-evidence/offline-fixtures-paid-plans-20261104.json.sha256 b/docs/benchmark-evidence/offline-fixtures-paid-plans-20261104.json.sha256 new file mode 100644 index 00000000..5312b0c0 --- /dev/null +++ b/docs/benchmark-evidence/offline-fixtures-paid-plans-20261104.json.sha256 @@ -0,0 +1 @@ +cc66984fd3dd78c17addeb979aea0e0bf40ff0f533ebcd683da4b7f532da0055 offline-fixtures-paid-plans-20261104.json diff --git a/docs/images/context-efficiency.png b/docs/images/context-efficiency.png index a041b686..0a9d62a8 100644 Binary files a/docs/images/context-efficiency.png and b/docs/images/context-efficiency.png differ diff --git a/docs/images/context-efficiency.svg b/docs/images/context-efficiency.svg index 0ebae922..3cce3816 100644 --- a/docs/images/context-efficiency.svg +++ b/docs/images/context-efficiency.svg @@ -1,6 +1,6 @@ Offline context, retrieval, and grounded benchmark results -Artifact-driven offline benchmark report with context efficiency, retrieval quality, and grounded behavior reported separately. Structure-aware chunking reports 740.3 to 214.3 retrieved tokens per question. The JSON-shape payload proxy reports 24,590 full versus 11,138 compact tokens; MCP transport was not measured; provider billing was not measured. Retrieved candidate quality is Recall@5 1.000, Hit@5 1.000, and answer-token recall 1.000 across 26 questions. Packed context quality is Recall@5 1.000, Hit@5 1.000, and answer-token recall 1.000 across 26 questions. Grounded checks score 5 of 5 answerable queries grounded and 6 of 6 abstention queries rejected, including 1 of 1 quarantined-evidence checks. Decision accuracy is 1.000 across 11 decisions. Source artifact SHA-256 025183601c5a28a37daf9326a061010244bc9a3f9f7db1e3e6d4f866eaafc660. +Artifact-driven offline benchmark report with context efficiency, retrieval quality, and grounded behavior reported separately. Structure-aware chunking reports 740.3 to 214.3 retrieved tokens per question. The JSON-shape payload proxy reports 24,590 full versus 11,138 compact tokens; MCP transport was not measured; provider billing was not measured. Retrieved candidate quality is Recall@5 1.000, Hit@5 1.000, and answer-token recall 1.000 across 26 questions. Packed context quality is Recall@5 1.000, Hit@5 1.000, and answer-token recall 1.000 across 26 questions. Grounded checks score 5 of 5 answerable queries grounded and 6 of 6 abstention queries rejected, including 1 of 1 quarantined-evidence checks. Decision accuracy is 1.000 across 11 decisions. Source artifact SHA-256 cc66984fd3dd78c17addeb979aea0e0bf40ff0f533ebcd683da4b7f532da0055. @@ -65,6 +65,6 @@ Quarantined-evidence probe 1 / 1 abstained Decision accuracy 1.000 (11 / 11); the quarantine probe is included in the abstention total. -SOURCE SHA-256 025183601c5a +SOURCE SHA-256 cc66984fd3dd JSON proxy only; MCP transport not measured; provider billing not measured. diff --git a/docs/images/evidence-backed-agent-examples.png b/docs/images/evidence-backed-agent-examples.png index 47911885..9683d93c 100644 Binary files a/docs/images/evidence-backed-agent-examples.png and b/docs/images/evidence-backed-agent-examples.png differ diff --git a/docs/images/evidence-backed-agent-examples.svg b/docs/images/evidence-backed-agent-examples.svg index 5631c9c1..3b8ddac7 100644 --- a/docs/images/evidence-backed-agent-examples.svg +++ b/docs/images/evidence-backed-agent-examples.svg @@ -1,6 +1,6 @@ Three evidence-backed Engraphis agent behaviors - A three-card summary of deterministic offline fixtures. Focused context returns 740.3 to 214.3 tokens while retaining Recall at 5 of 1.000. A grounded answer returns support for 5/5 answerable questions. An unsupported question safely abstains for 6/6 off-topic questions. Reproduce with eval.chunking_eval and eval.grounded. Exact commands and config digests are registered in BENCHMARKS.md. Public-safe artifact SHA-256: 025183601c5a28a37daf9326a061010244bc9a3f9f7db1e3e6d4f866eaafc660. + A three-card summary of deterministic offline fixtures. Focused context returns 740.3 to 214.3 tokens while retaining Recall at 5 of 1.000. A grounded answer returns support for 5/5 answerable questions. An unsupported question safely abstains for 6/6 off-topic questions. Reproduce with eval.chunking_eval and eval.grounded. Exact commands and config digests are registered in BENCHMARKS.md. Public-safe artifact SHA-256: cc66984fd3dd78c17addeb979aea0e0bf40ff0f533ebcd683da4b7f532da0055. @@ -47,5 +47,5 @@ Reproduce: eval.chunking_eval + eval.grounded - SHA256 025183601c5a28a37daf9326a061010244bc9a3f9f7db1e3e6d4f866eaafc660 + SHA256 cc66984fd3dd78c17addeb979aea0e0bf40ff0f533ebcd683da4b7f532da0055 diff --git a/engraphis/backends/sync_relay.py b/engraphis/backends/sync_relay.py index 85be16f1..b0e3b1fb 100644 --- a/engraphis/backends/sync_relay.py +++ b/engraphis/backends/sync_relay.py @@ -112,10 +112,72 @@ def decode_sync_e2ee_key(value: object) -> bytes: return key +def sync_e2ee_key_source() -> str: + """Return where the Cloud Sync key comes from, without disclosing it. + + The environment stays authoritative so an operator-managed deployment cannot be + silently replaced by a key generated in the dashboard. Otherwise a key generated on + this device lives in the same owner-only state directory as the sync credential. + """ + + env_value = os.environ.get(SYNC_E2EE_KEY_ENV) + if env_value is not None and str(env_value).strip(): + return "environment" + return "state_file" if _sync_e2ee_key_path().exists() else "none" + + +def load_sync_e2ee_key() -> str: + """Read the saved owner-only key, failing closed when it is absent or unsafe.""" + + try: + value = read_private_text( + _sync_e2ee_key_path(), max_bytes=128, allow_missing=True + ) + except (OSError, UnsafeStateFile): + raise RelayError( + "saved Cloud Sync encryption key is unreadable; generate or configure a new one", + status=409, + ) from None + if value is None or not value.strip(): + raise RelayError( + "Cloud Sync needs a 32-byte end-to-end encryption key in " + + SYNC_E2EE_KEY_ENV, + status=409, + ) + return value.strip() + + +def has_saved_sync_e2ee_key() -> bool: + """Return whether a saved key exists. The key itself is never returned here.""" + + try: + return bool(load_sync_e2ee_key()) + except RelayError: + return False + + +def save_sync_e2ee_key(value: object) -> str: + """Validate and persist one key, returning its normalized unpadded form. + + The key is written through ``atomic_private_text`` (0600, owner-only directory) and is + never logged. Callers must show it to the operator exactly once; a later call cannot + read it back through the API. + """ + + raw = decode_sync_e2ee_key(value) + normalized = base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=") + _atomic_private_text(_sync_e2ee_key_path(), normalized) + return normalized + + def configured_sync_e2ee_key(value: object = None) -> bytes: - """Return an explicit key or fail closed before a Cloud upload can begin.""" - configured = os.environ.get(SYNC_E2EE_KEY_ENV) if value is None else value - return decode_sync_e2ee_key(configured) + """Return an explicit, environment, or saved key; fail closed before any upload.""" + if value is not None: + return decode_sync_e2ee_key(value) + env_value = os.environ.get(SYNC_E2EE_KEY_ENV) + if env_value is not None and str(env_value).strip(): + return decode_sync_e2ee_key(env_value) + return decode_sync_e2ee_key(load_sync_e2ee_key()) def _new_e2ee_cipher(key: bytes): @@ -237,6 +299,10 @@ def _sync_read_only_path() -> Path: return _sync_token_path().with_name("sync.read_only") +def _sync_e2ee_key_path() -> Path: + return _sync_token_path().with_name("sync.e2ee_key") + + def _atomic_private_text(path: Path, value: str) -> None: """Atomically write one owner-only state value next to the sync credential.""" atomic_private_text(path, value + "\n", harden_parent=True) diff --git a/engraphis/cloud_authz.py b/engraphis/cloud_authz.py index a834518a..da3ee2bc 100644 --- a/engraphis/cloud_authz.py +++ b/engraphis/cloud_authz.py @@ -15,6 +15,8 @@ import logging from typing import Any, Optional +from engraphis.hosted_client import account_url + logger = logging.getLogger("engraphis.cloud_authz") # Machine-readable reason codes the control plane may return. @@ -37,7 +39,7 @@ "Your subscription has expired. Renew your plan to continue using Team features." ), REASON_SEAT_LIMIT_EXCEEDED: ( - "All named seats are in use. Upgrade your plan to add more team members." + "All named seats are in use. Add a seat in the Engraphis Cloud account portal." ), REASON_CROSS_ORG_DENIED: ( "Access denied: this credential belongs to a different organization." @@ -62,6 +64,10 @@ REASON_ENTITLEMENT_EXPIRED, REASON_SEAT_LIMIT_EXCEEDED, }) +#: The only reason values this client will translate or log. An unrecognized body is a +#: generic denial, and a long one is discarded before it can reach a log line. +KNOWN_REASONS = frozenset(_DENIAL_MESSAGES) +_MAX_REASON_CHARS = 64 def interpret_denial( @@ -89,6 +95,10 @@ def interpret_denial( reason = str(detail.get("reason") or "") elif isinstance(detail, str): reason = detail + # A denial body is untrusted input. Keep only a short, fixed-vocabulary code; never + # log or render an arbitrary provider string. + if len(reason) > _MAX_REASON_CHARS: + reason = "" message = _DENIAL_MESSAGES.get(reason) if message is None: @@ -104,12 +114,14 @@ def interpret_denial( "upgrade_url": None, "retryable": reason == REASON_TOKEN_STALE, } - # Prefer the server-provided upgrade_url; fall back to the known billing path. + # Prefer the server-provided upgrade_url; fall back to the real account portal. The + # old relative "/billing/upgrade" path does not exist on the dashboard host and + # produced a 404 exactly when a paying customer was trying to fix billing. upgrade_url = body.get("upgrade_url") if upgrade_url and isinstance(upgrade_url, str): result["upgrade_url"] = upgrade_url elif reason in _UPGRADE_REASONS: - result["upgrade_url"] = "/billing/upgrade" + result["upgrade_url"] = account_url() return result diff --git a/engraphis/cloud_features.py b/engraphis/cloud_features.py index 321ab2e5..b6051b0c 100644 --- a/engraphis/cloud_features.py +++ b/engraphis/cloud_features.py @@ -108,11 +108,15 @@ class CloudFeatureError(RuntimeError): """A bounded, redacted managed-cloud failure suitable for an HTTP/UI boundary.""" def __init__(self, message: str, *, status: Optional[int] = None, - transient: bool = False, code: Optional[str] = None) -> None: + transient: bool = False, code: Optional[str] = None, + reason: Optional[str] = None) -> None: super().__init__(message) self.status = status self.transient = transient self.code = code + # A fixed-vocabulary server reason (never provider text). The dashboard uses it to + # choose the right recovery action: billing, seats, reconnect, or administrator. + self.reason = reason class _NoRedirect(urllib.request.HTTPRedirectHandler): @@ -126,6 +130,35 @@ def redirect_request(self, req, fp, code, msg, headers, newurl): # escaped from this error path as a raw traceback. _DRAIN_FAILURES = (OSError, ValueError, http.client.HTTPException) +#: Maximum bytes read from an error body to recover one allow-listed reason code. The +#: body itself never crosses the boundary; this is deliberately much smaller than a normal +#: response and is discarded after the reason is extracted. +MAX_ERROR_BODY_BYTES = 8 * 1024 + + +def _denial_reason(status: int, raw: bytes) -> Optional[str]: + """Return a known denial reason from a bounded error body, never the body itself. + + The private service may include implementation details in an error payload. Only a + fixed reason code is allowed through; an oversized, malformed, or unknown body becomes + ``None`` and the caller falls back to its status-based recovery copy. + """ + + if not raw or len(raw) > MAX_ERROR_BODY_BYTES: + return None + try: + body = json.loads(raw.decode("utf-8")) + except (UnicodeDecodeError, ValueError, RecursionError): + return None + if not isinstance(body, dict): + return None + try: + from engraphis.cloud_authz import KNOWN_REASONS, interpret_denial + reason = interpret_denial(status, body).get("reason") + except Exception: # noqa: BLE001 - presentation metadata must never mask the failure + return None + return reason if isinstance(reason, str) and reason in KNOWN_REASONS else None + def managed_compute_consent(service: Any = None, workspace: Optional[str] = None) -> bool: """Return explicit approval for this workspace; account connection never grants it. @@ -505,13 +538,15 @@ def _request(self, method: str, path: str, payload: Optional[dict] = None) -> di raw = response.read(MAX_RESPONSE_BYTES + 1) except urllib.error.HTTPError as exc: message, transient = _public_http_error(exc.code) - # Do not inspect or reflect provider diagnostics: they can contain internal - # details. Drain only to release the connection, and guard the drain and close - # independently because both can fail for a malformed/truncated response. + # Read the error body once, bounded. Only a fixed-vocabulary reason code is + # extracted; provider diagnostics never reach the dashboard. The same bytes + # drain the connection, and both the read and close are guarded because a + # malformed or truncated response can fail either independently. + raw_error = b"" try: - exc.read(MAX_RESPONSE_BYTES + 1) + raw_error = exc.read(MAX_ERROR_BODY_BYTES + 1) except _DRAIN_FAILURES: - pass + raw_error = b"" finally: try: exc.close() @@ -521,6 +556,7 @@ def _request(self, method: str, path: str, payload: Optional[dict] = None) -> di message, status=exc.code, transient=transient, + reason=_denial_reason(exc.code, raw_error), ) from None except (urllib.error.URLError, TimeoutError, OSError, http.client.HTTPException) as exc: raise CloudFeatureError( diff --git a/engraphis/dashboard_assets/index.html b/engraphis/dashboard_assets/index.html index 95997f22..e176b228 100644 --- a/engraphis/dashboard_assets/index.html +++ b/engraphis/dashboard_assets/index.html @@ -940,6 +940,7 @@

This installation

Cloud sync—IncludedIncluded Managed automation—IncludedIncluded Portfolio analytics—IncludedIncluded + Managed Jev decisionsLocal heuristicsIncluded · up to 500 calls/dayIncluded · up to 500 calls/day Shared workspaces——Included Members and roles——Included Remote agent access——Included diff --git a/engraphis/dashboard_assets/ledger.css b/engraphis/dashboard_assets/ledger.css index 707b7e7b..13afed38 100644 --- a/engraphis/dashboard_assets/ledger.css +++ b/engraphis/dashboard_assets/ledger.css @@ -1112,6 +1112,7 @@ body[data-theme="sepia"] .graph-header { .automation-policy-form { padding: 0; } .automation-policy-note { margin: 0; color: var(--c-mid); font-size: 12px; line-height: 1.55; } .automation-policy-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } +.sync-key-value { display: block; padding: 10px 12px; border: 1px solid var(--c-line); border-radius: 5px; background: var(--c-raised); color: var(--c-fg); font: 12px/1.5 var(--mono); overflow-wrap: anywhere; user-select: all; } .billing-toggle { display: flex; align-items: center; gap: 8px; color: var(--c-mid); } .plan-cards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin-bottom: 16px; } .plan-card { display: grid; gap: 10px; padding: 20px; border: 1px solid var(--c-line); border-radius: 5px; background: var(--c-surface); } diff --git a/engraphis/dashboard_assets/ledger.js b/engraphis/dashboard_assets/ledger.js index 5a1a1571..89031df5 100644 --- a/engraphis/dashboard_assets/ledger.js +++ b/engraphis/dashboard_assets/ledger.js @@ -372,8 +372,18 @@ } if (!response.ok) { const error = new Error(errorMessage(payload, response.status)); + const detail = payload && payload.detail && typeof payload.detail === 'object' + ? payload.detail : null; error.status = response.status; - error.code = payload && ((payload.detail && payload.detail.code) || payload.code); + error.code = (detail && detail.code) || (payload && payload.code); + error.detail = detail; + error.reason = (detail && detail.reason) || (payload && payload.reason) || ''; + error.tierRequired = (detail && detail.tier_required) + || (payload && payload.tier_required) || ''; + error.upgradeUrl = (detail && detail.upgrade_url) + || (payload && payload.upgrade_url) || ''; + error.accountUrl = (detail && detail.account_url) + || (payload && payload.account_url) || ''; error.localBrowser = payload && payload.local_browser; throw error; } @@ -5969,7 +5979,16 @@ }; } if (status === 402 || status === 501) { - return { title: `${feature} needs Engraphis Pro`, detail: lockReason(), action: 'plan' }; + const seat = error.reason === 'seat_limit_exceeded'; + return { + title: seat ? 'All Team seats are in use' : `${feature} needs Engraphis Pro`, + detail: seat + ? 'Add a named seat in the Engraphis Cloud account portal, or remove a member who no longer needs access.' + : lockReason(), + action: seat ? 'account' : 'plan', + actionLabel: seat ? 'Manage seats' : '', + href: seat ? (safeUrl(error.accountUrl) || hostedAccountUrl('seat_limit')) : '', + }; } if (status === 401) { if (error.code === 'cloud_unconfigured' && licenseTrialAvailable()) { @@ -5979,6 +5998,24 @@ title: 'Reconnect Engraphis Cloud', detail: 'This installation’s Engraphis Cloud session expired or was revoked. Connect again to resume hosted features; your plan is unchanged.', action: 'reconnect', + actionLabel: 'Open Engraphis Cloud', + href: safeUrl(error.accountUrl) || hostedAccountUrl('reconnect'), + }; + } + if (status === 403) { + const disabled = error.reason === 'member_disabled'; + const role = error.reason === 'insufficient_role'; + return { + title: disabled ? 'Your account is disabled' + : role ? 'Administrator permission required' : 'Access denied', + detail: disabled + ? 'An organization administrator can restore this member account. Your local memories are unaffected.' + : role + ? 'Ask an organization owner or administrator to grant the role this action needs.' + : 'This credential belongs to a different organization or lacks the required role. Open the account portal to check access.', + action: 'account', + actionLabel: 'Open account portal', + href: safeUrl(error.accountUrl) || hostedAccountUrl('account'), }; } return null; @@ -5989,10 +6026,11 @@ const actions = node('div', 'automation-policy-actions'); if (failure.action === 'consent') { actions.append(button('Review workspace processing', 'primary-button', () => switchManageTab('settings'))); - } else if (failure.action === 'reconnect') { - const href = hostedAccountUrl('reconnect'); + } else if (failure.action === 'reconnect' || failure.action === 'account') { + const href = safeUrl(failure.href) || hostedAccountUrl(failure.action); if (href) { - const link = node('a', 'secondary-button', 'Open Engraphis Cloud'); + const link = node('a', 'secondary-button', + failure.actionLabel || 'Open Engraphis Cloud'); link.href = href; link.target = '_blank'; link.rel = 'noopener'; @@ -6558,12 +6596,22 @@ } } else if (!ready) { const missingKey = !hasKey; + const invalid = state.syncStatus.key_state === 'invalid'; + const canGenerate = state.syncStatus.can_generate_key === true; + const canRotate = state.syncStatus.can_rotate_key === true; const detail = missingKey - ? state.syncStatus.key_state === 'invalid' - ? 'The configured Cloud Sync encryption key is invalid. Replace it with a 32-byte URL-safe base64 key, then restart this dashboard.' - : 'Cloud Sync needs a user-held encryption key. Configure ENGRAPHIS_SYNC_E2EE_KEY on this device, then restart this dashboard.' + ? invalid + ? 'The saved Cloud Sync encryption key is invalid. Generate a replacement; every device that syncs this workspace must use the same key.' + : canGenerate + ? 'Cloud Sync needs a user-held encryption key. Generate one here; it is stored on this device and shown only once.' + : 'Cloud Sync needs a user-held encryption key. Configure ENGRAPHIS_SYNC_E2EE_KEY on this device, then restart this dashboard.' : 'Cloud Sync encryption support is unavailable. Install engraphis[cloud-sync] and restart this dashboard.'; target.append(node('p', 'automation-policy-note', detail)); + if (missingKey && canGenerate) { + actions.append(button('Create sync key', 'primary-button', () => createSyncKey(false))); + } else if (missingKey && canRotate) { + actions.append(button('Replace sync key', 'secondary-button', () => createSyncKey(true))); + } const guide = node('a', 'secondary-button', 'Cloud Sync setup guide'); guide.href = 'https://github.com/Coding-Dev-Tools/engraphis/blob/main/docs/SYNC.md#configure-a-customer-installation'; guide.target = '_blank'; @@ -6573,6 +6621,54 @@ target.append(actions); } + function renderCreatedSyncKey(target, key, status) { + target.replaceChildren( + node('h3', '', 'Cloud Sync key created'), + node('p', 'automation-policy-note', + 'Store this key somewhere safe. It is shown only once, and every device that syncs this workspace needs it.'), + node('code', 'sync-key-value', key), + ); + const actions = node('div', 'automation-policy-actions'); + actions.append(button('Copy key', 'primary-button', async () => { + try { + if (!navigator.clipboard) throw new Error('clipboard unavailable'); + await navigator.clipboard.writeText(key); + showNotice('Cloud Sync key copied.'); + } catch (_) { + showNotice('Copy failed. Select the key and copy it manually.'); + } + })); + actions.append(button('I stored the key', 'secondary-button', () => renderSyncStatus(status))); + target.append(actions); + } + + async function createSyncKey(rotate) { + const target = byId('sync-result'); + if (!target) return; + if (rotate && !window.confirm( + 'Replace the saved Cloud Sync key? Data already synced under the old key cannot be read until every device uses the new one.' + )) return; + const request = beginScopedRequest('sync-key'); + try { + const result = await api('/sync/key', { + method: 'POST', + body: { rotate: Boolean(rotate) }, + signal: request.signal, + }); + if (!isCurrentScopedRequest(request)) return; + if (!result || !result.key) throw new Error('Engraphis did not return a key.'); + const status = await api('/sync/status', { signal: request.signal }); + if (!isCurrentScopedRequest(request)) return; + state.syncStatus = status; + renderCreatedSyncKey(target, result.key, status); + } catch (error) { + if (!isCurrentScopedRequest(request)) return; + const failure = hostedFailure('sync', error); + if (failure) renderHostedFailure(target, failure); + else target.replaceChildren(empty(`Could not create a Cloud Sync key: ${error.message}`, 'form-error')); + } + } + async function loadSync() { const request = beginScopedRequest('sync-status'); const target = byId('sync-result'); @@ -6640,6 +6736,7 @@ 'Growth, retention, decay and entity Analytics', 'Auto Consolidation with hosted retention policies', 'Auto Dreaming with reviewable managed proposals', + 'Managed Jev decisions: up to 500 calls per person per day', 'Private account and billing support', ], team: [ @@ -6718,6 +6815,13 @@ local: 'Not connected', }; + function licenseTrialDaysRemaining(license = state.license, now = Date.now()) { + const ends = Number(license && license.trial && license.trial.ends_at); + if (!(ends > 0)) return null; + const remaining = Math.ceil((ends * 1000 - now) / 86_400_000); + return Number.isFinite(remaining) ? Math.max(0, remaining) : null; + } + function licenseTrialEnds(license = state.license) { const ends = Number(license && license.trial && license.trial.ends_at); const date = ends > 0 ? validDate(ends) : null; @@ -6762,7 +6866,13 @@ target.append(box); }; if (license.error) callout('Hosted authorization unavailable', text(license.error)); - if (access === 'trial_expired') { + if (access === 'trial') { + const daysLeft = licenseTrialDaysRemaining(license); + const when = daysLeft === null ? 'Your free trial is active' + : daysLeft === 0 ? 'Your free trial ends today' + : `Your free trial ends in ${daysLeft} day${daysLeft === 1 ? '' : 's'}`; + callout(when, `${ends ? `It is active until ${ends}. ` : ''}Subscribe to keep Cloud Sync, Analytics, Automation, and Team access.`); + } else if (access === 'trial_expired') { callout(`Your free trial has ended${ends ? ` on ${ends}` : ''}`, 'Hosted features are locked. Everything you wrote is still in your local database and fully usable. The trial runs once per account, so restoring hosted features means subscribing.'); } else if (access === 'lapsed') { const reason = LICENSE_STATUS_NOTES[license.entitlement_status]; @@ -6783,6 +6893,42 @@ }); target.append(list); } + // Benefits included with a paid plan that are not hosted feature keys. This is what + // makes the paid plan legible: Jev is the largest included benefit and has no lock row. + const services = license.included_services && typeof license.included_services === 'object' + ? Object.values(license.included_services).filter(item => item && typeof item === 'object') + : []; + if (services.length) { + const list = node('ul', 'license-features'); + list.setAttribute('aria-label', 'Included with paid plans'); + services.forEach(service => { + const calls = Number(service.calls_per_person_per_day); + const label = text(service.label || 'Included service'); + const detail = Number.isSafeInteger(calls) && calls > 0 + ? `${label} · up to ${calls.toLocaleString()} calls per person per day` + : label; + const item = node('li', '', `★ ${detail}`); + item.setAttribute('aria-label', `${label}: included with paid plans`); + list.append(item); + }); + target.append( + node('p', 'automation-policy-note', 'Included with Pro and Team'), + list, + ); + } + // A server grant this build cannot render must never look like a lock on something the + // customer bought. Name it and ask for an update instead of silently dropping it. + const unrecognized = Array.isArray(license.unrecognized_features) + ? license.unrecognized_features.filter(item => typeof item === 'string' && item) + : []; + if (access !== 'inactive' && unrecognized.length) { + const box = node('p', 'license-callout'); + box.append( + node('strong', '', 'Your plan includes a newer capability'), + document.createTextNode(` ${unrecognized.map(item => text(item)).join(', ')}. Update Engraphis to use it.`), + ); + target.append(box); + } if (access === 'active') target.append(node('p', 'plan-support', 'Thank you for supporting Engraphis. Your subscription funds hosted infrastructure and ongoing development.')); } diff --git a/engraphis/entitlement_contract.json b/engraphis/entitlement_contract.json new file mode 100644 index 00000000..4ca0e8bb --- /dev/null +++ b/engraphis/entitlement_contract.json @@ -0,0 +1,93 @@ +{ + "schema": "engraphis-entitlement-contract/v1", + "source": "Read-only mirror of the private control plane's hosted entitlement contract. The server authorizes every paid operation; this file only decides which plan advertises a capability.", + "plans": { + "free": [], + "pro": [ + "analytics", + "automation", + "consolidation", + "dreaming", + "sync" + ], + "team": [ + "analytics", + "automation", + "consolidation", + "dreaming", + "sync", + "team" + ] + }, + "features": { + "analytics": { + "plan": "pro", + "label": "Analytics", + "status": "available" + }, + "automation": { + "plan": "pro", + "label": "Automation", + "status": "available" + }, + "consolidation": { + "plan": "pro", + "label": "Auto Consolidation", + "status": "available" + }, + "dreaming": { + "plan": "pro", + "label": "Auto Dreaming", + "status": "available" + }, + "sync": { + "plan": "pro", + "label": "Cloud Sync", + "status": "available" + }, + "team": { + "plan": "team", + "label": "Team administration", + "status": "available" + }, + "export": { + "plan": "free", + "label": "Local workspace export", + "status": "free" + }, + "compliance_export": { + "plan": "team", + "label": "Compliance export", + "status": "declared" + }, + "hosted_multi_user_roles": { + "plan": "team", + "label": "Team roster and roles", + "status": "declared" + }, + "hosted_team_audit_export": { + "plan": "team", + "label": "Team audit export", + "status": "declared" + }, + "hosted_scoped_agent_tokens": { + "plan": "team", + "label": "Scoped agent tokens", + "status": "declared" + } + }, + "included_services": { + "managed_jev": { + "label": "Managed Jev decisions", + "plan": "pro", + "calls_per_person_per_day": 500 + } + }, + "limits": { + "trial_days_by_plan": { + "pro": 7, + "team": 14 + }, + "grace_hours": 24 + } +} diff --git a/engraphis/entitlements.py b/engraphis/entitlements.py new file mode 100644 index 00000000..0d3f67b2 --- /dev/null +++ b/engraphis/entitlements.py @@ -0,0 +1,301 @@ +"""Read-only mirror of the private hosted entitlement contract. + +This module answers one question: which plan *advertises* a hosted capability. It never +authorizes anything. Every paid operation is still gated on the control plane's response, +and the dashboard only renders what the contract declares. + +Keeping one machine-readable mirror here -- instead of three hand-maintained copies in the +client and its tests -- is the point. ``scripts/check_commercial_manifest.py`` compares this +file with the private contract in CI, so a capability that exists only server-side fails the +release gate instead of silently disappearing from a paying customer's dashboard. +""" +from __future__ import annotations + +import json +from functools import lru_cache +from pathlib import Path +from typing import Optional + + +SCHEMA = "engraphis-entitlement-contract/v1" +PLANS = ("free", "pro", "team") +STATUSES = ("available", "free", "declared") +CONTRACT_PATH = Path(__file__).with_name("entitlement_contract.json") + +_MAX_KEY_CHARS = 64 +_MAX_UNRECOGNIZED = 32 + + +@lru_cache(maxsize=1) +def contract() -> dict: + """Return the validated contract, cached for the life of the process.""" + + data = json.loads(CONTRACT_PATH.read_text(encoding="utf-8")) + _validate(data) + return data + + +def reload() -> None: + """Drop the cached contract. Tests and release tooling only.""" + + contract.cache_clear() + + +def _require(condition: object, message: str) -> None: + if not condition: + raise ValueError("entitlement contract: " + message) + + +def _validate(data: object) -> None: + """Reject a malformed contract before any caller can misread it. + + The checks are deliberately structural rather than a JSON Schema dependency: the core + must keep running on the numpy-only floor, and a clear ``ValueError`` is more useful to + a release engineer than a schema-library stack trace. + """ + + _require(isinstance(data, dict), "root must be a JSON object") + _require(data.get("schema") == SCHEMA, "schema must be " + SCHEMA) + + plans = data.get("plans") + _require(isinstance(plans, dict), "plans must be an object") + _require(set(plans) == set(PLANS), "plans must name free, pro and team") + + features = data.get("features") + _require(isinstance(features, dict) and features, "features must be a non-empty object") + + for plan in PLANS: + names = plans[plan] + _require(isinstance(names, list), "plans.%s must be a list" % plan) + _require( + all(isinstance(name, str) and name.strip() for name in names), + "plans.%s must contain non-empty feature names" % plan, + ) + _require( + len(names) == len(set(names)), + "plans.%s must not repeat a feature" % plan, + ) + for name in names: + spec = features.get(name) + _require( + isinstance(spec, dict), + "plans.%s names an unknown feature: %s" % (plan, name), + ) + # ``plan`` is the *minimum* plan that grants a feature; Team inherits Pro. + declared_plan = spec.get("plan") + if plan == "team": + _require( + declared_plan in ("pro", "team"), + "plans.team contains %s, which declares plan %r" + % (name, declared_plan), + ) + else: + _require( + declared_plan == plan, + "plans.%s contains %s, which declares plan %r" + % (plan, name, declared_plan), + ) + _require( + spec.get("status") == "available", + "plans.%s contains %s, which is not available" % (plan, name), + ) + + _require( + set(plans["pro"]).issubset(set(plans["team"])), + "the team plan must include every pro feature", + ) + + for name, spec in features.items(): + _require( + isinstance(name, str) and name.strip() and len(name) <= _MAX_KEY_CHARS, + "feature names must be non-empty and at most %d characters" % _MAX_KEY_CHARS, + ) + _require(isinstance(spec, dict), "feature %s must be an object" % name) + _require(spec.get("plan") in PLANS, "feature %s names an unknown plan" % name) + _require( + isinstance(spec.get("label"), str) and bool(spec["label"].strip()), + "feature %s needs a non-empty label" % name, + ) + _require( + spec.get("status") in STATUSES, + "feature %s has an unknown status" % name, + ) + if spec["status"] == "free": + _require( + spec["plan"] == "free", + "free feature %s must resolve to the free plan" % name, + ) + _require( + name not in plans["pro"] and name not in plans["team"], + "free feature %s must not be advertised as paid" % name, + ) + if spec["status"] == "available": + _require( + name in plans[spec["plan"]], + "available feature %s is missing from plans.%s" % (name, spec["plan"]), + ) + if spec["plan"] == "pro": + _require( + name in plans["team"], + "available pro feature %s is missing from plans.team" % name, + ) + if spec["status"] == "declared": + _require( + not any(name in names for names in plans.values()), + "declared feature %s must not be granted until it is available" % name, + ) + + included = data.get("included_services", {}) + _require(isinstance(included, dict), "included_services must be an object") + for name, spec in included.items(): + _require( + isinstance(name, str) and name.strip() and len(name) <= _MAX_KEY_CHARS, + "included service names must be non-empty and bounded", + ) + _require(isinstance(spec, dict), "included service %s must be an object" % name) + _require(spec.get("plan") in PLANS, "included service %s names an unknown plan" % name) + _require( + isinstance(spec.get("label"), str) and bool(spec["label"].strip()), + "included service %s needs a non-empty label" % name, + ) + calls = spec.get("calls_per_person_per_day") + if calls is not None: + _require( + isinstance(calls, int) and not isinstance(calls, bool) and calls > 0, + "included service %s calls_per_person_per_day must be a positive integer" + % name, + ) + + limits = data.get("limits") + _require(isinstance(limits, dict), "limits must be an object") + days = limits.get("trial_days_by_plan") + _require( + isinstance(days, dict) and set(days) == {"pro", "team"}, + "limits.trial_days_by_plan must name pro and team", + ) + for plan, value in days.items(): + _require( + isinstance(value, int) and not isinstance(value, bool) and value > 0, + "limits.trial_days_by_plan.%s must be a positive integer" % plan, + ) + grace = limits.get("grace_hours") + _require( + isinstance(grace, int) and not isinstance(grace, bool) and grace > 0, + "limits.grace_hours must be a positive integer", + ) + + +def features_for(plan: object) -> list: + """Return the hosted features a plan advertises, sorted and never guessed. + + ``local`` is the client's own alias for an unconnected installation and resolves to the + free plan, exactly as the server treats an unknown or unpaid plan. + """ + + key = str(plan or "").strip().lower() + if key == "local": + key = "free" + return sorted(contract()["plans"].get(key, ())) + + +def feature_spec(feature: object) -> Optional[dict]: + """Return one feature's contract entry, or ``None`` when this build does not know it.""" + + key = str(feature or "").strip().lower() + if not key or len(key) > _MAX_KEY_CHARS: + return None + spec = contract()["features"].get(key) + return spec if isinstance(spec, dict) else None + + +def required_plans() -> dict: + """Return every known capability mapped to the minimum plan that grants it.""" + + plans = {name: str(spec["plan"]) for name, spec in contract()["features"].items()} + plans.update({ + name: str(spec["plan"]) + for name, spec in contract().get("included_services", {}).items() + }) + return plans + + +def included_services() -> dict: + """Return benefits included with a paid plan but not gated as hosted features. + + These are informational: the server still authorizes every call. Keeping them out of + the feature-row list prevents a tick-box that implies a live grant when the account + portal may have a different allowance state. + """ + + return { + name: dict(spec) + for name, spec in contract().get("included_services", {}).items() + } + + +def required_plan(feature: object) -> str: + """Return the advertised minimum plan for a feature. + + Unknown names keep the historical ``pro`` fallback so a new server key never tells a + Team customer to buy Pro. Callers that need to distinguish "unknown" from "Pro" use + :func:`feature_spec`. + """ + + spec = feature_spec(feature) + return str(spec["plan"]) if spec else "pro" + + +def labels() -> dict: + """Return the labels for features this build can actually render.""" + + return { + name: str(spec["label"]) + for name, spec in contract()["features"].items() + if spec.get("status") == "available" + } + + +def is_known(feature: object) -> bool: + """Return whether this build has any contract entry for *feature*.""" + + return feature_spec(feature) is not None + + +def unrecognized(granted: object, plan: object) -> list: + """Return granted keys this build does not know, bounded and sorted. + + A future server release may grant a capability this client predates. The grant must + never become a feature row (it cannot be rendered or verified here), but staying silent + is how a paying customer ends up with a lock over something they bought. The caller + surfaces this list as "update Engraphis to use it" instead. + """ + + if not isinstance(granted, (list, tuple)): + return [] + values = set() + for item in granted: + if not isinstance(item, str): + continue + key = item.strip().lower() + if key and len(key) <= _MAX_KEY_CHARS: + values.add(key) + if "automation" in values: + values.update(("consolidation", "dreaming")) + allowed = set(features_for(plan)) + known = contract()["features"] + return sorted(name for name in values if name not in allowed and name not in known)[ + :_MAX_UNRECOGNIZED + ] + + +def trial_days_by_plan() -> dict: + """Return the disclosed trial duration for each paid plan.""" + + days = contract()["limits"]["trial_days_by_plan"] + return {plan: int(days[plan]) for plan in ("pro", "team")} + + +def grace_hours() -> int: + """Return the disclosed hosted-account write-grace window.""" + + return int(contract()["limits"]["grace_hours"]) diff --git a/engraphis/hosted_client.py b/engraphis/hosted_client.py index a1931ac9..e97f9769 100644 --- a/engraphis/hosted_client.py +++ b/engraphis/hosted_client.py @@ -16,6 +16,9 @@ from typing import Optional from urllib.parse import urlsplit, urlunsplit +from engraphis.entitlements import required_plan as _contract_required_plan +from engraphis.entitlements import required_plans as _contract_required_plans + TRIAL_DAYS = 7 TRIAL_SECONDS = 7 * 24 * 60 * 60 @@ -52,22 +55,11 @@ ), } -_REQUIRED_PLAN = { - "analytics": "pro", - "automation": "pro", - "consolidation": "pro", - "dreaming": "pro", - "export": "pro", - "compliance_export": "team", - "sync": "pro", - "team": "team", - # Team-only capabilities named in commercial_manifest.json. Without explicit entries - # these fell through to the "pro" default, so a Team capability would tell a customer - # who already holds Pro to buy Pro again. - "hosted_team_audit_export": "team", - "hosted_scoped_agent_tokens": "team", - "hosted_multi_user_roles": "team", -} +# One read-only mirror of the private hosted entitlement contract. This table decides which +# plan a *message* advertises; it never authorizes anything. The contract lives in +# ``engraphis/entitlement_contract.json`` so the client, its tests, and the release gate all +# read the same file instead of maintaining three copies that drift independently. +_REQUIRED_PLAN = _contract_required_plans() class CloudUrlUnresolved(ValueError): @@ -95,7 +87,7 @@ def __init__(self, message: str, *, feature: Optional[str] = None): def required_plan(feature: str) -> str: """Return the advertised minimum hosted plan for a feature.""" - return _REQUIRED_PLAN.get(str(feature or "").strip().lower(), "pro") + return _contract_required_plan(feature) def _safe_target(value: str) -> str: diff --git a/engraphis/routes/v2_api.py b/engraphis/routes/v2_api.py index 3ec59f65..fc14c6cd 100644 --- a/engraphis/routes/v2_api.py +++ b/engraphis/routes/v2_api.py @@ -15,6 +15,7 @@ import logging import math import os +import secrets import threading import time import urllib.error @@ -29,6 +30,7 @@ from pydantic import BaseModel, Field, StrictBool, StrictInt from starlette.concurrency import run_in_threadpool +from engraphis import entitlements as _entitlements from engraphis import licensing from engraphis.commercial import trial_days_by_plan from engraphis.config import DEFAULT_RELAY_URL, canonicalize_relay_url, settings @@ -300,8 +302,12 @@ def _managed_error_message(exc) -> str: return message -def _managed_call(fn, *args, **kwargs): - """Map the public cloud-client protocol onto structured dashboard errors.""" +def _managed_call(fn, *args, feature: Optional[str] = None, **kwargs): + """Map the public cloud-client protocol onto structured dashboard errors. + + ``feature`` names the capability being attempted so a denial can carry the plan that + actually sells it. It is consumed here and never forwarded to the cloud client. + """ from engraphis.cloud_features import CloudFeatureError try: @@ -324,10 +330,40 @@ def _managed_call(fn, *args, **kwargs): else: logger.debug("managed cloud operation failed (%s, status=%s, transient=%s)", type(exc).__name__, exc.status, exc.transient) + reason = getattr(exc, "reason", None) detail = {"error": _managed_error_message(exc), "managed_cloud": True, "transient": exc.transient} if exc.code in {"consent_required", "cloud_unconfigured"}: detail["code"] = exc.code + if reason: + detail["reason"] = reason + if exc.status in {401, 402, 403} and exc.code not in { + "consent_required", "cloud_unconfigured", + }: + # A denied paid action must tell the customer what fixes it. The status alone + # cannot distinguish a lapsed plan, a full Team seat list, a revoked session, + # and a role problem; those need four different next steps. + fallback = ( + "entitlement_expired" if exc.status == 402 + else "token_stale" if exc.status == 401 + else "insufficient_role" + ) + detail["reason"] = reason or fallback + detail["account_url"] = licensing.account_url() + tier = licensing.required_plan(feature) if feature else "" + if reason == "seat_limit_exceeded": + tier = "team" + if tier in ("pro", "team"): + detail["tier_required"] = tier + detail["upgrade_url"] = ( + licensing.upgrade_url(tier) + if exc.status == 402 + else licensing.account_url() + ) + else: + # No feature name means no safe plan to sell. The account portal is + # always correct; guessing "pro" would upsell the wrong product. + detail["upgrade_url"] = licensing.account_url() raise HTTPException( status_code=exc.status or 503, detail=detail, @@ -1783,7 +1819,7 @@ def ensure_current_local_intent(): "already submitted work may continue until Cloud responds."} def rejected(error=exc): raise error - return _managed_call(rejected) + return _managed_call(rejected, feature="automation") # Do not hold a database write lock over the network call. Compare the local # revision atomically when applying its acknowledgement instead, so a late # enable cannot undo an opt-out from another tab or process. @@ -2343,7 +2379,8 @@ def analytics(workspace: Optional[str] = None): from engraphis.cloud_features import run_managed_job ws = workspace or _require_ws() - envelope = _managed_call(run_managed_job, service(), ws, "analytics") + envelope = _managed_call(run_managed_job, service(), ws, "analytics", + feature="analytics") result = envelope.get("result", envelope) if isinstance(result, dict) and isinstance(envelope, dict): if "state" in envelope and "state" not in result: @@ -2357,7 +2394,8 @@ def analytics_job_result(job_id: str, workspace: Optional[str] = None): from engraphis.cloud_features import get_analytics_job_result ws = workspace or _require_ws() - return _managed_call(get_analytics_job_result, service(), ws, job_id) + return _managed_call(get_analytics_job_result, service(), ws, job_id, + feature="analytics") @router.get("/analytics/export") @@ -2457,13 +2495,15 @@ def automation_get(workspace: Optional[str] = None): "error": "The selected workspace does not exist.", "managed_cloud": True, }) - cloud = _managed_call(CloudFeatureClient.from_environment, workspace_id) - policy = _managed_call(cloud.get_policy, workspace_id) + cloud = _managed_call(CloudFeatureClient.from_environment, workspace_id, + feature="automation") + policy = _managed_call(cloud.get_policy, workspace_id, feature="automation") try: unconfigured = int(policy.get("version", -1)) == 0 except (AttributeError, TypeError, ValueError, OverflowError): unconfigured = False - recent = _managed_call(cloud.list_jobs, workspace_id, limit=10) + recent = _managed_call(cloud.list_jobs, workspace_id, limit=10, + feature="automation") recent_jobs = recent.get("jobs") if isinstance(recent, dict) else [] if not isinstance(recent_jobs, list): recent_jobs = [] @@ -2502,7 +2542,8 @@ def automation_bootstrap(workspace: Optional[str] = None): "error": "The selected workspace does not exist.", "managed_cloud": True, }) - cloud = _managed_call(CloudFeatureClient.from_environment, bootstrap_workspace_id) + cloud = _managed_call(CloudFeatureClient.from_environment, bootstrap_workspace_id, + feature="automation") default_policy = { "enabled": True, "cadence_minutes": 1440, @@ -2512,7 +2553,8 @@ def automation_bootstrap(workspace: Optional[str] = None): "infer": False, } with _automation_bootstrap_lock(cloud.organization_id, bootstrap_workspace_id): - policy = _managed_call(cloud.get_policy, bootstrap_workspace_id) + policy = _managed_call(cloud.get_policy, bootstrap_workspace_id, + feature="automation") try: unconfigured = int(policy.get("version", -1)) == 0 except (AttributeError, TypeError, ValueError, OverflowError): @@ -2524,10 +2566,11 @@ def automation_bootstrap(workspace: Optional[str] = None): if phase != "policy_saved": if phase != "snapshot_uploaded": workspace_id, snapshot = _managed_call( - build_managed_snapshot, service(), ws + build_managed_snapshot, service(), ws, feature="automation" ) receipt = _managed_call( - cloud.upload_snapshot, workspace_id, snapshot + cloud.upload_snapshot, workspace_id, snapshot, + feature="automation", ) generation = ( receipt.get("generation", snapshot["generation"]) @@ -2541,7 +2584,8 @@ def automation_bootstrap(workspace: Optional[str] = None): "snapshot_uploaded", generation=int(generation), ) - _managed_call(cloud.save_policy, bootstrap_workspace_id, default_policy) + _managed_call(cloud.save_policy, bootstrap_workspace_id, default_policy, + feature="automation") save_automation_bootstrap_phase( service(), cloud.organization_id, @@ -2568,8 +2612,9 @@ def automation_set(req: _AutomationReq, workspace: Optional[str] = None): "error": "The selected workspace does not exist.", "managed_cloud": True, }) - cloud = _managed_call(CloudFeatureClient.from_environment, workspace_id) - current = _managed_call(cloud.get_policy, workspace_id) + cloud = _managed_call(CloudFeatureClient.from_environment, workspace_id, + feature="automation") + current = _managed_call(cloud.get_policy, workspace_id, feature="automation") cadence_minutes = ( req.cadence_hours * 60 if req.cadence_hours is not None else int(_automation_policy_value(current, "cadence_minutes", 1440)) @@ -2596,9 +2641,12 @@ def automation_set(req: _AutomationReq, workspace: Optional[str] = None): if enabled: # Upload only when managed processing is actually enabled. Merely viewing or # disabling a cloud policy must never transfer memory content. - workspace_id, snapshot = _managed_call(build_managed_snapshot, service(), ws) - _managed_call(cloud.upload_snapshot, workspace_id, snapshot) - saved = _managed_call(cloud.save_policy, workspace_id, policy) + workspace_id, snapshot = _managed_call(build_managed_snapshot, service(), ws, + feature="automation") + _managed_call(cloud.upload_snapshot, workspace_id, snapshot, + feature="automation") + saved = _managed_call(cloud.save_policy, workspace_id, policy, + feature="automation") return { **policy, **saved, @@ -2617,7 +2665,8 @@ def maintenance_run(req: _MaintenanceReq, workspace: Optional[str] = None): from engraphis.cloud_features import run_managed_job ws = _require_ws(workspace) - envelope = _managed_call(run_managed_job, service(), ws, "consolidate") + envelope = _managed_call(run_managed_job, service(), ws, "consolidate", + feature="automation") result = envelope.get("result", envelope) return { "dry_run": True, @@ -3140,35 +3189,27 @@ def code_export(workspace: str, repo: str, # ── license ─────────────────────────────────────────────────────────────────── -# Mirror of the private control plane's plan→feature table (read-only reference: -# the hosted entitlement contract the hosted plan-feature contract). Plans are lowercase -# and only ``pro``/``team`` are paid; any other value — unknown, empty, or mis-cased — -# resolves to no features, exactly as the server treats it. +# One read-only mirror of the private control plane's plan→feature contract, loaded from +# ``engraphis/entitlement_contract.json``. Plans are lowercase and only ``pro``/``team`` are +# paid; any other value — unknown, empty, or mis-cased — resolves to no features, exactly as +# the server treats it. # -# The server's own keys are {analytics, automation, export, sync, team}. This client's -# commercial manifest additionally names Auto Consolidation and Auto Dreaming, which the -# server grants under ``automation``. They are expanded here so the dashboard can never -# draw a lock on a capability the customer's plan already includes. +# The server grants Auto Consolidation and Auto Dreaming under ``automation``; this client +# lists them separately so a paying customer never sees a lock on a capability the plan +# already includes. ``_normalized_features`` expands that alias on the way in. _AUTOMATION_FEATURES = ("automation", "consolidation", "dreaming") -_PRO_FEATURES = ("analytics", "sync") + _AUTOMATION_FEATURES +_PRO_FEATURES = tuple(_entitlements.features_for("pro")) _HOSTED_ENTITLEMENTS = { - "free": (), - "local": (), + "free": tuple(_entitlements.features_for("free")), + "local": tuple(_entitlements.features_for("local")), "pro": _PRO_FEATURES, - "team": _PRO_FEATURES + ("team",), + "team": tuple(_entitlements.features_for("team")), } # Labels for the dashboard's entitlement list; it renders one row per key and ticks the -# ones ``features`` contains. Naming follows the customer-facing vocabulary in -# .env.example and the commercial manifest. -_FEATURE_LABELS = { - "analytics": "Analytics", - "automation": "Automation", - "consolidation": "Auto Consolidation", - "dreaming": "Auto Dreaming", - "sync": "Cloud Sync", - "team": "Team administration", -} +# ones ``features`` contains. Only capabilities this build can render are labelled, so the +# set of known labels stays equal to the Team plan's grant. +_FEATURE_LABELS = dict(_entitlements.labels()) def entitled_features(plan: str) -> list: @@ -3179,7 +3220,7 @@ def entitled_features(plan: str) -> list: Cloud and gated on its response status. """ - return sorted(_HOSTED_ENTITLEMENTS.get(str(plan or "").strip().lower(), ())) + return _entitlements.features_for(plan) # ── authoritative plan resolution ───────────────────────────────────────────────────── @@ -3434,6 +3475,12 @@ def _normalized_features(values: object, plan: str) -> list: return sorted(granted & allowed & set(_FEATURE_LABELS)) +def _unrecognized_features(values: object, plan: str) -> list: + """Return server grants this build cannot render, so the dashboard can say so.""" + + return _entitlements.unrecognized(values, plan) + + def _session_entitlement_snapshot() -> tuple[dict, Optional[str]]: """Read the session once; return its entitlement and the digest of those bytes. @@ -3465,6 +3512,9 @@ def _session_entitlement_snapshot() -> tuple[dict, Optional[str]]: # own plan table fills it in. "features": _normalized_features(declared.get("cloud_features"), plan) if active else [], + "unrecognized_features": _unrecognized_features( + declared.get("cloud_features"), plan + ) if active else [], "cloud_access_active": active, "organization_id": str(declared.get("organization_id") or ""), "fetched_at": float(declared.get("entitlement_checked_at") or 0.0), @@ -3552,6 +3602,8 @@ def _read_entitlement_cache_snapshot() -> tuple[dict, Optional[str]]: resolved = { "plan": plan, "features": _normalized_features(value.get("features"), plan) if active else [], + "unrecognized_features": _unrecognized_features(value.get("features"), plan) + if active else [], "cloud_access_active": active, "organization_id": organization_id, "fetched_at": fetched_at, @@ -4061,6 +4113,7 @@ def _resolved_entitlement(known: dict, *, source: str) -> dict: resolved = { "plan": known["plan"], "features": list(known["features"]), + "unrecognized_features": list(known.get("unrecognized_features") or []), "source": source, "cloud_access_active": known["cloud_access_active"], "checked_at": known["fetched_at"], @@ -4116,9 +4169,18 @@ def hosted_plan_summary() -> dict: # every hosted call is about to reject. One rule for both: the features are the ones # that are live right now. features = list(entitlement["features"]) if state in ("active", "trial") else [] + # A server grant this build cannot render is not a grant either, but silence is how a + # paying customer ends up staring at a lock over something they bought. Keep it out of + # ``features`` (the lock-badge loop would tick a row it cannot verify) and surface it + # separately so the dashboard can ask for an update. + unrecognized = ( + list(entitlement.get("unrecognized_features") or []) + if state in ("active", "trial") else [] + ) return { "plan": entitlement["plan"], "features": features, + "unrecognized_features": unrecognized, "plan_source": entitlement["source"], "cloud_access_active": entitlement["cloud_access_active"], "plan_checked_at": entitlement["checked_at"], @@ -4192,6 +4254,12 @@ def get_license(): "plan": plan, "features": summary["features"], "known_features": dict(_FEATURE_LABELS), + # Benefits included with a paid plan that are not gated as hosted feature keys. + # Informational only: the server still authorizes every call. + "included_services": _entitlements.included_services(), + # Keys the control plane granted that this build does not know. They are never + # rendered as feature rows; the dashboard offers an update instead of a lock. + "unrecognized_features": summary["unrecognized_features"], # Diagnostics for support: which resolution rule produced this plan — one of # ``environment`` (the operator override), ``session`` (the entitlement the cloud # returned on registration/refresh), ``cloud`` (the cached entitlements read), @@ -4312,11 +4380,11 @@ def _relay_url() -> str: def sync_status(): """Report local sync readiness without disclosing the encryption key.""" from engraphis.backends.sync_relay import ( - SYNC_E2EE_KEY_ENV, RelayError, _new_e2ee_cipher, configured_sync_e2ee_key, has_sync_token, + sync_e2ee_key_source, sync_read_only, ) from engraphis.cloud_session import CloudSessionError, configured @@ -4330,13 +4398,14 @@ def sync_status(): # A connected account cannot sync without the separate, user-held E2EE key. # Validate the same key shape and optional crypto backend as the relay transport, # but return only bounded status labels; never serialize key bytes or input. - key_state = "missing" if not os.environ.get(SYNC_E2EE_KEY_ENV, "").strip() else "invalid" + key_source = sync_e2ee_key_source() + key_state = "missing" has_key = False encryption_available = False try: key = configured_sync_e2ee_key() except RelayError: - pass + key_state = "invalid" if key_source != "none" else "missing" else: has_key = True key_state = "ready" @@ -4352,6 +4421,11 @@ def sync_status(): "ready": available and local_prerequisites_ready, "has_key": has_key, "key_state": key_state, + # Where the key came from, without ever including it. The dashboard only offers + # generation when no key exists; environment-managed deployments keep precedence. + "key_source": key_source, + "can_generate_key": key_source == "none", + "can_rotate_key": key_source == "state_file", "encryption_available": encryption_available, "local_prerequisites_ready": local_prerequisites_ready, "has_cloud_session": has_cloud_session, @@ -4369,6 +4443,60 @@ def sync_status(): } +class _SyncKeyReq(BaseModel): + rotate: StrictBool = False + + +@router.post("/sync/key") +def create_sync_key(req: Optional[_SyncKeyReq] = None): + """Create a user-held Cloud Sync key once, on this device. + + The key is returned exactly once and written only to the owner-only state directory. + It is never logged, never re-readable through the API, and never overrides an + operator-provisioned ``ENGRAPHIS_SYNC_E2EE_KEY``. Rotation is explicit because data + already synced under the old key cannot be read until every device uses the new one. + """ + from fastapi.responses import JSONResponse + + from engraphis.backends.sync_relay import ( + SYNC_E2EE_KEY_ENV, + RelayError, + has_saved_sync_e2ee_key, + save_sync_e2ee_key, + ) + + if os.environ.get(SYNC_E2EE_KEY_ENV, "").strip(): + raise HTTPException(status_code=409, detail={ + "error": "Cloud Sync key is managed by %s." % SYNC_E2EE_KEY_ENV, + "code": "sync_key_managed_by_environment", + }) + rotate = bool(req.rotate) if req is not None else False + if has_saved_sync_e2ee_key() and not rotate: + raise HTTPException(status_code=409, detail={ + "error": "A Cloud Sync key already exists on this device. Rotating it makes " + "data synced under the old key unreadable until every device uses " + "the new key.", + "code": "sync_key_exists", + }) + try: + key = save_sync_e2ee_key(secrets.token_urlsafe(32)) + except (OSError, RelayError, ValueError): + raise HTTPException(status_code=503, detail={ + "error": "The Cloud Sync key could not be saved on this device.", + "code": "sync_key_unwritable", + }) from None + return JSONResponse( + { + "configured": True, + "key": key, + "key_source": "state_file", + "env_var": SYNC_E2EE_KEY_ENV, + "restart_required": False, + }, + headers={"Cache-Control": "no-store"}, + ) + + def _sync_all(svc) -> dict: """Push every workspace's memories to the relay and pull every peer's — the shared core behind the dashboard's explicit 'Sync now' action. diff --git a/pyproject.toml b/pyproject.toml index e23dd269..b382beb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -239,7 +239,7 @@ include = ["engraphis*", "scripts*", "eval*", "integrations.commandcode"] # runtime artifacts such as dashboard_assets/__pycache__/*.pyc after a local compile check. # vendor/**/* covers nested vendor assets too (vendor/* alone doesn't cross "/"). "engraphis.dashboard_assets" = ["*.html", "*.css", "*.js", "*.png", "*.ico", "vendor/*", "vendor/**/*"] -"engraphis" = ["commercial_manifest.json"] +"engraphis" = ["commercial_manifest.json", "entitlement_contract.json"] "eval" = [ "BASELINES.md", "EVIDENCE.md", diff --git a/scripts/check_commercial_manifest.py b/scripts/check_commercial_manifest.py index 923cd8ab..cecad121 100644 --- a/scripts/check_commercial_manifest.py +++ b/scripts/check_commercial_manifest.py @@ -11,6 +11,14 @@ ROOT = Path(__file__).resolve().parents[1] MANIFEST_PATH = ROOT / "engraphis" / "commercial_manifest.json" +CONTRACT_PATH = ROOT / "engraphis" / "entitlement_contract.json" +CONTRACT_SCHEMA = "engraphis-entitlement-contract/v1" +TEAM_ONLY_CAPABILITIES = frozenset({ + "hosted_multi_user_roles", + "hosted_team_audit_export", + "hosted_scoped_agent_tokens", +}) +AUTOMATION_ALIASES = frozenset({"consolidation", "dreaming"}) sys.path.insert(0, str(ROOT)) @@ -71,6 +79,193 @@ def _checkout_mapping(manifest: dict, errors: list[str]) -> dict: return mapping +def _read_contract(errors: list[str]) -> dict: + """Load the checked-in entitlement mirror, reporting rather than raising.""" + + try: + contract = json.loads(CONTRACT_PATH.read_text(encoding="utf-8")) + except (OSError, ValueError) as exc: + _fail(errors, "entitlement contract is missing or unreadable: %s" % exc) + return {} + if not isinstance(contract, dict) or contract.get("schema") != CONTRACT_SCHEMA: + _fail(errors, "entitlement contract schema must be " + CONTRACT_SCHEMA) + return contract if isinstance(contract, dict) else {} + return contract + + +def _check_entitlement_contract(manifest: dict, errors: list[str]) -> dict: + """Validate the single plan→feature mirror against the public commercial manifest. + + The client reads this file at runtime, so the release gate has to prove it is complete + before a paid capability can silently disappear from a customer's dashboard. + """ + + contract = _read_contract(errors) + if not contract: + return contract + plans = contract.get("plans") + features = contract.get("features") + if not isinstance(plans, dict) or set(plans) != {"free", "pro", "team"}: + _fail(errors, "entitlement contract plans must name free, pro and team") + return contract + if not isinstance(features, dict) or not features: + _fail(errors, "entitlement contract features must be a non-empty object") + return contract + + for plan in ("free", "pro", "team"): + names = plans.get(plan) + if not isinstance(names, list) or len(names) != len(set(names)): + _fail(errors, "entitlement contract plans.%s must be a unique list" % plan) + continue + for name in names: + spec = features.get(name) + if not isinstance(spec, dict) or spec.get("status") != "available": + _fail( + errors, + "entitlement contract plans.%s names a feature that is not " + "available: %s" % (plan, name), + ) + continue + # ``plan`` is the minimum grant; Team inherits every Pro feature. + declared_plan = spec.get("plan") + allowed = declared_plan in ("pro", "team") if plan == "team" else declared_plan == plan + if not allowed: + _fail( + errors, + "entitlement contract plans.%s names %s, which declares plan %r" + % (plan, name, declared_plan), + ) + if not set(plans["pro"]).issubset(set(plans["team"])): + _fail(errors, "entitlement contract team plan must include every pro feature") + + for name, spec in features.items(): + if not isinstance(spec, dict): + _fail(errors, "entitlement contract feature %s must be an object" % name) + continue + if spec.get("plan") not in {"free", "pro", "team"}: + _fail(errors, "entitlement contract feature %s names an unknown plan" % name) + if not isinstance(spec.get("label"), str) or not spec["label"].strip(): + _fail(errors, "entitlement contract feature %s needs a non-empty label" % name) + if spec.get("status") not in {"available", "free", "declared"}: + _fail(errors, "entitlement contract feature %s has an unknown status" % name) + if spec.get("status") == "declared" and any( + name in names for names in plans.values() + ): + _fail(errors, "entitlement contract declares but still grants %s" % name) + if spec.get("status") == "available": + if name not in plans.get(spec.get("plan"), ()): + _fail( + errors, + "entitlement contract omits available feature %s from its plan" % name, + ) + if spec.get("plan") == "pro" and name not in plans.get("team", ()): + _fail( + errors, + "entitlement contract omits pro feature %s from the team plan" % name, + ) + + included = contract.get("included_services", {}) + if not isinstance(included, dict): + _fail(errors, "entitlement contract included_services must be an object") + else: + for name, spec in included.items(): + if not isinstance(spec, dict): + _fail(errors, "included service %s must be an object" % name) + continue + if spec.get("plan") not in {"free", "pro", "team"}: + _fail(errors, "included service %s names an unknown plan" % name) + if not isinstance(spec.get("label"), str) or not spec["label"].strip(): + _fail(errors, "included service %s needs a non-empty label" % name) + calls = spec.get("calls_per_person_per_day") + if calls is not None and ( + not isinstance(calls, int) or isinstance(calls, bool) or calls <= 0 + ): + _fail( + errors, + "included service %s calls_per_person_per_day must be positive" + % name, + ) + + # Local workspace export is free and documented free. It must never re-enter the paid + # vocabulary, or a Pro/Team row starts advertising something the free tier owns. + export = features.get("export") + if not isinstance(export, dict) or export.get("plan") != "free" or export.get("status") != "free": + _fail(errors, "entitlement contract must keep local export on the free plan") + if "export" in plans.get("pro", ()) or "export" in plans.get("team", ()): + _fail(errors, "entitlement contract must not advertise local export as paid") + + # Every hosted capability the manifest turns on must resolve deliberately. + for name, enabled in (manifest.get("features") or {}).items(): + if not enabled or not name.startswith("hosted_") or name == "hosted_authorization": + continue + key = name.replace("hosted_opt_in_cloud_", "") + spec = features.get(key) + if not isinstance(spec, dict): + _fail(errors, "manifest sells %s but the entitlement contract omits it" % name) + continue + expected = "team" if name in TEAM_ONLY_CAPABILITIES else "pro" + if spec.get("plan") != expected: + _fail(errors, "manifest capability %s must resolve to %s" % (name, expected)) + + limits = contract.get("limits") or {} + trial = manifest.get("trial") or {} + if limits.get("trial_days_by_plan") != trial.get("days_by_plan"): + _fail(errors, "entitlement contract trial days differ from the manifest") + lifecycle = manifest.get("entitlement_lifecycle") or {} + if limits.get("grace_hours") != lifecycle.get("max_grace_hours"): + _fail(errors, "entitlement contract grace hours differ from the manifest") + return contract + + +def _check_cloud_contract(cloud: dict, manifest: dict, errors: list[str]) -> None: + """Compare the checked-in mirror with the private control plane's contract. + + The private service publishes only a secret-free contract (no price ids, no keys). When + it names ``hosted_entitlements``, the plan→feature sets must match the client mirror + exactly; a new server grant that never reached this repository is a release blocker, + not something a paying customer discovers as a lock. + """ + + if not isinstance(cloud, dict): + _fail(errors, "cloud contract must be an object") + return + # The private service has published both shapes: the original schema_version 1 + # contract and the later engraphis-public-offer/v1 document. Accept either, then + # apply the same trial and entitlement-table comparisons to both. + if not ( + cloud.get("schema_version") == 1 + or cloud.get("schema") == "engraphis-public-offer/v1" + ): + _fail( + errors, + "cloud contract schema must be schema_version 1 or " + "engraphis-public-offer/v1", + ) + return + if (cloud.get("trial") or {}).get("days_by_plan") != (manifest.get("trial") or {}).get( + "days_by_plan" + ): + _fail(errors, "cloud trial authority differs from the public per-plan contract") + hosted = cloud.get("hosted_entitlements") + if hosted is None: + return + if not isinstance(hosted, dict): + _fail(errors, "cloud contract hosted_entitlements must be an object") + return + contract = _read_contract(errors) + if not contract: + return + for plan in ("free", "pro", "team"): + local = set((contract.get("plans") or {}).get(plan) or ()) - AUTOMATION_ALIASES + remote = set(hosted.get(plan) or ()) + if local != remote: + _fail( + errors, + "entitlement contract plans.%s differs from the private cloud contract: " + "client=%s cloud=%s" % (plan, sorted(local), sorted(remote)), + ) + + def _check_repository(manifest: dict, errors: list[str]) -> None: if manifest.get("schema") != "engraphis-commercial/v2": _fail(errors, "commercial manifest schema must be engraphis-commercial/v2") @@ -124,6 +319,8 @@ def _check_repository(manifest: dict, errors: list[str]) -> None: if manifest.get("billing") != expected_billing: _fail(errors, "Stripe must be the sole launch billing authority") + _check_entitlement_contract(manifest, errors) + pyproject = (ROOT / "pyproject.toml").read_text(encoding="utf-8") match = re.search(r'^version\s*=\s*"([^"]+)"', pyproject, re.MULTILINE) if not match or match.group(1) != manifest["version"]: @@ -235,12 +432,7 @@ def main() -> int: _check_repository(manifest, errors) if args.cloud_contract: cloud = json.loads(args.cloud_contract.read_text(encoding="utf-8-sig")) - schema_valid = ( - cloud.get("schema_version") == 1 - or cloud.get("schema") == "engraphis-public-offer/v1" - ) - if not schema_valid or cloud.get("trial", {}).get("days_by_plan") != manifest["trial"]["days_by_plan"]: - _fail(errors, "cloud trial authority differs from the public per-plan contract") + _check_cloud_contract(cloud, manifest, errors) if args.website_root: _check_website(manifest, args.website_root.resolve(), errors) if errors: diff --git a/tests/e2e/ledger.spec.js b/tests/e2e/ledger.spec.js index b453d2eb..c76e9375 100644 --- a/tests/e2e/ledger.spec.js +++ b/tests/e2e/ledger.spec.js @@ -46,6 +46,7 @@ async function mockApi(page, options = {}) { requests.automationPolicies = []; requests.automationBootstraps = []; requests.syncRuns = []; + requests.syncKeyRequests = []; requests.details = []; requests.documentImports = []; requests.contextSavingsQueries = []; @@ -281,7 +282,22 @@ async function mockApi(page, options = {}) { llmStatus.extractor = llmStatus.extractor_enabled ? 'llm_structured' : 'none'; return ok({ ok: true, extractor_enabled: llmStatus.extractor_enabled, persisted: true }); } - if (path === '/sync/status') return ok(options.syncStatus || { available: false, last: null }); + if (path === '/sync/status') { + const body = requests.syncKeyRequests.length && options.syncStatusAfterKey + ? options.syncStatusAfterKey + : options.syncStatus || { available: false, last: null }; + return ok(body); + } + if (path === '/sync/key') { + requests.syncKeyRequests.push(JSON.parse(request.postData() || '{}')); + return ok({ + configured: true, + key: options.syncKey || 'A'.repeat(43), + key_source: 'state_file', + env_var: 'ENGRAPHIS_SYNC_E2EE_KEY', + restart_required: false, + }); + } if (path === '/sync/run') { requests.syncRuns.push(JSON.parse(request.postData() || '{}')); const summary = options.syncRun || { @@ -1621,6 +1637,34 @@ test('Ledger guides a connected account with a missing Cloud Sync key', async ({ expect(requests.syncRuns).toEqual([]); }); +test('Ledger creates a Cloud Sync key once and confirms it was stored', async ({ page }) => { + const requests = await mockApi(page, { + syncStatus: { + available: true, ready: false, has_key: false, key_state: 'missing', + can_generate_key: true, can_rotate_key: false, + encryption_available: true, local_prerequisites_ready: false, last: null, + }, + syncStatusAfterKey: { + available: true, ready: true, has_key: true, key_state: 'ready', + can_generate_key: false, can_rotate_key: true, + encryption_available: true, local_prerequisites_ready: true, last: null, + }, + syncKey: 'A'.repeat(43), + }); + await page.goto('/?view=manage&tab=sync'); + + const result = page.locator('#sync-result'); + await result.getByRole('button', { name: 'Create sync key' }).click(); + await expect(result).toContainText('Cloud Sync key created'); + await expect(result.locator('.sync-key-value')).toHaveText('A'.repeat(43)); + expect(requests.syncKeyRequests).toEqual([{ rotate: false }]); + + await result.getByRole('button', { name: 'I stored the key' }).click(); + await expect(result).toContainText('Encryption keyValid'); + await expect(result).toContainText('Sync readinessReady'); + expect(requests.syncRuns).toEqual([]); +}); + test('Ledger explains an invalid Cloud Sync key without exposing it', async ({ page }) => { const requests = await mockApi(page, { syncStatus: { @@ -1634,7 +1678,9 @@ test('Ledger explains an invalid Cloud Sync key without exposing it', async ({ p const result = page.locator('#sync-result'); await expect(result).toContainText('Encryption keyInvalid'); - await expect(result).toContainText('Replace it with a 32-byte URL-safe base64 key'); + await expect(result).toContainText( + 'The saved Cloud Sync encryption key is invalid. Generate a replacement' + ); await expect(result.getByRole('button', { name: 'Sync now' })).toBeDisabled(); expect(requests.syncRuns).toEqual([]); }); @@ -2825,6 +2871,30 @@ test('Ledger gives active Pro members direct Cloud access and saves hosted polic expect(errors).toEqual([]); }); +test('Ledger names the days remaining in a live trial', async ({ page }) => { + const live = license(); + live.plan = 'pro'; + live.access_state = 'trial'; + live.is_trial = true; + live.cloud_access_active = true; + live.plan_source = 'session'; + live.trial = { + ...live.trial, + active: true, + available: false, + used: false, + ends_at: Date.now() / 1000 + 2.5 * 86_400, + }; + await mockApi(page, { license: live }); + await page.goto('/'); + await page.getByRole('button', { name: 'Settings' }).click(); + await page.getByRole('tab', { name: 'Plans & billing' }).click(); + + await expect(page.locator('#license-status-body')).toContainText( + 'Your free trial ends in 3 days', + ); +}); + test('Ledger omits unknown Team trial duration from an older license response', async ({ page }) => { const legacy = license(); delete legacy.trial.days_by_plan; @@ -2866,6 +2936,10 @@ test('billing cadence selects the exact Pro and Team checkout target', async ({ await expect(page.locator('.plan-card.featured .plan-benefits')).toContainText( 'Cloud Sync, Analytics, Auto Consolidation, and Auto Dreaming', ); + await expect(page.locator('.plan-card.featured .plan-benefit-list')).toContainText( + 'Managed Jev decisions: up to 500 calls per person per day', + ); + await expect(page.locator('.comparison-table')).toContainText('Managed Jev decisions'); const pro = page.locator('#plan-cards [data-pro-cta="pro"]'); const team = page.locator('#plan-cards [data-pro-cta="team"]'); diff --git a/tests/test_benchmark_evidence.py b/tests/test_benchmark_evidence.py index bede7085..c024dd41 100644 --- a/tests/test_benchmark_evidence.py +++ b/tests/test_benchmark_evidence.py @@ -34,8 +34,8 @@ ROOT = Path(__file__).resolve().parents[1] -PUBLIC_OFFLINE_ARTIFACT = "offline-fixtures-release-1.7.12-20261102.json" -PUBLIC_OFFLINE_SHA = "025183601c5a28a37daf9326a061010244bc9a3f9f7db1e3e6d4f866eaafc660" +PUBLIC_OFFLINE_ARTIFACT = "offline-fixtures-paid-plans-20261104.json" +PUBLIC_OFFLINE_SHA = "cc66984fd3dd78c17addeb979aea0e0bf40ff0f533ebcd683da4b7f532da0055" @pytest.fixture(scope="module") diff --git a/tests/test_client_launch.py b/tests/test_client_launch.py index 5a267a13..236d0327 100644 --- a/tests/test_client_launch.py +++ b/tests/test_client_launch.py @@ -34,19 +34,18 @@ pytest.importorskip("fastapi", reason="full-stack extra not installed") from engraphis import hosted_client, update_check # noqa: E402 +from engraphis.entitlements import features_for # noqa: E402 from engraphis.routes import v2_api # noqa: E402 REPO_ROOT = Path(__file__).resolve().parents[1] UPDATER = REPO_ROOT / "scripts" / "update.py" DASHBOARD_JS = REPO_ROOT / "engraphis" / "dashboard_assets" / "ledger.js" -# The private control plane's plan→feature table, mirrored from (read-only) -# the hosted entitlement contract plan-feature mapping. A drift here means a -# purchased capability silently renders as locked. +# The private control plane's plan→feature table has exactly one checked-in mirror: +# engraphis/entitlement_contract.json. Import it instead of maintaining another copy here, +# so a capability cannot be added to one surface and silently dropped from another. SERVER_HOSTED_ENTITLEMENTS = { - "free": set(), - "pro": {"analytics", "automation", "sync"}, - "team": {"analytics", "automation", "sync", "team"}, + plan: set(features_for(plan)) for plan in ("free", "pro", "team") } # Named separately by this client's commercial manifest; the server grants both under # ``automation``, so any plan granting ``automation`` must grant these too. diff --git a/tests/test_entitlement_contract.py b/tests/test_entitlement_contract.py new file mode 100644 index 00000000..2a5bbf46 --- /dev/null +++ b/tests/test_entitlement_contract.py @@ -0,0 +1,184 @@ +"""One plan→feature mirror, and a loud failure when it drifts. + +The client used to keep three hand-maintained copies of the private control plane's hosted +entitlement table (the runtime, plus two test mirrors), and each copy was only ever +compared to itself. These tests pin the replacement: one checked-in contract, one loader, +and a runtime surface that reports a server grant it cannot render instead of silently +hiding it behind a lock. +""" +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from engraphis import commercial, entitlements +from engraphis.hosted_client import required_plan + + +ROOT = Path(__file__).resolve().parents[1] +CONTRACT = ROOT / "engraphis" / "entitlement_contract.json" + + +def test_the_contract_is_valid_json_and_declares_every_plan(): + data = json.loads(CONTRACT.read_text(encoding="utf-8")) + + assert data["schema"] == entitlements.SCHEMA + assert set(data["plans"]) == {"free", "pro", "team"} + assert set(data["features"]) >= set(data["plans"]["pro"]) + + +def test_a_malformed_contract_is_rejected_instead_of_misread(): + with pytest.raises(ValueError): + entitlements._validate({"schema": "not-the-contract"}) + with pytest.raises(ValueError): + entitlements._validate( + { + "schema": entitlements.SCHEMA, + "plans": {"free": [], "pro": ["sync"], "team": []}, + "features": { + "sync": {"plan": "pro", "label": "Cloud Sync", "status": "available"} + }, + "limits": {"trial_days_by_plan": {"pro": 7, "team": 14}, "grace_hours": 24}, + } + ) + + +def test_features_for_is_case_and_whitespace_insensitive_and_treats_local_as_free(): + assert entitlements.features_for(" PRO ") == entitlements.features_for("pro") + assert entitlements.features_for("local") == [] + assert entitlements.features_for("free") == [] + assert entitlements.features_for("enterprise") == [] + assert entitlements.features_for(None) == [] + + +def test_team_grants_every_pro_feature_and_one_more(): + pro = set(entitlements.features_for("pro")) + team = set(entitlements.features_for("team")) + + assert pro and pro < team + assert team - pro == {"team"} + + +def test_the_contract_covers_every_hosted_capability_the_manifest_sells(): + manifest = commercial.manifest() + + for name, enabled in manifest["features"].items(): + if not enabled or not name.startswith("hosted_") or name == "hosted_authorization": + continue + key = name.replace("hosted_opt_in_cloud_", "") + spec = entitlements.feature_spec(key) + assert isinstance(spec, dict), key + expected = "team" if name in { + "hosted_multi_user_roles", + "hosted_team_audit_export", + "hosted_scoped_agent_tokens", + } else "pro" + assert spec["plan"] == expected, name + assert required_plan(key) == expected, name + + +def test_local_export_is_free_and_never_advertised_as_a_paid_grant(): + assert entitlements.feature_spec("export")["plan"] == "free" + assert required_plan("export") == "free" + assert "export" not in entitlements.features_for("pro") + assert "export" not in entitlements.features_for("team") + + +def test_unknown_features_keep_the_conservative_pro_fallback(): + assert entitlements.feature_spec("not-a-real-feature") is None + assert required_plan("not-a-real-feature") == "pro" + assert entitlements.unrecognized(["not-a-real-feature"], "pro") == ["not-a-real-feature"] + + +def test_unknown_grants_are_reported_but_never_become_feature_rows(): + granted = ["analytics", "future_capability", "future_capability"] + + assert entitlements.unrecognized(granted, "pro") == ["future_capability"] + assert entitlements.unrecognized(["analytics", "automation"], "pro") == [] + + +def test_managed_jev_is_an_included_paid_benefit_with_a_bounded_allowance(): + service = entitlements.included_services()["managed_jev"] + + assert service["plan"] == "pro" + assert service["calls_per_person_per_day"] == 500 + assert entitlements.required_plan("managed_jev") == "pro" + # It is informational, not a hosted feature row: no lock badge, no tick box. + assert "managed_jev" not in entitlements.labels() + + +def test_license_exposes_the_included_paid_benefit(monkeypatch): + pytest.importorskip("fastapi", reason="full-stack extra not installed") + from engraphis.routes import v2_api + + monkeypatch.setenv("ENGRAPHIS_CLOUD_PLAN", "pro") + payload = v2_api.get_license() + + assert payload["included_services"]["managed_jev"]["calls_per_person_per_day"] == 500 + assert "managed_jev" not in payload["features"] + + +def test_trial_and_grace_limits_match_the_commercial_manifest(): + manifest = commercial.manifest() + + assert entitlements.trial_days_by_plan() == manifest["trial"]["days_by_plan"] + assert entitlements.grace_hours() == manifest["entitlement_lifecycle"]["max_grace_hours"] + + +def test_the_release_gate_accepts_the_checked_in_contract(): + from scripts.check_commercial_manifest import _check_repository + + errors: list[str] = [] + _check_repository(commercial.manifest(), errors) + + assert errors == [] + + +def test_the_distribution_ships_the_contract(): + pyproject = (ROOT / "pyproject.toml").read_text(encoding="utf-8") + manifest = (ROOT / "MANIFEST.in").read_text(encoding="utf-8") + + assert "entitlement_contract.json" in pyproject + assert "include engraphis/entitlement_contract.json" in manifest + + +def test_license_reports_an_unknown_server_grant_without_unlocking_a_row(monkeypatch): + pytest.importorskip("fastapi", reason="full-stack extra not installed") + from engraphis.routes import v2_api + + monkeypatch.setattr( + v2_api, + "_plan_entitlement", + lambda: { + "plan": "pro", + "features": v2_api.entitled_features("pro"), + "unrecognized_features": ["future_capability"], + "source": "session", + "cloud_access_active": True, + "checked_at": 0.0, + "status": "", + "is_trial": False, + "trial_consumed": True, + "trial_ends_at": 0.0, + }, + ) + + payload = v2_api.get_license() + + assert payload["unrecognized_features"] == ["future_capability"] + assert "future_capability" not in payload["features"] + assert set(payload["features"]) <= set(payload["known_features"]) + + +def test_normalization_drops_an_unknown_grant_from_the_feature_list(monkeypatch): + pytest.importorskip("fastapi", reason="full-stack extra not installed") + from engraphis.routes import v2_api + + assert v2_api._normalized_features(["analytics", "future_capability"], "pro") == [ + "analytics" + ] + assert v2_api._unrecognized_features(["analytics", "future_capability"], "pro") == [ + "future_capability" + ] diff --git a/tests/test_hosted_plan_resolution.py b/tests/test_hosted_plan_resolution.py index ca312e1e..6c9616dd 100644 --- a/tests/test_hosted_plan_resolution.py +++ b/tests/test_hosted_plan_resolution.py @@ -43,6 +43,7 @@ pytest.importorskip("fastapi", reason="full-stack extra not installed") from engraphis import cloud_session # noqa: E402 +from engraphis.entitlements import features_for # noqa: E402 from engraphis.routes import v2_api # noqa: E402 REPO_ROOT = Path(__file__).resolve().parents[1] @@ -52,11 +53,9 @@ CONTROL_URL = "https://control.example.test" # The authoritative payload shape, mirrored from (read-only) -# the hosted entitlement contract the hosted entitlement response / the hosted plan-feature contract. +# engraphis/entitlement_contract.json. One import target, three consumers. SERVER_HOSTED_ENTITLEMENTS = { - "free": [], - "pro": ["analytics", "automation", "sync"], - "team": ["analytics", "automation", "sync", "team"], + plan: list(features_for(plan)) for plan in ("free", "pro", "team") } # The trial disclosure this client now depends on, mirrored from (read-only) # the hosted entitlement response and the hosted registration response. Both surfaces diff --git a/tests/test_licensing_launch.py b/tests/test_licensing_launch.py index f97846cc..cd9b506f 100644 --- a/tests/test_licensing_launch.py +++ b/tests/test_licensing_launch.py @@ -23,14 +23,15 @@ from engraphis.cloud_session import CloudSessionError # ── the private control plane's vocabulary, mirrored from (read-only) ────────── -# the hosted entitlement contract:20-28 and security.py:37-38. -# A mismatch here means a purchased feature silently never activates on the client. +# engraphis/entitlement_contract.json is the single checked-in mirror. Import it rather +# than maintaining a second copy: a mismatch here means a purchased feature silently never +# activates on the client. +from engraphis.entitlements import features_for + SERVER_PLANS = {"free", "pro", "team"} SERVER_PAID_PLANS = {"pro", "team"} SERVER_HOSTED_ENTITLEMENTS = { - "free": set(), - "pro": {"analytics", "automation", "export", "sync"}, - "team": {"analytics", "automation", "export", "sync", "team"}, + plan: set(features_for(plan)) for plan in ("free", "pro", "team") } # The compatibility scalar is the Pro duration; Team uses the per-plan offer. SERVER_TRIAL_DURATION_SECONDS = 604_800 diff --git a/tests/test_managed_denial_metadata.py b/tests/test_managed_denial_metadata.py new file mode 100644 index 00000000..f79e0abf --- /dev/null +++ b/tests/test_managed_denial_metadata.py @@ -0,0 +1,141 @@ +"""Denial metadata that turns a paid-feature rejection into a recovery action. + +The dashboard can only offer the right button if the client tells it *why* the call was +denied. These tests pin the fixed-vocabulary reason, the plan that actually sells the +feature, the account destination, and the promise that provider text never crosses. +""" +from __future__ import annotations + +import io +import urllib.error + +import pytest + +pytest.importorskip("fastapi", reason="full-stack extra not installed") + +from fastapi import HTTPException # noqa: E402 + +from engraphis import cloud_authz, licensing # noqa: E402 +from engraphis import cloud_features # noqa: E402 +from engraphis.cloud_features import CloudFeatureClient, CloudFeatureError # noqa: E402 +from engraphis.routes import v2_api # noqa: E402 + + +class _Opener: + def __init__(self, status: int, body: bytes): + self.status = status + self.body = body + + def open(self, request, timeout=None): + raise urllib.error.HTTPError( + "https://compute.example.test/private", self.status, "failure", {}, + io.BytesIO(self.body), + ) + + +def _client_response(monkeypatch, status: int, body: bytes) -> CloudFeatureError: + monkeypatch.setattr( + cloud_features, "build_pinned_https_opener", + lambda *handlers: _Opener(status, body), + ) + client = CloudFeatureClient("https://compute.example.test", "org_1", "token") + with pytest.raises(CloudFeatureError) as caught: + client._request("GET", "/private") + return caught.value + + +def test_a_known_reason_is_extracted_without_reflecting_the_body(monkeypatch): + error = _client_response( + monkeypatch, + 402, + b'{"reason":"entitlement_expired","detail":"internal https://backend.invalid"}', + ) + + assert error.status == 402 + assert error.reason == "entitlement_expired" + assert "internal" not in str(error) + assert "backend.invalid" not in str(error) + + +@pytest.mark.parametrize( + "body", + ( + b"not json", + b'{"reason":"not_a_real_reason"}', + b'{"detail":"missing reason"}', + b'{"reason":"' + b"x" * 200 + b'"}', + b'{"reason":"entitlement_expired"}' + b"x" * (cloud_features.MAX_ERROR_BODY_BYTES + 1), + ), +) +def test_unknown_or_oversized_reason_bodies_degrade_to_none(monkeypatch, body): + error = _client_response(monkeypatch, 402, body) + + assert error.reason is None + + +def test_a_402_names_the_plan_that_sells_the_feature(monkeypatch): + def fail(): + raise CloudFeatureError("denied", status=402, reason="entitlement_expired") + + with pytest.raises(HTTPException) as caught: + v2_api._managed_call(fail, feature="sync") + + detail = caught.value.detail + assert detail["reason"] == "entitlement_expired" + assert detail["tier_required"] == "pro" + assert detail["upgrade_url"] == licensing.upgrade_url("pro") + assert detail["account_url"] == licensing.account_url() + assert detail["transient"] is False + + +def test_a_full_seat_list_routes_to_the_account_portal_not_the_pro_plan(monkeypatch): + def fail(): + raise CloudFeatureError("denied", status=402, reason="seat_limit_exceeded") + + with pytest.raises(HTTPException) as caught: + v2_api._managed_call(fail, feature="automation") + + detail = caught.value.detail + assert detail["reason"] == "seat_limit_exceeded" + assert detail["tier_required"] == "team" + assert detail["upgrade_url"] == licensing.upgrade_url("team") + + +def test_a_session_denial_reconnects_at_the_account_portal(monkeypatch): + def fail(): + raise CloudFeatureError("denied", status=401) + + with pytest.raises(HTTPException) as caught: + v2_api._managed_call(fail, feature="analytics") + + detail = caught.value.detail + assert detail["reason"] == "token_stale" + assert detail["upgrade_url"] == licensing.account_url() + assert detail["account_url"] == licensing.account_url() + + +def test_a_role_denial_does_not_advertise_a_plan(monkeypatch): + def fail(): + raise CloudFeatureError("denied", status=403, reason="insufficient_role") + + with pytest.raises(HTTPException) as caught: + v2_api._managed_call(fail, feature="team") + + detail = caught.value.detail + assert detail["reason"] == "insufficient_role" + assert detail["tier_required"] == "team" + assert detail["upgrade_url"] == licensing.account_url() + + +def test_cloud_authz_fallback_is_the_real_account_portal(): + result = cloud_authz.interpret_denial(402, {"reason": "entitlement_expired"}) + + assert result["upgrade_url"] == licensing.account_url() + assert "/billing/upgrade" not in result["upgrade_url"] + + +def test_the_seat_message_tells_a_team_owner_to_add_a_seat(): + result = cloud_authz.interpret_denial(402, {"reason": "seat_limit_exceeded"}) + + assert "Add a seat" in result["message"] + assert "Upgrade your plan" not in result["message"] diff --git a/tests/test_release_infrastructure.py b/tests/test_release_infrastructure.py index eaa815c9..dda9f52f 100644 --- a/tests/test_release_infrastructure.py +++ b/tests/test_release_infrastructure.py @@ -215,8 +215,8 @@ def test_ci_and_release_audit_production_image_dependencies(): assert "tesseract-ocr" in _text("Dockerfile") assert ( _text("Dockerfile").splitlines()[1] - == "FROM python:3.11-slim@sha256:" - "b7a9e5f6a93d6d4a31f129bbd4bdee69efccb6ded0fb230d78aafaf37af98377 AS base" + == "FROM python:3.11.18-slim@sha256:" + "db20d8506e2f15b6db39b40b6015ca46bae855dec6dd326d462cfe79ae972893 AS base" ) assert "Verify production image OCR runtime" in ci assert "Verify production image OCR runtime" in release diff --git a/tests/test_sync_key_management.py b/tests/test_sync_key_management.py new file mode 100644 index 00000000..17acba4e --- /dev/null +++ b/tests/test_sync_key_management.py @@ -0,0 +1,125 @@ +"""In-product Cloud Sync key generation. + +A paying customer must be able to reach a working sync without editing an environment +variable. These tests pin the whole promise: one-time reveal, owner-only storage, explicit +rotation, environment precedence, and a status payload that never contains the key. +""" +from __future__ import annotations + +import base64 +import json +import os +import stat + +import pytest + +from engraphis.backends import sync_relay + +pytest.importorskip("fastapi", reason="full-stack extra not installed") + +from fastapi import HTTPException # noqa: E402 + +from engraphis.routes import v2_api # noqa: E402 + + +KEY_A = base64.urlsafe_b64encode(bytes(range(32))).decode("ascii").rstrip("=") +KEY_B = base64.urlsafe_b64encode(bytes(range(1, 33))).decode("ascii").rstrip("=") + + +@pytest.fixture() +def state_dir(monkeypatch, tmp_path): + root = tmp_path / ".engraphis" + monkeypatch.setenv("ENGRAPHIS_STATE_DIR", str(root)) + monkeypatch.delenv(sync_relay.SYNC_E2EE_KEY_ENV, raising=False) + return root + + +def _payload(response): + return json.loads(response.body) + + +def test_generated_key_round_trips_and_status_never_discloses_it(state_dir): + response = v2_api.create_sync_key() + payload = _payload(response) + + assert response.status_code == 200 + assert response.headers["cache-control"] == "no-store" + assert payload["configured"] is True + assert payload["key_source"] == "state_file" + assert payload["restart_required"] is False + assert len(payload["key"]) == 43 + assert (state_dir / "sync.e2ee_key").is_file() + + key = payload["key"] + assert sync_relay.configured_sync_e2ee_key() == sync_relay.decode_sync_e2ee_key(key) + + status = v2_api.sync_status() + assert status["key_source"] == "state_file" + assert status["has_key"] is True + assert status["key_state"] == "ready" + assert status["can_generate_key"] is False + assert status["can_rotate_key"] is True + assert key not in json.dumps(status) + + +def test_a_second_generation_refuses_to_overwrite(state_dir): + first = _payload(v2_api.create_sync_key())["key"] + + with pytest.raises(HTTPException) as caught: + v2_api.create_sync_key() + + assert caught.value.status_code == 409 + assert caught.value.detail["code"] == "sync_key_exists" + assert sync_relay.configured_sync_e2ee_key() == sync_relay.decode_sync_e2ee_key(first) + + +def test_rotation_is_explicit_and_replaces_the_saved_key(state_dir): + first = _payload(v2_api.create_sync_key())["key"] + second = _payload( + v2_api.create_sync_key(v2_api._SyncKeyReq(rotate=True)) + )["key"] + + assert second != first + assert sync_relay.configured_sync_e2ee_key() == sync_relay.decode_sync_e2ee_key(second) + + +def test_an_environment_key_keeps_precedence_and_cannot_be_replaced(state_dir, monkeypatch): + monkeypatch.setenv(sync_relay.SYNC_E2EE_KEY_ENV, KEY_A) + sync_relay.save_sync_e2ee_key(KEY_B) + + assert sync_relay.sync_e2ee_key_source() == "environment" + assert sync_relay.configured_sync_e2ee_key() == sync_relay.decode_sync_e2ee_key(KEY_A) + + with pytest.raises(HTTPException) as caught: + v2_api.create_sync_key() + + assert caught.value.detail["code"] == "sync_key_managed_by_environment" + + +def test_saved_key_is_owner_only(state_dir): + if os.name == "nt": + pytest.skip("POSIX permission semantics") + v2_api.create_sync_key() + + mode = stat.S_IMODE((state_dir / "sync.e2ee_key").stat().st_mode) + assert mode == 0o600 + + +def test_status_reports_an_invalid_saved_key_without_disclosing_it(state_dir): + state_dir.mkdir(parents=True, exist_ok=True) + (state_dir / "sync.e2ee_key").write_text("not-a-valid-key", encoding="utf-8") + + status = v2_api.sync_status() + + assert status["key_source"] == "state_file" + assert status["has_key"] is False + assert status["key_state"] == "invalid" + assert status["can_generate_key"] is False + assert status["can_rotate_key"] is True + assert "not-a-valid-key" not in json.dumps(status) + + +def test_configured_key_fails_closed_when_absent(state_dir): + with pytest.raises(sync_relay.RelayError): + sync_relay.configured_sync_e2ee_key() + assert sync_relay.sync_e2ee_key_source() == "none" diff --git a/tests/test_v2_service_binding.py b/tests/test_v2_service_binding.py index ffe6fc42..e23b61e0 100644 --- a/tests/test_v2_service_binding.py +++ b/tests/test_v2_service_binding.py @@ -161,7 +161,11 @@ def from_environment(workspace_id): monkeypatch.setattr(v2_api, "_service", _AutomationService()) monkeypatch.setattr(cloud_features, "CloudFeatureClient", _CloudFactory) - monkeypatch.setattr(v2_api, "_managed_call", lambda fn, *args, **kwargs: fn(*args, **kwargs)) + monkeypatch.setattr( + v2_api, + "_managed_call", + lambda fn, *args, feature=None, **kwargs: fn(*args, **kwargs), + ) result = v2_api.automation_get("acme") @@ -224,7 +228,11 @@ def save_phase(service, organization_id, workspace_id, value, **kwargs): monkeypatch.setattr(v2_api, "_service", _AutomationService()) monkeypatch.setattr(v2_api, "_AUTOMATION_BOOTSTRAP_LOCKS", {}) - monkeypatch.setattr(v2_api, "_managed_call", lambda fn, *args, **kwargs: fn(*args, **kwargs)) + monkeypatch.setattr( + v2_api, + "_managed_call", + lambda fn, *args, feature=None, **kwargs: fn(*args, **kwargs), + ) monkeypatch.setattr(cloud_features, "CloudFeatureClient", _CloudFactory) monkeypatch.setattr(cloud_features, "build_managed_snapshot", build_snapshot) monkeypatch.setattr(