From 0b3f2dd05ee07cbf91ff0d71ee8c7078635bcf0e Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools <274649000+Coding-Dev-Tools@users.noreply.github.com> Date: Tue, 3 Nov 2026 07:12:52 -0500 Subject: [PATCH 01/13] feat(entitlements): add one checked-in plan-feature contract The client kept three hand-maintained copies of the private hosted entitlement table, and each copy was only ever compared to itself. Add a single machine-readable mirror, load both runtime surfaces from it, and make the release gate compare it with the private contract so a new capability cannot silently disappear from a paying customer's dashboard. Local workspace export stays free and is explicitly kept out of the paid vocabulary. Unknown server grants are reported as unrecognized_features instead of being dropped without explanation. --- MANIFEST.in | 1 + engraphis/entitlement_contract.json | 86 +++++++++ engraphis/entitlements.py | 261 +++++++++++++++++++++++++++ engraphis/hosted_client.py | 26 +-- engraphis/routes/v2_api.py | 63 ++++--- pyproject.toml | 2 +- scripts/check_commercial_manifest.py | 182 ++++++++++++++++++- tests/test_client_launch.py | 11 +- tests/test_entitlement_contract.py | 163 +++++++++++++++++ tests/test_hosted_plan_resolution.py | 7 +- tests/test_licensing_launch.py | 11 +- 11 files changed, 751 insertions(+), 62 deletions(-) create mode 100644 engraphis/entitlement_contract.json create mode 100644 engraphis/entitlements.py create mode 100644 tests/test_entitlement_contract.py diff --git a/MANIFEST.in b/MANIFEST.in index 7b4a94914..9655b7522 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/engraphis/entitlement_contract.json b/engraphis/entitlement_contract.json new file mode 100644 index 000000000..7b11d5b39 --- /dev/null +++ b/engraphis/entitlement_contract.json @@ -0,0 +1,86 @@ +{ + "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" + } + }, + "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 000000000..2f2b09aa9 --- /dev/null +++ b/engraphis/entitlements.py @@ -0,0 +1,261 @@ +"""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, + ) + + 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 feature mapped to the plan that grants it.""" + + return {name: str(spec["plan"]) for name, spec in contract()["features"].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 a1931ac98..e97f9769e 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 3ec59f658..001d87192 100644 --- a/engraphis/routes/v2_api.py +++ b/engraphis/routes/v2_api.py @@ -29,6 +29,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 @@ -3140,35 +3141,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 +3172,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 +3427,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 +3464,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 +3554,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 +4065,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 +4121,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 +4206,9 @@ def get_license(): "plan": plan, "features": summary["features"], "known_features": dict(_FEATURE_LABELS), + # 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), diff --git a/pyproject.toml b/pyproject.toml index e23dd269a..b382beb60 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 923cd8abe..77abad2b4 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,171 @@ 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, + ) + + # 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 +297,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 +410,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/test_client_launch.py b/tests/test_client_launch.py index 5a267a13f..236d0327a 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 000000000..9c9de171e --- /dev/null +++ b/tests/test_entitlement_contract.py @@ -0,0 +1,163 @@ +"""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_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 ca312e1ec..6c9616dda 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 f97846ccd..cd9b506f1 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 From 68327ecd09dd8e9dcf1e20ce7d28477dfddcdf78 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools <274649000+Coding-Dev-Tools@users.noreply.github.com> Date: Tue, 3 Nov 2026 07:37:00 -0500 Subject: [PATCH 02/13] docs(licensing): describe the checked-in entitlement mirror Record that entitlement_contract.json is presentation metadata only and that the release gate compares it with the private hosted contract. --- CHANGELOG.md | 4 ++++ docs/LICENSING.md | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8acfdad20..e381b8eb0 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/docs/LICENSING.md b/docs/LICENSING.md index 0a41ed0ee..f370ef263 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 From d1a743bee5cd6438082b4a8a677fa338a92850a0 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools <274649000+Coding-Dev-Tools@users.noreply.github.com> Date: Tue, 3 Nov 2026 07:38:15 -0500 Subject: [PATCH 03/13] feat(dashboard): surface an unrecognized hosted grant instead of a lock A server capability this client predates was dropped from the feature list without explanation. Render license.unrecognized_features as an update prompt so a paying customer never reads a lock as "not bought". --- engraphis/dashboard_assets/ledger.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/engraphis/dashboard_assets/ledger.js b/engraphis/dashboard_assets/ledger.js index 5a1a15714..ba423f77f 100644 --- a/engraphis/dashboard_assets/ledger.js +++ b/engraphis/dashboard_assets/ledger.js @@ -6783,6 +6783,19 @@ }); target.append(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.')); } From 1943103f8edaeaff32c8230433602fe1fe550cb8 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools <274649000+Coding-Dev-Tools@users.noreply.github.com> Date: Tue, 3 Nov 2026 23:55:37 -0500 Subject: [PATCH 04/13] feat(sync): generate and rotate the Cloud Sync E2EE key in-product A paying customer can now create the key from the dashboard instead of editing config.env. The key is written once to an owner-only state file, shown once, never re-readable through the API, and never overrides an operator-provisioned environment key. Rotation stays explicit because data synced under the old key is unreadable until every device switches. --- docs/SYNC.md | 48 ++++++---- engraphis/backends/sync_relay.py | 72 +++++++++++++- engraphis/routes/v2_api.py | 150 +++++++++++++++++++++++++----- tests/test_sync_key_management.py | 125 +++++++++++++++++++++++++ 4 files changed, 350 insertions(+), 45 deletions(-) create mode 100644 tests/test_sync_key_management.py diff --git a/docs/SYNC.md b/docs/SYNC.md index b3432b508..c72475728 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/engraphis/backends/sync_relay.py b/engraphis/backends/sync_relay.py index 85be16f16..b0e3b1fba 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/routes/v2_api.py b/engraphis/routes/v2_api.py index 001d87192..22e4d8901 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 @@ -301,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: @@ -325,10 +330,38 @@ 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}: + # 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, @@ -1784,7 +1817,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. @@ -2344,7 +2377,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: @@ -2358,7 +2392,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") @@ -2458,13 +2493,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 = [] @@ -2503,7 +2540,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, @@ -2513,7 +2551,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): @@ -2525,10 +2564,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"]) @@ -2542,7 +2582,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, @@ -2569,8 +2610,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)) @@ -2597,9 +2639,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, @@ -2618,7 +2663,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, @@ -4329,11 +4375,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 @@ -4347,13 +4393,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" @@ -4369,6 +4416,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, @@ -4386,6 +4438,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/tests/test_sync_key_management.py b/tests/test_sync_key_management.py new file mode 100644 index 000000000..17acba4e3 --- /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" From 300c38b550ef33bb4db8daf3166f23cc3e351740 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools <274649000+Coding-Dev-Tools@users.noreply.github.com> Date: Tue, 3 Nov 2026 23:55:44 -0500 Subject: [PATCH 05/13] feat(dashboard): return actionable denial metadata for paid features Extract a fixed-vocabulary reason from a bounded error body, map it to the plan that sells the feature and the real account portal, and never forward provider text. The dashboard uses the reason to offer the right recovery: billing, seats, reconnect, or administrator. --- engraphis/cloud_authz.py | 10 +- engraphis/cloud_features.py | 48 +++++++-- engraphis/dashboard_assets/ledger.css | 1 + engraphis/dashboard_assets/ledger.js | 112 +++++++++++++++++++-- tests/test_dashboard_v2.py | 5 + tests/test_managed_denial_metadata.py | 140 ++++++++++++++++++++++++++ tests/test_v2_service_binding.py | 12 ++- 7 files changed, 309 insertions(+), 19 deletions(-) create mode 100644 tests/test_managed_denial_metadata.py diff --git a/engraphis/cloud_authz.py b/engraphis/cloud_authz.py index a834518a7..9dd78ae74 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." @@ -104,12 +106,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 321ab2e5e..24fa57003 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 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 != "unknown" 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/ledger.css b/engraphis/dashboard_assets/ledger.css index 707b7e7b0..13afed385 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 ba423f77f..dd275e048 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'); diff --git a/tests/test_dashboard_v2.py b/tests/test_dashboard_v2.py index c27b082ca..35346fe99 100644 --- a/tests/test_dashboard_v2.py +++ b/tests/test_dashboard_v2.py @@ -2415,6 +2415,11 @@ def fail_with(exc): "managed_cloud": True, "transient": False, "code": "cloud_unconfigured", + # A 401 carries the one safe recovery destination even when no feature name + # was supplied: the account portal, never a guessed plan checkout. + "reason": "token_stale", + "account_url": v2_api.licensing.account_url(), + "upgrade_url": v2_api.licensing.account_url(), } diff --git a/tests/test_managed_denial_metadata.py b/tests/test_managed_denial_metadata.py new file mode 100644 index 000000000..6ead29bb9 --- /dev/null +++ b/tests/test_managed_denial_metadata.py @@ -0,0 +1,140 @@ +"""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":"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_v2_service_binding.py b/tests/test_v2_service_binding.py index ffe6fc420..e23b61e05 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( From 1b14ba8ebc51ce580c06af85fff28359653f996e Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools <274649000+Coding-Dev-Tools@users.noreply.github.com> Date: Wed, 4 Nov 2026 00:09:30 -0500 Subject: [PATCH 06/13] fix(dashboard): keep cloud-unconfigured errors free of sales metadata A local cloud_unconfigured 401 is not a paid-feature denial. Keep the structured code-only error so the dashboard does not offer a token refresh or an upgrade that cannot fix the missing account connection. --- engraphis/routes/v2_api.py | 4 +++- tests/test_dashboard_v2.py | 5 ----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/engraphis/routes/v2_api.py b/engraphis/routes/v2_api.py index 22e4d8901..82ae95767 100644 --- a/engraphis/routes/v2_api.py +++ b/engraphis/routes/v2_api.py @@ -337,7 +337,9 @@ def _managed_call(fn, *args, feature: Optional[str] = None, **kwargs): detail["code"] = exc.code if reason: detail["reason"] = reason - if exc.status in {401, 402, 403}: + 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. diff --git a/tests/test_dashboard_v2.py b/tests/test_dashboard_v2.py index 35346fe99..c27b082ca 100644 --- a/tests/test_dashboard_v2.py +++ b/tests/test_dashboard_v2.py @@ -2415,11 +2415,6 @@ def fail_with(exc): "managed_cloud": True, "transient": False, "code": "cloud_unconfigured", - # A 401 carries the one safe recovery destination even when no feature name - # was supplied: the account portal, never a guessed plan checkout. - "reason": "token_stale", - "account_url": v2_api.licensing.account_url(), - "upgrade_url": v2_api.licensing.account_url(), } From 0bc5e44a823c181d884b787f0449e374a448cd5d Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools <274649000+Coding-Dev-Tools@users.noreply.github.com> Date: Wed, 4 Nov 2026 00:10:22 -0500 Subject: [PATCH 07/13] docs(benchmarks): refresh offline evidence for the paid-plans changes The entitlement, sync-key, and denial-metadata changes alter checked-in producer files, so generate a new immutable artifact, recompute the suite digest and artifact SHA, and repin the README, benchmark guide, tests, and both rendered SVG/PNG provenance pairs. --- BENCHMARKS.md | 12 +- README.md | 2 +- .../offline-fixtures-paid-plans-20261104.json | 702 ++++++++++++++++++ ...e-fixtures-paid-plans-20261104.json.sha256 | 1 + docs/images/context-efficiency.png | Bin 61488 -> 61488 bytes docs/images/context-efficiency.svg | 4 +- .../images/evidence-backed-agent-examples.png | Bin 273392 -> 273392 bytes .../images/evidence-backed-agent-examples.svg | 4 +- tests/test_benchmark_evidence.py | 4 +- 9 files changed, 716 insertions(+), 13 deletions(-) create mode 100644 docs/benchmark-evidence/offline-fixtures-paid-plans-20261104.json create mode 100644 docs/benchmark-evidence/offline-fixtures-paid-plans-20261104.json.sha256 diff --git a/BENCHMARKS.md b/BENCHMARKS.md index 6cb1a2c76..98a4b39e3 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 +`e0149fad454938b5f1e33bec1670ab8aba979e34178fb1080d8763f12678401d`, 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 +`af1df1560f96b0a08a23ca4af3604e4e381401c9ad3ebec503d382c0b2a595f5`. 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/README.md b/README.md index b38847f32..fda8f0076 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 e0149fad4549; 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/benchmark-evidence/offline-fixtures-paid-plans-20261104.json b/docs/benchmark-evidence/offline-fixtures-paid-plans-20261104.json new file mode 100644 index 000000000..6d7429a01 --- /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": "af1df1560f96b0a08a23ca4af3604e4e381401c9ad3ebec503d382c0b2a595f5", + "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": "ae2f3c7ba4cf646e8115e4b4f0ab5127cfdfa897964f827d489138790371aff1", + "engraphis/cloud_features.py": "907d640810aa720374e0642b32f761fc3a2233750e0eb84ca49c1467b8f0d6a9", + "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": "64896b60c9a6fee4035d8fa158896c8750fb5eeaab8bfc8599da6c727b30b2e3", + "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": "c15bb131743725c2a8216d98fdce5a77b40f6d5d8227e4864a50a6fb5db42e00", + "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 000000000..01d3faea3 --- /dev/null +++ b/docs/benchmark-evidence/offline-fixtures-paid-plans-20261104.json.sha256 @@ -0,0 +1 @@ +e0149fad454938b5f1e33bec1670ab8aba979e34178fb1080d8763f12678401d offline-fixtures-paid-plans-20261104.json diff --git a/docs/images/context-efficiency.png b/docs/images/context-efficiency.png index a041b6868fed93b18b4632cb602f944d8dc82e17..c9bc79c072fe5d5b49242630ec45f6888bc1b7ca 100644 GIT binary patch delta 153 zcmV~$yA{GP3;@6)b5O8>WLdwO6s%!c_9|(^CZS;*UP?MP;Bj}H%W*FIZ5@EsXUodf z$$}>iq8J@Tp@p?*u0E)u+6)APG!>dyM6CkmZ#Q~um$hc;DUQt3BLWy3#F!){EERpS bw4maJnS_|Jn+=T)Nky>s*Y>_XKlkq+ 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 e0149fad454938b5f1e33bec1670ab8aba979e34178fb1080d8763f12678401d. @@ -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 e0149fad4549 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 479118856da0d8576587ca3244012a6a83b6fe05..03728eea886ab55c935baa028b831f96e7e5b9a9 100644 GIT binary patch delta 165 zcmWNLyAi@L3_y=hL74@ZfJU-qOQt0guz?lJV{k{2qYc9_OIC14Cg5=W-n)C-I&AB( z+(rYWRCA#~$%}@H=Dpg4ks+&O&54tFLYArs0!29biY~GcVEHZL->>rs$`pFb&M7eF uBxYq&jFgaq3z?Km2%S~mH3*$0QemgQAJFxCe>~gVc5~a$?eJ;G)8!9d^fNU8 delta 165 zcmV~$yAi@L3;@vMQ&46BN-7%3vSq7I7Evr~1bT`b6?ZTU1293#tl*Lnc<;9jzioJp z6NH3?7_cS{MJb!QP+$c{KnO9;Rg_#j+l-AGTlB)*RJ}68^E-_1$9c*rnOdw)QG^mu uEsT~aHX{;oCaFnW-BKu?GZaoG#$ZM>68i13uJ^w5-O~3 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: e0149fad454938b5f1e33bec1670ab8aba979e34178fb1080d8763f12678401d. @@ -47,5 +47,5 @@ Reproduce: eval.chunking_eval + eval.grounded - SHA256 025183601c5a28a37daf9326a061010244bc9a3f9f7db1e3e6d4f866eaafc660 + SHA256 e0149fad454938b5f1e33bec1670ab8aba979e34178fb1080d8763f12678401d diff --git a/tests/test_benchmark_evidence.py b/tests/test_benchmark_evidence.py index bede7085c..4cf1b562e 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 = "e0149fad454938b5f1e33bec1670ab8aba979e34178fb1080d8763f12678401d" @pytest.fixture(scope="module") From 560c56282d08f42fc9b747318c7f5e3c4c986a86 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools <274649000+Coding-Dev-Tools@users.noreply.github.com> Date: Wed, 4 Nov 2026 00:18:52 -0500 Subject: [PATCH 08/13] fix(docker): update Python base image to 3.11.18 (#314) CVE-2026-115557 in the Python 3.11.17 runtime is fixed in 3.11.18. Pin the new multi-arch digest so the release grype gate can pass. --- Dockerfile | 2 +- tests/test_release_infrastructure.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0b5e611e4..6f7796894 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/tests/test_release_infrastructure.py b/tests/test_release_infrastructure.py index eaa815c93..dda9f52f3 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 From a24dc3ea4b1c814dcbfc3e0827555148be6653a7 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools <274649000+Coding-Dev-Tools@users.noreply.github.com> Date: Wed, 4 Nov 2026 00:21:37 -0500 Subject: [PATCH 09/13] test(e2e): expect the in-product sync-key recovery copy The dashboard now directs an invalid saved key to generate a replacement instead of editing config.env. Update the browser expectation to the new recovery text. --- tests/e2e/ledger.spec.js | 176 ++++++++++++++++++++------------------- 1 file changed, 89 insertions(+), 87 deletions(-) diff --git a/tests/e2e/ledger.spec.js b/tests/e2e/ledger.spec.js index b453d2eb9..0bbf271ad 100644 --- a/tests/e2e/ledger.spec.js +++ b/tests/e2e/ledger.spec.js @@ -1634,7 +1634,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([]); }); @@ -4104,89 +4106,89 @@ for (const withReadableFile of [true, false]) { expect(errors).toEqual([]); }); } - - -test('Automation editor preserves zero idle and enforces the actual API numeric bounds', async ({ page }) => { - const requests = await mockApi(page, { - automationPolicy: { enabled: false, cadence_hours: 1, dream: false, - dream_min_new: 2, dream_idle_minutes: 0, infer: false, version: 1 }, - }); - await page.goto('/'); - await page.getByRole('button', { name: 'Settings' }).click(); - await page.getByRole('tab', { name: 'Automation' }).click(); - const fields = [ - ['#automation-cadence', '1', '720', '1'], - ['#automation-dream-min', '2', '10000', '2'], - ['#automation-dream-idle', '0', '10080', '0'], - ]; - for (const [selector, minimum, maximum, value] of fields) { - const field = page.locator(selector); - await expect(field).toHaveAttribute('min', minimum); - await expect(field).toHaveAttribute('max', maximum); - await expect(field).toHaveAttribute('required', ''); - await expect(field).toHaveValue(value); - for (const invalid of [String(Number(minimum) - 1), String(Number(maximum) + 1), '', '2.5']) { - await field.fill(invalid); - expect(await field.evaluate(input => input.checkValidity())).toBe(false); - } - await field.fill(maximum); - expect(await field.evaluate(input => input.checkValidity())).toBe(true); - } - await page.locator('#automation-dream-idle').fill('0'); - await page.getByRole('button', { name: 'Save hosted policy', exact: true }).click(); - await expect.poll(() => requests.automationPolicies.length).toBe(1); - expect(requests.automationPolicies[0]).toMatchObject({ cadence_hours: 720, - dream_min_new: 10000, dream_idle_minutes: 0, dream_enabled: false, infer: false }); - await page.reload(); - await page.getByRole('button', { name: 'Settings' }).click(); - await page.getByRole('tab', { name: 'Automation' }).click(); - await expect(page.locator('#automation-dream-idle')).toHaveValue('0'); -}); - -test('Automation editor uses service defaults for missing or null numeric values', async ({ page }) => { - await mockApi(page, { automationPolicy: { enabled: false, cadence_hours: null, - dream_enabled: null, dream_min_new: null, dream_idle_minutes: null, infer: null, version: 1 } }); - await page.goto('/'); - await page.getByRole('button', { name: 'Settings' }).click(); - await page.getByRole('tab', { name: 'Automation' }).click(); - await expect(page.locator('#automation-cadence')).toHaveValue('24'); - await expect(page.locator('#automation-dream-min')).toHaveValue('25'); - await expect(page.locator('#automation-dream-idle')).toHaveValue('15'); - await expect(page.locator('#automation-dream')).toBeChecked(); -}); - - -for (const cadenceMinutes of [15, 90]) { - test(`Automation cadence ${cadenceMinutes} minutes survives unrelated saves and explicit one-hour edits`, async ({ page }) => { - const errors = browserErrors(page); - const requests = await mockApi(page, { - automationPolicy: { enabled: false, cadence_minutes: cadenceMinutes, cadence_hours: 1, - dream: false, dream_min_new: 2, dream_idle_minutes: 0, infer: false, version: 1 }, - }); - await page.goto('/'); - await page.getByRole('button', { name: 'Settings' }).click(); - await page.getByRole('tab', { name: 'Automation' }).click(); - const result = page.locator('#automation-result'); - const hours = page.locator('#automation-cadence'); - await expect(hours).toHaveValue('1'); - await page.locator('#automation-dream-min').fill('3'); - await page.getByRole('button', { name: 'Save hosted policy', exact: true }).click(); - await expect.poll(() => requests.automationPolicies.length).toBe(1); - expect(requests.automationPolicies[0]).not.toHaveProperty('cadence_hours'); - expect(requests.automationPolicies[0]).toMatchObject({ dream_min_new: 3, dream_idle_minutes: 0 }); - await expect(result).toContainText(`Current cadence: every ${cadenceMinutes} minutes.`); - - await page.reload(); - await page.getByRole('button', { name: 'Settings' }).click(); - await page.getByRole('tab', { name: 'Automation' }).click(); - await expect(result).toContainText(`Current cadence: every ${cadenceMinutes} minutes.`); - await expect(hours).toHaveValue('1'); - // Real input intent matters even though the initial display is already 1. - await hours.fill('1'); - await page.getByRole('button', { name: 'Save hosted policy', exact: true }).click(); - await expect.poll(() => requests.automationPolicies.length).toBe(2); - expect(requests.automationPolicies[1]).toMatchObject({ cadence_hours: 1, dream_min_new: 3 }); - await expect(result).toContainText('Current cadence: every 60 minutes.'); - expect(errors).toEqual([]); - }); -} + + +test('Automation editor preserves zero idle and enforces the actual API numeric bounds', async ({ page }) => { + const requests = await mockApi(page, { + automationPolicy: { enabled: false, cadence_hours: 1, dream: false, + dream_min_new: 2, dream_idle_minutes: 0, infer: false, version: 1 }, + }); + await page.goto('/'); + await page.getByRole('button', { name: 'Settings' }).click(); + await page.getByRole('tab', { name: 'Automation' }).click(); + const fields = [ + ['#automation-cadence', '1', '720', '1'], + ['#automation-dream-min', '2', '10000', '2'], + ['#automation-dream-idle', '0', '10080', '0'], + ]; + for (const [selector, minimum, maximum, value] of fields) { + const field = page.locator(selector); + await expect(field).toHaveAttribute('min', minimum); + await expect(field).toHaveAttribute('max', maximum); + await expect(field).toHaveAttribute('required', ''); + await expect(field).toHaveValue(value); + for (const invalid of [String(Number(minimum) - 1), String(Number(maximum) + 1), '', '2.5']) { + await field.fill(invalid); + expect(await field.evaluate(input => input.checkValidity())).toBe(false); + } + await field.fill(maximum); + expect(await field.evaluate(input => input.checkValidity())).toBe(true); + } + await page.locator('#automation-dream-idle').fill('0'); + await page.getByRole('button', { name: 'Save hosted policy', exact: true }).click(); + await expect.poll(() => requests.automationPolicies.length).toBe(1); + expect(requests.automationPolicies[0]).toMatchObject({ cadence_hours: 720, + dream_min_new: 10000, dream_idle_minutes: 0, dream_enabled: false, infer: false }); + await page.reload(); + await page.getByRole('button', { name: 'Settings' }).click(); + await page.getByRole('tab', { name: 'Automation' }).click(); + await expect(page.locator('#automation-dream-idle')).toHaveValue('0'); +}); + +test('Automation editor uses service defaults for missing or null numeric values', async ({ page }) => { + await mockApi(page, { automationPolicy: { enabled: false, cadence_hours: null, + dream_enabled: null, dream_min_new: null, dream_idle_minutes: null, infer: null, version: 1 } }); + await page.goto('/'); + await page.getByRole('button', { name: 'Settings' }).click(); + await page.getByRole('tab', { name: 'Automation' }).click(); + await expect(page.locator('#automation-cadence')).toHaveValue('24'); + await expect(page.locator('#automation-dream-min')).toHaveValue('25'); + await expect(page.locator('#automation-dream-idle')).toHaveValue('15'); + await expect(page.locator('#automation-dream')).toBeChecked(); +}); + + +for (const cadenceMinutes of [15, 90]) { + test(`Automation cadence ${cadenceMinutes} minutes survives unrelated saves and explicit one-hour edits`, async ({ page }) => { + const errors = browserErrors(page); + const requests = await mockApi(page, { + automationPolicy: { enabled: false, cadence_minutes: cadenceMinutes, cadence_hours: 1, + dream: false, dream_min_new: 2, dream_idle_minutes: 0, infer: false, version: 1 }, + }); + await page.goto('/'); + await page.getByRole('button', { name: 'Settings' }).click(); + await page.getByRole('tab', { name: 'Automation' }).click(); + const result = page.locator('#automation-result'); + const hours = page.locator('#automation-cadence'); + await expect(hours).toHaveValue('1'); + await page.locator('#automation-dream-min').fill('3'); + await page.getByRole('button', { name: 'Save hosted policy', exact: true }).click(); + await expect.poll(() => requests.automationPolicies.length).toBe(1); + expect(requests.automationPolicies[0]).not.toHaveProperty('cadence_hours'); + expect(requests.automationPolicies[0]).toMatchObject({ dream_min_new: 3, dream_idle_minutes: 0 }); + await expect(result).toContainText(`Current cadence: every ${cadenceMinutes} minutes.`); + + await page.reload(); + await page.getByRole('button', { name: 'Settings' }).click(); + await page.getByRole('tab', { name: 'Automation' }).click(); + await expect(result).toContainText(`Current cadence: every ${cadenceMinutes} minutes.`); + await expect(hours).toHaveValue('1'); + // Real input intent matters even though the initial display is already 1. + await hours.fill('1'); + await page.getByRole('button', { name: 'Save hosted policy', exact: true }).click(); + await expect.poll(() => requests.automationPolicies.length).toBe(2); + expect(requests.automationPolicies[1]).toMatchObject({ cadence_hours: 1, dream_min_new: 3 }); + await expect(result).toContainText('Current cadence: every 60 minutes.'); + expect(errors).toEqual([]); + }); +} From a401b7a6be18084da2bd7d6625485d6f1bff1a8b Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools <274649000+Coding-Dev-Tools@users.noreply.github.com> Date: Wed, 4 Nov 2026 00:22:31 -0500 Subject: [PATCH 10/13] chore(e2e): restore the original line endings around the sync-key update The expectation edit rewrote a mixed-ending block as CRLF and inflated the diff. Restore the file's prior endings so the net change is only the recovery copy assertion. --- tests/e2e/ledger.spec.js | 172 +++++++++++++++++++-------------------- 1 file changed, 86 insertions(+), 86 deletions(-) diff --git a/tests/e2e/ledger.spec.js b/tests/e2e/ledger.spec.js index 0bbf271ad..8f589ab70 100644 --- a/tests/e2e/ledger.spec.js +++ b/tests/e2e/ledger.spec.js @@ -4106,89 +4106,89 @@ for (const withReadableFile of [true, false]) { expect(errors).toEqual([]); }); } - - -test('Automation editor preserves zero idle and enforces the actual API numeric bounds', async ({ page }) => { - const requests = await mockApi(page, { - automationPolicy: { enabled: false, cadence_hours: 1, dream: false, - dream_min_new: 2, dream_idle_minutes: 0, infer: false, version: 1 }, - }); - await page.goto('/'); - await page.getByRole('button', { name: 'Settings' }).click(); - await page.getByRole('tab', { name: 'Automation' }).click(); - const fields = [ - ['#automation-cadence', '1', '720', '1'], - ['#automation-dream-min', '2', '10000', '2'], - ['#automation-dream-idle', '0', '10080', '0'], - ]; - for (const [selector, minimum, maximum, value] of fields) { - const field = page.locator(selector); - await expect(field).toHaveAttribute('min', minimum); - await expect(field).toHaveAttribute('max', maximum); - await expect(field).toHaveAttribute('required', ''); - await expect(field).toHaveValue(value); - for (const invalid of [String(Number(minimum) - 1), String(Number(maximum) + 1), '', '2.5']) { - await field.fill(invalid); - expect(await field.evaluate(input => input.checkValidity())).toBe(false); - } - await field.fill(maximum); - expect(await field.evaluate(input => input.checkValidity())).toBe(true); - } - await page.locator('#automation-dream-idle').fill('0'); - await page.getByRole('button', { name: 'Save hosted policy', exact: true }).click(); - await expect.poll(() => requests.automationPolicies.length).toBe(1); - expect(requests.automationPolicies[0]).toMatchObject({ cadence_hours: 720, - dream_min_new: 10000, dream_idle_minutes: 0, dream_enabled: false, infer: false }); - await page.reload(); - await page.getByRole('button', { name: 'Settings' }).click(); - await page.getByRole('tab', { name: 'Automation' }).click(); - await expect(page.locator('#automation-dream-idle')).toHaveValue('0'); -}); - -test('Automation editor uses service defaults for missing or null numeric values', async ({ page }) => { - await mockApi(page, { automationPolicy: { enabled: false, cadence_hours: null, - dream_enabled: null, dream_min_new: null, dream_idle_minutes: null, infer: null, version: 1 } }); - await page.goto('/'); - await page.getByRole('button', { name: 'Settings' }).click(); - await page.getByRole('tab', { name: 'Automation' }).click(); - await expect(page.locator('#automation-cadence')).toHaveValue('24'); - await expect(page.locator('#automation-dream-min')).toHaveValue('25'); - await expect(page.locator('#automation-dream-idle')).toHaveValue('15'); - await expect(page.locator('#automation-dream')).toBeChecked(); -}); - - -for (const cadenceMinutes of [15, 90]) { - test(`Automation cadence ${cadenceMinutes} minutes survives unrelated saves and explicit one-hour edits`, async ({ page }) => { - const errors = browserErrors(page); - const requests = await mockApi(page, { - automationPolicy: { enabled: false, cadence_minutes: cadenceMinutes, cadence_hours: 1, - dream: false, dream_min_new: 2, dream_idle_minutes: 0, infer: false, version: 1 }, - }); - await page.goto('/'); - await page.getByRole('button', { name: 'Settings' }).click(); - await page.getByRole('tab', { name: 'Automation' }).click(); - const result = page.locator('#automation-result'); - const hours = page.locator('#automation-cadence'); - await expect(hours).toHaveValue('1'); - await page.locator('#automation-dream-min').fill('3'); - await page.getByRole('button', { name: 'Save hosted policy', exact: true }).click(); - await expect.poll(() => requests.automationPolicies.length).toBe(1); - expect(requests.automationPolicies[0]).not.toHaveProperty('cadence_hours'); - expect(requests.automationPolicies[0]).toMatchObject({ dream_min_new: 3, dream_idle_minutes: 0 }); - await expect(result).toContainText(`Current cadence: every ${cadenceMinutes} minutes.`); - - await page.reload(); - await page.getByRole('button', { name: 'Settings' }).click(); - await page.getByRole('tab', { name: 'Automation' }).click(); - await expect(result).toContainText(`Current cadence: every ${cadenceMinutes} minutes.`); - await expect(hours).toHaveValue('1'); - // Real input intent matters even though the initial display is already 1. - await hours.fill('1'); - await page.getByRole('button', { name: 'Save hosted policy', exact: true }).click(); - await expect.poll(() => requests.automationPolicies.length).toBe(2); - expect(requests.automationPolicies[1]).toMatchObject({ cadence_hours: 1, dream_min_new: 3 }); - await expect(result).toContainText('Current cadence: every 60 minutes.'); - expect(errors).toEqual([]); - }); -} + + +test('Automation editor preserves zero idle and enforces the actual API numeric bounds', async ({ page }) => { + const requests = await mockApi(page, { + automationPolicy: { enabled: false, cadence_hours: 1, dream: false, + dream_min_new: 2, dream_idle_minutes: 0, infer: false, version: 1 }, + }); + await page.goto('/'); + await page.getByRole('button', { name: 'Settings' }).click(); + await page.getByRole('tab', { name: 'Automation' }).click(); + const fields = [ + ['#automation-cadence', '1', '720', '1'], + ['#automation-dream-min', '2', '10000', '2'], + ['#automation-dream-idle', '0', '10080', '0'], + ]; + for (const [selector, minimum, maximum, value] of fields) { + const field = page.locator(selector); + await expect(field).toHaveAttribute('min', minimum); + await expect(field).toHaveAttribute('max', maximum); + await expect(field).toHaveAttribute('required', ''); + await expect(field).toHaveValue(value); + for (const invalid of [String(Number(minimum) - 1), String(Number(maximum) + 1), '', '2.5']) { + await field.fill(invalid); + expect(await field.evaluate(input => input.checkValidity())).toBe(false); + } + await field.fill(maximum); + expect(await field.evaluate(input => input.checkValidity())).toBe(true); + } + await page.locator('#automation-dream-idle').fill('0'); + await page.getByRole('button', { name: 'Save hosted policy', exact: true }).click(); + await expect.poll(() => requests.automationPolicies.length).toBe(1); + expect(requests.automationPolicies[0]).toMatchObject({ cadence_hours: 720, + dream_min_new: 10000, dream_idle_minutes: 0, dream_enabled: false, infer: false }); + await page.reload(); + await page.getByRole('button', { name: 'Settings' }).click(); + await page.getByRole('tab', { name: 'Automation' }).click(); + await expect(page.locator('#automation-dream-idle')).toHaveValue('0'); +}); + +test('Automation editor uses service defaults for missing or null numeric values', async ({ page }) => { + await mockApi(page, { automationPolicy: { enabled: false, cadence_hours: null, + dream_enabled: null, dream_min_new: null, dream_idle_minutes: null, infer: null, version: 1 } }); + await page.goto('/'); + await page.getByRole('button', { name: 'Settings' }).click(); + await page.getByRole('tab', { name: 'Automation' }).click(); + await expect(page.locator('#automation-cadence')).toHaveValue('24'); + await expect(page.locator('#automation-dream-min')).toHaveValue('25'); + await expect(page.locator('#automation-dream-idle')).toHaveValue('15'); + await expect(page.locator('#automation-dream')).toBeChecked(); +}); + + +for (const cadenceMinutes of [15, 90]) { + test(`Automation cadence ${cadenceMinutes} minutes survives unrelated saves and explicit one-hour edits`, async ({ page }) => { + const errors = browserErrors(page); + const requests = await mockApi(page, { + automationPolicy: { enabled: false, cadence_minutes: cadenceMinutes, cadence_hours: 1, + dream: false, dream_min_new: 2, dream_idle_minutes: 0, infer: false, version: 1 }, + }); + await page.goto('/'); + await page.getByRole('button', { name: 'Settings' }).click(); + await page.getByRole('tab', { name: 'Automation' }).click(); + const result = page.locator('#automation-result'); + const hours = page.locator('#automation-cadence'); + await expect(hours).toHaveValue('1'); + await page.locator('#automation-dream-min').fill('3'); + await page.getByRole('button', { name: 'Save hosted policy', exact: true }).click(); + await expect.poll(() => requests.automationPolicies.length).toBe(1); + expect(requests.automationPolicies[0]).not.toHaveProperty('cadence_hours'); + expect(requests.automationPolicies[0]).toMatchObject({ dream_min_new: 3, dream_idle_minutes: 0 }); + await expect(result).toContainText(`Current cadence: every ${cadenceMinutes} minutes.`); + + await page.reload(); + await page.getByRole('button', { name: 'Settings' }).click(); + await page.getByRole('tab', { name: 'Automation' }).click(); + await expect(result).toContainText(`Current cadence: every ${cadenceMinutes} minutes.`); + await expect(hours).toHaveValue('1'); + // Real input intent matters even though the initial display is already 1. + await hours.fill('1'); + await page.getByRole('button', { name: 'Save hosted policy', exact: true }).click(); + await expect.poll(() => requests.automationPolicies.length).toBe(2); + expect(requests.automationPolicies[1]).toMatchObject({ cadence_hours: 1, dream_min_new: 3 }); + await expect(result).toContainText('Current cadence: every 60 minutes.'); + expect(errors).toEqual([]); + }); +} From 39e22be6c158d6653729c8fd4f7a39069742b283 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools <274649000+Coding-Dev-Tools@users.noreply.github.com> Date: Wed, 4 Nov 2026 00:43:57 -0500 Subject: [PATCH 11/13] feat(paid): surface the included Jev benefit and trial runway - Publish Managed Jev as an included paid benefit (500 calls per person per day) in the entitlement contract, license payload, plan cards, comparison table, and plan status. - Show the days remaining in a live trial instead of only the end date. - Restrict denial reasons to the fixed vocabulary before logging or returning them. - Cover the in-product sync-key flow, the trial runway, and the included benefit with Python and browser tests. --- engraphis/cloud_authz.py | 8 +++ engraphis/cloud_features.py | 4 +- engraphis/dashboard_assets/index.html | 1 + engraphis/dashboard_assets/ledger.js | 39 +++++++++++++- engraphis/entitlement_contract.json | 7 +++ engraphis/entitlements.py | 44 +++++++++++++++- engraphis/routes/v2_api.py | 3 ++ scripts/check_commercial_manifest.py | 22 ++++++++ tests/e2e/ledger.spec.js | 74 ++++++++++++++++++++++++++- tests/test_entitlement_contract.py | 21 ++++++++ tests/test_managed_denial_metadata.py | 1 + 11 files changed, 218 insertions(+), 6 deletions(-) diff --git a/engraphis/cloud_authz.py b/engraphis/cloud_authz.py index 9dd78ae74..da3ee2bc5 100644 --- a/engraphis/cloud_authz.py +++ b/engraphis/cloud_authz.py @@ -64,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( @@ -91,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: diff --git a/engraphis/cloud_features.py b/engraphis/cloud_features.py index 24fa57003..b6051b0c3 100644 --- a/engraphis/cloud_features.py +++ b/engraphis/cloud_features.py @@ -153,11 +153,11 @@ def _denial_reason(status: int, raw: bytes) -> Optional[str]: if not isinstance(body, dict): return None try: - from engraphis.cloud_authz import interpret_denial + 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 != "unknown" else 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: diff --git a/engraphis/dashboard_assets/index.html b/engraphis/dashboard_assets/index.html index 95997f221..e176b2289 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.js b/engraphis/dashboard_assets/ledger.js index dd275e048..89031df5a 100644 --- a/engraphis/dashboard_assets/ledger.js +++ b/engraphis/dashboard_assets/ledger.js @@ -6736,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: [ @@ -6814,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; @@ -6858,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]; @@ -6879,6 +6893,29 @@ }); 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) diff --git a/engraphis/entitlement_contract.json b/engraphis/entitlement_contract.json index 7b11d5b39..4ca0e8bba 100644 --- a/engraphis/entitlement_contract.json +++ b/engraphis/entitlement_contract.json @@ -76,6 +76,13 @@ "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, diff --git a/engraphis/entitlements.py b/engraphis/entitlements.py index 2f2b09aa9..0d3f67b21 100644 --- a/engraphis/entitlements.py +++ b/engraphis/entitlements.py @@ -145,6 +145,27 @@ def _validate(data: object) -> None: "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") @@ -188,9 +209,28 @@ def feature_spec(feature: object) -> Optional[dict]: def required_plans() -> dict: - """Return every known feature mapped to the plan that grants it.""" + """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. - return {name: str(spec["plan"]) for name, spec in contract()["features"].items()} + 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: diff --git a/engraphis/routes/v2_api.py b/engraphis/routes/v2_api.py index 82ae95767..fc14c6cdb 100644 --- a/engraphis/routes/v2_api.py +++ b/engraphis/routes/v2_api.py @@ -4254,6 +4254,9 @@ 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"], diff --git a/scripts/check_commercial_manifest.py b/scripts/check_commercial_manifest.py index 77abad2b4..cecad1216 100644 --- a/scripts/check_commercial_manifest.py +++ b/scripts/check_commercial_manifest.py @@ -164,6 +164,28 @@ def _check_entitlement_contract(manifest: dict, errors: list[str]) -> dict: "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") diff --git a/tests/e2e/ledger.spec.js b/tests/e2e/ledger.spec.js index 8f589ab70..c76e9375a 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: { @@ -2827,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; @@ -2868,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_entitlement_contract.py b/tests/test_entitlement_contract.py index 9c9de171e..2a5bbf467 100644 --- a/tests/test_entitlement_contract.py +++ b/tests/test_entitlement_contract.py @@ -99,6 +99,27 @@ def test_unknown_grants_are_reported_but_never_become_feature_rows(): 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() diff --git a/tests/test_managed_denial_metadata.py b/tests/test_managed_denial_metadata.py index 6ead29bb9..f79e0abf6 100644 --- a/tests/test_managed_denial_metadata.py +++ b/tests/test_managed_denial_metadata.py @@ -63,6 +63,7 @@ def test_a_known_reason_is_extracted_without_reflecting_the_body(monkeypatch): 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), ), ) From 087eab280f1dbfe379eac87482592a49dc0111ec Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools <274649000+Coding-Dev-Tools@users.noreply.github.com> Date: Wed, 4 Nov 2026 00:45:50 -0500 Subject: [PATCH 12/13] docs(benchmarks): rebind paid-plans evidence to the included-services change The included-services contract, entitlement validation, and dashboard copy change four checked-in producer files. Re-record their hashes, recompute the suite digest and artifact SHA, and repin the README, benchmark guide, tests, and both SVG/PNG provenance pairs. --- BENCHMARKS.md | 4 ++-- README.md | 2 +- .../offline-fixtures-paid-plans-20261104.json | 10 +++++----- ...e-fixtures-paid-plans-20261104.json.sha256 | 2 +- docs/images/context-efficiency.png | Bin 61488 -> 61488 bytes docs/images/context-efficiency.svg | 4 ++-- .../images/evidence-backed-agent-examples.png | Bin 273392 -> 273392 bytes .../images/evidence-backed-agent-examples.svg | 4 ++-- tests/test_benchmark_evidence.py | 2 +- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/BENCHMARKS.md b/BENCHMARKS.md index 98a4b39e3..15629a992 100644 --- a/BENCHMARKS.md +++ b/BENCHMARKS.md @@ -96,12 +96,12 @@ interpretation and do not count as additional benchmark-quality gains. Every exact public aggregate retained below comes from the checked-in, public-safe [`offline-fixtures-paid-plans-20261104.json`](docs/benchmark-evidence/offline-fixtures-paid-plans-20261104.json) artifact. Its SHA-256 is -`e0149fad454938b5f1e33bec1670ab8aba979e34178fb1080d8763f12678401d`, 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 -`af1df1560f96b0a08a23ca4af3604e4e381401c9ad3ebec503d382c0b2a595f5`. 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)`: diff --git a/README.md b/README.md index fda8f0076..08b38a589 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 e0149fad4549; 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/benchmark-evidence/offline-fixtures-paid-plans-20261104.json b/docs/benchmark-evidence/offline-fixtures-paid-plans-20261104.json index 6d7429a01..9afc20482 100644 --- a/docs/benchmark-evidence/offline-fixtures-paid-plans-20261104.json +++ b/docs/benchmark-evidence/offline-fixtures-paid-plans-20261104.json @@ -103,7 +103,7 @@ ], "schema": "engraphis-public-offline-fixtures/v1", "suite": { - "digest": "af1df1560f96b0a08a23ca4af3604e4e381401c9ad3ebec503d382c0b2a595f5", + "digest": "93d671089dd360954fce2c97ce1fdd105f87166ba9224e644d61ce15f9857e79", "digest_method": "sha256(canonical compact JSON mapping each sorted path to its file SHA-256)", "files": { "engraphis/__init__.py": "1cde9f19e05ddc4f5f28517e00b0a7bd5ebf09d6be8ec2681cebf5b698195d70", @@ -131,8 +131,8 @@ "engraphis/backends/vector_numpy.py": "c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72", "engraphis/backends/vector_sqlitevec.py": "6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b", "engraphis/build_info.py": "624c22471e56d4c4047160808c4245488292af611564d1a63ca437605bbb414f", - "engraphis/cloud_authz.py": "ae2f3c7ba4cf646e8115e4b4f0ab5127cfdfa897964f827d489138790371aff1", - "engraphis/cloud_features.py": "907d640810aa720374e0642b32f761fc3a2233750e0eb84ca49c1467b8f0d6a9", + "engraphis/cloud_authz.py": "6d86e17c4bdd4d3897ddc4b8bae44059d47c8763860745e37ee94fa10cdae7be", + "engraphis/cloud_features.py": "ffc7dc0fa690b94248c24807497784407a262365f110514e6a193724c7f4f55f", "engraphis/cloud_session.py": "cc6ab27c6af69b5c676e721ab06c520730e205f5bbffe27818650edfb71d4b63", "engraphis/commercial.py": "184f312066a9e682e51a0abeff042f1c0e8eed2d47470157b23930b5a17633aa", "engraphis/config.py": "7a1aa04246bda341f26866c5145854412e8c157d9880212fe1d7579ac986b764", @@ -188,7 +188,7 @@ "engraphis/engines/recall.py": "f979580d065599c07acbc3add59f71e52d9a186a2104b0e257daa7959ea88d26", "engraphis/engines/reweight.py": "91ec5815f5d356a7068c36133d24405334450f361f902f99275bed3ccebffd49", "engraphis/engines/thoughts.py": "4adb9c8a9bcfe736cb42fff9b5ce24631da6ec473d178e83f1c176fde3b8b814", - "engraphis/entitlements.py": "64896b60c9a6fee4035d8fa158896c8750fb5eeaab8bfc8599da6c727b30b2e3", + "engraphis/entitlements.py": "05a4826de62a3b0ad061be4325f502a5928c6db78ea118e32ae5ac2c8f480717", "engraphis/factory.py": "be26899074d80b2f96fff9a5a70969894e818dc71e2cfdf4f7ee965f1f4cc550", "engraphis/file_guard.py": "44eccaeca3afef7149bfdfea2321c325472253ee39a9e0bc24c720b3da3d685b", "engraphis/graphdata.py": "f5dc93395f52203c66deaeec8bc58d5c1f990a35a535151e0b9a84cecf7d8430", @@ -217,7 +217,7 @@ "engraphis/redirector.py": "5ba964b81f09008c9369180cb49d247519000763274aabc5e046215b12fa641d", "engraphis/routes/__init__.py": "f0d59080212cfa0d9b50877bca28e5832d1ae917899500d52c68b1c8821231af", "engraphis/routes/memory.py": "9ca066e1762eeeaafd9790ed57d730efcaceb57742dbe0c830ad8764500ff9e5", - "engraphis/routes/v2_api.py": "c15bb131743725c2a8216d98fdce5a77b40f6d5d8227e4864a50a6fb5db42e00", + "engraphis/routes/v2_api.py": "647b9ec769127849da0a148c0f520766407e01e2ccfc73952ec332e25fdfa4e2", "engraphis/routes/vault.py": "503304c177cf4d81bec5a9836731c0c32b62159175dea6d0fb8ce30c97a2bc6b", "engraphis/service.py": "dec62fa2912c19b13c904e1d7585babc5c55edfd995846f0bab33d62a20dff85", "engraphis/service_context.py": "0cd84538ca18b07d6440fa4cb0147510eacef2af5cf04b7a0519f2ac15a7ff87", diff --git a/docs/benchmark-evidence/offline-fixtures-paid-plans-20261104.json.sha256 b/docs/benchmark-evidence/offline-fixtures-paid-plans-20261104.json.sha256 index 01d3faea3..5312b0c04 100644 --- a/docs/benchmark-evidence/offline-fixtures-paid-plans-20261104.json.sha256 +++ b/docs/benchmark-evidence/offline-fixtures-paid-plans-20261104.json.sha256 @@ -1 +1 @@ -e0149fad454938b5f1e33bec1670ab8aba979e34178fb1080d8763f12678401d offline-fixtures-paid-plans-20261104.json +cc66984fd3dd78c17addeb979aea0e0bf40ff0f533ebcd683da4b7f532da0055 offline-fixtures-paid-plans-20261104.json diff --git a/docs/images/context-efficiency.png b/docs/images/context-efficiency.png index c9bc79c072fe5d5b49242630ec45f6888bc1b7ca..0a9d62a83389f6c555cf291449e245f6b336d284 100644 GIT binary patch delta 153 zcmWN^p%nr#3;AEf_;u1FvowP>80b es&`2UX0IfJsl=%2=-QPSBDL4&ZL_<-kDnjK?l7SM delta 153 zcmV~$yA{GP3;@6)b5O8>WLdwO6s%!c_9|(^CZS;*UP?MP;Bj}H%W*FIZ5@EsXUodf z$$}>iq8J@Tp@p?*u0E)u+6)APG!>dyM6CkmZ#Q~um$hc;DUQt3BLWy3#F!){EERpS bw4maJnS_|Jn+=T)Nky>s*Y>_XKlkq+ 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 e0149fad454938b5f1e33bec1670ab8aba979e34178fb1080d8763f12678401d. +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 e0149fad4549 +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 03728eea886ab55c935baa028b831f96e7e5b9a9..9683d93c3cdfbaaa9ef5e6e81fd3e42a96e94696 100644 GIT binary patch delta 165 zcmWN~yA1*{3;-PVCg&B2FBai~yoI(IFUyk~vr*B@zp8<*VNJGkib8 z<2cotQWkOxW-N7NHEShv)~3*)IDtEO455{3NrEXAUW}#yF`j>KdfxUECMa3SD^~HM v*;)ju9FS{5vE=LmT*$FS0zh?);sH}kOD*H+j+bkn`)cm%xo=*5yF2^=n-4Yz delta 165 zcmWNLyAi@L3_y=hL74@ZfJU-qOQt0guz?lJV{k{2qYc9_OIC14Cg5=W-n)C-I&AB( z+(rYWRCA#~$%}@H=Dpg4ks+&O&54tFLYArs0!29biY~GcVEHZL->>rs$`pFb&M7eF uBxYq&jFgaq3z?Km2%S~mH3*$0QemgQAJFxCe>~gVc5~a$?eJ;G)8!9d^fNU8 diff --git a/docs/images/evidence-backed-agent-examples.svg b/docs/images/evidence-backed-agent-examples.svg index db92eda1b..3b8ddac71 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: e0149fad454938b5f1e33bec1670ab8aba979e34178fb1080d8763f12678401d. + 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 e0149fad454938b5f1e33bec1670ab8aba979e34178fb1080d8763f12678401d + SHA256 cc66984fd3dd78c17addeb979aea0e0bf40ff0f533ebcd683da4b7f532da0055 diff --git a/tests/test_benchmark_evidence.py b/tests/test_benchmark_evidence.py index 4cf1b562e..c024dd41a 100644 --- a/tests/test_benchmark_evidence.py +++ b/tests/test_benchmark_evidence.py @@ -35,7 +35,7 @@ ROOT = Path(__file__).resolve().parents[1] PUBLIC_OFFLINE_ARTIFACT = "offline-fixtures-paid-plans-20261104.json" -PUBLIC_OFFLINE_SHA = "e0149fad454938b5f1e33bec1670ab8aba979e34178fb1080d8763f12678401d" +PUBLIC_OFFLINE_SHA = "cc66984fd3dd78c17addeb979aea0e0bf40ff0f533ebcd683da4b7f532da0055" @pytest.fixture(scope="module") From 3adb658697d06a62b1e3424e59b822c544ee3f03 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools <274649000+Coding-Dev-Tools@users.noreply.github.com> Date: Wed, 4 Nov 2026 18:44:42 -0500 Subject: [PATCH 13/13] docs(readme): add Pro and Team upgrade benefits (#321) ## Why upgrade to Pro or Team in the README - Adds a prominent upgrade section after the local feature list. - Explains memory health analytics, scheduled upkeep, encrypted sync, team roles/audit, and the included Jev allowance. - Shows how Jev helps agentic memory: safe vs risky actions, contradictory vs reinforcing memories, supported vs unsupported answers, and complete vs unfinished work. - Adds pricing, no-card trial details, comparison link, and Pro/Team signup links. Tests: documentation contracts, Pro CTA, plugin distribution, and skill package pass; ruff clean. --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index b38847f32..288cecf19 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,18 @@ Connect a coding agent over MCP with the [agent setup guide](https://github.com/ - **Operator control:** Keep the core local and offline-capable, with explicit controls for hosted services and external LLM providers. - **Free Jev on paid plans:** Pro and Team include managed Jev decisions up to 500 calls per person per day, with no personal provider key. +## Why upgrade to Pro or Team + +The local core stays free and offline-capable. Pro and Team add hosted workflows for memory that has to follow you across agents, machines, and teammates: + +- **See memory health:** Analytics surfaces growth, retention, and decay forecasts so you can review what is becoming stale before it affects recall. +- **Keep memory maintained:** Schedule Auto Consolidation and Auto Dreaming. They produce proposals for you to review, and local governance still decides what changes. +- **Sync across machines:** Opt into encrypted Cloud Sync for eligible workspaces with a key you hold. Sync and readable managed processing are separate choices. +- **Give a team an operating picture:** Named seats, admin/member/viewer roles, scoped credentials, and a 30-day account activity log visible to owners and admins. Shared hosted Team memory and hosted MCP are not yet available. +- **Add judgment with Jev:** Pro and Team include up to 500 managed Jev calls per person per day: typed, advisory checks for risky commands, contradictory memories, evidence support, and completion claims. No personal TypeSafe key or overage charge. Jev helps an agent separate safe from risky actions, contradictory from reinforcing memories, supported from unsupported answers, and complete from unfinished work, while permissions and tests stay in charge. + +Pro is $10/month or $100/year for one owner. Team is $20/seat/month or $200/seat/year. Both start with an email-confirmed, no-card trial (7 active days for Pro, 14 active days for Team). [Compare plans](https://github.com/Coding-Dev-Tools/engraphis/blob/main/docs/HOSTED_PLANS.md) · [Start a Pro trial](https://api.engraphis.com/account?plan=pro&interval=monthly&utm_source=engraphis&utm_medium=readme&utm_campaign=pro_conversion&utm_content=readme_upgrade#billing) · [Start a Team trial](https://api.engraphis.com/account?plan=team&interval=monthly&seats=5&utm_source=engraphis&utm_medium=readme&utm_campaign=pro_conversion&utm_content=readme_team_upgrade#billing). + ## Benchmarks 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.