From 7d59883a3e1145974c6430a9df6e397977354122 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools <274649000+Coding-Dev-Tools@users.noreply.github.com> Date: Wed, 14 Oct 2026 07:33:54 -0400 Subject: [PATCH 01/15] feat(security): add fail-closed host file protection engine and Command Code hook - Implement file_guard.py for host-mediated protection of sensitive files - Add Command Code PreToolUse hook integration in integrations/commandcode/file_guard_hook.py - Support --file-guard flag in scripts/install_cc_hook.py with idempotent hook registration - Add documentation in docs/FILE_PROTECTION.md and update SECURITY.md and README.md - Add comprehensive unit tests in tests/test_file_guard.py and tests/test_install_cc_hook.py --- README.md | 1 + SECURITY.md | 9 + docs/FILE_PROTECTION.md | 58 +++++++ engraphis/file_guard.py | 173 ++++++++++++++++++++ integrations/commandcode/file_guard_hook.py | 37 +++++ scripts/install_cc_hook.py | 78 ++++++--- tests/test_file_guard.py | 129 +++++++++++++++ tests/test_install_cc_hook.py | 18 ++ 8 files changed, 483 insertions(+), 20 deletions(-) create mode 100644 docs/FILE_PROTECTION.md create mode 100644 engraphis/file_guard.py create mode 100644 integrations/commandcode/file_guard_hook.py create mode 100644 tests/test_file_guard.py diff --git a/README.md b/README.md index 50750274b..20f3a04dc 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ See [hosted plans and Jev details](https://github.com/Coding-Dev-Tools/engraphis - [Cloud Sync](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/docs/SYNC.md) - [Pi extension](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/integrations/pi/README.md) - [Memory write review](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/docs/WRITE_REVIEW.md) +- [Coding-agent file protection](docs/FILE_PROTECTION.md) - [Security policy](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/SECURITY.md) - [Hosted plans and licensing](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/docs/HOSTED_PLANS.md) diff --git a/SECURITY.md b/SECURITY.md index b758e5142..52ee7b52e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -99,6 +99,15 @@ hardcoded excludes. Anyone who can reach an authenticated local mutation route has the authority of that local installation, so do not share its bearer token. +### 5a. Coding-agent file writes + +Memory governance is not a filesystem sandbox. For hosts that mediate tool calls, Engraphis ships +a fail-closed protected-path evaluator and an optional Command Code `PreToolUse` hook. Its policy +lives outside the repository, direct file tools are checked before execution, and general shell +access is denied by default because command inspection cannot guarantee non-mutation. See +[`docs/FILE_PROTECTION.md`](docs/FILE_PROTECTION.md) for setup, host integration, and limitations. +Use OS ACLs or read-only mounts when the agent can invoke tools outside that hook. + ### 6. Local resource and database ingestion - Uploaded and folder-imported files are size/count bounded, marked `trusted:false`, and parsed as data. Missing optional PDF/OCR/transcription tools fail explicitly. diff --git a/docs/FILE_PROTECTION.md b/docs/FILE_PROTECTION.md new file mode 100644 index 000000000..a1928cb41 --- /dev/null +++ b/docs/FILE_PROTECTION.md @@ -0,0 +1,58 @@ +# Protecting files from coding-agent writes + +Engraphis memory governance does not, by itself, mediate filesystem access. A coding agent with an +unrestricted shell has the same file authority as its operating-system user. Engraphis therefore +ships a **host-facing, fail-closed policy evaluator** and an optional Command Code `PreToolUse` +hook; it does not claim that memory permissions are a filesystem sandbox. + +## Command Code setup + +Create an owner-private policy outside the repository: + +```json +{ + "version": 1, + "roots": [ + { + "path": "/absolute/path/to/repository", + "protect": [".env", ".github/workflows/**", "legal/**"], + "allow_shell": false + } + ] +} +``` + +```bash +mkdir -p ~/.engraphis +chmod 700 ~/.engraphis +$EDITOR ~/.engraphis/file-protection.json +chmod 600 ~/.engraphis/file-protection.json +python scripts/install_cc_hook.py --file-guard +``` + +The default path is `~/.engraphis/file-protection.json`; set +`ENGRAPHIS_FILE_PROTECTION_POLICY` to an absolute operator-controlled file when needed. The hook +blocks matching `Write`, `Edit`, `MultiEdit`, and `NotebookEdit` targets. It also blocks `Bash` +entirely whenever any rule has `allow_shell: false`: inspecting a shell command cannot prove that +redirections, subprocesses, or invoked scripts will not mutate a protected file. A missing, +malformed, symlinked, non-owner-owned, or group/world-writable policy fails closed. + +Set `allow_shell: true` only if a stronger OS/container sandbox independently makes the protected +paths read-only. This option merely allows the shell tool; it does not parse or secure commands. + +## Other agent hosts + +Call `engraphis.file_guard.load_policy()` and `decide_tool_call()` at the host's tool-authorization +boundary before executing a filesystem mutation. Do not ask the model to call the guard itself: +enforcement must be outside the model-controlled tool sequence. Hosts without a pre-execution hook +must use filesystem ACLs, a read-only bind mount, or another sandbox. Git hooks and CI are useful +detection layers but cannot prevent the initial overwrite. + +## Limits + +- A process with permission to change ACLs, remount paths, edit the hook configuration, or attach to + the host can bypass local controls. Run the agent with least privilege. +- Hard-link aliases cannot be recognized from a path pattern alone; use filesystem isolation when + hostile workspace contents are in scope. +- The bundled hook covers the named Command Code tools. Add new mutating tool names to the host + matcher before enabling them. diff --git a/engraphis/file_guard.py b/engraphis/file_guard.py new file mode 100644 index 000000000..683ae0164 --- /dev/null +++ b/engraphis/file_guard.py @@ -0,0 +1,173 @@ +"""Host-facing policy for protecting files from coding-agent writes. + +Engraphis does not mediate an agent's filesystem access. This module therefore +provides a small, dependency-free decision point for hosts that *do* mediate +tool calls. Policies live outside the repository by default so the agent being +constrained cannot weaken its own policy with an ordinary repository edit. +""" +from __future__ import annotations + +from dataclasses import dataclass +import fnmatch +import json +import os +from pathlib import Path +import stat +from typing import Any, Mapping, Optional, Sequence + + +POLICY_VERSION = 1 +DEFAULT_POLICY_PATH = Path.home() / ".engraphis" / "file-protection.json" +SHELL_TOOLS = frozenset({"bash", "shell", "terminal", "execute", "runcommand"}) +FILE_TOOLS = frozenset({"write", "edit", "multiedit", "notebookedit"}) + + +class FileProtectionError(ValueError): + """The operator-owned protection policy is absent, unsafe, or malformed.""" + + +@dataclass(frozen=True) +class ProtectionRule: + root: Path + patterns: tuple[str, ...] + allow_shell: bool = False + + +@dataclass(frozen=True) +class GuardDecision: + allowed: bool + reason: str + matched_pattern: Optional[str] = None + + +def _canonical(path: Path) -> Path: + return Path(os.path.realpath(os.path.expanduser(os.fspath(path)))) + + +def _validate_policy_file(path: Path) -> None: + """Require an owner-controlled regular file on POSIX. + + Windows ACLs do not map to POSIX mode bits, so Windows hosts must enforce + ownership when installing the hook. Symlinks are rejected everywhere. + """ + try: + info = path.lstat() + except FileNotFoundError as exc: + raise FileProtectionError(f"file-protection policy not found: {path}") from exc + if not stat.S_ISREG(info.st_mode) or path.is_symlink(): + raise FileProtectionError("file-protection policy must be a regular file") + if os.name == "posix": + if info.st_uid != os.geteuid(): + raise FileProtectionError("file-protection policy must be owned by the current user") + if info.st_mode & (stat.S_IWGRP | stat.S_IWOTH): + raise FileProtectionError( + "file-protection policy must not be group- or world-writable" + ) + + +def load_policy(path: Optional[Path] = None) -> tuple[ProtectionRule, ...]: + """Load and strictly validate an operator-owned JSON policy.""" + policy_path = Path(os.path.abspath(os.path.expanduser(os.fspath( + path or Path(os.environ.get( + "ENGRAPHIS_FILE_PROTECTION_POLICY", DEFAULT_POLICY_PATH, + )) + )))) + _validate_policy_file(policy_path) + try: + payload = json.loads(policy_path.read_text(encoding="utf-8")) + except (OSError, UnicodeError, json.JSONDecodeError) as exc: + raise FileProtectionError(f"cannot read file-protection policy: {exc}") from exc + if not isinstance(payload, dict) or payload.get("version") != POLICY_VERSION: + raise FileProtectionError(f"file-protection policy version must be {POLICY_VERSION}") + raw_rules = payload.get("roots") + if not isinstance(raw_rules, list) or not raw_rules: + raise FileProtectionError("file-protection policy requires a non-empty roots list") + rules = [] + for raw in raw_rules: + if not isinstance(raw, dict): + raise FileProtectionError("each file-protection root must be an object") + root_value = raw.get("path") + patterns = raw.get("protect") + if not isinstance(root_value, str) or not Path(root_value).expanduser().is_absolute(): + raise FileProtectionError("each protected root path must be absolute") + if not isinstance(patterns, list) or not patterns or not all( + isinstance(item, str) and item and not Path(item).is_absolute() + for item in patterns + ): + raise FileProtectionError("protect must contain non-empty relative patterns") + normalized_patterns = [] + for pattern in patterns: + normalized = pattern.replace("\\", "/") + while normalized.startswith("./"): + normalized = normalized[2:] + if normalized in {"", ".", ".."} or normalized.startswith("../"): + raise FileProtectionError("protected patterns may not escape their root") + normalized_patterns.append(normalized) + rules.append(ProtectionRule( + root=_canonical(Path(root_value)), + patterns=tuple(normalized_patterns), + allow_shell=raw.get("allow_shell", False) is True, + )) + # A direct file-edit tool must not be able to rewrite the policy that will + # adjudicate its next call. Host hook configuration remains a host/OS + # boundary and should likewise be owner-protected. + rules.append(ProtectionRule(policy_path.parent, (policy_path.name,), allow_shell=True)) + return tuple(rules) + + +def _rule_for_path(path: Path, rules: Sequence[ProtectionRule]) -> tuple[Optional[ProtectionRule], Optional[str]]: + # Check both the named path and its resolved target. The former protects a + # protected symlink entry; the latter prevents an unprotected alias from + # reaching a protected target. + named = Path(os.path.abspath(os.path.expanduser(os.fspath(path)))) + candidates = (named, _canonical(named)) + for rule in rules: + for candidate in candidates: + try: + relative = candidate.relative_to(rule.root).as_posix() + except ValueError: + continue + for pattern in rule.patterns: + # ``fnmatch`` makes ``dir/**`` useful for descendants; the explicit + # directory comparison also protects the directory entry itself. + prefix = pattern[:-3].rstrip("/") if pattern.endswith("/**") else None + if fnmatch.fnmatchcase(relative, pattern) or relative == prefix: + return rule, pattern + return None, None + + +def decide_tool_call( + tool_name: str, + tool_input: Mapping[str, Any], + *, + cwd: Path, + rules: Sequence[ProtectionRule], +) -> GuardDecision: + """Return whether a host-mediated coding tool call may proceed. + + General shell tools are denied for a protected root by default because + command-string inspection cannot prove that redirections, subprocesses, or + scripts will leave protected paths untouched. An operator can explicitly + opt out with ``allow_shell`` after providing a stronger OS sandbox. + """ + normalized_tool = str(tool_name or "").replace("_", "").casefold() + current = _canonical(cwd) + if normalized_tool in SHELL_TOOLS and any(not rule.allow_shell for rule in rules): + return GuardDecision(False, "shell tools are disabled while file protection is active") + if normalized_tool not in FILE_TOOLS: + return GuardDecision(True, "tool does not expose a recognized direct file write") + values = [] + for key in ("file_path", "path", "notebook_path"): + value = tool_input.get(key) + if isinstance(value, str) and value: + values.append(value) + if not values: + return GuardDecision(False, "file-writing tool did not provide a target path") + for value in values: + target = Path(value) + if not target.is_absolute(): + target = current / target + _, pattern = _rule_for_path(target, rules) + if pattern is not None: + return GuardDecision(False, "target matches an operator-protected path", pattern) + return GuardDecision(True, "targets do not match protected paths") diff --git a/integrations/commandcode/file_guard_hook.py b/integrations/commandcode/file_guard_hook.py new file mode 100644 index 000000000..7abb25079 --- /dev/null +++ b/integrations/commandcode/file_guard_hook.py @@ -0,0 +1,37 @@ +"""Fail-closed Command Code PreToolUse hook for operator-protected files.""" +from __future__ import annotations + +import json +import os +from pathlib import Path +import sys + +from engraphis.file_guard import FileProtectionError, decide_tool_call, load_policy + + +def main() -> int: + try: + payload = json.load(sys.stdin) + if not isinstance(payload, dict): + raise ValueError("hook payload must be an object") + raw_tool_input = payload.get("tool_input") + tool_input = raw_tool_input if isinstance(raw_tool_input, dict) else {} + decision = decide_tool_call( + str(payload.get("tool_name") or ""), + tool_input, + cwd=Path(str(payload.get("cwd") or os.getcwd())), + rules=load_policy(), + ) + except (FileProtectionError, OSError, ValueError, TypeError) as exc: + # A configured security hook must never fail open. + print(f"Engraphis file guard denied the tool call: {exc}", file=sys.stderr) + return 2 + if decision.allowed: + return 0 + detail = f" (matched {decision.matched_pattern})" if decision.matched_pattern else "" + print(f"Engraphis file guard denied the tool call: {decision.reason}{detail}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/install_cc_hook.py b/scripts/install_cc_hook.py index a00261acd..2161a4907 100644 --- a/scripts/install_cc_hook.py +++ b/scripts/install_cc_hook.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- -"""Install the Engraphis Command Code SessionStart hook into the user-scope settings file. +"""Install Engraphis Command Code hooks into the user-scope settings file. Idempotent: running twice updates the existing entry rather than duplicating it. Backup is written to ``.bak-engraphis-`` on first write only. Usage: python scripts/install_cc_hook.py + python scripts/install_cc_hook.py --file-guard python scripts/install_cc_hook.py --uninstall """ from __future__ import annotations @@ -21,6 +22,11 @@ or Path.home() / ".commandcode" / "settings.json") HOOK_PATH = Path(__file__).resolve().parent.parent / "integrations" / "commandcode" / "session_start_hook.py" HOOK_KEY = "cc-engraphis-session-start" +FILE_GUARD_PATH = ( + Path(__file__).resolve().parent.parent + / "integrations" / "commandcode" / "file_guard_hook.py" +) +FILE_GUARD_KEY = "cc-engraphis-file-guard" def _utc_stamp() -> str: @@ -69,6 +75,15 @@ def _entry_command() -> str: return _hook_entry()["command"] +def _file_guard_entry() -> dict: + return { + "type": "command", + "command": f"python \"{FILE_GUARD_PATH}\"", + "name": FILE_GUARD_KEY, + "timeout": 5, + } + + def _is_our_entry(entry: dict) -> bool: """Match by stable ``name`` (HOOK_KEY) first, then fall back to command string. @@ -76,9 +91,11 @@ def _is_our_entry(entry: dict) -> bool: primary key; the command match stays as a backstop for legacy entries written by older versions of this script. """ - if entry.get("name") == HOOK_KEY: + if entry.get("name") in {HOOK_KEY, FILE_GUARD_KEY}: return True - return entry.get("command", "") == _entry_command() + return entry.get("command", "") in { + _entry_command(), _file_guard_entry()["command"], + } def _session_start_has_our_entry(hooks: list) -> bool: @@ -131,7 +148,7 @@ def _refresh_existing_wrappers(hooks: list) -> bool: return refreshed -def install() -> None: +def install(*, enable_file_guard: bool = False) -> None: settings = _read_settings(SETTINGS_PATH) hooks = settings.setdefault("hooks", {}).setdefault("SessionStart", []) # Idempotency: refresh our entry in-place inside any wrapper that already @@ -140,6 +157,21 @@ def install() -> None: # no prior wrapper mentions us. if not _refresh_existing_wrappers(hooks): hooks.append({"hooks": [_hook_entry()]}) + if enable_file_guard: + guard_hooks = settings.setdefault("hooks", {}).setdefault("PreToolUse", []) + found = False + for wrapper in guard_hooks: + entries = wrapper.get("hooks", []) or [] + if any(entry.get("name") == FILE_GUARD_KEY for entry in entries): + wrapper["hooks"] = [ + entry for entry in entries if entry.get("name") != FILE_GUARD_KEY + ] + [_file_guard_entry()] + found = True + if not found: + guard_hooks.append({ + "matcher": "Write|Edit|MultiEdit|NotebookEdit|Bash", + "hooks": [_file_guard_entry()], + }) _backup(SETTINGS_PATH) _write_settings(SETTINGS_PATH, settings) print(f"installed SessionStart hook into {SETTINGS_PATH}") @@ -147,21 +179,20 @@ def install() -> None: def uninstall() -> None: settings = _read_settings(SETTINGS_PATH) - if "hooks" not in settings or "SessionStart" not in settings["hooks"]: - print(f"no SessionStart hook entry in {SETTINGS_PATH}") + if "hooks" not in settings: + print(f"no Engraphis hook entry in {SETTINGS_PATH}") return - cleaned: list = [] - for wrapper in settings["hooks"]["SessionStart"]: - stripped = _strip_our_entries(wrapper) - if stripped is not None: - cleaned.append(stripped) - elif wrapper is settings["hooks"]["SessionStart"][0]: - # No-op, but explicit: a wrapper that becomes empty after - # stripping is dropped (caller removed via ``cleaned.append``). - pass - settings["hooks"]["SessionStart"] = cleaned - if not settings["hooks"]["SessionStart"]: - del settings["hooks"]["SessionStart"] + for event in ("SessionStart", "PreToolUse"): + if event not in settings["hooks"]: + continue + cleaned = [] + for wrapper in settings["hooks"][event]: + stripped = _strip_our_entries(wrapper) + if stripped is not None: + cleaned.append(stripped) + settings["hooks"][event] = cleaned + if not cleaned: + del settings["hooks"][event] if not settings["hooks"]: del settings["hooks"] _backup(SETTINGS_PATH) @@ -170,17 +201,24 @@ def uninstall() -> None: def main() -> int: - parser = argparse.ArgumentParser(description=__doc__.split("\n\n", 1)[0]) + parser = argparse.ArgumentParser(description=(__doc__ or "").split("\n\n", 1)[0]) parser.add_argument("--uninstall", action="store_true", help="Remove the Engraphis SessionStart hook from the user settings.") + parser.add_argument( + "--file-guard", action="store_true", + help="Also install the fail-closed protected-file PreToolUse hook.", + ) args = parser.parse_args() if not HOOK_PATH.exists(): print(f"error: hook script not found at {HOOK_PATH}", file=sys.stderr) return 2 + if args.file_guard and not FILE_GUARD_PATH.exists(): + print(f"error: file guard hook not found at {FILE_GUARD_PATH}", file=sys.stderr) + return 2 if args.uninstall: uninstall() else: - install() + install(enable_file_guard=args.file_guard) return 0 diff --git a/tests/test_file_guard.py b/tests/test_file_guard.py new file mode 100644 index 000000000..60c73bc41 --- /dev/null +++ b/tests/test_file_guard.py @@ -0,0 +1,129 @@ +from __future__ import annotations + +import json +import os +from pathlib import Path + +import pytest + +from engraphis.file_guard import ( + FileProtectionError, + decide_tool_call, + load_policy, +) + + +def _policy(tmp_path: Path, root: Path, **overrides) -> Path: + payload = { + "version": 1, + "roots": [{ + "path": str(root), + "protect": [".env", ".github/workflows/**", "legal/*.md"], + **overrides, + }], + } + path = tmp_path / "policy.json" + path.write_text(json.dumps(payload), encoding="utf-8") + path.chmod(0o600) + return path + + +def test_direct_writes_are_denied_only_for_matching_paths(tmp_path: Path) -> None: + root = tmp_path / "repo" + root.mkdir() + rules = load_policy(_policy(tmp_path, root)) + + denied = decide_tool_call( + "Write", {"file_path": str(root / ".env")}, cwd=root, rules=rules, + ) + nested = decide_tool_call( + "Edit", {"file_path": ".github/workflows/release.yml"}, cwd=root, rules=rules, + ) + allowed = decide_tool_call( + "Edit", {"file_path": "src/app.py"}, cwd=root, rules=rules, + ) + + assert not denied.allowed and denied.matched_pattern == ".env" + assert not nested.allowed and nested.matched_pattern == ".github/workflows/**" + assert allowed.allowed + + +def test_shell_is_fail_closed_even_when_cwd_is_outside_root(tmp_path: Path) -> None: + root = tmp_path / "repo" + root.mkdir() + rules = load_policy(_policy(tmp_path, root)) + decision = decide_tool_call( + "Bash", {"command": f"rm {root / '.env'}"}, cwd=tmp_path, rules=rules, + ) + assert not decision.allowed + assert "shell tools are disabled" in decision.reason + + +def test_operator_can_allow_shell_when_os_sandbox_supplies_enforcement(tmp_path: Path) -> None: + root = tmp_path / "repo" + root.mkdir() + rules = load_policy(_policy(tmp_path, root, allow_shell=True)) + assert decide_tool_call("Bash", {"command": "pytest"}, cwd=root, rules=rules).allowed + + +def test_missing_target_on_writer_fails_closed(tmp_path: Path) -> None: + root = tmp_path / "repo" + root.mkdir() + rules = load_policy(_policy(tmp_path, root)) + assert not decide_tool_call("Write", {}, cwd=root, rules=rules).allowed + + +def test_policy_file_protects_itself(tmp_path: Path) -> None: + root = tmp_path / "repo" + root.mkdir() + policy = _policy(tmp_path, root) + rules = load_policy(policy) + decision = decide_tool_call( + "Edit", {"file_path": str(policy)}, cwd=root, rules=rules, + ) + assert not decision.allowed + assert decision.matched_pattern == policy.name + + +def test_policy_rejects_relative_roots_and_unsafe_permissions(tmp_path: Path) -> None: + malformed = tmp_path / "malformed.json" + malformed.write_text( + json.dumps({"version": 1, "roots": [{"path": "repo", "protect": [".env"]}]}), + encoding="utf-8", + ) + with pytest.raises(FileProtectionError, match="absolute"): + load_policy(malformed) + + if os.name == "posix": + malformed.chmod(0o666) + with pytest.raises(FileProtectionError, match="group- or world-writable"): + load_policy(malformed) + + +def test_policy_symlink_is_rejected(tmp_path: Path) -> None: + root = tmp_path / "repo" + root.mkdir() + policy = _policy(tmp_path, root) + alias = tmp_path / "alias.json" + try: + alias.symlink_to(policy) + except OSError: + pytest.skip("symlinks are unavailable") + with pytest.raises(FileProtectionError, match="regular file"): + load_policy(alias) + + +def test_protected_symlink_name_cannot_escape_matching(tmp_path: Path) -> None: + root = tmp_path / "repo" + root.mkdir() + target = tmp_path / "outside" + target.write_text("secret", encoding="utf-8") + link = root / ".env" + try: + link.symlink_to(target) + except OSError: + pytest.skip("symlinks are unavailable") + rules = load_policy(_policy(tmp_path, root)) + assert not decide_tool_call( + "Write", {"file_path": str(link)}, cwd=root, rules=rules, + ).allowed diff --git a/tests/test_install_cc_hook.py b/tests/test_install_cc_hook.py index 876c5db09..17b7d69d7 100644 --- a/tests/test_install_cc_hook.py +++ b/tests/test_install_cc_hook.py @@ -60,6 +60,24 @@ def test_install_is_idempotent_on_repeat_runs(fake_settings) -> None: assert entries[0]["hooks"][0]["command"] == module._hook_entry()["command"] +def test_file_guard_install_is_explicit_idempotent_and_fail_closed(fake_settings) -> None: + module, settings_path = fake_settings + module.install(enable_file_guard=True) + module.install(enable_file_guard=True) + payload = json.loads(settings_path.read_text(encoding="utf-8")) + entries = payload["hooks"]["PreToolUse"] + assert len(entries) == 1 + assert entries[0]["matcher"] == "Write|Edit|MultiEdit|NotebookEdit|Bash" + assert entries[0]["hooks"] == [module._file_guard_entry()] + + +def test_default_install_does_not_enable_file_guard(fake_settings) -> None: + module, settings_path = fake_settings + module.install() + payload = json.loads(settings_path.read_text(encoding="utf-8")) + assert "PreToolUse" not in payload["hooks"] + + def test_install_does_not_disturb_other_session_start_entries(fake_settings) -> None: module, settings_path = fake_settings settings_path.write_text( From 3552ce4789f74ad38563fdb63d4310440272019b Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools <274649000+Coding-Dev-Tools@users.noreply.github.com> Date: Fri, 30 Oct 2026 04:29:36 -0400 Subject: [PATCH 02/15] feat: sync local desktop-memory, stdio bridge, and history-integrity work - Add the dependency-free stdio MCP bridge so desktop clients share one warm engine - Add desktop-memory evaluation and closure/session-lineage test coverage - Update recall, store, sync, dashboard, and MCP internals for governed temporal history, session lineage, and deterministic measurements - Refresh documentation, benchmark evidence, and local browser-auth behavior - Record the 1.7.10 candidate changelog and version markers --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .claude-plugin/skill-assets.sha256 | 6 +- .gitattributes | 33 +- .github/FUNDING.yml | 1 - .github/workflows/release.yml | 6 + AGENTS.md | 8 +- BENCHMARKS.md | 220 +- CHANGELOG.md | 34 + MANIFEST.in | 60 +- README.md | 199 +- SECURITY.md | 429 +- docs/AGENT_CONNECT.md | 372 +- docs/CONFIGURATION.md | 214 +- docs/DOCUMENT_IMPORT.md | 301 +- docs/HOSTED_PLANS.md | 6 +- docs/LLM_PROVIDERS.md | 609 +- docs/MCP_CONTRACT.json | 6 +- docs/MCP_TOOLS.md | 610 +- docs/OBSIDIAN_IMPORT.md | 251 +- docs/RELEASE_1_7_9.md | 10 +- docs/SYNC.md | 16 +- docs/WRITE_REVIEW.md | 152 +- ...line-fixtures-desktop-memory-20261020.json | 699 + ...xtures-desktop-memory-20261020.json.sha256 | 1 + ...ne-fixtures-governed-history-20261018.json | 697 + ...ures-governed-history-20261018.json.sha256 | 1 + ...line-fixtures-memory-timeout-20261020.json | 699 + ...xtures-memory-timeout-20261020.json.sha256 | 1 + ...line-fixtures-release-review-20261018.json | 697 + ...xtures-release-review-20261018.json.sha256 | 1 + ...ne-fixtures-temporal-history-20261018.json | 697 + ...ures-temporal-history-20261018.json.sha256 | 1 + docs/images/context-efficiency.png | Bin 90849 -> 61327 bytes docs/images/context-efficiency.svg | 4 +- .../images/evidence-backed-agent-examples.png | Bin 142025 -> 272888 bytes .../images/evidence-backed-agent-examples.svg | 10 +- engraphis/__init__.py | 80 +- engraphis/backends/embedder_deterministic.py | 18 +- engraphis/backends/embedder_st.py | 7 + engraphis/classic_assets/dashboard.js | 3686 ++--- engraphis/commercial_manifest.json | 206 +- engraphis/core/documents.py | 10 +- engraphis/core/engine.py | 57 +- engraphis/core/graph_scene.py | 8 +- engraphis/core/interfaces.py | 83 + engraphis/core/mutations.py | 317 +- engraphis/core/recall.py | 47 +- engraphis/core/savings.py | 326 +- engraphis/core/schema.py | 93 +- engraphis/core/scoring.py | 10 +- engraphis/core/store.py | 631 +- engraphis/core/sync.py | 187 +- engraphis/dashboard_app.py | 83 +- engraphis/dashboard_assets/index.html | 1727 +-- engraphis/dashboard_assets/ledger.js | 12212 ++++++++-------- engraphis/local_auth.py | 65 + engraphis/mcp_cli.py | 10 +- engraphis/mcp_http_cli.py | 336 +- engraphis/mcp_server.py | 210 +- engraphis/mcp_stdio_bridge.py | 157 + engraphis/routes/v2_api.py | 45 +- engraphis/service.py | 350 +- engraphis/static/dashboard.js | 3686 ++--- eval/desktop_memory.py | 52 + eval/local_benchmark_queue.py | 12 +- eval/native_coverage_scaling.py | 198 +- .../commandcode/session_start_hook.py | 35 +- integrations/prime_agent/pyproject.toml | 110 +- .../src/engraphis_prime_agent/agent.py | 1204 +- .../src/engraphis_prime_agent/installer.py | 614 +- .../src/engraphis_prime_agent/mcp_client.py | 850 +- .../src/engraphis_prime_agent/tools.py | 888 +- pyproject.toml | 585 +- requirements.txt | 42 +- run_preflight.ps1 | 103 + scripts/entry.py | 2 +- scripts/install_cc_hook.py | 452 +- scripts/rebind_offline_evidence.py | 2 +- scripts/templates/benchmark_examples.svg | 6 +- scripts/update.py | 1772 +-- scripts/validate_compose_contract.py | 328 +- scripts/verify_distribution_contents.py | 3 + scripts/verify_release_artifacts.py | 254 +- tests/e2e/ask-home.spec.js | 638 +- tests/e2e/history-project.spec.js | 258 +- tests/e2e/ledger.spec.js | 5562 +++---- tests/e2e/memory-workflow.spec.js | 948 +- tests/e2e/workspace-routing.spec.js | 530 +- tests/e2e/workspace-smoke.spec.js | 119 +- tests/test_backends_factories.py | 20 + tests/test_benchmark_evidence.py | 2554 ++-- .../test_campaign_candidate_compatibility.py | 196 +- tests/test_closure_observation_graph.py | 316 + tests/test_context_evidence_preservation.py | 330 +- tests/test_context_packer.py | 494 +- tests/test_core_store.py | 54 + tests/test_dashboard_auth_placement.py | 2 +- tests/test_dashboard_v2.py | 22 +- tests/test_desktop_memory_workflow.py | 208 + tests/test_deterministic_measurements.py | 33 + tests/test_documentation_contracts.py | 836 +- tests/test_documents.py | 24 +- tests/test_ended_session_derivations.py | 298 + tests/test_governed_concurrency.py | 614 +- tests/test_graph_explorer_v2.py | 28 +- tests/test_history_scope.py | 30 + tests/test_local_browser_access.py | 223 + tests/test_mcp_memory_concurrency.py | 323 + tests/test_memory_closure_model.py | 219 + tests/test_memory_revisions.py | 206 +- tests/test_mutation_closure_version.py | 158 + tests/test_packaging.py | 9 +- tests/test_queue_status_publication.py | 43 + tests/test_release_infrastructure.py | 4 +- tests/test_round17_fixes.py | 36 + tests/test_session_idempotent.py | 7 +- tests/test_session_lineage_independent.py | 178 + tests/test_setup_plugin_distribution.py | 4 +- tests/test_startup_transform_gates.py | 11 +- tests/test_sync.py | 5 +- tests/test_sync_closure_history.py | 328 + tests/test_sync_dashboard.py | 50 + .../test_temporal_integration_second_pass.py | 186 + tests/test_workflow_diagnostics.py | 2 +- 125 files changed, 31502 insertions(+), 23818 deletions(-) delete mode 100644 .github/FUNDING.yml create mode 100644 docs/benchmark-evidence/offline-fixtures-desktop-memory-20261020.json create mode 100644 docs/benchmark-evidence/offline-fixtures-desktop-memory-20261020.json.sha256 create mode 100644 docs/benchmark-evidence/offline-fixtures-governed-history-20261018.json create mode 100644 docs/benchmark-evidence/offline-fixtures-governed-history-20261018.json.sha256 create mode 100644 docs/benchmark-evidence/offline-fixtures-memory-timeout-20261020.json create mode 100644 docs/benchmark-evidence/offline-fixtures-memory-timeout-20261020.json.sha256 create mode 100644 docs/benchmark-evidence/offline-fixtures-release-review-20261018.json create mode 100644 docs/benchmark-evidence/offline-fixtures-release-review-20261018.json.sha256 create mode 100644 docs/benchmark-evidence/offline-fixtures-temporal-history-20261018.json create mode 100644 docs/benchmark-evidence/offline-fixtures-temporal-history-20261018.json.sha256 create mode 100644 engraphis/mcp_stdio_bridge.py create mode 100644 eval/desktop_memory.py create mode 100644 run_preflight.ps1 create mode 100644 tests/test_closure_observation_graph.py create mode 100644 tests/test_desktop_memory_workflow.py create mode 100644 tests/test_deterministic_measurements.py create mode 100644 tests/test_ended_session_derivations.py create mode 100644 tests/test_local_browser_access.py create mode 100644 tests/test_mcp_memory_concurrency.py create mode 100644 tests/test_memory_closure_model.py create mode 100644 tests/test_mutation_closure_version.py create mode 100644 tests/test_queue_status_publication.py create mode 100644 tests/test_session_lineage_independent.py create mode 100644 tests/test_sync_closure_history.py create mode 100644 tests/test_temporal_integration_second_pass.py diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 2f2799575..134343c9e 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ "name": "engraphis-memory", "source": "./", "description": "Discipline for giving agents durable, scoped, explainable memory across sessions and repos with the Engraphis MCP tools.", - "version": "1.7.9" + "version": "1.7.10" } ] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 51092b400..aa48b9a66 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "engraphis-memory", - "version": "1.7.9", + "version": "1.7.10", "description": "Give agents durable, scoped, explainable memory across sessions and repos via the Engraphis MCP tools. Use when you learn something worth keeping, need prior context before acting, or ask why/how a fact changed. Covers remember/recall, why/timeline, forget/pin/correct, sessions, and code search.", "author": { "name": "The Engraphis Authors", diff --git a/.claude-plugin/skill-assets.sha256 b/.claude-plugin/skill-assets.sha256 index b4e4873ad..bf4ffca9d 100644 --- a/.claude-plugin/skill-assets.sha256 +++ b/.claude-plugin/skill-assets.sha256 @@ -1,6 +1,6 @@ -c1d184247775c8b3bf4d3f185d54fcac983aca8c54638a05d85b21e20db3ae7c .claude-plugin/marketplace.json -c5d0c26f28c9ee14092f9deaf24c98dd8bef49d971fef2b7a537ffb1ab9f2887 .claude-plugin/plugin.json -aeee7a94671ceb306fe2d24c5acc9f2d96ad8a8e7410536566799eea6265f080 skills/engraphis-memory/SKILL.md +8da43b92bc5ece3e7bdc63779713f76552a7bcd46adaf5ff12c0427dbfad98ae .claude-plugin/marketplace.json +b1adecc8e456a3dbe168adb7adc6a9c7c7ae63429d38b88c02722eaf293a9fba .claude-plugin/plugin.json 055655db84af07561d002f0c69744313d8413c39f3e873f941f0fa0b1e76dc66 skills/engraphis-memory/references/CONVENTIONS.md 9d090a03f5b3f36a34d91f66b72c3844591f6915755ac3a6c6ba5f1b16977de5 skills/engraphis-memory/references/SCOPING.md 4c612f734cc3f50e5566752c6956e43bd432b3c26177e16cde91e196ac9e9b5b skills/engraphis-memory/references/TOOLS.md +aeee7a94671ceb306fe2d24c5acc9f2d96ad8a8e7410536566799eea6265f080 skills/engraphis-memory/SKILL.md diff --git a/.gitattributes b/.gitattributes index bea5b22d4..b736e5f66 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,13 +1,20 @@ -* whitespace=cr-at-eol -*.pyd binary -*.so binary -*.dll binary -.github/workflows/*.yml text eol=lf -.github/workflows/*.yaml text eol=lf -*.sh text eol=lf -deploy/*.lock text eol=lf -deploy/*.licenses.json text eol=lf -.claude-plugin/*.json text eol=lf -.claude-plugin/skill-assets.sha256 text eol=lf -skills/engraphis-memory/*.md text eol=lf -skills/engraphis-memory/references/*.md text eol=lf +* whitespace=cr-at-eol +*.pyd binary +*.so binary +*.dll binary +*.py text eol=lf +.github/workflows/*.yml text eol=lf +.github/workflows/*.yaml text eol=lf +*.sh text eol=lf +# Frozen corpus/evidence manifests hash exact bytes, including executable oracles. +eval/datasets/** text eol=lf +eval/artifacts/** text eol=lf +# Historical reports include both LF and CRLF snapshots; preserve their signed bytes. +docs/benchmark-evidence/** -text +engraphis/**/vendor/*.js text eol=lf +deploy/*.lock text eol=lf +deploy/*.licenses.json text eol=lf +.claude-plugin/*.json text eol=lf +.claude-plugin/skill-assets.sha256 text eol=lf +skills/engraphis-memory/*.md text eol=lf +skills/engraphis-memory/references/*.md text eol=lf diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index e464132c8..000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -buy_me_a_coffee: Jaixii diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c5298a60b..aba3092ac 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -89,6 +89,9 @@ jobs: python -m eval.ablation python -m eval.reinforcement python -m eval.adversarial_memory_security + python -m eval.grounded + python -m eval.code_arm + python -m eval.evidence_contracts python -m pip_audit --local --skip-editable - name: Capture the exact build environment and Python SBOM @@ -328,6 +331,9 @@ jobs: python -m eval.ablation python -m eval.reinforcement python -m eval.adversarial_memory_security + python -m eval.grounded + python -m eval.code_arm + python -m eval.evidence_contracts artifact-core-py39: name: Python 3.9 installed release artifacts diff --git a/AGENTS.md b/AGENTS.md index 96d3a43cd..23fffa9c3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,7 +29,7 @@ most common mistake here. | Status | Primary scoped, bi-temporal, interface-driven implementation. | Compatibility/reference implementation with flat namespaces. | | Model | Scoped + bi-temporal + typed; interface-driven. | Single flat `namespace` string per memory. | | Code | `engraphis/core/`, `engraphis/backends/`, `eval/`, `tests/`, `scripts/migrate_to_v2.py` | `engraphis/app.py`, `config.py`, `models.py`, `routes/`, `stores/`, `engines/`, `llm/`, `static/` | -| Data | new v2 schema (`SCHEMA_VERSION = 18`) | `engraphis_v1.db` | +| Data | new v2 schema (`SCHEMA_VERSION = 19`) | `engraphis_v1.db` | | Entry | `engraphis.MemoryEngine.create()` / `engraphis.create_memory_engine()` → `engraphis/factory.py` → `core/engine.py` | Internal reference only; never a public launcher | **Rule:** build new capability on **v2** (`core/` + `backends/`) behind the interfaces. @@ -210,7 +210,7 @@ These are pure, unit-tested functions — change them only with a corresponding --- -## 5. Data model cheat-sheet (`core/interfaces.py`, `core/schema.py` — `SCHEMA_VERSION = 18`) +## 5. Data model cheat-sheet (`core/interfaces.py`, `core/schema.py` — `SCHEMA_VERSION = 19`) - **Scope hierarchy:** `workspace → repo → session → memory`. Scopes: `session|repo|workspace|user`. - **Bi-temporal validity on every record:** world-time `valid_from/valid_to` + @@ -232,6 +232,10 @@ These are pure, unit-tested functions — change them only with a corresponding - **Erasure markers contain no memory content.** `memory_tombstones.export_class` is strictly `never_export|remote_erasure`; only `remote_erasure` may cross a sync boundary. - **Vectors are stored L2-normalized** so cosine similarity == dot product. +- **Closure history is append-only:** `memory_closures` preserves each known closure. + Historical visibility uses the earliest endpoint known at `known_at`; the scalar + `valid_to` pair is only the current compatibility summary. Sync v4 transports the + observation union; earlier clients must upgrade before receiving v4 bundles. --- diff --git a/BENCHMARKS.md b/BENCHMARKS.md index 2fcaa05d4..4e5707fdc 100644 --- a/BENCHMARKS.md +++ b/BENCHMARKS.md @@ -6,102 +6,102 @@ those results. When this document and the code disagree, the code is the source The current expansion has a separate [results and workload report](docs/BENCHMARK_EXPANSION_RESULTS.md), [execution runbook](docs/BENCHMARK_EXPANSION_RUNBOOK.md), and [proposed stage budgets](docs/BENCHMARK_STAGE_BUDGETS.md). Completed external retrieval diagnostics -are review artifacts with explicit denominators and uncertainty. The coding pilot uses Codex -OAuth only and retains fixture exclusions and interrupted calls. Official QA, competitor -scores and capacity qualification remain separate experiments. - -For the locked operator sequence for a public canonical run, see -[`docs/PUBLIC_BENCHMARK_RUNBOOK.md`](docs/PUBLIC_BENCHMARK_RUNBOOK.md). - -The current measured improvement priorities and their evidence boundaries are in -[`docs/BENCHMARK_IMPROVEMENT_PRIORITIES.md`](docs/BENCHMARK_IMPROVEMENT_PRIORITIES.md). -The implementation exposes three opt-in comparison controls: `packing_mode="coverage"` -for complete evidence units across sources, source-bound `exact_value` fields on the -Smart/Classic/service write paths, and `retrieval_recipe="conversation"` or `"long_session"` -for the measured depth/budget starting points. `"legacy"` packing and `"default"` -retrieval remain the defaults until development, validation and untouched-holdout gates -show a workload-specific benefit. +are review artifacts with explicit denominators and uncertainty. The coding pilot uses Codex +OAuth only and retains fixture exclusions and interrupted calls. Official QA, competitor +scores and capacity qualification remain separate experiments. + +For the locked operator sequence for a public canonical run, see +[`docs/PUBLIC_BENCHMARK_RUNBOOK.md`](docs/PUBLIC_BENCHMARK_RUNBOOK.md). + +The current measured improvement priorities and their evidence boundaries are in +[`docs/BENCHMARK_IMPROVEMENT_PRIORITIES.md`](docs/BENCHMARK_IMPROVEMENT_PRIORITIES.md). +The implementation exposes three opt-in comparison controls: `packing_mode="coverage"` +for complete evidence units across sources, source-bound `exact_value` fields on the +Smart/Classic/service write paths, and `retrieval_recipe="conversation"` or `"long_session"` +for the measured depth/budget starting points. `"legacy"` packing and `"default"` +retrieval remain the defaults until development, validation and untouched-holdout gates +show a workload-specific benefit. `python -m eval.evidence_contracts` checks exact-action validation and compares legacy and coverage packing on small deterministic development fixtures. It runs in the full offline CI matrix and the NumPy-only Python 3.9 job. These fixtures -test boundary correctness; they do not estimate external QA or model task success. The -[current diagnostic](docs/benchmark-evidence/evidence-contracts-20260921-v11.json) -withholds the unchanged oversized, unpunctuated fixture at its 24-token budget: -no fitting sentence boundary proves that the remaining condition text can be dropped. -The prior literal-preservation result remains in the historical artifact. Tight budgets -can therefore return fewer exact values under the complete-group contract. -Action contracts also require the host to supply the actual source text when creating -and validating a contract. The source revision digest and literal offsets must match; -four additional diagnostic cases reject forged or stale bindings. Authorization -remains the responsibility of the trusted host. -Eight fixed query-window cases also check that unrelated bound values cannot displace the -requested evidence. Their original retention expectations remain unchanged: 4/8 passed on -`0244c45f`, 8/8 on `c3a86295`, and 5/8 under the current complete-source rule. Three tight -bound-value cases now withhold their binding. A separate source-completeness gate checks -the same eight inputs, improving from 5/8 to 8/8, including roomy binding controls. -These development fixtures and their source hashes remain separate from external quality -measurements. - -Five safe-withholding cases improve from 0/5 against `65f4becd` to 5/5. A separate -five-case retention population exposes a tradeoff: the earlier v8 implementation -retained the expected literals in 5/5 cases, while the complete-unit rule retains -0/5 at those unchanged tight budgets. These unpunctuated records do not fit as -complete units. Their original inputs, expected literals and outcomes remain in -the diagnostic; safe omission is not counted as successful literal retention. - -Eight distant-restriction cases improve from 5/8 on `98e8f5c2` to 8/8, including -roomy retention controls. Exact binding now requires the complete meaningful source, -regardless of language or recognized qualifier words. Boundary whitespace may be trimmed -only outside the literal. Coverage withholds the bound occurrence when that source cannot -fit, while it may still select other unbound evidence. This also prevents -`Only use ALPHA in production` from becoming `Only use ALPHA`. Complete-unit safety -remains 16/16 against 8/16 on `98e8f5c2` in its separate diagnostic and CI gate. -Headers, titles and source attribution remain inside the budget; expansion cannot restore -an incomplete binding. This conservative rule retains source text without inferring its -meaning or authorizing a downstream action. - -The default legacy packer also withholds exact binding metadata when its selected -excerpt omits part of the complete source. Eight fixed cases improve -from 4/8 on `833e3e92` to 8/8: four incomplete bindings are suppressed and four -complete bindings remain. All eight retain identical context text and token -accounting. The literal can still appear as ordinary text; this diagnostic measures -binding safety, not a retrieval or answer-quality gain. - -Twelve additional French, Chinese and English-synonym cases exercise both modes at fixed -budgets. Against `c3a86295`, the complete binding contract improves from 10/12 to 12/12 -for legacy and from 5/12 to 12/12 for coverage. Unsafe bindings fall from two to zero -and seven to zero, respectively. Coverage literal leaks fall from seven to zero; all five -roomy controls retain their exact bindings in both modes. Independent token accounting -matches the rendered context and all budgets are honored. The gate separately rejects -omitted roomy bindings, incorrect spans and token-accounting errors. This is a fixed -development population, not a claim of general multilingual understanding. - -Corpus replay rejects non-boolean trust and answerability labels before execution. -Direct campaign records also require boolean trust labels and reject contradictory -nested trust metadata. Peer evidence IDs must name a recorded source; returned -backend IDs must agree with that source or resolve through recorded episode lineage. -Frozen campaign scope and trust take precedence over peer labels, and unrecognized labels -remain unknown. Core/service recall results report the recipe's effective output -limit as `effective_k`; recall receipts preserve it as `metadata.k` together with -`retrieval_recipe` and normalized `packing_mode`, independently of the number of -results actually returned. Historical receipts without a mode remain valid and -do not acquire an inferred value. -Campaign adapters report the frozen manifest's evaluated revision; direct adapters -retain `unknown` provenance unless explicitly bound. These checks protect result -interpretation and do not count as additional benchmark-quality gains. - -### Public numeric evidence registry +test boundary correctness; they do not estimate external QA or model task success. The +[current diagnostic](docs/benchmark-evidence/evidence-contracts-20260921-v11.json) +withholds the unchanged oversized, unpunctuated fixture at its 24-token budget: +no fitting sentence boundary proves that the remaining condition text can be dropped. +The prior literal-preservation result remains in the historical artifact. Tight budgets +can therefore return fewer exact values under the complete-group contract. +Action contracts also require the host to supply the actual source text when creating +and validating a contract. The source revision digest and literal offsets must match; +four additional diagnostic cases reject forged or stale bindings. Authorization +remains the responsibility of the trusted host. +Eight fixed query-window cases also check that unrelated bound values cannot displace the +requested evidence. Their original retention expectations remain unchanged: 4/8 passed on +`0244c45f`, 8/8 on `c3a86295`, and 5/8 under the current complete-source rule. Three tight +bound-value cases now withhold their binding. A separate source-completeness gate checks +the same eight inputs, improving from 5/8 to 8/8, including roomy binding controls. +These development fixtures and their source hashes remain separate from external quality +measurements. + +Five safe-withholding cases improve from 0/5 against `65f4becd` to 5/5. A separate +five-case retention population exposes a tradeoff: the earlier v8 implementation +retained the expected literals in 5/5 cases, while the complete-unit rule retains +0/5 at those unchanged tight budgets. These unpunctuated records do not fit as +complete units. Their original inputs, expected literals and outcomes remain in +the diagnostic; safe omission is not counted as successful literal retention. + +Eight distant-restriction cases improve from 5/8 on `98e8f5c2` to 8/8, including +roomy retention controls. Exact binding now requires the complete meaningful source, +regardless of language or recognized qualifier words. Boundary whitespace may be trimmed +only outside the literal. Coverage withholds the bound occurrence when that source cannot +fit, while it may still select other unbound evidence. This also prevents +`Only use ALPHA in production` from becoming `Only use ALPHA`. Complete-unit safety +remains 16/16 against 8/16 on `98e8f5c2` in its separate diagnostic and CI gate. +Headers, titles and source attribution remain inside the budget; expansion cannot restore +an incomplete binding. This conservative rule retains source text without inferring its +meaning or authorizing a downstream action. + +The default legacy packer also withholds exact binding metadata when its selected +excerpt omits part of the complete source. Eight fixed cases improve +from 4/8 on `833e3e92` to 8/8: four incomplete bindings are suppressed and four +complete bindings remain. All eight retain identical context text and token +accounting. The literal can still appear as ordinary text; this diagnostic measures +binding safety, not a retrieval or answer-quality gain. + +Twelve additional French, Chinese and English-synonym cases exercise both modes at fixed +budgets. Against `c3a86295`, the complete binding contract improves from 10/12 to 12/12 +for legacy and from 5/12 to 12/12 for coverage. Unsafe bindings fall from two to zero +and seven to zero, respectively. Coverage literal leaks fall from seven to zero; all five +roomy controls retain their exact bindings in both modes. Independent token accounting +matches the rendered context and all budgets are honored. The gate separately rejects +omitted roomy bindings, incorrect spans and token-accounting errors. This is a fixed +development population, not a claim of general multilingual understanding. + +Corpus replay rejects non-boolean trust and answerability labels before execution. +Direct campaign records also require boolean trust labels and reject contradictory +nested trust metadata. Peer evidence IDs must name a recorded source; returned +backend IDs must agree with that source or resolve through recorded episode lineage. +Frozen campaign scope and trust take precedence over peer labels, and unrecognized labels +remain unknown. Core/service recall results report the recipe's effective output +limit as `effective_k`; recall receipts preserve it as `metadata.k` together with +`retrieval_recipe` and normalized `packing_mode`, independently of the number of +results actually returned. Historical receipts without a mode remain valid and +do not acquire an inferred value. +Campaign adapters report the frozen manifest's evaluated revision; direct adapters +retain `unknown` provenance unless explicitly bound. These checks protect result +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-pr253-four-gap-stack-20261008.json`](docs/benchmark-evidence/offline-fixtures-pr253-four-gap-stack-20261008.json) artifact. Its +[`offline-fixtures-memory-timeout-20261020.json`](docs/benchmark-evidence/offline-fixtures-memory-timeout-20261020.json) artifact. Its SHA-256 is -`e423e99ba683c1928984903aef715ad97440c9717cecb3bd3f6fe4f6746ea7d0`, also recorded in the -adjacent `.sha256` file. The artifact contains no raw questions, answers, prompts, customer data, -or per-record content fingerprints. - +`3b1d2127e290e0ae6fb6c411263d9c61bec98d1b27e408d3da4cb2d51fa81f9f`, 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 -`e1b7fcea31cb7bf8f8ec85b56e097a0cbfc7d496aef4aef51de73b849b2d17c6`. The artifact defines +`2fa807f78e18cd2b4fe59da9ea6f73c4a7e517cdfd42ff2a8d8e6c7844d2e891`. 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)`: @@ -126,24 +126,24 @@ 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-pr253-four-gap-stack-20261008.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-memory-timeout-20261020.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-pr253-four-gap-stack-20261008.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-memory-timeout-20261020.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). -Fresh diagnostics retain explicit source-case identities so confidence intervals -cluster whole conversations even when question IDs do not encode their case. -Metrics with no eligible questions remain `null` (unscored), including fresh and -resumed runs. Artifact parsing, checksum validation, and queue receipts bind the -same byte snapshot. Comparisons require consistent dataset and repair bindings -while allowing the producer implementation to change between versions. - +Fresh diagnostics retain explicit source-case identities so confidence intervals +cluster whole conversations even when question IDs do not encode their case. +Metrics with no eligible questions remain `null` (unscored), including fresh and +resumed runs. Artifact parsing, checksum validation, and queue receipts bind the +same byte snapshot. Comparisons require consistent dataset and repair bindings +while allowing the producer implementation to change between versions. + ## What we measure today (all offline, no API key) Most Engraphis evals score **retrieval**, not end-to-end QA. The separate productivity benchmark @@ -186,9 +186,9 @@ frontier-model QA score. context packing; additive `packed_quality` fields score only chunks admitted to reader context. Payload proxies are sampled once per question, independently of the number of timed iterations; they are not serialized MCP envelopes or transport responses. In the - registered CodeMem run, 26 payload samples total **24,590** full-proxy - `engraphis.regex.v1` tokens versus **11,138** compact-proxy tokens, avoiding **13,452** proxy - tokens (**54.71% lower**), while 260 recalls are timed. Packed context across the same 26 + registered CodeMem run, 26 payload samples total **24,590** full-proxy + `engraphis.regex.v1` tokens versus **11,138** compact-proxy tokens, avoiding **13,452** proxy + tokens (**54.71% lower**), while 260 recalls are timed. Packed context across the same 26 samples averages **85.38** tokens and reaches **108** under a 1,500-token cap; Recall@5, hit@5, and answer-token recall remain 1.000 for the legacy candidate-page view. The registered v9 artifact predates `packed_quality`, so no packed-quality aggregate is published from it. @@ -335,15 +335,15 @@ per-question records, explicit exclusions, fixed-budget context curves, and dete stratified or paired bootstrap confidence intervals. Every run names its token counter. Noncanonical offline fixtures may identify a deterministic estimate; canonical public evidence requires the exact pinned reader tokenizer and immutable model revision. The lightweight CI -fixtures validate that machinery; they are not a claim about external benchmark performance. - -Public journey and external retrieval exports identify producer code by unique -repository-relative source names. Private dataset, conversation, repair, and -LongMemEval-V2 inputs use stable role names such as `inputs/dataset` or -`inputs/haystack`; their directories are never published. -Each name remains bound to its SHA-256 and byte count. The shared envelope keeps -basename-only defaults for other callers and historical artifacts; exporters opt in -with explicit `source_names` and verify the completed envelope against evaluated bytes. +fixtures validate that machinery; they are not a claim about external benchmark performance. + +Public journey and external retrieval exports identify producer code by unique +repository-relative source names. Private dataset, conversation, repair, and +LongMemEval-V2 inputs use stable role names such as `inputs/dataset` or +`inputs/haystack`; their directories are never published. +Each name remains bound to its SHA-256 and byte count. The shared envelope keeps +basename-only defaults for other callers and historical artifacts; exporters opt in +with explicit `source_names` and verify the completed envelope against evaluated bytes. The benchmark context metric reads strict recall usage fields rather than inferring prompt size: `budget_tokens`, `context_tokens`, `source_tokens`, `saved_tokens`, `savings_ratio`, diff --git a/CHANGELOG.md b/CHANGELOG.md index a350b2924..c82382596 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,40 @@ All notable changes to Engraphis are documented here. Format loosely follows ## [Unreleased] +## [1.7.10] - 2026-10-18 + +Candidate package version: **1.7.10**. Publication and hosted-service qualification remain separate. + +- Preserve repeated validity closures in schema 19 so later corrections cannot change + fixed historical queries. Apply the same knowledge-time rule to recall, graph evidence, + browsing, scoring, workspace copies, and exports. Sync v4 carries the closure history + and accepts v1-v3 input; older peers must upgrade before receiving v4 bundles. +- Preserve original session lineage when correcting, revising, or approving repo and + workspace memories after that session ends. Ordinary writes and session-scoped + memories retain active-session checks. +- Include additional closure observations in optimistic memory versions, rejecting + concurrent history changes without changing existing scalar-only version hashes. +- Prevent dashboard sync from sending a managed Cloud credential to a custom relay. + Preserve authorization failures during read-only pulls so the dashboard offers recovery. +- Reject XML document types through the parser, including UTF-16 XML and Office containers. +- Enforce bound-workspace access for session reads/closure and memory pinning, + reinforcement, and validity closure. +- Search all scoped history pages for `why` and `timeline`, preserving relevant older facts. +- Make dense measurement tokenization linear while preserving the deterministic features. +- Pin the default MiniLM model revision so newer loaders that omit resolved commit + metadata can still identify its persistent embedding space. +- Require MCP SDK 1.30.0 or later in the 1.x line, including the upstream HTTP + session-retention security fix. +- Ship both Command Code hooks and the benchmark renderer template in wheel and source + distributions; installed hooks use the installation's Python interpreter. +- Remove unsupported hosted Automation controls, align supported input bounds with Cloud, + and run grounded, code-arm, and evidence-contract gates in release validation. +- Preserve byte-hashed evaluation fixtures with LF checkout rules on Windows. +- Preserve immutable historical report bytes and retry transient Windows reader locks + when publishing local benchmark queue status. +- Include automatic same-origin loopback dashboard sessions without user-entered tokens, + retaining remote authentication and reviewed-write protections. + ## [1.7.9] - 2026-09-29 - Classify local `engraphis_decide` command advice conservatively. Chained, piped, diff --git a/MANIFEST.in b/MANIFEST.in index 8edd2fb0e..518ea0d67 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,29 +1,31 @@ -global-exclude *.pyc -global-exclude *.pyo - -recursive-include engraphis/static *.html *.css *.js *.png *.ico -recursive-include engraphis/static/vendor * -recursive-include engraphis/classic_assets *.html *.css *.js *.png *.ico -recursive-include engraphis/classic_assets/vendor * -recursive-include engraphis/dashboard_assets *.html *.css *.js *.png *.ico -recursive-include engraphis/dashboard_assets/vendor * -include engraphis/commercial_manifest.json -include LICENSE NOTICE README.md CHANGELOG.md BENCHMARKS.md -include docs/RECALL_RECOVERY.md -include docs/REWORK_EXECUTION.md docs/ENGINE_CAPACITY_PROTOCOL.md docs/CODING_ACCEPTANCE_CORPUS.md -include docs/PAID_EVALUATION_PROPOSAL.md -include docs/DOCUMENT_IMPORT.md docs/OBSIDIAN_IMPORT.md -include docs/images/context-efficiency.svg -include pyproject.toml -include .env.example requirements.txt -include docker-entrypoint.sh Dockerfile docker-compose.yml docker-compose.lan.yml -include deploy/force-graph-1.51.4.licenses.json -include deploy/force-graph-1.51.4.yarn.lock -include railway.json -recursive-include eval *.py -include eval/BASELINES.md -include eval/EVIDENCE.md -recursive-include eval/configs *.json -recursive-include eval/datasets *.jsonl -include eval/datasets/locomo10_repair_manifest.json -recursive-include tests *.py +global-exclude *.pyc +global-exclude *.pyo + +recursive-include engraphis/static *.html *.css *.js *.png *.ico +recursive-include engraphis/static/vendor * +recursive-include engraphis/classic_assets *.html *.css *.js *.png *.ico +recursive-include engraphis/classic_assets/vendor * +recursive-include engraphis/dashboard_assets *.html *.css *.js *.png *.ico +recursive-include engraphis/dashboard_assets/vendor * +include engraphis/commercial_manifest.json +recursive-include integrations/commandcode *.py +recursive-include scripts/templates *.svg +include LICENSE NOTICE README.md CHANGELOG.md BENCHMARKS.md +include docs/RECALL_RECOVERY.md +include docs/REWORK_EXECUTION.md docs/ENGINE_CAPACITY_PROTOCOL.md docs/CODING_ACCEPTANCE_CORPUS.md +include docs/PAID_EVALUATION_PROPOSAL.md +include docs/DOCUMENT_IMPORT.md docs/OBSIDIAN_IMPORT.md +include docs/images/context-efficiency.svg +include pyproject.toml +include .env.example requirements.txt +include docker-entrypoint.sh Dockerfile docker-compose.yml docker-compose.lan.yml +include deploy/force-graph-1.51.4.licenses.json +include deploy/force-graph-1.51.4.yarn.lock +include railway.json +recursive-include eval *.py +include eval/BASELINES.md +include eval/EVIDENCE.md +recursive-include eval/configs *.json +recursive-include eval/datasets *.jsonl +include eval/datasets/locomo10_repair_manifest.json +recursive-include tests *.py diff --git a/README.md b/README.md index 20f3a04dc..4cdcae1b7 100644 --- a/README.md +++ b/README.md @@ -1,109 +1,90 @@ -# Engraphis - -[![PyPI version](https://img.shields.io/pypi/v/engraphis.svg)](https://pypi.org/project/engraphis/) -[![License](https://img.shields.io/badge/license-Apache--2.0-green.svg)](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/LICENSE) - -**Persistent, local-first memory for AI agents.** Engraphis stores scoped project knowledge, retrieves relevant evidence across vector, lexical, graph, and code search, and returns bounded context with sources an agent can inspect. - -The local engine uses SQLite and works offline. It keeps changes over time instead of silently replacing facts, and grounded recall cites retrieved memories or abstains when evidence is weak. - -

- Engraphis local knowledge graph showing relationships between remembered entities -
- Explore memories and their relationships in the local dashboard. -

- -## Install and try it - -The full local application requires Python 3.10 or newer. The NumPy-only core supports Python 3.9+. - -```bash -python -m pip install "engraphis[all]" -engraphis-dashboard -``` - -The dashboard opens at [http://127.0.0.1:8700](http://127.0.0.1:8700). Local use needs no cloud account or API key. - -To use the Python service directly: - -```python -from engraphis.service import MemoryService - -memory = MemoryService.create("engraphis.db") -memory.remember("Auth migrated from JWT to PASETO.", workspace="acme", repo="api") -hit = memory.recall("Why did we change auth?", workspace="acme", repo="api") -print(hit["context"]) -``` - -Connect a coding agent over MCP with the [agent setup guide](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/docs/AGENT_CONNECT.md). - -## What it provides - -- **Continuity:** Organize memories by workspace, repository, and session, with a last-session handoff. -- **Useful recall:** Combine vector, lexical, graph, and code-aware retrieval, then pack results to a hard context budget. -- **Grounded answers:** Preserve provenance and temporal history; cite support or abstain when it is insufficient. -- **Operator control:** Keep the core local and offline-capable, with explicit controls for hosted services and external LLM providers. - -## 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. - -

- 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 e423e99ba683; 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. -

- -| Fixture | Result | Quality check | -|---|---|---| -| Structure-aware chunking | Mean retrieved top-5 context: 740.3 → 214.3 tokens per question (71.1% lower, 18 questions) | Recall@5 = 1.000 in both modes | -| Recall payload proxy | JSON-shape proxy: 24,590 → 11,138 tokens (54.71% lower, 26 samples; 260 timed recalls) | Candidate and packed Recall@5, Hit@5, and answer-token recall are each 1.000 | -| Grounded decisions | 5/5 answerable queries grounded; 6/6 abstention queries rejected, including 1/1 quarantined-evidence check | 11/11 decisions correct | - -The payload figure is a serialized JSON-shape estimate, not an MCP transport measurement or provider billing total. See the [Benchmark methodology](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/BENCHMARKS.md) for artifact identity, counting methods, reproduction commands, external-evaluation boundaries, and limitations. - -## Optional Jev assistance - -Use **Review with Jev** in the local dashboard to check whether selected evidence can answer a question or compare two memories for a possible contradiction. Remote review sends only selected, bounded excerpts and the supplied question after per-call consent and `public` or `internal` classification. Secret, malformed, and unknown sensitivity classifications are blocked. The existing MCP decision tool handles command and completion reviews. - -Recall route selection is experimental and BYOK-only. Smart MCP opts in through `engraphis_recall_context` with `allow_remote=true` and a `public` or `internal` classification. Classic MCP also requires `planning="auto"` and `jev_assisted=true`. Jev receives the original query and bounded deterministic routes, not recalled memory bodies; it cannot change scope, time, type, or trust filters. Local behavior is the default. Uncertain, malformed, or failed requests fall back to deterministic behavior. Jev cannot write memories, change grounded-recall requirements, authorize commands, or certify completion. Direct BYOK use may incur provider charges. - -Jev-assisted recall planning is experimental. In an exploratory comparison using 40 public synthetic tasks, Jev selected a route on all 40 calls but did not change nDCG@5, Recall@5, or answer-token coverage. This fixture does not establish a benefit on held-out user workloads, so no retrieval-quality improvement is claimed. - -Managed Jev is currently `not_yet_available` pending release acceptance and -service-capacity qualification; client configuration does not enable it. After -enablement, every legitimate Pro user and each eligible Team named seat, including paid -viewers, with an active paid, trial, or test entitlement receives managed Jev at no -additional customer charge and without a personal provider key. - -Each individual receives all three independent rolling limits: **100 evaluated questions per rolling hour, 1,000 per rolling five hours, and 2,000 per rolling 24 hours**. Usage is per -person, not pooled across a Team and not monthly. Each evaluated question counts once; -if a batch is evaluated, every question counts, and command review evaluates two -questions. Admitted attempts that fail or are interrupted remain counted. No overage is -charged. The account portal reports each member's remaining usage across all three -windows; usage returns as earlier questions leave each rolling window. - -The existing production fleet guard remains 100 questions per day across the service. -That limit conflicts with these per-person rolling caps and may pause or reject requests -earlier. Resolve capacity and the fleet guard before launch. The supported decisions are -advisory; current synthetic fixtures do not demonstrate Jev accuracy or cost savings. -See [hosted plans and Jev details](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/docs/HOSTED_PLANS.md#included-system-1-decision-engine-jev). - -## Guides - -- [Configuration reference](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/docs/CONFIGURATION.md) -- [MCP tool reference](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/docs/MCP_TOOLS.md) -- [Agent and LLM provider setup](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/docs/LLM_PROVIDERS.md) -- [Architecture and query planning](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/docs/ARCHITECTURE_V3.md#query-planning) -- [Docker deployment](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/docs/DOCKER.md) -- [Document import](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/docs/DOCUMENT_IMPORT.md) -- [Cloud Sync](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/docs/SYNC.md) -- [Pi extension](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/integrations/pi/README.md) -- [Memory write review](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/docs/WRITE_REVIEW.md) -- [Coding-agent file protection](docs/FILE_PROTECTION.md) -- [Security policy](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/SECURITY.md) -- [Hosted plans and licensing](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/docs/HOSTED_PLANS.md) - -## License - -Engraphis is licensed under Apache-2.0. The license does not grant trademark rights. See [LICENSE](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/LICENSE), [NOTICE](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/NOTICE), and the [licensing guide](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/docs/LICENSING.md). The hosted control plane and managed services are private services. +# Engraphis + +[![PyPI version](https://img.shields.io/pypi/v/engraphis.svg)](https://pypi.org/project/engraphis/) +[![License](https://img.shields.io/badge/license-Apache--2.0-green.svg)](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/LICENSE) + +**Persistent, local-first memory for AI agents.** Engraphis stores scoped project knowledge, retrieves relevant evidence across vector, lexical, graph, and code search, and returns bounded context with sources an agent can inspect. + +The local engine uses SQLite and works offline. It keeps changes over time instead of silently replacing facts, and grounded recall cites retrieved memories or abstains when evidence is weak. + +

+ Engraphis local knowledge graph showing relationships between remembered entities +
+ Explore memories and their relationships in the local dashboard. +

+ +## Install and try it + +The full local application requires Python 3.10 or newer. The NumPy-only core supports Python 3.9+. + +```bash +python -m pip install "engraphis[all]" +engraphis-dashboard +``` + +The dashboard opens at [http://127.0.0.1:8700](http://127.0.0.1:8700). Local use needs no cloud account or API key. + +To use the Python service directly: + +```python +from engraphis.service import MemoryService + +memory = MemoryService.create("engraphis.db") +memory.remember("Auth migrated from JWT to PASETO.", workspace="acme", repo="api") +hit = memory.recall("Why did we change auth?", workspace="acme", repo="api") +print(hit["context"]) +``` + +Connect a coding agent over MCP with the [agent setup guide](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/docs/AGENT_CONNECT.md). + +## What it provides + +- **Continuity:** Organize memories by workspace, repository, and session, with a last-session handoff. +- **Useful recall:** Combine vector, lexical, graph, and code-aware retrieval, then pack results to a hard context budget. +- **Grounded answers:** Preserve provenance and temporal history; cite support or abstain when it is insufficient. +- **Operator control:** Keep the core local and offline-capable, with explicit controls for hosted services and external LLM providers. + +## 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. + +

+ 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 3b1d2127e290; 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. +

+ +| Fixture | Result | Quality check | +|---|---|---| +| Structure-aware chunking | Mean retrieved top-5 context: 740.3 → 214.3 tokens per question (71.1% lower, 18 questions) | Recall@5 = 1.000 in both modes | +| Recall payload proxy | JSON-shape proxy: 24,590 → 11,138 tokens (54.71% lower, 26 samples; 260 timed recalls) | Candidate and packed Recall@5, Hit@5, and answer-token recall are each 1.000 | +| Grounded decisions | 5/5 answerable queries grounded; 6/6 abstention queries rejected, including 1/1 quarantined-evidence check | 11/11 decisions correct | + +The payload figure is a serialized JSON-shape estimate, not an MCP transport measurement or provider billing total. See the [Benchmark methodology](https://github.com/Coding-Dev-Tools/engraphis/blob/v1.7.10/BENCHMARKS.md) for artifact identity, counting methods, reproduction commands, external-evaluation boundaries, and limitations. + +## Optional Jev assistance + +Jev is Engraphis's advisory decision engine. **It is included free with every paid plan: Pro and Team.** Every legitimate Pro user and each eligible Team named seat, including paid viewers, with an active paid, trial, or test entitlement receives managed Jev at no additional customer charge and without a personal provider key. Local advisory decisions stay free for everyone; BYOK, a personal TypeSafe key, is the optional path for non-paid or direct use, and its provider charges may apply. + +Managed Jev is currently `not_yet_available` pending release acceptance and service-capacity qualification, so client configuration does not enable it yet. Each individual receives all three independent rolling limits: **100 evaluated questions per rolling hour, 1,000 per rolling five hours, and 2,000 per rolling 24 hours**. Usage is per person, not pooled across a Team and not monthly; if a batch is evaluated, every question counts, and admitted attempts that fail or are interrupted remain counted. No overage is charged, and the account portal reports each member's remaining usage across all three windows. + +The existing production fleet guard remains 100 questions per day across the service and may pause or reject requests earlier; resolve capacity and the fleet guard before launch. Jev is advisory only: it cannot write memories, change grounded-recall requirements, authorize commands, or certify completion. Every remote call requires per-call consent: `allow_remote=true` plus a `data_classification` of `public` or `internal`; `offline_mode=true` blocks remote requests. Recall route selection is experimental and BYOK-only; no retrieval-quality improvement is claimed. See [hosted plans and Jev details](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/docs/HOSTED_PLANS.md#included-system-1-decision-engine-jev). + +## Guides + +- [Configuration reference](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/docs/CONFIGURATION.md) +- [MCP tool reference](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/docs/MCP_TOOLS.md) +- [Agent and LLM provider setup](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/docs/LLM_PROVIDERS.md) +- [Architecture and query planning](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/docs/ARCHITECTURE_V3.md#query-planning) +- [Docker deployment](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/docs/DOCKER.md) +- [Document import](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/docs/DOCUMENT_IMPORT.md) +- [Cloud Sync](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/docs/SYNC.md) +- [Pi extension](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/integrations/pi/README.md) +- [Memory write review](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/docs/WRITE_REVIEW.md) +- [Coding-agent file protection](https://github.com/Coding-Dev-Tools/engraphis/blob/main/docs/FILE_PROTECTION.md) +- [Security policy](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/SECURITY.md) +- [Hosted plans and licensing](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/docs/HOSTED_PLANS.md) + +## License + +Engraphis is licensed under Apache-2.0. The license does not grant trademark rights. See [LICENSE](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/LICENSE), [NOTICE](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/NOTICE), and the [licensing guide](https://github.com/Coding-Dev-Tools/engraphis/blob/522def372c45a26d46bc7a05829964ae0240f3a9/docs/LICENSING.md). The hosted control plane and managed services are private services. diff --git a/SECURITY.md b/SECURITY.md index 52ee7b52e..4e1bdaf66 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,210 +1,219 @@ -# Security Policy - -Engraphis is **local-first**: by default it binds to `127.0.0.1`, stores everything in a -local SQLite file, and sends nothing to third parties except the LLM provider *you* -configure. Most risk surfaces only appear when you expose it on a network or feed it -untrusted content. - -## Reporting a vulnerability - -Email **security@engraphis.dev** with details and a proof of concept. Please do not open a -public issue for undisclosed vulnerabilities. We aim to acknowledge within 3 business days -and to ship a fix or mitigation for confirmed high-severity issues within 30 days. -Coordinated disclosure is appreciated. - -## Threat model & controls - -### 1. Untrusted ingested content / memory poisoning -Memories may originate from web pages, tool output, or other untrusted sources. - -**Controls:** -- Size caps on content, title, keywords, and metadata -- Control-character stripping (NUL, BEL, ANSI/terminal escapes) -- Strict typing/enums for memory type and scope -- Provenance on every memory (`provenance.source`) -- No destructive overwrite: contradictions resolved by bi-temporal invalidation -- Governance is explicit, scope-checked, and audited -- **Deterministic quarantine for explicitly untrusted payloads:** a narrow, offline policy - recognizes instruction override, deferred-trigger, impersonation, concealment, and secret- - exfiltration shapes. A match is retained with content-free reason codes and an audit event, - but receives no vector/index/graph/evolution/retention side effects and is invisible to - normal recall. It remains available to governed historical inspection; it is never deleted. -- **Grounded-answer trust boundary:** memories marked untrusted or quarantined cannot supply - support, citations, extractive answer text, LLM synthesis sources, or recall reinforcement. - This is independent of write-time quarantine, protecting legacy/imported records too. Grounded - recall also applies normalized instruction-signal checks independently of the caller's trust - label, providing defense in depth for an accidentally or maliciously mislabeled import. -- **Prompt-safe recall by default:** ordinary engine, service, HTTP, and MCP recall excludes - every record lacking explicit trusted provenance. `include_untrusted=True` is an explicit - Python/service inspection path, not an agent prompt surface; quarantined payloads stay out of - both paths except governed historical inspection. -- **Legacy rescan:** run `python -m scripts.rescan_poisoning --db engraphis.db` to report - unlabelled/external rows, then add `--apply` to fail them closed and quarantine matching - payloads. The operation preserves the immutable record and writes content-free audit evidence. -- Optional LLM extraction and retention supervision send bounded content to the configured - provider only when explicitly enabled. Keep both disabled for a fully local write path. - -> Scope: the policy is an explainable containment control, not a general-purpose detector of -> truthfulness or every prompt-injection variant. Explicit inspection can expose non-quarantined -> input with provenance; it must not be fed to an agent or model. The deterministic delayed-trigger fixture is -> reproducible with `python -m eval.redteam_poisoning`; it covers obvious untrusted payloads, a -> detector bypass through ingest, and a mislabeled trusted case. Its metrics apply only to those -> declared synthetic cases. - -**Dashboard XSS (fixed):** Memory content rendered as markdown is now sanitized via -DOMPurify at all render sites. Verified against payloads with `onerror` handlers. - -### 2. Network exposure & authentication -- **Loopback by default** (`ENGRAPHIS_HOST=127.0.0.1`) -- **Optional bearer token** (`ENGRAPHIS_API_TOKEN`): constant-time comparison (single - shared implementation in the local customer runtime) -- **No local Team identity service:** the public dashboard is single-user. Team logins, - invitations, roles, named seats, token rotation, and organization audit are hosted and are - not mounted by this package. -- **CORS allow-list** defaults to loopback only -- If exposed beyond localhost: put behind reverse proxy with **TLS + rate limiting** - -### 3. Scope isolation -- Every read takes a `SearchFilter`; tools only return memories within requested `workspace`/`repo` -- Every write targeting a memory by ID re-validates scope membership -- Workspace creation is not controlled by a process-wide allow-list; each operation still - carries its explicit workspace/repo/session scope and authenticated personal workspaces - enforce their owner boundary - -### 4. Secrets & data at rest -- `.env`, `*.db`, `*.db-wal`, `*.db-shm` are git-ignored and must never be logged. Gitignore is - not a runtime trust boundary: Engraphis never searches the working directory for `.env`. - Configuration files are limited to owner-private `~/.engraphis/config.env` or an absolute - owner-private regular file selected by `ENGRAPHIS_ENV_FILE`; explicit process variables win. -- **Encryption at rest (opt-in):** `ENGRAPHIS_DB_KEY` / `ENGRAPHIS_DB_KEY_FILE` + - `pip install "engraphis[encryption]"` → AES-256 via SQLCipher. Whole-file; lose key = lose data. - Off by default; without it, protect with filesystem permissions + full-disk encryption. -- Review your LLM provider's data-handling terms. - -### 5. Code indexing -`engraphis_index_repo` parses source files under a path you give it, with the same trust boundary as -any other local tool the agent has. Path is attacker-controlled if agent's instructions are. -Canonical roots are restricted to the working, home, or temporary directories by default. -Set `ENGRAPHIS_INDEX_ROOTS` to a path-separator-delimited absolute-path operator allow-list to -replace those defaults for nonstandard mounts or a narrower deployment boundary. -Dashboard and REST `POST /api/code/index` use the stricter single-root boundary -`ENGRAPHIS_HTTP_INDEX_ROOT`: submitted paths resolve beneath that root. It defaults to the first -`ENGRAPHIS_INDEX_ROOTS` entry, or the current directory. An explicit HTTP root (or fallback -entry) must be absolute; an explicit HTTP root is included in the engine-approved set. MCP and -CLI indexing retain the `ENGRAPHIS_INDEX_ROOTS` allow-list semantics. -`max_files`/`max_file_bytes` bound resource use, not access within an allowed root. Traversal -does not follow file symlinks outside the root, prunes dependency/build directories during the -walk, and honors the root `.engraphisignore` without allowing negation rules to re-expose -hardcoded excludes. -Anyone who can reach an authenticated local mutation route has the authority of that local -installation, so do not share its bearer token. - -### 5a. Coding-agent file writes - -Memory governance is not a filesystem sandbox. For hosts that mediate tool calls, Engraphis ships -a fail-closed protected-path evaluator and an optional Command Code `PreToolUse` hook. Its policy -lives outside the repository, direct file tools are checked before execution, and general shell -access is denied by default because command inspection cannot guarantee non-mutation. See -[`docs/FILE_PROTECTION.md`](docs/FILE_PROTECTION.md) for setup, host integration, and limitations. -Use OS ACLs or read-only mounts when the agent can invoke tools outside that hook. - -### 6. Local resource and database ingestion -- Uploaded and folder-imported files are size/count bounded, marked `trusted:false`, and parsed - as data. Missing optional PDF/OCR/transcription tools fail explicitly. -- The import UI's `derive_facts` option is a separate explicit opt-in. If an LLM/custom - extractor is configured, selected file content may be sent to that provider; leave it off - for a strictly local import. The default and chunk extractors remain fully local. -- Audio/video transcription runs only when `ENGRAPHIS_WHISPER_MODEL` is configured. Depending on - the faster-whisper model name, the underlying library may download a model; use an absolute - local model path when strictly offline operation is required. -- PostgreSQL introspection makes an outbound connection using the caller-provided DSN. The DSN is - never stored, returned, placed in receipts, or included in an error; only a one-way source digest - is retained. Use a read-only database account and limit network reachability at the OS/firewall - layer. - -### 7. Read-only graph server -`engraphis-graph-server` has no mutation routes, disables recall reinforcement and receipt writes, -and refuses non-loopback binding unless `ENGRAPHIS_GRAPH_TOKEN` (or `ENGRAPHIS_API_TOKEN`) is set. -The token protects access, not transport confidentiality; use TLS at a reverse proxy off-host. - -### 8. Privacy receipts -Operation receipts exclude raw memory/query content, workspace/repo names, raw IDs, and actor -identities. Each workspace chain has a separately maintained local count/head anchor, so ordinary -row modification, reordering, interior deletion, and tail truncation are detected. The actor/scope -digests are pseudonymous, not anonymous: predictable values may be guessable. The chain is -unkeyed and its local anchor lives in the same database, so an attacker able to rewrite the whole -database can recompute both. Preserve an exported `head` + `count` outside the database and pass -them back as `expected_head` / `expected_count` when independent evidence is required. - -### 9. Supply chain -- Core runs on `numpy` alone; heavy components gated behind extras -- Code-graph backend falls back to regex indexer on any failure -- Pin versions and run `pip audit` in your environment - -### 10. Hosted-service clients - -- **Cloud authorization:** the public package accepts only short-lived scoped access tokens or - a rotating refresh credential bound to its bootstrap `device` or `member` subject. It contains - no paid-key parser, signer, issuer, local feature gate, or long-lived-key relay exchange. -- **Credential-origin binding:** a saved rotating credential remains bound to the control/compute - URLs recorded for its family; environment changes cannot redirect it. A standalone sync token - likewise requires `ENGRAPHIS_SYNC_TOKEN_ORIGIN` to match the relay origin. -- **Server authority:** every hosted and cost-bearing operation is authorized by the private - control plane; local plan labels and upgrade URLs are presentation metadata only. -- **Cloud Sync and managed-compute privacy:** Cloud Sync encrypts eligible shared-workspace - changes end-to-end before they leave the device, so Engraphis Cloud cannot read their contents. - Managed compute is separate: Analytics, Auto Dreaming, and Auto Consolidation upload a - readable bounded snapshot. Consent travels with the cloud account: connecting an installation - to Engraphis Cloud accepts the terms covering managed compute, so a connected installation is - allowed and an installation with no cloud session is never allowed. Operators may override with - `ENGRAPHIS_MANAGED_COMPUTE_CONSENT` (`0` opts a connected installation back out). The snapshot - carries normal and sensitive memory content, excludes secret-class and session-scoped rows, and - is capped at 16 MiB. Secret-class memories are excluded before serialization and rejected again - by the hosted service. -- **Cloud Sync rollback bounds:** version-3 snapshots chain per-device generations, state hashes, - and tombstone checkpoints. Previously observed rollback is rejected, but first contact remains - unanchored/incomplete until the hosted service supplies an authenticated workspace manifest: - a local client cannot prove that an untrusted relay did not withhold an unseen device. -- **Trial and grace are separate:** new email-confirmed trials last 7 active days for Pro - and 14 active days for Team. Existing grants retain their recorded deadlines until explicitly - extended. Only eligible, currently active trials can be extended, with the total duration - measured from the original verified start; expired trials are never restarted. A separately - bounded, maximum-24-hour local workspace-write grace never extends the trial, subscription, - Cloud Sync, managed compute, Team access, seats, or credentials. -- **Remote URL validation:** hosted endpoints require HTTPS except explicit loopback use, - reject embedded credentials and redirects, require globally routable resolved addresses, - and pin credential-bearing TLS connections to a vetted address while verifying the - original hostname. -- **Bounded I/O:** credential-bearing JSON responses are read through strict byte limits; - malformed, oversized, or authoritative denial responses fail closed. -- **HTTP response headers:** every entrypoint sends `X-Content-Type-Options: nosniff`, - `X-Frame-Options: DENY`, a `frame-ancestors 'none'` CSP, `Referrer-Policy`, and - `Permissions-Policy`; HSTS is added on HTTPS requests only. Override the CSP with - `ENGRAPHIS_CSP` (empty string disables) or HSTS with `ENGRAPHIS_HSTS`. -### 11. Private hosted service boundary - -The public package accepts only customer mode. License issuance, billing fulfillment, Team -identity, hosted relay storage, managed compute, worker execution, transactional email, and -vendor administration are built and operated from a private repository. Their signing keys, -credentials, databases, rate limits, and operational runbooks are not distributed in this -repository or its release artifacts. - -## Known limitations -- Rate limiting: in-process limiter ships (`ENGRAPHIS_RATE_LIMIT`, off by default); - use reverse proxy for multi-process/distributed -- Encryption at rest is opt-in for the local memory DB (SQLCipher via - `ENGRAPHIS_DB_KEY`). Protect customer credential state and backups separately. -- Cloud Sync requires an authorized device to hold its workspace encryption key. The relay cannot - recover a lost key or decrypt its ciphertext; `secret` memories are never uploaded. -- Per-token scope/tenant authorization is partial: isolate distinct tenants by running - one instance each -- Legacy v1 REST server/dashboard is a compatibility surface; prefer v2/MCP path -- **Open-core enforcement ceiling:** the Apache-licensed client can be modified. Do not rely - on local checks to protect proprietary value. Paid authority and cost-bearing features - execute on the private hosted service, where customer forks cannot bypass authorization. - -## Supported versions -The latest published stable release is the supported line. Release-candidate documentation on -`main` does not retire the previously published line: support moves only when matching artifacts -are available from both PyPI and GitHub Releases. Pin a version and watch releases for -advisories. +# Security Policy + +Engraphis is **local-first**: by default it binds to `127.0.0.1`, stores everything in a +local SQLite file, and sends nothing to third parties except the LLM provider *you* +configure. Most risk surfaces only appear when you expose it on a network or feed it +untrusted content. + +## Reporting a vulnerability + +Email **security@engraphis.dev** with details and a proof of concept. Please do not open a +public issue for undisclosed vulnerabilities. We aim to acknowledge within 3 business days +and to ship a fix or mitigation for confirmed high-severity issues within 30 days. +Coordinated disclosure is appreciated. + +## Threat model & controls + +### 1. Untrusted ingested content / memory poisoning +Memories may originate from web pages, tool output, or other untrusted sources. + +**Controls:** +- Size caps on content, title, keywords, and metadata +- Control-character stripping (NUL, BEL, ANSI/terminal escapes) +- Strict typing/enums for memory type and scope +- Provenance on every memory (`provenance.source`) +- No destructive overwrite: contradictions resolved by bi-temporal invalidation +- Governance is explicit, scope-checked, and audited +- **Deterministic quarantine for explicitly untrusted payloads:** a narrow, offline policy + recognizes instruction override, deferred-trigger, impersonation, concealment, and secret- + exfiltration shapes. A match is retained with content-free reason codes and an audit event, + but receives no vector/index/graph/evolution/retention side effects and is invisible to + normal recall. It remains available to governed historical inspection; it is never deleted. +- **Grounded-answer trust boundary:** memories marked untrusted or quarantined cannot supply + support, citations, extractive answer text, LLM synthesis sources, or recall reinforcement. + This is independent of write-time quarantine, protecting legacy/imported records too. Grounded + recall also applies normalized instruction-signal checks independently of the caller's trust + label, providing defense in depth for an accidentally or maliciously mislabeled import. +- **Prompt-safe recall by default:** ordinary engine, service, HTTP, and MCP recall excludes + every record lacking explicit trusted provenance. `include_untrusted=True` is an explicit + Python/service inspection path, not an agent prompt surface; quarantined payloads stay out of + both paths except governed historical inspection. +- **Legacy rescan:** run `python -m scripts.rescan_poisoning --db engraphis.db` to report + unlabelled/external rows, then add `--apply` to fail them closed and quarantine matching + payloads. The operation preserves the immutable record and writes content-free audit evidence. +- Optional LLM extraction and retention supervision send bounded content to the configured + provider only when explicitly enabled. Keep both disabled for a fully local write path. + +> Scope: the policy is an explainable containment control, not a general-purpose detector of +> truthfulness or every prompt-injection variant. Explicit inspection can expose non-quarantined +> input with provenance; it must not be fed to an agent or model. The deterministic delayed-trigger fixture is +> reproducible with `python -m eval.redteam_poisoning`; it covers obvious untrusted payloads, a +> detector bypass through ingest, and a mislabeled trusted case. Its metrics apply only to those +> declared synthetic cases. + +**Dashboard XSS (fixed):** Memory content rendered as markdown is now sanitized via +DOMPurify at all render sites. Verified against payloads with `onerror` handlers. + +### 2. Network exposure & authentication +- **Loopback by default** (`ENGRAPHIS_HOST=127.0.0.1`) +- **Optional bearer token** (`ENGRAPHIS_API_TOKEN`): constant-time comparison (single + shared implementation in the local customer runtime) +- **Automatic local dashboard access:** the browser on this computer obtains its + HttpOnly, SameSite session without entering an API token. Session issuance requires + a literal loopback peer, a loopback/localhost Host, an exact same-origin POST, and + the dashboard's custom browser header. Forwarding headers, nonlocal peers, DNS + hostnames, cross-site requests, and configured LAN-trust exceptions cannot mint it. + Existing browser confirmations and CSRF checks still govern reviewed writes. + Local programs already able to act as this user remain inside the local trust boundary. +- Pro and Team account sign-in belongs to optional hosted services; it does not gate + opening the local dashboard. Remote API and operator access still require credentials. +- **No local Team identity service:** the public dashboard is single-user. Team logins, + invitations, roles, named seats, token rotation, and organization audit are hosted and are + not mounted by this package. +- **CORS allow-list** defaults to loopback only +- If exposed beyond localhost: put behind reverse proxy with **TLS + rate limiting** + +### 3. Scope isolation +- Every read takes a `SearchFilter`; tools only return memories within requested `workspace`/`repo` +- Every write targeting a memory by ID re-validates scope membership +- Workspace creation is not controlled by a process-wide allow-list; each operation still + carries its explicit workspace/repo/session scope and authenticated personal workspaces + enforce their owner boundary + +### 4. Secrets & data at rest +- `.env`, `*.db`, `*.db-wal`, `*.db-shm` are git-ignored and must never be logged. Gitignore is + not a runtime trust boundary: Engraphis never searches the working directory for `.env`. + Configuration files are limited to owner-private `~/.engraphis/config.env` or an absolute + owner-private regular file selected by `ENGRAPHIS_ENV_FILE`; explicit process variables win. +- **Encryption at rest (opt-in):** `ENGRAPHIS_DB_KEY` / `ENGRAPHIS_DB_KEY_FILE` + + `pip install "engraphis[encryption]"` → AES-256 via SQLCipher. Whole-file; lose key = lose data. + Off by default; without it, protect with filesystem permissions + full-disk encryption. +- Review your LLM provider's data-handling terms. + +### 5. Code indexing +`engraphis_index_repo` parses source files under a path you give it, with the same trust boundary as +any other local tool the agent has. Path is attacker-controlled if agent's instructions are. +Canonical roots are restricted to the working, home, or temporary directories by default. +Set `ENGRAPHIS_INDEX_ROOTS` to a path-separator-delimited absolute-path operator allow-list to +replace those defaults for nonstandard mounts or a narrower deployment boundary. +Dashboard and REST `POST /api/code/index` use the stricter single-root boundary +`ENGRAPHIS_HTTP_INDEX_ROOT`: submitted paths resolve beneath that root. It defaults to the first +`ENGRAPHIS_INDEX_ROOTS` entry, or the current directory. An explicit HTTP root (or fallback +entry) must be absolute; an explicit HTTP root is included in the engine-approved set. MCP and +CLI indexing retain the `ENGRAPHIS_INDEX_ROOTS` allow-list semantics. +`max_files`/`max_file_bytes` bound resource use, not access within an allowed root. Traversal +does not follow file symlinks outside the root, prunes dependency/build directories during the +walk, and honors the root `.engraphisignore` without allowing negation rules to re-expose +hardcoded excludes. +Anyone who can reach an authenticated local mutation route has the authority of that local +installation, so do not share its bearer token. + +### 5a. Coding-agent file writes + +Memory governance is not a filesystem sandbox. For hosts that mediate tool calls, Engraphis ships +a fail-closed protected-path evaluator and an optional Command Code `PreToolUse` hook. Its policy +lives outside the repository, direct file tools are checked before execution, and general shell +access is denied by default because command inspection cannot guarantee non-mutation. See +[`docs/FILE_PROTECTION.md`](docs/FILE_PROTECTION.md) for setup, host integration, and limitations. +Use OS ACLs or read-only mounts when the agent can invoke tools outside that hook. + +### 6. Local resource and database ingestion +- Uploaded and folder-imported files are size/count bounded, marked `trusted:false`, and parsed + as data. Missing optional PDF/OCR/transcription tools fail explicitly. +- The import UI's `derive_facts` option is a separate explicit opt-in. If an LLM/custom + extractor is configured, selected file content may be sent to that provider; leave it off + for a strictly local import. The default and chunk extractors remain fully local. +- Audio/video transcription runs only when `ENGRAPHIS_WHISPER_MODEL` is configured. Depending on + the faster-whisper model name, the underlying library may download a model; use an absolute + local model path when strictly offline operation is required. +- PostgreSQL introspection makes an outbound connection using the caller-provided DSN. The DSN is + never stored, returned, placed in receipts, or included in an error; only a one-way source digest + is retained. Use a read-only database account and limit network reachability at the OS/firewall + layer. + +### 7. Read-only graph server +`engraphis-graph-server` has no mutation routes, disables recall reinforcement and receipt writes, +and refuses non-loopback binding unless `ENGRAPHIS_GRAPH_TOKEN` (or `ENGRAPHIS_API_TOKEN`) is set. +The token protects access, not transport confidentiality; use TLS at a reverse proxy off-host. + +### 8. Privacy receipts +Operation receipts exclude raw memory/query content, workspace/repo names, raw IDs, and actor +identities. Each workspace chain has a separately maintained local count/head anchor, so ordinary +row modification, reordering, interior deletion, and tail truncation are detected. The actor/scope +digests are pseudonymous, not anonymous: predictable values may be guessable. The chain is +unkeyed and its local anchor lives in the same database, so an attacker able to rewrite the whole +database can recompute both. Preserve an exported `head` + `count` outside the database and pass +them back as `expected_head` / `expected_count` when independent evidence is required. + +### 9. Supply chain +- Core runs on `numpy` alone; heavy components gated behind extras +- Code-graph backend falls back to regex indexer on any failure +- Pin versions and run `pip audit` in your environment + +### 10. Hosted-service clients + +- **Cloud authorization:** the public package accepts only short-lived scoped access tokens or + a rotating refresh credential bound to its bootstrap `device` or `member` subject. It contains + no paid-key parser, signer, issuer, local feature gate, or long-lived-key relay exchange. +- **Credential-origin binding:** a saved rotating credential remains bound to the control/compute + URLs recorded for its family; environment changes cannot redirect it. A standalone sync token + likewise requires `ENGRAPHIS_SYNC_TOKEN_ORIGIN` to match the relay origin. +- **Server authority:** every hosted and cost-bearing operation is authorized by the private + control plane; local plan labels and upgrade URLs are presentation metadata only. +- **Cloud Sync and managed-compute privacy:** Cloud Sync encrypts eligible shared-workspace + changes end-to-end before they leave the device, so Engraphis Cloud cannot read their contents. + Managed compute is separate: Analytics, Auto Dreaming, and Auto Consolidation upload a + readable bounded snapshot. Consent travels with the cloud account: connecting an installation + to Engraphis Cloud accepts the terms covering managed compute, so a connected installation is + allowed and an installation with no cloud session is never allowed. Operators may override with + `ENGRAPHIS_MANAGED_COMPUTE_CONSENT` (`0` opts a connected installation back out). The snapshot + carries normal and sensitive memory content, excludes secret-class and session-scoped rows, and + is capped at 16 MiB. Secret-class memories are excluded before serialization and rejected again + by the hosted service. +- **Cloud Sync rollback bounds:** version-3 snapshots chain per-device generations, state hashes, + and tombstone checkpoints. Previously observed rollback is rejected, but first contact remains + unanchored/incomplete until the hosted service supplies an authenticated workspace manifest: + a local client cannot prove that an untrusted relay did not withhold an unseen device. +- **Trial and grace are separate:** new email-confirmed trials last 7 active days for Pro + and 14 active days for Team. Existing grants retain their recorded deadlines until explicitly + extended. Only eligible, currently active trials can be extended, with the total duration + measured from the original verified start; expired trials are never restarted. A separately + bounded, maximum-24-hour local workspace-write grace never extends the trial, subscription, + Cloud Sync, managed compute, Team access, seats, or credentials. +- **Remote URL validation:** hosted endpoints require HTTPS except explicit loopback use, + reject embedded credentials and redirects, require globally routable resolved addresses, + and pin credential-bearing TLS connections to a vetted address while verifying the + original hostname. +- **Bounded I/O:** credential-bearing JSON responses are read through strict byte limits; + malformed, oversized, or authoritative denial responses fail closed. +- **HTTP response headers:** every entrypoint sends `X-Content-Type-Options: nosniff`, + `X-Frame-Options: DENY`, a `frame-ancestors 'none'` CSP, `Referrer-Policy`, and + `Permissions-Policy`; HSTS is added on HTTPS requests only. Override the CSP with + `ENGRAPHIS_CSP` (empty string disables) or HSTS with `ENGRAPHIS_HSTS`. +### 11. Private hosted service boundary + +The public package accepts only customer mode. License issuance, billing fulfillment, Team +identity, hosted relay storage, managed compute, worker execution, transactional email, and +vendor administration are built and operated from a private repository. Their signing keys, +credentials, databases, rate limits, and operational runbooks are not distributed in this +repository or its release artifacts. + +## Known limitations +- Rate limiting: in-process limiter ships (`ENGRAPHIS_RATE_LIMIT`, off by default); + use reverse proxy for multi-process/distributed +- Encryption at rest is opt-in for the local memory DB (SQLCipher via + `ENGRAPHIS_DB_KEY`). Protect customer credential state and backups separately. +- Cloud Sync requires an authorized device to hold its workspace encryption key. The relay cannot + recover a lost key or decrypt its ciphertext; `secret` memories are never uploaded. +- Per-token scope/tenant authorization is partial: isolate distinct tenants by running + one instance each +- Legacy v1 REST server/dashboard is a compatibility surface; prefer v2/MCP path +- **Open-core enforcement ceiling:** the Apache-licensed client can be modified. Do not rely + on local checks to protect proprietary value. Paid authority and cost-bearing features + execute on the private hosted service, where customer forks cannot bypass authorization. + +## Supported versions +The latest published stable release is the supported line. Release-candidate documentation on +`main` does not retire the previously published line: support moves only when matching artifacts +are available from both PyPI and GitHub Releases. Pin a version and watch releases for +advisories. diff --git a/docs/AGENT_CONNECT.md b/docs/AGENT_CONNECT.md index 815123bf7..7f2a39066 100644 --- a/docs/AGENT_CONNECT.md +++ b/docs/AGENT_CONNECT.md @@ -1,174 +1,198 @@ -# Agent Connect - -The public Engraphis dashboard is a single-user local application. It does not mount Team -accounts, invitations, roles, seats, organization audit, or per-member token administration. -Those capabilities live in **Engraphis Team Cloud**. - -## Local agents remain free - -For one person on one machine, run the local MCP server. No hosted account is required: - -```bash -pip install "engraphis[mcp]" -engraphis-init -claude mcp add engraphis -- engraphis-mcp -``` - -The local server exposes the same memory semantics while keeping the database on your machine. -It is Smart MCP by default: agents use the nine compact routine tools and discover/execute advanced -capabilities automatically when needed. There is no profile choice or manual escalation. If a -legacy client pins direct tool names, configure `engraphis-mcp-classic` instead. -Use `ENGRAPHIS_API_TOKEN` only when protecting a local HTTP surface; it is not a Team identity or -seat credential. - -## Local MCP over HTTP - -For a local MCP client that requires HTTP rather than stdio, install the same `mcp` extra and run -the packaged loopback server: - -```bash -pip install "engraphis[mcp]" -engraphis-mcp-http # Smart MCP at http://127.0.0.1:8711/mcp -# equivalent: engraphis mcp-http -``` - -`--transport sse` selects the legacy SSE transport. `--host` deliberately accepts loopback -addresses only: the standalone FastMCP transport does not carry the dashboard's authentication -middleware. Do not expose it through a LAN address or proxy. For a remote deployment, install -`engraphis[all]`, set a strong `ENGRAPHIS_API_TOKEN`, terminate TLS, and use the dashboard's -authenticated `/mcp` endpoint instead. - -Use `engraphis-mcp-http --classic` only for an existing integration that requires the 36 direct -tool names. New integrations should keep the nine-tool Smart default. - -Engraphis documents and tests generic MCP transports; it does not claim client-specific support -unless that client has a maintained setup guide and integration test. - -## Host-owned conversation history - -An SDK or HTTP host that already owns the conversation transcript can call -`POST /api/adaptive-context`. It accepts `query`, `history`, scope, and token-budget fields and -returns either a bounded history slice or grounded retrieved context. This is deliberately an -HTTP API, not an MCP tool: models should not receive or call it. Smart MCP does not require native -deferred `tool_search`; a client that explicitly supports that OpenAI feature may use it as an -additional optimization, never as a requirement. - -## Connect through Team Cloud - -Use the official hosted dashboard when several people or remote agents need one managed -organization: - -1. The organization owner starts Team or purchases a subscription in Engraphis Cloud. -2. The owner invites named members and assigns roles in the hosted dashboard. -3. An organization owner or admin generates a one-time **connect token** (`engr_ct_…`) for - their own connected machine. The account portal shows the exact command to run. -4. That owner or admin runs the command on the machine being connected (see below). -5. The hosted service rechecks organization membership, role, scopes, entitlement version, and - workspace binding on every request. - -Members consume named seats; devices do not. Disabling a member or releasing their seat revokes -their hosted access without distributing an account-wide license key. - -The hosted onboarding flow provides the exact endpoint and client snippet for the member's -organization. Do not substitute the URL of a public self-hosted image: that image intentionally -has no Team identity backend. - -## Connect a machine: `engraphis connect` - -Copy the command from your account portal and run it on the machine you are connecting: - -```bash -engraphis connect --token engr_ct_... -``` - -Before redeeming a short-lived token, you can validate the configured control/compute endpoints -and the private session-file path without reading, sending, or storing any credential: - -```bash -engraphis connect --preflight --control-url https://api.engraphis.com -``` - -This local preflight validates endpoint safety and DNS resolution plus session-file storage. It -does not make an authenticated request and cannot verify private-service membership, billing, -token validity, seats, roles, or workspace access; redeeming the portal-issued token remains the -real integration step. - -That redeems the token against `POST /v1/devices/connect` on the control plane and writes the -owner-only session file `~/.engraphis/cloud_session.json` (mode `0600`). The dashboard, the MCP -server, and Cloud Sync all read that file, so no environment secret is needed afterwards. The -saved control and compute URLs are bound to that rotating credential family: later environment -changes cannot redirect its bearer credentials. Reconnect with a fresh portal token to change -either endpoint. Rerun `engraphis connect` on every machine you want connected. - -Useful options: - -| Option | Effect | -| --- | --- | -| `--token -` | Read the token from stdin, so it never enters shell history. | -| `--preflight` | Validate endpoints and session storage without a token or control-plane HTTP request. | -| `--workspace WS_ID` | Bind this device to a single workspace. | -| `--label TEXT` | Name this installation in your account portal. | -| `--device-name TEXT` | Override the device name (defaults to the hostname). | -| `--control-url URL` | Select the control plane for a new connection/preflight; reconnect to change a saved credential family's endpoint. | -| `--compute-url URL` | Select managed compute for a new connection (also `ENGRAPHIS_CLOUD_COMPUTE_URL`); reconnect to change it later. | -| `--json` | Print a redacted, machine-readable summary. | - -The summary accepts only bounded, printable metadata from the documented response shape. -Credentials, malformed nested values, and terminal-control text are never echoed to terminal or -JSON output. - -The same command is installed as `engraphis-connect`, matching the other `engraphis-*` scripts. - -Connect tokens are **single-use and short-lived**. The service answers every refusal, whether expired, -already redeemed, or never valid, with the same `401`, so the client reports all three -possibilities and the fix is always the same: generate a new token in the account portal. A `402` -means the subscription itself has lapsed; fix billing rather than the token. - -Because the token is single-use, the client checks that it can actually write the session file -*before* redeeming it. If the state directory is not writable, or `cloud_session.json` has been -replaced by a symlink, a hard link, or a directory, the command fails immediately, names the path -to fix, and sends nothing. Your token is untouched, so you can correct the path and rerun the -same command rather than issuing a new token. - -The token is a credential. It is sent in the request body and nowhere else; it is never -printed, never logged, and never written to disk. What *is* written is the rotating refresh -credential the service returns, which is why the session file is owner-only. - -The command also mints a stable per-installation identity at `~/.engraphis/client_identity.json` -(random ULIDs, not a hardware fingerprint) so reconnecting the same machine updates the existing -installation instead of registering a new device every time. Both files honour -`ENGRAPHIS_STATE_DIR`; a distinct state directory is a distinct installation. - -## Credential lifecycle - -Hosted access uses short-lived access tokens plus rotating refresh credentials. A refresh family -has an absolute lifetime and rotation never extends it. Only credential hashes are stored by the -service; the raw replacement is returned once and must be kept in an owner-only local state file -or secrets manager. - -Customer-side environment variables are documented in [`.env.example`](../.env.example). Values -come from the process environment or the owner-private `~/.engraphis/config.env`; an explicit -`ENGRAPHIS_ENV_FILE` must be an absolute owner-private regular file. Engraphis never searches the -working directory for `.env`. Prefer the `~/.engraphis/cloud_session.json` that -`engraphis connect --token` writes over long-lived environment secrets: it holds a rotating -credential, is owner-only, and keeps its bound endpoints and replacement credential up to date. -Environment secrets are for non-interactive deployments that cannot run the connect command. - -## Trial and grace - -The no-card trial starts after email confirmation and lasts **7 active days for Pro or 14 active days for Team**. -`workspace_write_grace` is a private-control-plane account-continuity state capped at **24 -hours**. It never extends the trial, hosted agent access, Team membership, seats, Cloud Sync, or -managed compute, and it does not restrict the free local MCP server. - -## Security notes - -- Use only the HTTPS endpoint shown by the official hosted dashboard. -- Never put refresh credentials, access tokens, or account keys in a repository or support log. -- Bind every hosted credential to the intended organization and workspace. -- Give automation the minimum scopes it needs and revoke unused devices. -- Keep local and hosted responsibilities clear: the public client transports authorized - requests; the private control plane owns identity, seats, policy, and revocation. - -See [Licensing](LICENSING.md) for the source/service boundary and [Cloud Sync](SYNC.md) for the -relay client contract. +# Agent Connect + +The public Engraphis dashboard is a single-user local application. It does not mount Team +accounts, invitations, roles, seats, organization audit, or per-member token administration. +Those capabilities live in **Engraphis Team Cloud**. + +## Local agents remain free + +For one person on one machine, run the local MCP server. No hosted account is required: + +```bash +pip install "engraphis[mcp]" +engraphis-init +claude mcp add engraphis -- engraphis-mcp +``` + +The local server exposes the same memory semantics while keeping the database on your machine. +It is Smart MCP by default: agents use the nine compact routine tools and discover/execute advanced +capabilities automatically when needed. There is no profile choice or manual escalation. If a +legacy client pins direct tool names, configure `engraphis-mcp-classic` instead. +Use `ENGRAPHIS_API_TOKEN` only when protecting a local HTTP surface; it is not a Team identity or +seat credential. + +The dashboard on the same computer opens automatically without an account or token entry. +Its browser session is established locally while remote API clients still supply their bearer +credential. Pro and Team account connection is a separate, optional hosted-service flow. + +## Local MCP over HTTP + +For a local MCP client that requires HTTP rather than stdio, install the same `mcp` extra and run +the packaged loopback server: + +```bash +pip install "engraphis[mcp]" +engraphis-mcp-http # Smart MCP at http://127.0.0.1:8711/mcp +# equivalent: engraphis mcp-http +``` + +`--transport sse` selects the legacy SSE transport. `--host` deliberately accepts loopback +addresses only: the standalone FastMCP transport does not carry the dashboard's authentication +middleware. Do not expose it through a LAN address or proxy. For a remote deployment, install +`engraphis[all]`, set a strong `ENGRAPHIS_API_TOKEN`, terminate TLS, and use the dashboard's +authenticated `/mcp` endpoint instead. + +Use `engraphis-mcp-http --classic` only for an existing integration that requires the 36 direct +tool names. New integrations should keep the nine-tool Smart default. + +When several desktop apps use the same local installation, they can share that running HTTP +service instead of loading a separate embedding model per app: + +```bash +engraphis-mcp --connect http://127.0.0.1:8711/mcp +``` + +Configure each stdio client with command `engraphis-mcp` and arguments +`["--connect", "http://127.0.0.1:8711/mcp"]`. Start `engraphis-mcp-http` first and pin its +`ENGRAPHIS_DB_PATH` to the same database as the dashboard. The bridge accepts numeric loopback +HTTP URLs only, disables proxies and redirects, and forwards MCP session and protocol headers. +It requires the shared service to remain running. This avoids desktop initialization timeouts +and repeated model loads without changing the stored memories or embedding model. + +A successful `engraphis_remember` returns a memory ID and resolved workspace/repo. Check +`prompt_eligible` and `review_state`: `stored: true` alone does not mean externally sourced +content is approved for prompt recall. Verify storage with `engraphis_get_memory`, and use the +same workspace/repo or returned `session_id` for subsequent recall. Memory saves require an +actual Engraphis tool call; connecting an app does not copy its entire chat history. + +Engraphis documents and tests generic MCP transports; it does not claim client-specific support +unless that client has a maintained setup guide and integration test. + +## Host-owned conversation history + +An SDK or HTTP host that already owns the conversation transcript can call +`POST /api/adaptive-context`. It accepts `query`, `history`, scope, and token-budget fields and +returns either a bounded history slice or grounded retrieved context. This is deliberately an +HTTP API, not an MCP tool: models should not receive or call it. Smart MCP does not require native +deferred `tool_search`; a client that explicitly supports that OpenAI feature may use it as an +additional optimization, never as a requirement. + +## Connect through Team Cloud + +Use the official hosted dashboard when several people or remote agents need one managed +organization: + +1. The organization owner starts Team or purchases a subscription in Engraphis Cloud. +2. The owner invites named members and assigns roles in the hosted dashboard. +3. An organization owner or admin generates a one-time **connect token** (`engr_ct_…`) for + their own connected machine. The account portal shows the exact command to run. +4. That owner or admin runs the command on the machine being connected (see below). +5. The hosted service rechecks organization membership, role, scopes, entitlement version, and + workspace binding on every request. + +Members consume named seats; devices do not. Disabling a member or releasing their seat revokes +their hosted access without distributing an account-wide license key. + +The hosted onboarding flow provides the exact endpoint and client snippet for the member's +organization. Do not substitute the URL of a public self-hosted image: that image intentionally +has no Team identity backend. + +## Connect a machine: `engraphis connect` + +Copy the command from your account portal and run it on the machine you are connecting: + +```bash +engraphis connect --token engr_ct_... +``` + +Before redeeming a short-lived token, you can validate the configured control/compute endpoints +and the private session-file path without reading, sending, or storing any credential: + +```bash +engraphis connect --preflight --control-url https://api.engraphis.com +``` + +This local preflight validates endpoint safety and DNS resolution plus session-file storage. It +does not make an authenticated request and cannot verify private-service membership, billing, +token validity, seats, roles, or workspace access; redeeming the portal-issued token remains the +real integration step. + +That redeems the token against `POST /v1/devices/connect` on the control plane and writes the +owner-only session file `~/.engraphis/cloud_session.json` (mode `0600`). The dashboard, the MCP +server, and Cloud Sync all read that file, so no environment secret is needed afterwards. The +saved control and compute URLs are bound to that rotating credential family: later environment +changes cannot redirect its bearer credentials. Reconnect with a fresh portal token to change +either endpoint. Rerun `engraphis connect` on every machine you want connected. + +Useful options: + +| Option | Effect | +| --- | --- | +| `--token -` | Read the token from stdin, so it never enters shell history. | +| `--preflight` | Validate endpoints and session storage without a token or control-plane HTTP request. | +| `--workspace WS_ID` | Bind this device to a single workspace. | +| `--label TEXT` | Name this installation in your account portal. | +| `--device-name TEXT` | Override the device name (defaults to the hostname). | +| `--control-url URL` | Select the control plane for a new connection/preflight; reconnect to change a saved credential family's endpoint. | +| `--compute-url URL` | Select managed compute for a new connection (also `ENGRAPHIS_CLOUD_COMPUTE_URL`); reconnect to change it later. | +| `--json` | Print a redacted, machine-readable summary. | + +The summary accepts only bounded, printable metadata from the documented response shape. +Credentials, malformed nested values, and terminal-control text are never echoed to terminal or +JSON output. + +The same command is installed as `engraphis-connect`, matching the other `engraphis-*` scripts. + +Connect tokens are **single-use and short-lived**. The service answers every refusal, whether expired, +already redeemed, or never valid, with the same `401`, so the client reports all three +possibilities and the fix is always the same: generate a new token in the account portal. A `402` +means the subscription itself has lapsed; fix billing rather than the token. + +Because the token is single-use, the client checks that it can actually write the session file +*before* redeeming it. If the state directory is not writable, or `cloud_session.json` has been +replaced by a symlink, a hard link, or a directory, the command fails immediately, names the path +to fix, and sends nothing. Your token is untouched, so you can correct the path and rerun the +same command rather than issuing a new token. + +The token is a credential. It is sent in the request body and nowhere else; it is never +printed, never logged, and never written to disk. What *is* written is the rotating refresh +credential the service returns, which is why the session file is owner-only. + +The command also mints a stable per-installation identity at `~/.engraphis/client_identity.json` +(random ULIDs, not a hardware fingerprint) so reconnecting the same machine updates the existing +installation instead of registering a new device every time. Both files honour +`ENGRAPHIS_STATE_DIR`; a distinct state directory is a distinct installation. + +## Credential lifecycle + +Hosted access uses short-lived access tokens plus rotating refresh credentials. A refresh family +has an absolute lifetime and rotation never extends it. Only credential hashes are stored by the +service; the raw replacement is returned once and must be kept in an owner-only local state file +or secrets manager. + +Customer-side environment variables are documented in [`.env.example`](../.env.example). Values +come from the process environment or the owner-private `~/.engraphis/config.env`; an explicit +`ENGRAPHIS_ENV_FILE` must be an absolute owner-private regular file. Engraphis never searches the +working directory for `.env`. Prefer the `~/.engraphis/cloud_session.json` that +`engraphis connect --token` writes over long-lived environment secrets: it holds a rotating +credential, is owner-only, and keeps its bound endpoints and replacement credential up to date. +Environment secrets are for non-interactive deployments that cannot run the connect command. + +## Trial and grace + +The no-card trial starts after email confirmation and lasts **7 active days for Pro or 14 active days for Team**. +`workspace_write_grace` is a private-control-plane account-continuity state capped at **24 +hours**. It never extends the trial, hosted agent access, Team membership, seats, Cloud Sync, or +managed compute, and it does not restrict the free local MCP server. + +## Security notes + +- Use only the HTTPS endpoint shown by the official hosted dashboard. +- Never put refresh credentials, access tokens, or account keys in a repository or support log. +- Bind every hosted credential to the intended organization and workspace. +- Give automation the minimum scopes it needs and revoke unused devices. +- Keep local and hosted responsibilities clear: the public client transports authorized + requests; the private control plane owns identity, seats, policy, and revocation. + +See [Licensing](LICENSING.md) for the source/service boundary and [Cloud Sync](SYNC.md) for the +relay client contract. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 1f0199eeb..765d6149d 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -1,107 +1,107 @@ -# Configuration reference - -Configuration comes from process environment variables and the owner-private -`~/.engraphis/config.env` file. `ENGRAPHIS_ENV_FILE` can select another absolute, owner-private -regular file. Process variables take precedence, and Engraphis never searches the working -directory for `.env` files. - -The table lists the current supported settings. The public package runs in customer mode; hosted -control-plane, relay, compute, and worker implementations are private services. - -| Env Var | Default | Description | -|---------|---------|-------------| -| `ENGRAPHIS_ENV_FILE` | `~/.engraphis/config.env` | Optional trusted config leaf selected before trusted values load. Its bounded dependency-free parser performs no interpolation. An explicit value must be an absolute path to an owner-private regular file; arbitrary working-directory `.env` files are ignored. | -| `ENGRAPHIS_DB_PATH` | Source: `/engraphis.db`; installed: platform user-data directory | SQLite database file. Installed defaults are `%LOCALAPPDATA%\engraphis\engraphis.db` (Windows), `~/Library/Application Support/engraphis/engraphis.db` (macOS), and `$XDG_DATA_HOME/engraphis/engraphis.db` or `~/.local/share/engraphis/engraphis.db` (Linux). The environment variable overrides every default; a relative value is resolved from the trusted `~/.engraphis/config.env` directory so launch CWD cannot select a different workspace database. | -| `ENGRAPHIS_SQLITE_DURABILITY` | `durable` | Writable file databases use WAL and FULL commit synchronization. Explicit `balanced` selects NORMAL, which can lose recent acknowledged writes after OS/power failure. Effective settings appear in diagnostics; see [SQLite durability](https://github.com/Coding-Dev-Tools/engraphis/blob/main/docs/SQLITE_DURABILITY.md). | -| `ENGRAPHIS_HOST` | `127.0.0.1` | Server bind address | -| `ENGRAPHIS_PORT` | `8700` | Dashboard port. A platform-injected `$PORT` (Railway/Fly/Heroku) takes precedence over this value for the dashboard bind; Compose pins both to `ENGRAPHIS_COMPOSE_PORT` so the mapping stays in sync | -| `ENGRAPHIS_SERVICE_MODE` | `customer` | The public package supports only `customer`; hosted vendor, relay, compute, and worker roles are not distributed here | -| `ENGRAPHIS_API_TOKEN` | Not set | Optional bearer credential for this single-user local customer node; never reuse a hosted credential | -| `ENGRAPHIS_CORS_ORIGINS` | loopback on `ENGRAPHIS_PORT` | Comma-separated REST CORS allow-list; defaults to `127.0.0.1` and `localhost` on the configured port | -| `ENGRAPHIS_INDEX_ROOTS` | Working, home, and temporary directories | Optional path-separator-delimited absolute-path allow-list that replaces the default roots accepted by local code indexing | -| `ENGRAPHIS_HTTP_INDEX_ROOT` | First `ENGRAPHIS_INDEX_ROOTS` entry, or current directory | Single root for dashboard and REST `POST /api/code/index`; submitted paths resolve beneath it. An explicit root (or fallback entry) must be absolute; an explicit HTTP root is included in the engine-approved set. MCP and CLI indexing continue to use `ENGRAPHIS_INDEX_ROOTS`. | -| `ENGRAPHIS_DB_KEY` | Not set | Encrypt the database at rest (SQLCipher). Or use `ENGRAPHIS_DB_KEY_FILE` | -| `ENGRAPHIS_EMBED_MODEL` | `sentence-transformers/all-MiniLM-L6-v2` | sentence-transformers model | -| `ENGRAPHIS_MCP_PRELOAD_EMBEDDER` | `auto` | Standalone MCP launchers import optional semantic dependencies on the launcher thread on Windows before serving requests. Set `0` to disable or `1` to enable on any platform; model loading and backend fallback policy remain unchanged. | -| `ENGRAPHIS_EMBED_REVISION` | Not set | Optional immutable lowercase 40-hex Hugging Face commit for the embedding model. Loaded Hub commits or local artifact manifests identify persistent vector spaces; unresolved mutable identities keep vector recall fail-closed. | -| `ENGRAPHIS_RERANK_MODEL` | Not set | Optional sentence-transformers cross-encoder reranker | -| `ENGRAPHIS_RERANK_REVISION` | Not set | Optional immutable lowercase 40-hex Hugging Face commit for the reranker | -| `ENGRAPHIS_REQUIRE_IMMUTABLE_MODELS` | `false` | When enabled, require a 40-hex commit before loading remote embedding models, rerankers, or chunk tokenizers; `local:` selectors and filesystem paths remain permitted | -| `ENGRAPHIS_REQUIRE_EXACT_BACKENDS` | `false` | When enabled, dashboard and standalone MCP startup fails if a configured optional backend is unavailable instead of silently falling back | -| `ENGRAPHIS_EXTRACTOR` | `none` | `none` = verbatim; `chunk` = offline structure-aware chunks; `llm` = free-form LLM facts; `llm_structured` = schema-validated facts + graph metadata | -| `ENGRAPHIS_CHUNK_TOKENIZER_MODEL` | Not set | Optional Hugging Face tokenizer used to enforce chunk budgets with the downstream reader's real tokenization; requires the optional `transformers` package | -| `ENGRAPHIS_CHUNK_TOKENIZER_REVISION` | Not set | Optional immutable tokenizer/model revision recorded in the chunk-counter identity; pin this for reproducible benchmark artifacts | -| `ENGRAPHIS_GRAPH_EXTRACTOR` | `regex` | `regex` = offline heuristic NER; `none` = disable heuristic text extraction (validated `llm_structured` metadata still feeds the graph) | -| `ENGRAPHIS_RETENTION_SUPERVISOR` | `none` | `none` = deterministic only; `llm` = sends a bounded excerpt to the configured provider for advisory ephemeral/normal/critical classification | -| `ENGRAPHIS_ALLOW_AUTOMATIC_CRITICAL_RETENTION` | `false` | Opt in only when an LLM supervisor may automatically assign the long-lived `critical` class; explicit user-selected critical retention is unaffected | -| `ENGRAPHIS_WHISPER_MODEL` | Not set | Enables local faster-whisper audio/video transcription | -| `ENGRAPHIS_POSTGRES_DSN` | Not set | CLI-only PostgreSQL source; used for the connection and never stored | -| `ENGRAPHIS_POSTGRES_CONNECT_TIMEOUT` | `10` | PostgreSQL introspection connection timeout in seconds (bounded to 1--120) | -| `ENGRAPHIS_POSTGRES_STATEMENT_TIMEOUT_MS` | `30000` | Per-introspection PostgreSQL statement timeout in milliseconds (bounded to 1--300000) | -| `ENGRAPHIS_GRAPH_TOKEN` | Not set | Bearer token for `engraphis-graph-server`; required off-loopback | -| `ENGRAPHIS_GRAPH_HOST` / `ENGRAPHIS_GRAPH_PORT` | `127.0.0.1` / `8720` | Read-only graph/recall server bind address | -| `ENGRAPHIS_LLM_PROVIDER` | `openai` | `openai \| anthropic \| google \| openrouter \| custom` | -| `ENGRAPHIS_LLM_MODEL` | `gpt-4o-mini` | Model name (provider-specific) | -| `ENGRAPHIS_LLM_API_KEY` | Not set | API key for chat/synthesis, `llm` / `llm_structured` extraction, and structured consolidation | -| `ENGRAPHIS_LLM_BASE_URL` | Not set | Base URL for openrouter / custom OpenAI-compatible endpoints | -| `ENGRAPHIS_DECISION_BACKEND` | `none` | `none` or `local` keeps advisory decisions local; `managed` uses the saved Cloud session and an included per-person rolling allowance after service acceptance; `auto` selects managed when configured and never switches to BYOK; explicit `byok` uses a personal TypeSafe key and may incur provider charges. Managed custom questions and query planning are unsupported. Legacy `typesafe`, `jev`, and `system1` mean BYOK. Remote calls also require per-call consent. | -| `ENGRAPHIS_DECISION_MODEL` | `jev-1.13.0` | Pinned model accepted by the Jev transport; other model identifiers are rejected. | -| `TYPESAFE_API_KEY` | Not set | Personal credential for explicit BYOK decisions; `JEV_API_KEY` is a fallback alias. Managed decisions use the saved Cloud session instead. | -| `TYPESAFE_BASE_URL` | `https://api.typesafe.ai` | Direct BYOK provider origin; does not change the managed session's bound Cloud control origin. | -| `ENGRAPHIS_LLM_AUTO_EXTRACT` | `0` | Opt in to switching the running engine to `llm_structured` after a successful live connection test; the dashboard's extraction Off button persists `0`, and its On button restores `1` | -| `ENGRAPHIS_FORWARDED_ALLOW_IPS` | *(none)* | Proxies trusted for forwarded client/TLS headers (`*` only when the service is reachable exclusively through that proxy) | -| `ENGRAPHIS_LOCAL_TRUSTED_PEERS` | *(none)* | Exact peers/CIDRs treated as local without forwarding headers; use only for trusted Docker/LAN peers, never public deployments | -| `ENGRAPHIS_UPDATE_CACHE` | `86400` | Update-check cache TTL in seconds, bounded to `1..31622400`; this is never a cache-file path | -| `ENGRAPHIS_UPDATE_CHECK` | Off | Opt-in release reminder surfaced in the dashboard, server startup log, and MCP. Update checks run only when this is set to an affirmative value; `0` keeps them off. | -| `ENGRAPHIS_UPDATE_URL` | Not set | Overrides the release-check source URL; the outbound client accepts HTTPS and rejects private/reserved destinations. | -| `ENGRAPHIS_CLOUD_CONTROL_URL` | hosted default | Official entitlement, organization, and credential control API. A saved rotating credential stays bound to the control endpoint recorded for its family; reconnect to change it. | -| `ENGRAPHIS_CLOUD_COMPUTE_URL` | hosted default | Official Analytics and managed-automation API. A saved rotating credential stays bound to its recorded compute endpoint; reconnect to change it. | -| `ENGRAPHIS_CLOUD_ORGANIZATION_ID` | Not set | Hosted organization bound to this customer session | -| `ENGRAPHIS_CLOUD_REFRESH_CREDENTIAL` | Not set | Bootstrap-only rotating hosted credential; after first use the owner-only cloud session replacement takes precedence | -| `ENGRAPHIS_CLOUD_TOKEN_SUBJECT` | `member` | Subject fixed during hosted bootstrap (`device` or `member`); set explicitly with an environment-only refresh credential | -| `ENGRAPHIS_CLOUD_ACCESS_TOKEN` | Not set | Optional short-lived access token for ephemeral jobs | -| `ENGRAPHIS_MANAGED_COMPUTE_CONSENT` | *(unset)* | Deny-only operator override: `0` pauses readable managed processing. A truthy value cannot grant approval. Each workspace requires explicit confirmation in Manage → Settings; encrypted sync is separate | - -Managed Jev is currently `not_yet_available` pending release acceptance and -service-capacity qualification; client configuration does not enable it. After -enablement, every legitimate Pro user and each eligible Team named seat, including paid -viewers, with an active paid, trial, or test entitlement receives managed Jev at no -additional customer charge and without a personal provider key. - -Each individual receives all three independent rolling limits: **100 evaluated questions per rolling hour, 1,000 per rolling five hours, and 2,000 per rolling 24 hours**. Usage is per -person, not pooled across a Team and not monthly. Each evaluated question counts once; -if a batch is evaluated, every question counts. A `guard_command` review evaluates two -questions, and each other supported workflow evaluates one. Admitted attempts that fail -or are interrupted remain counted. No overage is charged. The account portal reports -that member's remaining usage across all three windows; usage returns as earlier -questions leave each rolling window. - -The existing production fleet guard remains 100 questions per day across the service. It -conflicts with the per-person rolling caps and may pause or reject requests earlier, so -resolve capacity and the fleet guard before launch. No latency, accuracy, or cost-saving -guarantee is established by configuration or a successful health check. These rules are -service-enforced, not client configuration overrides. Selecting a backend does not -enable the service or satisfy release, provider-terms, capacity, or quality gates. See -[hosted plans](HOSTED_PLANS.md#included-system-1-decision-engine-jev). - -The four managed workflows require a command, a pair of facts, query/evidence, or -goal/output context and their fixed question schemas. Arbitrary `custom` and -`query_planning` payloads are rejected before credential refresh or network requests. -Experimental recall route planning requires explicit BYOK; managed failure never -silently selects a personal key. Remote consent and `public` or `internal` -classification remain mandatory, and `offline_mode=true` prevents remote requests. -Viewers can use direct Classic advisory decisions; generic Smart stateful execution -still requires admin and the private Team tool catalog is unchanged. - -Managed transport callers can supply `request_key` for explicit retry deduplication. A -duplicate admitted key returns 409 without an additional provider call or usage -increment; there is no stored answer replay or automatic retry. Admitted errors remain -counted. This parameter is not an environment setting or an MCP/dashboard field. - -The optional cross-encoder reranker is model- and hardware-dependent. Treat its quality and -latency as deployment-specific until a versioned model identity, exact configuration, and -reproducible evaluation artifact are available for the comparison being reported. - -See [`.env.example`](../.env.example) for the full variable inventory. Supply values through the -process environment or the trusted config file above; copying it to an arbitrary `./.env` does -not make Engraphis load it. +# Configuration reference + +Configuration comes from process environment variables and the owner-private +`~/.engraphis/config.env` file. `ENGRAPHIS_ENV_FILE` can select another absolute, owner-private +regular file. Process variables take precedence, and Engraphis never searches the working +directory for `.env` files. + +The table lists the current supported settings. The public package runs in customer mode; hosted +control-plane, relay, compute, and worker implementations are private services. + +| Env Var | Default | Description | +|---------|---------|-------------| +| `ENGRAPHIS_ENV_FILE` | `~/.engraphis/config.env` | Optional trusted config leaf selected before trusted values load. Its bounded dependency-free parser performs no interpolation. An explicit value must be an absolute path to an owner-private regular file; arbitrary working-directory `.env` files are ignored. | +| `ENGRAPHIS_DB_PATH` | Source: `/engraphis.db`; installed: platform user-data directory | SQLite database file. Installed defaults are `%LOCALAPPDATA%\engraphis\engraphis.db` (Windows), `~/Library/Application Support/engraphis/engraphis.db` (macOS), and `$XDG_DATA_HOME/engraphis/engraphis.db` or `~/.local/share/engraphis/engraphis.db` (Linux). The environment variable overrides every default; a relative value is resolved from the trusted `~/.engraphis/config.env` directory so launch CWD cannot select a different workspace database. | +| `ENGRAPHIS_SQLITE_DURABILITY` | `durable` | Writable file databases use WAL and FULL commit synchronization. Explicit `balanced` selects NORMAL, which can lose recent acknowledged writes after OS/power failure. Effective settings appear in diagnostics; see [SQLite durability](https://github.com/Coding-Dev-Tools/engraphis/blob/main/docs/SQLITE_DURABILITY.md). | +| `ENGRAPHIS_HOST` | `127.0.0.1` | Server bind address | +| `ENGRAPHIS_PORT` | `8700` | Dashboard port. A platform-injected `$PORT` (Railway/Fly/Heroku) takes precedence over this value for the dashboard bind; Compose pins both to `ENGRAPHIS_COMPOSE_PORT` so the mapping stays in sync | +| `ENGRAPHIS_SERVICE_MODE` | `customer` | The public package supports only `customer`; hosted vendor, relay, compute, and worker roles are not distributed here | +| `ENGRAPHIS_API_TOKEN` | Not set | Optional bearer credential for remote API/operator access to this single-user node; never reuse a hosted credential. The dashboard on the same computer opens automatically, including when this token is set; local users do not enter it. | +| `ENGRAPHIS_CORS_ORIGINS` | loopback on `ENGRAPHIS_PORT` | Comma-separated REST CORS allow-list; defaults to `127.0.0.1` and `localhost` on the configured port | +| `ENGRAPHIS_INDEX_ROOTS` | Working, home, and temporary directories | Optional path-separator-delimited absolute-path allow-list that replaces the default roots accepted by local code indexing | +| `ENGRAPHIS_HTTP_INDEX_ROOT` | First `ENGRAPHIS_INDEX_ROOTS` entry, or current directory | Single root for dashboard and REST `POST /api/code/index`; submitted paths resolve beneath it. An explicit root (or fallback entry) must be absolute; an explicit HTTP root is included in the engine-approved set. MCP and CLI indexing continue to use `ENGRAPHIS_INDEX_ROOTS`. | +| `ENGRAPHIS_DB_KEY` | Not set | Encrypt the database at rest (SQLCipher). Or use `ENGRAPHIS_DB_KEY_FILE` | +| `ENGRAPHIS_EMBED_MODEL` | `sentence-transformers/all-MiniLM-L6-v2` | sentence-transformers model | +| `ENGRAPHIS_MCP_PRELOAD_EMBEDDER` | `auto` | Standalone MCP launchers import optional semantic dependencies on the launcher thread on Windows before serving requests. Set `0` to disable or `1` to enable on any platform; model loading and backend fallback policy remain unchanged. | +| `ENGRAPHIS_EMBED_REVISION` | Not set | Optional immutable lowercase 40-hex Hugging Face commit for the embedding model. Loaded Hub commits or local artifact manifests identify persistent vector spaces; unresolved mutable identities keep vector recall fail-closed. | +| `ENGRAPHIS_RERANK_MODEL` | Not set | Optional sentence-transformers cross-encoder reranker | +| `ENGRAPHIS_RERANK_REVISION` | Not set | Optional immutable lowercase 40-hex Hugging Face commit for the reranker | +| `ENGRAPHIS_REQUIRE_IMMUTABLE_MODELS` | `false` | When enabled, require a 40-hex commit before loading remote embedding models, rerankers, or chunk tokenizers; `local:` selectors and filesystem paths remain permitted | +| `ENGRAPHIS_REQUIRE_EXACT_BACKENDS` | `false` | When enabled, dashboard and standalone MCP startup fails if a configured optional backend is unavailable instead of silently falling back | +| `ENGRAPHIS_EXTRACTOR` | `none` | `none` = verbatim; `chunk` = offline structure-aware chunks; `llm` = free-form LLM facts; `llm_structured` = schema-validated facts + graph metadata | +| `ENGRAPHIS_CHUNK_TOKENIZER_MODEL` | Not set | Optional Hugging Face tokenizer used to enforce chunk budgets with the downstream reader's real tokenization; requires the optional `transformers` package | +| `ENGRAPHIS_CHUNK_TOKENIZER_REVISION` | Not set | Optional immutable tokenizer/model revision recorded in the chunk-counter identity; pin this for reproducible benchmark artifacts | +| `ENGRAPHIS_GRAPH_EXTRACTOR` | `regex` | `regex` = offline heuristic NER; `none` = disable heuristic text extraction (validated `llm_structured` metadata still feeds the graph) | +| `ENGRAPHIS_RETENTION_SUPERVISOR` | `none` | `none` = deterministic only; `llm` = sends a bounded excerpt to the configured provider for advisory ephemeral/normal/critical classification | +| `ENGRAPHIS_ALLOW_AUTOMATIC_CRITICAL_RETENTION` | `false` | Opt in only when an LLM supervisor may automatically assign the long-lived `critical` class; explicit user-selected critical retention is unaffected | +| `ENGRAPHIS_WHISPER_MODEL` | Not set | Enables local faster-whisper audio/video transcription | +| `ENGRAPHIS_POSTGRES_DSN` | Not set | CLI-only PostgreSQL source; used for the connection and never stored | +| `ENGRAPHIS_POSTGRES_CONNECT_TIMEOUT` | `10` | PostgreSQL introspection connection timeout in seconds (bounded to 1--120) | +| `ENGRAPHIS_POSTGRES_STATEMENT_TIMEOUT_MS` | `30000` | Per-introspection PostgreSQL statement timeout in milliseconds (bounded to 1--300000) | +| `ENGRAPHIS_GRAPH_TOKEN` | Not set | Bearer token for `engraphis-graph-server`; required off-loopback | +| `ENGRAPHIS_GRAPH_HOST` / `ENGRAPHIS_GRAPH_PORT` | `127.0.0.1` / `8720` | Read-only graph/recall server bind address | +| `ENGRAPHIS_LLM_PROVIDER` | `openai` | `openai \| anthropic \| google \| openrouter \| custom` | +| `ENGRAPHIS_LLM_MODEL` | `gpt-4o-mini` | Model name (provider-specific) | +| `ENGRAPHIS_LLM_API_KEY` | Not set | API key for chat/synthesis, `llm` / `llm_structured` extraction, and structured consolidation | +| `ENGRAPHIS_LLM_BASE_URL` | Not set | Base URL for openrouter / custom OpenAI-compatible endpoints | +| `ENGRAPHIS_DECISION_BACKEND` | `none` | `none` or `local` keeps advisory decisions local; `managed` uses the saved Cloud session and an included per-person rolling allowance after service acceptance; `auto` selects managed when configured and never switches to BYOK; explicit `byok` uses a personal TypeSafe key and may incur provider charges. Managed custom questions and query planning are unsupported. Legacy `typesafe`, `jev`, and `system1` mean BYOK. Remote calls also require per-call consent. | +| `ENGRAPHIS_DECISION_MODEL` | `jev-1.13.0` | Pinned model accepted by the Jev transport; other model identifiers are rejected. | +| `TYPESAFE_API_KEY` | Not set | Personal credential for explicit BYOK decisions; `JEV_API_KEY` is a fallback alias. Managed decisions use the saved Cloud session instead. | +| `TYPESAFE_BASE_URL` | `https://api.typesafe.ai` | Direct BYOK provider origin; does not change the managed session's bound Cloud control origin. | +| `ENGRAPHIS_LLM_AUTO_EXTRACT` | `0` | Opt in to switching the running engine to `llm_structured` after a successful live connection test; the dashboard's extraction Off button persists `0`, and its On button restores `1` | +| `ENGRAPHIS_FORWARDED_ALLOW_IPS` | *(none)* | Proxies trusted for forwarded client/TLS headers (`*` only when the service is reachable exclusively through that proxy) | +| `ENGRAPHIS_LOCAL_TRUSTED_PEERS` | *(none)* | Exact peers/CIDRs treated as local without forwarding headers; use only for trusted Docker/LAN peers, never public deployments | +| `ENGRAPHIS_UPDATE_CACHE` | `86400` | Update-check cache TTL in seconds, bounded to `1..31622400`; this is never a cache-file path | +| `ENGRAPHIS_UPDATE_CHECK` | Off | Opt-in release reminder surfaced in the dashboard, server startup log, and MCP. Update checks run only when this is set to an affirmative value; `0` keeps them off. | +| `ENGRAPHIS_UPDATE_URL` | Not set | Overrides the release-check source URL; the outbound client accepts HTTPS and rejects private/reserved destinations. | +| `ENGRAPHIS_CLOUD_CONTROL_URL` | hosted default | Official entitlement, organization, and credential control API. A saved rotating credential stays bound to the control endpoint recorded for its family; reconnect to change it. | +| `ENGRAPHIS_CLOUD_COMPUTE_URL` | hosted default | Official Analytics and managed-automation API. A saved rotating credential stays bound to its recorded compute endpoint; reconnect to change it. | +| `ENGRAPHIS_CLOUD_ORGANIZATION_ID` | Not set | Hosted organization bound to this customer session | +| `ENGRAPHIS_CLOUD_REFRESH_CREDENTIAL` | Not set | Bootstrap-only rotating hosted credential; after first use the owner-only cloud session replacement takes precedence | +| `ENGRAPHIS_CLOUD_TOKEN_SUBJECT` | `member` | Subject fixed during hosted bootstrap (`device` or `member`); set explicitly with an environment-only refresh credential | +| `ENGRAPHIS_CLOUD_ACCESS_TOKEN` | Not set | Optional short-lived access token for ephemeral jobs | +| `ENGRAPHIS_MANAGED_COMPUTE_CONSENT` | *(unset)* | Deny-only operator override: `0` pauses readable managed processing. A truthy value cannot grant approval. Each workspace requires explicit confirmation in Manage → Settings; encrypted sync is separate | + +Managed Jev is currently `not_yet_available` pending release acceptance and +service-capacity qualification; client configuration does not enable it. After +enablement, every legitimate Pro user and each eligible Team named seat, including paid +viewers, with an active paid, trial, or test entitlement receives managed Jev at no +additional customer charge and without a personal provider key. + +Each individual receives all three independent rolling limits: **100 evaluated questions per rolling hour, 1,000 per rolling five hours, and 2,000 per rolling 24 hours**. Usage is per +person, not pooled across a Team and not monthly. Each evaluated question counts once; +if a batch is evaluated, every question counts. A `guard_command` review evaluates two +questions, and each other supported workflow evaluates one. Admitted attempts that fail +or are interrupted remain counted. No overage is charged. The account portal reports +that member's remaining usage across all three windows; usage returns as earlier +questions leave each rolling window. + +The existing production fleet guard remains 100 questions per day across the service. It +conflicts with the per-person rolling caps and may pause or reject requests earlier, so +resolve capacity and the fleet guard before launch. No latency, accuracy, or cost-saving +guarantee is established by configuration or a successful health check. These rules are +service-enforced, not client configuration overrides. Selecting a backend does not +enable the service or satisfy release, provider-terms, capacity, or quality gates. See +[hosted plans](HOSTED_PLANS.md#included-system-1-decision-engine-jev). + +The four managed workflows require a command, a pair of facts, query/evidence, or +goal/output context and their fixed question schemas. Arbitrary `custom` and +`query_planning` payloads are rejected before credential refresh or network requests. +Experimental recall route planning requires explicit BYOK; managed failure never +silently selects a personal key. Remote consent and `public` or `internal` +classification remain mandatory, and `offline_mode=true` prevents remote requests. +Viewers can use direct Classic advisory decisions; generic Smart stateful execution +still requires admin and the private Team tool catalog is unchanged. + +Managed transport callers can supply `request_key` for explicit retry deduplication. A +duplicate admitted key returns 409 without an additional provider call or usage +increment; there is no stored answer replay or automatic retry. Admitted errors remain +counted. This parameter is not an environment setting or an MCP/dashboard field. + +The optional cross-encoder reranker is model- and hardware-dependent. Treat its quality and +latency as deployment-specific until a versioned model identity, exact configuration, and +reproducible evaluation artifact are available for the comparison being reported. + +See [`.env.example`](../.env.example) for the full variable inventory. Supply values through the +process environment or the trusted config file above; copying it to an arbitrary `./.env` does +not make Engraphis load it. diff --git a/docs/DOCUMENT_IMPORT.md b/docs/DOCUMENT_IMPORT.md index 4bff3949f..ef4802096 100644 --- a/docs/DOCUMENT_IMPORT.md +++ b/docs/DOCUMENT_IMPORT.md @@ -1,150 +1,151 @@ -# Import local documents - -Engraphis imports local document collections into the v2 memory engine without uploading the -source. The dependency-free parser describes readable source material; the normal memory write -path then stores scoped, temporal memories and derives local search indexes. A source folder is -provenance, never an implicit `workspace`, `repo`, or `session` scope. - -Obsidian is a rich Markdown adapter within this architecture. Use -[the Obsidian adapter guide](OBSIDIAN_IMPORT.md) when frontmatter, aliases, wikilinks, block or -heading fragments, and attachment references matter. - -## CLI - -Preview is strict and creates no database records: - -```bash -engraphis import documents /path/to/collection --workspace acme --dry-run -``` - -After reviewing the preview, explicitly confirm the trusted-local write: - -```bash -engraphis import documents /path/to/collection --workspace acme --repo product --yes -``` - -Choose a session target when needed, or reuse an existing source collection identity: - -```bash -engraphis import documents /path/to/collection --workspace acme --repo product \ - --session ses_01EXAMPLE --scope session --source-id vlt_01EXAMPLE --yes -``` - -`engraphis-import documents ...` is the equivalent installed console entrypoint. Non-interactive -and JSON-mode writes require `--yes`; an interactive terminal asks for confirmation. Use `--db` -to select another v2 database, `--source-label` for a new collection’s display name, and -`--on-conflict error|replace|new` to choose the divergent-lineage policy. `--limit N` pauses a -write after N documents and leaves it resumable; a dry run always previews the whole collection. - -CLI imports never download an embedding model. The configured `ENGRAPHIS_EMBED_MODEL` must -already be cached or use a `local:/absolute/path` selector. To deliberately use Engraphis's -dependency-free deterministic hashing embedder instead, set `ENGRAPHIS_EMBED_MODEL` to an empty -value for the import process; recall will then expose lexical degraded mode rather than claiming -semantic-vector support. - -## Dashboard - -Open the dashboard and choose **Import local documents**. Select **Documents** mode, choose files -or a folder, then set the source label, workspace, optional repository/session, scope, memory -type, and conflict policy. **Preview import** performs zero writes and shows every candidate, -format, warning, skip, rejection, update, rename, conflict, and missing source. Check the local -document confirmation and choose **Import documents** only after reviewing that report. - -A new browser source requires a nonblank Source label; folder selection prefills its root folder -name. This label is part of the local source identity, so unrelated collections cannot silently -share a re-import lineage. Select a saved source when resuming or re-importing that collection. - -The browser processes selected bytes locally and does not retain a dashboard upload copy. The -wizard always runs behind the local owner-browser/CSRF boundary. In token deployments that -boundary is bound to the browser session minted from `ENGRAPHIS_API_TOKEN`; in zero-token -loopback mode the same CSRF confirmation applies to the loopback-only runtime, so the wizard -works without configuration. - -## Supported formats - -The built-in parser is intentionally small and uses only the Python standard library: - -| Format | Extensions | Preserved/readable structure | -|---|---|---| -| Markdown | `.md`, `.markdown`, `.mdown` | Canonical Markdown; the Obsidian adapter adds frontmatter and note-link metadata. | -| Plain text | `.txt`, `.text`, `.log` | Canonical text. | -| reStructuredText | `.rst`, `.rest` | Canonical text and simple underline headings. | -| HTML | `.html`, `.htm`, `.xhtml` | Original HTML plus readable text; scripts, styles, templates, and noscript content are excluded from readable text. | -| JSON | `.json`, `.jsonl`, `.ndjson` | Structured, readable JSON/JSON Lines where valid; malformed JSON is preserved as text with a warning. | -| CSV/TSV tables | `.csv`, `.tsv`, `.tab` | Original table text with bounded header/row metadata. | -| Word-processing documents (DOCX, ODT, RTF) | `.docx`, `.odt`, `.rtf` | Readable paragraphs/text via bounded ZIP/XML or conservative RTF parsing; rich layout, comments, and tracked changes are not reproduced. | -| Spreadsheets (XLSX, ODS) | `.xlsx`, `.ods` | Bounded worksheet/cell values as readable rows; formulas are never executed and workbook layout is not reproduced. | -| Presentations (PPTX, ODP) | `.pptx`, `.odp` | Bounded slide text in slide order; animations, speaker media, and visual layout are not reproduced. | -| EPUB | `.epub` | Readable spine/chapter text via bounded ZIP/XML/HTML parsing. | -| Configuration/XML text | `.yaml`, `.yml`, `.toml`, `.ini`, `.cfg`, `.xml` | Readable source text; credentials and secret-like values are rejected. | -| Source code | `.py`, `.pyi`, `.js`, `.jsx`, `.mjs`, `.cjs`, `.ts`, `.tsx`, `.go`, `.rs`, `.java`, `.cs`, `.c`, `.h`, `.cc`, `.cpp`, `.cxx`, `.hpp`, `.hh`, `.hxx`, `.sql`, `.tf`, `.tfvars`, `.hcl`, `.sh`, `.ps1`, `.rb`, `.php`, `.swift`, `.kt`, `.kts`, `.scala`, `.lua`, `.r`, `.css` | Canonical source text. Link, tag, and heading discovery is disabled for source code so examples do not create graph relationships. | - -The outer v2 importer also reuses Engraphis's existing local resource adapters. With -`engraphis[documents]`, PDF text and image OCR are available; OCR additionally needs the local -Tesseract executable. Audio/video transcription needs `engraphis[transcription]` and -`ENGRAPHIS_WHISPER_MODEL` must point to an existing local model file or directory. The importer -refuses a model name or missing path so this workflow cannot trigger a model download. - -The parser retains original readable content or structure and source metadata. It masks fenced and -inline code before generic tag/link discovery, so examples and snippets cannot manufacture source -relationships. It never executes HTML, Markdown plugins, scripts, macros, or embedded content. - -## Safety and privacy - -Only regular files below the selected root are considered. The scanner rejects a symlinked root, -skips symlinks and hidden/configuration paths, rejects common credential/key filenames and -secret-like contents, bounds individual documents and the whole collection, and verifies a file -did not change while it was read. ZIP containers have member-count, decompressed-size, -compression-ratio, path, and DTD/entity protections. Unsafe, malformed, unreadable, oversized, -binary, or unsupported files are catalogued per file; one failure does not stop the rest of the -collection. Reports never echo secret-like source content. - -Default filename exclusions include `.env` variants, credentials, secrets, tokens, recovery -codes, SSH identity files, and `.pem`, `.key`, `.p12`, and `.pfx` material. A collection is -bounded to 10,000 encountered files and 750 MB of read bytes; an individual adapter input is -bounded to 100 MB, while canonical memory text is capped at 100,000 characters and is rejected -rather than silently split. Containers are additionally capped at 2,000 members and 20 MB of -declared decompressed content. Invalid UTF-8/UTF-16 in permitted text is replaced explicitly and -reported as a parsing warning. - -Markdown uses the Obsidian adapter's stricter 2 MB raw-note limit before decoding. - -Review a preview before importing sensitive material. The local importer makes no network request -and does not copy source folders or attachments into a hidden second collection. - -CLI dry runs inspect an existing plaintext or configured SQLCipher manifest through an immutable, -query-only connection. They tolerate a pre-importer database as an empty manifest and refuse an -active uncheckpointed WAL instead of creating or consulting writable sidecars. - -## Re-import, history, and conflicts - -Each collection has a stable local source identity plus per-document path and content identity. -The same collection and target scope re-import idempotently: unchanged documents are not -duplicated, changed documents follow the selected conflict policy, and unique exact-content moves -can be reported as renames. Source files missing on a later scan are reported; their memories are -not hard-deleted. - -`replace` creates a temporal successor under the normal v2 rules, `new` creates a distinct memory, -and the default `error` reports a divergent lineage without silently overwriting it. Progress is -recorded per document. Rerun an interrupted or `--limit`-paused command with the same root and -target to resume safely; the final report includes imported, updated, renamed, skipped, rejected, -conflict, missing, warning, and error counts. - -## Optional adapters and limits - -The universal core parser deliberately has no hard dependency on OCR, PDF decoding, -audio/video transcription, office applications, or a hosted model. The outer importer invokes -only installed local adapters for PDF/OCR/transcription and reports a per-file rejection when an -adapter or local executable/model is unavailable. Legacy OLE Office files (`.doc`, `.xls`, -`.ppt`), encrypted or DRM-protected documents, unknown containers, and arbitrary binary files -are never guessed or decoded as text. A PDF adapter extracts embedded PDF text; it does not OCR -scanned PDF pages. Import page images separately when local OCR is required. - -The parser does not render HTML/CSS, execute JavaScript or macros, interpret every RST directive, -evaluate spreadsheet formulas, reproduce presentation/EPUB layout, or preserve every -rich-document annotation. YAML and TOML are preserved safely but are not fully interpreted; RTF, -spreadsheets, presentations, and EPUB are readable-text imports rather than editing-fidelity -conversions. Unsupported files remain explicit per-file report entries and can be added later -through the bounded adapter interface; “universal” never means guessing arbitrary binary bytes. -For Obsidian-specific unsupported syntax and attachment behavior, see -[the Obsidian adapter guide](OBSIDIAN_IMPORT.md). +# Import local documents + +Engraphis imports local document collections into the v2 memory engine without uploading the +source. The dependency-free parser describes readable source material; the normal memory write +path then stores scoped, temporal memories and derives local search indexes. A source folder is +provenance, never an implicit `workspace`, `repo`, or `session` scope. + +Obsidian is a rich Markdown adapter within this architecture. Use +[the Obsidian adapter guide](OBSIDIAN_IMPORT.md) when frontmatter, aliases, wikilinks, block or +heading fragments, and attachment references matter. + +## CLI + +Preview is strict and creates no database records: + +```bash +engraphis import documents /path/to/collection --workspace acme --dry-run +``` + +After reviewing the preview, explicitly confirm the trusted-local write: + +```bash +engraphis import documents /path/to/collection --workspace acme --repo product --yes +``` + +Choose a session target when needed, or reuse an existing source collection identity: + +```bash +engraphis import documents /path/to/collection --workspace acme --repo product \ + --session ses_01EXAMPLE --scope session --source-id vlt_01EXAMPLE --yes +``` + +`engraphis-import documents ...` is the equivalent installed console entrypoint. Non-interactive +and JSON-mode writes require `--yes`; an interactive terminal asks for confirmation. Use `--db` +to select another v2 database, `--source-label` for a new collection’s display name, and +`--on-conflict error|replace|new` to choose the divergent-lineage policy. `--limit N` pauses a +write after N documents and leaves it resumable; a dry run always previews the whole collection. + +CLI imports never download an embedding model. The configured `ENGRAPHIS_EMBED_MODEL` must +already be cached or use a `local:/absolute/path` selector. To deliberately use Engraphis's +dependency-free deterministic hashing embedder instead, set `ENGRAPHIS_EMBED_MODEL` to an empty +value for the import process; recall will then expose lexical degraded mode rather than claiming +semantic-vector support. + +## Dashboard + +Open the dashboard and choose **Import local documents**. Select **Documents** mode, choose files +or a folder, then set the source label, workspace, optional repository/session, scope, memory +type, and conflict policy. **Preview import** performs zero writes and shows every candidate, +format, warning, skip, rejection, update, rename, conflict, and missing source. Check the local +document confirmation and choose **Import documents** only after reviewing that report. + +A new browser source requires a nonblank Source label; folder selection prefills its root folder +name. This label is part of the local source identity, so unrelated collections cannot silently +share a re-import lineage. Select a saved source when resuming or re-importing that collection. + +The browser processes selected bytes locally and does not retain a dashboard upload copy. The +wizard always runs behind the local owner-browser/CSRF boundary. Its same-computer browser +session is established automatically and signed with `ENGRAPHIS_API_TOKEN` when configured, +or a process-local secret otherwise. Local users never enter that token. Legacy zero-token +loopback clients retain their existing CSRF confirmation, so the wizard works without +configuration. + +## Supported formats + +The built-in parser is intentionally small and uses only the Python standard library: + +| Format | Extensions | Preserved/readable structure | +|---|---|---| +| Markdown | `.md`, `.markdown`, `.mdown` | Canonical Markdown; the Obsidian adapter adds frontmatter and note-link metadata. | +| Plain text | `.txt`, `.text`, `.log` | Canonical text. | +| reStructuredText | `.rst`, `.rest` | Canonical text and simple underline headings. | +| HTML | `.html`, `.htm`, `.xhtml` | Original HTML plus readable text; scripts, styles, templates, and noscript content are excluded from readable text. | +| JSON | `.json`, `.jsonl`, `.ndjson` | Structured, readable JSON/JSON Lines where valid; malformed JSON is preserved as text with a warning. | +| CSV/TSV tables | `.csv`, `.tsv`, `.tab` | Original table text with bounded header/row metadata. | +| Word-processing documents (DOCX, ODT, RTF) | `.docx`, `.odt`, `.rtf` | Readable paragraphs/text via bounded ZIP/XML or conservative RTF parsing; rich layout, comments, and tracked changes are not reproduced. | +| Spreadsheets (XLSX, ODS) | `.xlsx`, `.ods` | Bounded worksheet/cell values as readable rows; formulas are never executed and workbook layout is not reproduced. | +| Presentations (PPTX, ODP) | `.pptx`, `.odp` | Bounded slide text in slide order; animations, speaker media, and visual layout are not reproduced. | +| EPUB | `.epub` | Readable spine/chapter text via bounded ZIP/XML/HTML parsing. | +| Configuration/XML text | `.yaml`, `.yml`, `.toml`, `.ini`, `.cfg`, `.xml` | Readable source text; credentials and secret-like values are rejected. | +| Source code | `.py`, `.pyi`, `.js`, `.jsx`, `.mjs`, `.cjs`, `.ts`, `.tsx`, `.go`, `.rs`, `.java`, `.cs`, `.c`, `.h`, `.cc`, `.cpp`, `.cxx`, `.hpp`, `.hh`, `.hxx`, `.sql`, `.tf`, `.tfvars`, `.hcl`, `.sh`, `.ps1`, `.rb`, `.php`, `.swift`, `.kt`, `.kts`, `.scala`, `.lua`, `.r`, `.css` | Canonical source text. Link, tag, and heading discovery is disabled for source code so examples do not create graph relationships. | + +The outer v2 importer also reuses Engraphis's existing local resource adapters. With +`engraphis[documents]`, PDF text and image OCR are available; OCR additionally needs the local +Tesseract executable. Audio/video transcription needs `engraphis[transcription]` and +`ENGRAPHIS_WHISPER_MODEL` must point to an existing local model file or directory. The importer +refuses a model name or missing path so this workflow cannot trigger a model download. + +The parser retains original readable content or structure and source metadata. It masks fenced and +inline code before generic tag/link discovery, so examples and snippets cannot manufacture source +relationships. It never executes HTML, Markdown plugins, scripts, macros, or embedded content. + +## Safety and privacy + +Only regular files below the selected root are considered. The scanner rejects a symlinked root, +skips symlinks and hidden/configuration paths, rejects common credential/key filenames and +secret-like contents, bounds individual documents and the whole collection, and verifies a file +did not change while it was read. ZIP containers have member-count, decompressed-size, +compression-ratio, path, and DTD/entity protections. Unsafe, malformed, unreadable, oversized, +binary, or unsupported files are catalogued per file; one failure does not stop the rest of the +collection. Reports never echo secret-like source content. + +Default filename exclusions include `.env` variants, credentials, secrets, tokens, recovery +codes, SSH identity files, and `.pem`, `.key`, `.p12`, and `.pfx` material. A collection is +bounded to 10,000 encountered files and 750 MB of read bytes; an individual adapter input is +bounded to 100 MB, while canonical memory text is capped at 100,000 characters and is rejected +rather than silently split. Containers are additionally capped at 2,000 members and 20 MB of +declared decompressed content. Invalid UTF-8/UTF-16 in permitted text is replaced explicitly and +reported as a parsing warning. + +Markdown uses the Obsidian adapter's stricter 2 MB raw-note limit before decoding. + +Review a preview before importing sensitive material. The local importer makes no network request +and does not copy source folders or attachments into a hidden second collection. + +CLI dry runs inspect an existing plaintext or configured SQLCipher manifest through an immutable, +query-only connection. They tolerate a pre-importer database as an empty manifest and refuse an +active uncheckpointed WAL instead of creating or consulting writable sidecars. + +## Re-import, history, and conflicts + +Each collection has a stable local source identity plus per-document path and content identity. +The same collection and target scope re-import idempotently: unchanged documents are not +duplicated, changed documents follow the selected conflict policy, and unique exact-content moves +can be reported as renames. Source files missing on a later scan are reported; their memories are +not hard-deleted. + +`replace` creates a temporal successor under the normal v2 rules, `new` creates a distinct memory, +and the default `error` reports a divergent lineage without silently overwriting it. Progress is +recorded per document. Rerun an interrupted or `--limit`-paused command with the same root and +target to resume safely; the final report includes imported, updated, renamed, skipped, rejected, +conflict, missing, warning, and error counts. + +## Optional adapters and limits + +The universal core parser deliberately has no hard dependency on OCR, PDF decoding, +audio/video transcription, office applications, or a hosted model. The outer importer invokes +only installed local adapters for PDF/OCR/transcription and reports a per-file rejection when an +adapter or local executable/model is unavailable. Legacy OLE Office files (`.doc`, `.xls`, +`.ppt`), encrypted or DRM-protected documents, unknown containers, and arbitrary binary files +are never guessed or decoded as text. A PDF adapter extracts embedded PDF text; it does not OCR +scanned PDF pages. Import page images separately when local OCR is required. + +The parser does not render HTML/CSS, execute JavaScript or macros, interpret every RST directive, +evaluate spreadsheet formulas, reproduce presentation/EPUB layout, or preserve every +rich-document annotation. YAML and TOML are preserved safely but are not fully interpreted; RTF, +spreadsheets, presentations, and EPUB are readable-text imports rather than editing-fidelity +conversions. Unsupported files remain explicit per-file report entries and can be added later +through the bounded adapter interface; “universal” never means guessing arbitrary binary bytes. +For Obsidian-specific unsupported syntax and attachment behavior, see +[the Obsidian adapter guide](OBSIDIAN_IMPORT.md). diff --git a/docs/HOSTED_PLANS.md b/docs/HOSTED_PLANS.md index cb17bb67a..dbfd60c9d 100644 --- a/docs/HOSTED_PLANS.md +++ b/docs/HOSTED_PLANS.md @@ -24,6 +24,10 @@ implementations are not part of this repository. Start or manage a hosted subscription in the [Engraphis account portal](https://api.engraphis.com/account?plan=pro&interval=monthly&utm_source=engraphis&utm_medium=docs&utm_campaign=pro_conversion&utm_content=hosted_plans_pricing#billing). +Managed Automation schedules consolidation and dreaming proposals. Results require +review and never automatically rewrite the local database; local governance tools +apply the changes you choose. + ## Included System 1 Decision Engine (Jev) After release acceptance and service enablement, every legitimate Pro user and each @@ -49,7 +53,7 @@ accuracy, or cost savings. The managed transport and MCP decision route require client **1.7.9 or newer**. The published 1.7.8 client has an experimental adapter but does not provide this route. -After 1.7.9 is published, upgrade the Python environment that launches your MCP host +Version 1.7.9 was published on October 1, 2026. Upgrade the Python environment that launches your MCP host and restart that host. See the [1.7.9 upgrade and release checklist](RELEASE_1_7_9.md). Installing a new client does not enable a deployment whose managed service is disabled. diff --git a/docs/LLM_PROVIDERS.md b/docs/LLM_PROVIDERS.md index 53918ce40..146418781 100644 --- a/docs/LLM_PROVIDERS.md +++ b/docs/LLM_PROVIDERS.md @@ -1,304 +1,305 @@ -# LLM providers and coding-agent connections - -Engraphis runs fully locally by default. An LLM is optional and is used only when you opt into -LLM extraction, cited synthesis, structured consolidation, or retention supervision. Memory -storage, local embeddings, conflict resolution, and recall do not require a provider. - -This is the complete provider reference. It also explains how to connect a Codex subscription -through MCP. Codex and Cohere Command are distinct products and use different setup paths. - -## Contents - -- [Choose a provider](#choose-a-provider) -- [Configure once](#configure-once) -- [OpenAI](#openai) -- [Anthropic Claude](#anthropic-claude) -- [Google Gemini](#google-gemini) -- [OpenRouter](#openrouter) -- [Ollama](#ollama) -- [Cohere Command](#cohere-command) -- [Other OpenAI-compatible endpoints](#other-openai-compatible-endpoints) -- [Codex subscription](#codex-subscription) -- [Command Code](#command-code) - -## Choose a provider - -| Provider | Engraphis mode | Interface | -| --- | --- | --- | -| OpenAI | `openai` | Native OpenAI Chat Completions | -| Anthropic Claude | `anthropic` | Native Anthropic Messages | -| Google Gemini | `google` | Native Gemini `generateContent` | -| OpenRouter | `openrouter` | OpenAI-compatible Chat Completions | -| Ollama | `custom` | Local OpenAI-compatible endpoint | -| Cohere Command | `custom` | Cohere Compatibility API | -| Another compatible endpoint | `custom` | OpenAI-compatible Chat Completions | -| Command Code Provider API | `custom` | OpenAI-compatible Chat Completions | - -## Configure once - -1. Add one provider's variables to the owner-private `~/.engraphis/config.env`, an absolute - owner-private file selected with `ENGRAPHIS_ENV_FILE`, or the process environment. Engraphis - does not search the working directory for `.env`. -2. Restart the dashboard, server, or MCP process that owns the shared Engraphis database. -3. In **Settings → Connect an LLM**, select **Test connection**. The dashboard picker offers the - named cloud models; configure custom endpoints through the same trusted sources. -4. Keep `ENGRAPHIS_EXTRACTOR=none` for fully local ingestion, or explicitly choose `llm` or - `llm_structured` after the connection succeeds. - -Every LLM setup uses these variables: - -| Variable | Purpose | -| --- | --- | -| `ENGRAPHIS_LLM_PROVIDER` | One of `openai`, `anthropic`, `google`, `openrouter`, or `custom`. | -| `ENGRAPHIS_LLM_MODEL` | A model identifier accepted by the selected provider and account. | -| `ENGRAPHIS_LLM_API_KEY` | Credential for the provider. It is never returned by the dashboard. | -| `ENGRAPHIS_LLM_BASE_URL` | Needed only to override a default or configure a compatible endpoint. | -| `ENGRAPHIS_LLM_EXTRA_HEADERS` | Optional JSON object of headers required by a compatible endpoint. | -| `ENGRAPHIS_LLM_EFFORT` | Reasoning effort for Claude models that think by default: `low`, `medium` (default), `high`, `xhigh`, or `max`. Ignored elsewhere. | - -The sample names below are Engraphis runtime defaults, not provider recommendations. Replace them -when your account or deployment uses a different model. - -Selecting a provider creates an egress path only for LLM-powered features you enable. With -`ENGRAPHIS_EXTRACTOR=chunk` or `none` and `ENGRAPHIS_RETENTION_SUPERVISOR=none`, normal ingest -and recall stay local. When extraction, synthesis, structured consolidation, or retention -supervision is enabled, the selected provider must receive the necessary text to perform that work. - -Provider errors do not expose API keys, configured endpoint URLs, or raw provider responses in the -dashboard. Features that support a local fallback degrade safely when a provider is unavailable; -confirm a successful connection before depending on LLM extraction in a workflow. - -For an OpenAI Codex subscription, use the local MCP connection documented in the Codex section -below. The subscription path does not use ``ENGRAPHIS_LLM_PROVIDER`` or an Engraphis LLM API key; -those settings remain for optional LLM calls made by Engraphis itself. - -## OpenAI - -OpenAI uses the native `openai` mode. Leave `ENGRAPHIS_LLM_BASE_URL` unset unless you deliberately -need a compatible proxy. - -```dotenv -ENGRAPHIS_LLM_PROVIDER=openai -ENGRAPHIS_LLM_MODEL=gpt-4o-mini -ENGRAPHIS_LLM_API_KEY= -``` - -For available models and API-key administration, use the [OpenAI API documentation](https://platform.openai.com/docs/overview). - -## Anthropic Claude - -Anthropic Claude uses the native `anthropic` mode and the Anthropic Messages API. Do not configure -it as `custom`; the native mode applies Anthropic's required request shape and headers. - -```dotenv -ENGRAPHIS_LLM_PROVIDER=anthropic -ENGRAPHIS_LLM_MODEL=claude-sonnet-5-5 -ENGRAPHIS_LLM_API_KEY= -``` - -Leave `ENGRAPHIS_LLM_BASE_URL` unset for the public API. For model and credential details, see -the [Anthropic API documentation](https://docs.anthropic.com/). - -Choosing a model: - -- `claude-sonnet-5-5` is the default. Extraction, consolidation summaries, and grounded synthesis - are bounded tasks, and it costs half as much per token as Opus. -- `claude-opus-5-5` suits the hardest consolidation and conflict-review work. Compare it against - the Sonnet default on your own data before paying for it, because the two are close on everyday - tasks. -- Retired ids such as `claude-3-5-sonnet-20241022` and `claude-3-5-haiku-20241022` are rejected by - the API; the connection test reports them as an HTTP 404. - -Opus 4.7 and later, Sonnet 5 and later, Fable and Mythos (including `claude-mythos-preview`) -reject `temperature` and similar sampling parameters, so Engraphis omits them for those models. -Opus 5 and later, Sonnet 5 and later, Fable and Mythos also think before answering by default. Engraphis sends `ENGRAPHIS_LLM_EFFORT` (`low`, -`medium`, `high`, `xhigh`, or `max`; default `medium`) for those models and keeps at least 4096 -output tokens available so hidden reasoning cannot crowd out the reply. Other providers and older -Claude models ignore the setting. - -## Google Gemini - -Google Gemini uses the native `google` mode and the Gemini `generateContent` API. The native mode -puts the API key and system instruction in the API-specific request fields. - -```dotenv -ENGRAPHIS_LLM_PROVIDER=google -ENGRAPHIS_LLM_MODEL=gemini-1.5-flash -ENGRAPHIS_LLM_API_KEY= -``` - -Leave `ENGRAPHIS_LLM_BASE_URL` unset for the public Gemini API. A service that merely hosts Google -models is not enough for `custom`; it must implement OpenAI Chat Completions. See the -[Gemini API documentation](https://ai.google.dev/gemini-api/docs) for models and credentials. - -## OpenRouter - -OpenRouter uses the named `openrouter` mode and its OpenAI-compatible request format. - -```dotenv -ENGRAPHIS_LLM_PROVIDER=openrouter -ENGRAPHIS_LLM_MODEL=openai/gpt-4o-mini -ENGRAPHIS_LLM_API_KEY= -``` - -Leave `ENGRAPHIS_LLM_BASE_URL` unset for OpenRouter's standard endpoint. Set it only when routing -through a compatible proxy. If that proxy needs extra headers, set -`ENGRAPHIS_LLM_EXTRA_HEADERS` to a JSON object, for example -`{"HTTP-Referer":"https://example.com"}`. See the [OpenRouter documentation](https://openrouter.ai/docs). - -## Ollama - -Ollama is a local OpenAI-compatible endpoint. Configure it as `custom`, not as a separate -provider value. Start Ollama and pull a chat model, then replace `` with an installed -model name: - -```dotenv -ENGRAPHIS_LLM_PROVIDER=custom -ENGRAPHIS_LLM_MODEL= -ENGRAPHIS_LLM_API_KEY=ollama -ENGRAPHIS_LLM_BASE_URL=http://localhost:11434/v1 -``` - -The custom client requires a non-empty key, although the default local Ollama server does not -authenticate it; use a real proxy token if you put Ollama behind an authenticated proxy. The base -URL ends in `/v1` because Engraphis appends `/chat/completions`. Loopback `http` is allowed for a -local service; a non-loopback endpoint must use HTTPS. - -## Cohere Command - -Cohere Command is a model family, not Command Code. Cohere exposes it through the -OpenAI-compatible Compatibility API, so configure Engraphis with `custom`, not an unsupported -native `cohere` provider value. - -```dotenv -ENGRAPHIS_LLM_PROVIDER=custom -ENGRAPHIS_LLM_MODEL= -ENGRAPHIS_LLM_API_KEY= -ENGRAPHIS_LLM_BASE_URL=https://api.cohere.ai/compatibility/v1 -``` - -Choose a Command model available to your Cohere account. The base URL is the Compatibility API -root, so Engraphis appends `/chat/completions`. See Cohere's -[Compatibility API documentation](https://docs.cohere.com/docs/compatibility-api). - -## Other OpenAI-compatible endpoints - -Use `custom` for a self-hosted gateway or compatibility API that accepts bearer authentication and -returns text at `choices[0].message.content`. - -```dotenv -ENGRAPHIS_LLM_PROVIDER=custom -ENGRAPHIS_LLM_MODEL= -ENGRAPHIS_LLM_API_KEY= -ENGRAPHIS_LLM_BASE_URL=https://provider.example/v1 -# ENGRAPHIS_LLM_EXTRA_HEADERS={"Header-Required-By-Provider":"value"} -``` - -Set the base URL to the API root before `/chat/completions`; Engraphis appends that final path. -The URL must be absolute, use HTTP or HTTPS, and omit embedded credentials, a query string, and a -fragment. HTTP is accepted only for a loopback endpoint such as a local development service. - -The custom client sends a model, system and user messages, plus optional temperature and token -limits. Endpoints that implement another protocol, such as Anthropic Messages, need a matching -native mode or adapter. If a test fails, confirm the base URL, model, credential, required headers, -and request and response shapes. - -## Codex subscription - -Codex subscription users connect the coding agent to Engraphis over MCP. This keeps the -conversation and subscription inside Codex; Engraphis runs locally and receives no Codex API key. - -```bash -pip install "engraphis[mcp]" -engraphis-init -codex mcp add engraphis -- engraphis-mcp -``` - -Verify the connection with ``codex mcp list`` and then call an Engraphis MCP tool from a -tool-enabled Codex session. The MCP connection is the supported Codex-subscription path; do not -configure a local model endpoint or invent an ``ENGRAPHIS_LLM_PROVIDER=codex`` value. - -## Command Code - -Command Code and Cohere Command are separate products. There are two ways to combine Command Code -with Engraphis: connect its coding agent to Engraphis over MCP, or use Command Provider as an -optional external LLM for Engraphis. These paths are independent. - -### Connect the Command Code agent over MCP - -Install the MCP surface and initialize a stable database path once: - -```bash -pip install "engraphis[mcp]" -engraphis-init -``` - -`engraphis-init` records an absolute `ENGRAPHIS_DB_PATH`. Use that same path for the dashboard and -the MCP server so memories written by Command Code appear in the same local store. Add a local -server, replacing the path with the one from initialization: - -```bash -cmd mcp add --scope local --env ENGRAPHIS_DB_PATH=/absolute/path/to/engraphis.db engraphis -- engraphis-mcp -``` - -Windows: `cmd` may resolve to `cmd.exe` (the built-in Windows command interpreter) -instead of the Command Code CLI. If `cmd mcp list` opens a Windows command-prompt window or -prints `'mcp' is not recognized`, the wrong `cmd` was found on `PATH`. Run `where cmd` and -ensure the Command Code CLI's install directory appears before `C:\Windows\System32`; -otherwise invoke the CLI by its absolute path (e.g. -`C:\Users\you\AppData\Roaming\Python\Scripts\cmd.exe`). - -All Command Code options precede the server name, and `--` separates the name from the stdio -command. `engraphis-mcp` runs locally over stdio; normal local use needs no HTTP endpoint or -Engraphis API key. - -| Scope | Use it when | Storage | -| --- | --- | --- | -| `local` | The connection is only for you in this project. This is the recommended first setup. | Command Code's per-project local configuration. | -| `project` | The team should share the server definition. | `.mcp.json` in the repository. Do not commit personal database paths or credentials. | -| `user` | The server should be available in all of your projects. | Your Command Code user configuration. | - -Use `cmd mcp add --scope project ...` or `cmd mcp add --scope user ...` for another scope. For a -committed project definition, keep machine-specific `ENGRAPHIS_DB_PATH` values outside the -repository or use a team-managed path that is safe to share. - -Verify the connection: - -```bash -cmd mcp list -cmd mcp get engraphis -``` - -(If these commands open a Windows command prompt instead of listing MCP servers, follow the -Windows note above -- `cmd` resolved to `cmd.exe`.) - -Start a normal Command Code session with `cmd`, open `/mcp`, and confirm that `engraphis` is -connected and exposes tools. Then ask Command Code: "Call `engraphis_stats` and show me the -result." A response with memory counts confirms the end-to-end connection. - -Command Code disables MCP tools in plan mode. Start a tool-enabled session before expecting it to -call `engraphis_recall`, `engraphis_remember`, or another Engraphis tool. For the broader memory -workflow, use the standalone [MCP tool reference](MCP_TOOLS.md) and Command Code's -[MCP documentation](https://commandcode.ai/docs/mcp). - -### Use Command Provider as Engraphis's LLM - -This optional setup lets Engraphis call Command Provider for LLM-powered features. It is separate -from the MCP connection above. - -```dotenv -ENGRAPHIS_LLM_PROVIDER=custom -ENGRAPHIS_LLM_MODEL= -ENGRAPHIS_LLM_API_KEY= -ENGRAPHIS_LLM_BASE_URL=https://api.commandcode.ai/provider/v1 -# ENGRAPHIS_LLM_EXTRA_HEADERS={"x-cmd-zdr":"1"} -``` - -Choose a Command Provider model that accepts OpenAI Chat Completions. Engraphis's `custom` client -adds `/chat/completions` to the base URL, so do not select a Claude model for this configuration: -Command Provider routes Claude models through its Anthropic Messages endpoint instead. - -`x-cmd-zdr: 1` is optional. It requests Command Provider's zero-data-retention routing and can -make a request fail when the selected model has no eligible upstream. Test the connection before -turning on LLM extraction or another provider-backed workflow. See the -[Command Provider API documentation](https://commandcode.ai/docs/provider). +# LLM providers and coding-agent connections + +Engraphis runs fully locally by default. An LLM is optional and is used only when you opt into +LLM extraction, cited synthesis, structured consolidation, or retention supervision. Memory +storage, local embeddings, conflict resolution, and recall do not require a provider. + +This is the complete provider reference. It also explains how to connect a Codex subscription +through MCP. Codex and Cohere Command are distinct products and use different setup paths. + +## Contents + +- [Choose a provider](#choose-a-provider) +- [Configure once](#configure-once) +- [OpenAI](#openai) +- [Anthropic Claude](#anthropic-claude) +- [Google Gemini](#google-gemini) +- [OpenRouter](#openrouter) +- [Ollama](#ollama) +- [Cohere Command](#cohere-command) +- [Other OpenAI-compatible endpoints](#other-openai-compatible-endpoints) +- [Codex subscription](#codex-subscription) +- [Command Code](#command-code) + +## Choose a provider + +| Provider | Engraphis mode | Interface | +| --- | --- | --- | +| OpenAI | `openai` | Native OpenAI Chat Completions | +| Anthropic Claude | `anthropic` | Native Anthropic Messages | +| Google Gemini | `google` | Native Gemini `generateContent` | +| OpenRouter | `openrouter` | OpenAI-compatible Chat Completions | +| Ollama | `custom` | Local OpenAI-compatible endpoint | +| Cohere Command | `custom` | Cohere Compatibility API | +| Another compatible endpoint | `custom` | OpenAI-compatible Chat Completions | +| Command Code Provider API | `custom` | OpenAI-compatible Chat Completions | + +## Configure once + +1. Add one provider's variables to the owner-private `~/.engraphis/config.env`, an absolute + owner-private file selected with `ENGRAPHIS_ENV_FILE`, or the process environment. Engraphis + does not search the working directory for `.env`. +2. Restart the dashboard, server, or MCP process that owns the shared Engraphis database. +3. In **Settings → Connect an LLM**, select **Test connection**. The dashboard picker offers the + named cloud models; configure custom endpoints through the same trusted sources. +4. Keep `ENGRAPHIS_EXTRACTOR=none` for fully local ingestion, or explicitly choose `llm` or + `llm_structured` after the connection succeeds. + +Every LLM setup uses these variables: + +| Variable | Purpose | +| --- | --- | +| `ENGRAPHIS_LLM_PROVIDER` | One of `openai`, `anthropic`, `google`, `openrouter`, or `custom`. | +| `ENGRAPHIS_LLM_MODEL` | A model identifier accepted by the selected provider and account. | +| `ENGRAPHIS_LLM_API_KEY` | Credential for the provider. It is never returned by the dashboard. | +| `ENGRAPHIS_LLM_BASE_URL` | Needed only to override a default or configure a compatible endpoint. | +| `ENGRAPHIS_LLM_EXTRA_HEADERS` | Optional JSON object of headers required by a compatible endpoint. | +| `ENGRAPHIS_LLM_EFFORT` | Reasoning effort for Claude models that think by default: `low`, `medium` (default), `high`, `xhigh`, or `max`. Ignored elsewhere. | + +The sample names below are Engraphis runtime defaults, not provider recommendations. Replace them +when your account or deployment uses a different model. + +Selecting a provider creates an egress path only for LLM-powered features you enable. With +`ENGRAPHIS_EXTRACTOR=chunk` or `none` and `ENGRAPHIS_RETENTION_SUPERVISOR=none`, normal ingest +and recall stay local. When extraction, synthesis, structured consolidation, or retention +supervision is enabled, the selected provider must receive the necessary text to perform that work. + +Provider errors do not expose API keys, configured endpoint URLs, or raw provider responses in the +dashboard. Features that support a local fallback degrade safely when a provider is unavailable; +confirm a successful connection before depending on LLM extraction in a workflow. + +For an OpenAI Codex subscription, use the local MCP connection documented in the Codex section +below. The subscription path does not use ``ENGRAPHIS_LLM_PROVIDER`` or an Engraphis LLM API key; +those settings remain for optional LLM calls made by Engraphis itself. + +## OpenAI + +OpenAI uses the native `openai` mode. Leave `ENGRAPHIS_LLM_BASE_URL` unset unless you deliberately +need a compatible proxy. + +```dotenv +ENGRAPHIS_LLM_PROVIDER=openai +ENGRAPHIS_LLM_MODEL=gpt-4o-mini +ENGRAPHIS_LLM_API_KEY= +``` + +For available models and API-key administration, use the [OpenAI API documentation](https://platform.openai.com/docs/overview). + +## Anthropic Claude + +Anthropic Claude uses the native `anthropic` mode and the Anthropic Messages API. Do not configure +it as `custom`; the native mode applies Anthropic's required request shape and headers. + +```dotenv +ENGRAPHIS_LLM_PROVIDER=anthropic +ENGRAPHIS_LLM_MODEL=claude-sonnet-5-5 +ENGRAPHIS_LLM_API_KEY= +``` + +Leave `ENGRAPHIS_LLM_BASE_URL` unset for the public API. For model and credential details, see +the [Anthropic API documentation](https://docs.anthropic.com/). + +Choosing a model: + +- `claude-sonnet-5-5` is the default. Extraction, consolidation summaries, and grounded synthesis + are bounded tasks, and it costs half as much per token as Opus. +- `claude-opus-5-5` suits the hardest consolidation and conflict-review work. Compare it against + the Sonnet default on your own data before paying for it, because the two are close on everyday + tasks. +- Retired ids such as `claude-3-5-sonnet-20241022` and `claude-3-5-haiku-20241022` are rejected by + the API; the connection test reports them as an HTTP 404. + +Opus 4.7 and later, Sonnet 5 and later, Fable and Mythos (including `claude-mythos-preview`) +reject `temperature` and similar sampling parameters, so Engraphis omits them for those models. +Opus 5 and later, Sonnet 5 and later, Fable and Mythos also think before answering by default. Engraphis sends `ENGRAPHIS_LLM_EFFORT` (`low`, +`medium`, `high`, `xhigh`, or `max`; default `medium`) for those models and keeps at least 4096 +output tokens available so hidden reasoning cannot crowd out the reply. Other providers and older +Claude models ignore the setting. + +## Google Gemini + +Google Gemini uses the native `google` mode and the Gemini `generateContent` API. The native mode +puts the API key and system instruction in the API-specific request fields. + +```dotenv +ENGRAPHIS_LLM_PROVIDER=google +ENGRAPHIS_LLM_MODEL=gemini-1.5-flash +ENGRAPHIS_LLM_API_KEY= +``` + +Leave `ENGRAPHIS_LLM_BASE_URL` unset for the public Gemini API. A service that merely hosts Google +models is not enough for `custom`; it must implement OpenAI Chat Completions. See the +[Gemini API documentation](https://ai.google.dev/gemini-api/docs) for models and credentials. + +## OpenRouter + +OpenRouter uses the named `openrouter` mode and its OpenAI-compatible request format. + +```dotenv +ENGRAPHIS_LLM_PROVIDER=openrouter +ENGRAPHIS_LLM_MODEL=openai/gpt-4o-mini +ENGRAPHIS_LLM_API_KEY= +``` + +Leave `ENGRAPHIS_LLM_BASE_URL` unset for OpenRouter's standard endpoint. Set it only when routing +through a compatible proxy. If that proxy needs extra headers, set +`ENGRAPHIS_LLM_EXTRA_HEADERS` to a JSON object, for example +`{"HTTP-Referer":"https://example.com"}`. See the [OpenRouter documentation](https://openrouter.ai/docs). + +## Ollama + +Ollama is a local OpenAI-compatible endpoint. Configure it as `custom`, not as a separate +provider value. Start Ollama and pull a chat model, then replace `` with an installed +model name: + +```dotenv +ENGRAPHIS_LLM_PROVIDER=custom +ENGRAPHIS_LLM_MODEL= +ENGRAPHIS_LLM_API_KEY=ollama +ENGRAPHIS_LLM_BASE_URL=http://localhost:11434/v1 +``` + +The custom client requires a non-empty key, although the default local Ollama server does not +authenticate it; use a real proxy token if you put Ollama behind an authenticated proxy. The base +URL ends in `/v1` because Engraphis appends `/chat/completions`. Loopback `http` is allowed for a +local service; a non-loopback endpoint must use HTTPS. + +## Cohere Command + +Cohere Command is a model family, not Command Code. Cohere exposes it through the +OpenAI-compatible Compatibility API, so configure Engraphis with `custom`, not an unsupported +native `cohere` provider value. + +```dotenv +ENGRAPHIS_LLM_PROVIDER=custom +ENGRAPHIS_LLM_MODEL= +ENGRAPHIS_LLM_API_KEY= +ENGRAPHIS_LLM_BASE_URL=https://api.cohere.ai/compatibility/v1 +``` + +Choose a Command model available to your Cohere account. The base URL is the Compatibility API +root, so Engraphis appends `/chat/completions`. See Cohere's +[Compatibility API documentation](https://docs.cohere.com/docs/compatibility-api). + +## Other OpenAI-compatible endpoints + +Use `custom` for a self-hosted gateway or compatibility API that accepts bearer authentication and +returns text at `choices[0].message.content`. + +```dotenv +ENGRAPHIS_LLM_PROVIDER=custom +ENGRAPHIS_LLM_MODEL= +ENGRAPHIS_LLM_API_KEY= +ENGRAPHIS_LLM_BASE_URL=https://provider.example/v1 +# ENGRAPHIS_LLM_EXTRA_HEADERS={"Header-Required-By-Provider":"value"} +``` + +Set the base URL to the API root before `/chat/completions`; Engraphis appends that final path. +The URL must be absolute, use HTTP or HTTPS, and omit embedded credentials, a query string, and a +fragment. HTTP is accepted only for a loopback endpoint such as a local development service. + +The custom client sends a model, system and user messages, plus optional temperature and token +limits. Endpoints that implement another protocol, such as Anthropic Messages, need a matching +native mode or adapter. If a test fails, confirm the base URL, model, credential, required headers, +and request and response shapes. + +## Codex subscription + +Codex subscription users connect the coding agent to Engraphis over MCP. This keeps the +conversation and subscription inside Codex; Engraphis runs locally and receives no Codex API key. + +```bash +pip install "engraphis[mcp]" +engraphis-init +codex mcp add engraphis -- engraphis-mcp +``` + +Verify the connection with ``codex mcp list`` and then call an Engraphis MCP tool from a +tool-enabled Codex session. The MCP connection is the supported Codex-subscription path; do not +configure a local model endpoint or invent an ``ENGRAPHIS_LLM_PROVIDER=codex`` value. + +## Command Code + +Command Code and Cohere Command are separate products. There are two ways to combine Command Code +with Engraphis: connect its coding agent to Engraphis over MCP, or use Command Provider as an +optional external LLM for Engraphis. These paths are independent. + +### Connect the Command Code agent over MCP + +Install the MCP surface and initialize a stable database path once: + +```bash +pip install "engraphis[mcp]" +engraphis-init +``` + +`engraphis-init` records an absolute `ENGRAPHIS_DB_PATH`. Use that same path for the dashboard and +the MCP server so memories written by Command Code appear in the same local store. Add a local +server, replacing the path with the one from initialization: + +```bash +cmd mcp add --scope local --env ENGRAPHIS_DB_PATH=/absolute/path/to/engraphis.db engraphis -- engraphis-mcp +``` + +Windows: `cmd` may resolve to `cmd.exe` (the built-in Windows command interpreter) +instead of the Command Code CLI. If `cmd mcp list` opens a Windows command-prompt window or +prints `'mcp' is not recognized`, the wrong `cmd` was found on `PATH`. Run `where cmd` and +ensure the Command Code CLI's install directory appears before `C:\Windows\System32`; +otherwise invoke the CLI by its absolute path (e.g. +`C:\Users\you\AppData\Roaming\Python\Scripts\cmd.exe`). + +All Command Code options precede the server name, and `--` separates the name from the stdio +command. `engraphis-mcp` runs locally over stdio; normal local use needs no HTTP endpoint or +Engraphis API key. + +| Scope | Use it when | Storage | +| --- | --- | --- | +| `local` | The connection is only for you in this project. This is the recommended first setup. | Command Code's per-project local configuration. | +| `project` | The team should share the server definition. | `.mcp.json` in the repository. Do not commit personal database paths or credentials. | +| `user` | The server should be available in all of your projects. | Your Command Code user configuration. | + +Use `cmd mcp add --scope project ...` or `cmd mcp add --scope user ...` for another scope. For a +committed project definition, keep machine-specific `ENGRAPHIS_DB_PATH` values outside the +repository or use a team-managed path that is safe to share. + +Verify the connection: + +```bash +cmd mcp list +cmd mcp get engraphis +``` + +(If these commands open a Windows command prompt instead of listing MCP servers, follow the +Windows note above -- `cmd` resolved to `cmd.exe`.) + +Start a normal Command Code session with `cmd`, open `/mcp`, and confirm that `engraphis` is +connected and exposes tools. Then ask Command Code to discover the local memory store health +action with `engraphis_discover_actions` and call its returned schema through +`engraphis_execute_read`. A response with memory counts confirms the end-to-end connection. + +Command Code disables MCP tools in plan mode. Start a tool-enabled session before expecting it to +call `engraphis_recall`, `engraphis_remember`, or another Engraphis tool. For the broader memory +workflow, use the standalone [MCP tool reference](MCP_TOOLS.md) and Command Code's +[MCP documentation](https://commandcode.ai/docs/mcp). + +### Use Command Provider as Engraphis's LLM + +This optional setup lets Engraphis call Command Provider for LLM-powered features. It is separate +from the MCP connection above. + +```dotenv +ENGRAPHIS_LLM_PROVIDER=custom +ENGRAPHIS_LLM_MODEL= +ENGRAPHIS_LLM_API_KEY= +ENGRAPHIS_LLM_BASE_URL=https://api.commandcode.ai/provider/v1 +# ENGRAPHIS_LLM_EXTRA_HEADERS={"x-cmd-zdr":"1"} +``` + +Choose a Command Provider model that accepts OpenAI Chat Completions. Engraphis's `custom` client +adds `/chat/completions` to the base URL, so do not select a Claude model for this configuration: +Command Provider routes Claude models through its Anthropic Messages endpoint instead. + +`x-cmd-zdr: 1` is optional. It requests Command Provider's zero-data-retention routing and can +make a request fail when the selected model has no eligible upstream. Test the connection before +turning on LLM extraction or another provider-backed workflow. See the +[Command Provider API documentation](https://commandcode.ai/docs/provider). diff --git a/docs/MCP_CONTRACT.json b/docs/MCP_CONTRACT.json index 62d20e96d..c3f2e23c8 100644 --- a/docs/MCP_CONTRACT.json +++ b/docs/MCP_CONTRACT.json @@ -1,6 +1,6 @@ { "schema": "engraphis-mcp-contract/v1", - "sha256": "c45b4e1de83058b50b6da9def894fd5d5d8a5cc7c37466cd9dbef65c1fcf2b5d", + "sha256": "cc15a2a2425557ec13be88f97635081c84fb48533206ad49486643e5e6b2f59d", "surfaces": { "classic": [ { @@ -2513,7 +2513,7 @@ "readOnlyHint": false, "title": "Remember a fact" }, - "description": "Store a memory so it can be recalled in later turns, sessions, or repos.\n\nUse this whenever you learn something worth keeping: a convention, a decision and its\nrationale, a bug's cause and fix, a user preference, or a reusable procedure.\n\nReturns:\n str: JSON ``{\"id\",\"workspace\",\"repo\",\"scope\",\"mtype\",\"stored\":true,\"op\"}`` where\n ``op`` is ``\"add\"`` (new), ``\"noop\"`` (matched an existing memory almost exactly \u2014\n that one was reinforced, ``id`` points to it), or ``\"invalidate\"`` (superseded an\n existing memory on the same subject \u2014 see ``superseded`` for the old id(s); history\n is preserved, never deleted), ``\"relate\"`` (kept both uncertain neighboring claims and\n linked them), or ``\"quarantined\"`` (a suspicious explicitly untrusted payload was\n retained for governance inspection but excluded from normal recall). Quarantine returns\n content-free ``policy`` and ``reasons`` codes. Returns ``\"Error: \"`` if\n validation fails.", + "description": "Store a memory so it can be recalled in later turns, sessions, or repos.\n\nUse this whenever you learn something worth keeping: a convention, a decision and its\nrationale, a bug's cause and fix, a user preference, or a reusable procedure.\n\nReturns:\n str: JSON with ``id``, resolved scope, ``stored``, ``op``, ``review_state`` and\n ``prompt_eligible``. Stored memories may still require review before recall. Here\n ``op`` is ``\"add\"`` (new), ``\"noop\"`` (matched an existing memory almost exactly \u2014\n that one was reinforced, ``id`` points to it), or ``\"invalidate\"`` (superseded an\n existing memory on the same subject \u2014 see ``superseded`` for the old id(s); history\n is preserved, never deleted), ``\"relate\"`` (kept both uncertain neighboring claims and\n linked them), or ``\"quarantined\"`` (a suspicious explicitly untrusted payload was\n retained for governance inspection but excluded from normal recall). Quarantine returns\n content-free ``policy`` and ``reasons`` codes. Returns ``\"Error: \"`` if\n validation fails.", "inputSchema": { "properties": { "claim_kind": { @@ -3720,7 +3720,7 @@ "readOnlyHint": false, "title": "Remember a durable fact" }, - "description": "Store a durable fact with safe default provenance and deduplication.", + "description": "Store a fact; check prompt_eligible and review_state before recall.", "inputSchema": { "properties": { "claim_kind": { diff --git a/docs/MCP_TOOLS.md b/docs/MCP_TOOLS.md index a38c70efe..0bc69c246 100644 --- a/docs/MCP_TOOLS.md +++ b/docs/MCP_TOOLS.md @@ -1,302 +1,308 @@ -# MCP tool reference - -`engraphis-mcp` is the zero-configuration Smart MCP gateway. It initially exposes nine concise -tools: `engraphis_session`, `engraphis_recall_context`, `engraphis_remember`, -`engraphis_discover_actions`, `engraphis_execute_read`, `engraphis_execute_action`, -`engraphis_get_memory`, `engraphis_update_memory`, and `engraphis_conflict_review`. Agents use -the routine tools directly; for any advanced capability, they discover the best action and execute -the returned, version-bound capability ID. Discovery returns the precise schema and side-effect -class, and execution revalidates availability, scope, authorization, and arguments. - -### Smart tool inventory - -| Tool | What it does | -|---|---| -| `engraphis_session` | Starts or resumes a session, or ends it with a next-session handoff. | -| `engraphis_recall_context` | Returns one compact, bounded context packet for routine agent work. | -| `engraphis_remember` | Stores a routine durable memory with safe default provenance and deduplication. | -| `engraphis_discover_actions` | Returns exact schemas for a small set of matching advanced actions. | -| `engraphis_execute_read` | Executes only a discovered action that is read-only and idempotent. | -| `engraphis_execute_action` | Executes a discovered write, admin, or destructive-capable action. | -| `engraphis_get_memory` | Returns one governed memory record, excluding non-prompt-eligible content. | -| `engraphis_update_memory` | Edits memory metadata; content changes use the governed correction path. | -| `engraphis_conflict_review` | Lists pending, quarantined, or conflicting memories for review. | - -The Smart gateway exposes these nine tools directly; advanced capabilities remain available through -discovery and the validated executors. - -For a deliberate cross-agent handoff, pass the source ID as `session_id` when starting -`engraphis_session`, or as `resume_from_session_id` to `engraphis_start_session`. The source must -be an ended session owned by the same authenticated user in the exact resolved workspace and -repository. Missing, active, deleted, or unauthorized sources fail closed; the tool never -substitutes a recent handoff. The returned `bootstrap` is -bounded to 512 regex-counted content tokens, at most six open threads, and per-field character -limits. `handoff_source` labels the source start/end times in UTC. Sessions have no automatic -age expiry; those timestamps show age but do not guarantee freshness. - -### Smart routine schemas are reduced by design - -The two routine Smart tools deliberately accept smaller allow-lists than their Classic -namesakes; advanced controls are discoverable rather than routine: - -| Smart tool | Accepted parameters | -|---|---| -| `engraphis_remember` | `content`, `workspace`, `repo`, `session_id`, `mtype`, `importance`, `subject_key`, `claim_kind`, optional source-bound `exact_value`/`exact_value_type`; safe provenance is fixed internally | -| `engraphis_recall_context` | `query`, `workspace`, `repo`, `session_id`, `k`, `token_budget`, `packing_mode`, `retrieval_recipe`, `format`, optional Jev controls `allow_remote`, `data_classification`; always compact, no `response_mode` | - -`format="gist"` is a compatibility option for the same budgeted, cited evidence as -`full`. It preserves the same selected text and whitespace; it does not apply an -additional summary or promise extra token savings. Source IDs remain in `sources`. - -Jev-assisted recall planning requires explicit BYOK and is opt-in. On the Smart context tool, -set `allow_remote=true` and -`data_classification="public"` or `"internal"`; that call both enables bounded route selection -and gives per-call consent for remote processing. The default stays deterministic and local. -Classic recall tools additionally require `planning="auto"` and `jev_assisted=true`. -The provider receives the query and bounded local routes, not recalled memory bodies. Jev cannot -change scope, time, type, or trust filters; uncertainty and failures retain deterministic route -order and appear in `planning_advisory`. Leaving the Jev controls at their defaults keeps local -deterministic behavior. -Managed access does not admit `query_planning`: it fails closed before credential refresh or -network requests, retaining deterministic route order with a visible fallback. Neither `managed` -nor `auto` silently switches to BYOK. - - -No user profile choice or tool switching is required. The dashboard `/mcp` endpoint and -`engraphis-mcp-http` use this Smart surface by default. `engraphis-mcp-classic` (or -`engraphis-mcp-http --classic`) preserves the 39 direct tools below for integrations that pin -their historical names and response shapes. - -Hosts which already own chat history should use `POST /api/adaptive-context`, not an MCP action. -The gateway works in general MCP clients without native deferred tool search; clients that -explicitly support OpenAI's deferred `tool_search` can apply it as an optional host optimization. - -### Workspace routing - -Session starts accept an omitted workspace, resolving an explicit value, then the saved repo -mapping, then `default`. Routine remember and recall calls also inherit an omitted workspace -from an authorized supplied session before consulting the repo mapping. Local recall with no -workspace, repo, or session stays broad. -Explicit workspace/repo values that conflict with a supplied session are rejected; invalid or -unauthorized sessions never fall back. An explicit `workspace="default"` overrides a mapping. - -Session and remember responses report the resolved workspace and `workspace_source` (`explicit`, -`project`, or `default`, plus `session` on inherited writes) so the agent can show its destination. -Pass the returned `session_id` on subsequent recall and write calls to retain that scope. -There is no implicit server-global current session, and changing the dashboard workspace -selector does not change agent arguments. See [workspace organization](WORKSPACE_ORGANIZATION.md) -for project setup, hook overrides, and organizing existing memories. - -Saved project mappings live in the shared database, per authenticated caller or standalone -local context. The Classic `engraphis_list_workspaces`, `engraphis_get_workspace_routing`, and -`engraphis_set_workspace_routing` capabilities are available through Smart discovery and the -validated executors; saving a mapping still requires workspace access. Classic batch remember, -record-event, ingest, and proactive recall retain their explicit workspace contract: pass the -resolved destination returned by the session. - -### Standalone semantic startup - -On Windows, the standalone stdio and HTTP launchers import optional semantic dependencies -on the launcher thread before serving MCP requests. This prevents the first semantic tool -call from stalling during a native dependency import in a worker thread. Models are still -loaded lazily, and exact-backend validation retains its normal failure policy. Dependency -import diagnostics go to stderr so stdout stays reserved for the stdio protocol. - -`ENGRAPHIS_MCP_PRELOAD_EMBEDDER=auto` enables this startup step on Windows when a semantic -embedding or reranking model is configured. Configured sources are validated before -optional imports, including the immutable revision policy. Set `0` to disable it or `1` to -enable it on other platforms. An unavailable optional dependency is left to the configured -backend fallback policy. - -## Classic direct-tool inventory - -The following inventory applies to the Classic compatibility server. Start with -`engraphis_recall_context` when an agent needs prompt-ready context, and use -`engraphis_remember` when it learns a durable fact. - -Retrieval responses (`engraphis_recall`, `engraphis_recall_context`, -`engraphis_recall_grounded`, and `engraphis_answer`) always declare -`degraded_mode`, `semantic_support`, `embedding_mode`, and `vector_search_ready`. A `true` -degraded flag means the active backend is not a declared semantic embedder (the bundled -deterministic fallback is feature hashing with lexical overlap), the persistent vector space is -rebuilding or does not match the configured embedder, or the vector index failed for that -request. `vector_search_ready` is the authoritative vector-arm status. When -`semantic_support=false`, vector retrieval and semantic-cosine evidence are both disabled; -recall remains lexical/graph/code based and grounded answers use lexical support only. A -request-local index failure instead reports `vector_search_ready=false` while semantic support -can remain available for exact support scoring from the configured embedder and stored vectors. - -Classic `engraphis_recall` and `engraphis_recall_context` also accept `jev_assisted` (default -`false`), `allow_remote` (default `false`), and `data_classification` (optional). Use the same -explicit remote-consent requirements described for the Smart context tool above. - -Trust boundary: normal local-agent memory creation is prompt-visible immediately after validation; -it does not require owner approval. The default `agent` source covers `engraphis_remember`, -`engraphis_ingest`, and dashboard intent writes. External sources remain `pending` regardless of -a caller-supplied `trusted` label, and detector matches are `quarantined` immediately. Pending -and quarantined records are available only to explicit inspection workflows and never appear in -prompt-ready MCP recall or context, `engraphis_why`, or `engraphis_timeline`, nor can they feed -resolution, links, graph/code backfill, or derived prompt context. `include_untrusted=True` is -inspection-only and must never be copied into a model prompt. - -MCP deliberately has no approval tool. Approval is only for external evidence: it -creates a fresh, audited `approved` successor while retaining the reviewed source and its -provenance. In the local product it is available only through the CSRF-bound dashboard review -action (with `ENGRAPHIS_API_TOKEN`) or the interactive TTY command -`python -m scripts.approve_memory MEM_ID --reason "..."`; the command rejects redirected input -and requires a typed confirmation. Local operators can use `engraphis-cli review list` and the -dry-run-first `engraphis-cli review approve` for scoped batches; quarantined records are excluded. -Hosted approval is an owner/admin action of the private hosted service. Direct in-process -`MemoryEngine` use is a trusted-code boundary for code that already has local database authority, -not a transport permission. - -For the full memory trust model, automatic schema-11 classification, and operator recovery, see -the [memory write trust model](WRITE_REVIEW.md) and [recall recovery guide](RECALL_RECOVERY.md). - -### Receipt-chain failure behavior - -Receipt recording is deliberately fail-closed: if verification finds a fork, cycle, or -disconnected chain with no safe predecessor, the Store refuses to append and -`engraphis_verify_receipts` continues to report the invalid chain. The completed service -operation is not rolled back or reported as failed solely because its follow-up receipt could -not be recorded. Its JSON result contains `"receipt": null` and a content-free -`"receipt_warning":{"code":"receipt_chain_integrity_failure",...}` marker. Repair or restore -the receipt chain before treating subsequent receipt continuity as audit evidence. - -Emitted recall receipts include the normalized `packing_mode` (`legacy` or `coverage`) -alongside the retrieval recipe and effective depth. Historical receipts remain valid; -an omitted mode means it was not recorded, and is not inferred from current defaults. - -| Category | Tool | What it does | -|---|---|---| -| Write | `engraphis_remember` | Stores a fact and resolves it as a new memory, reinforcement, safe supersession, or related memory. | -| Write | `engraphis_remember_many` | Stores a fan-out batch of facts in one transaction: within-batch dedup/supersession, plus evidence-labeled edges between siblings sharing a `subject_key` or declared `evidence_source`. | -| Write | `engraphis_record_event` | Appends one raw occurrence to the event ledger; event rows are not recalled, deduplicated, reinforced, or consolidated as memories. | -| Write | `engraphis_link` | Connects two related memories. | -| Write | `engraphis_ingest` | Applies the configured extractor (`chunk`, `llm`, or `llm_structured`). With `none`, it stores one verbatim memory. | -| Write | `engraphis_ingest_postgres_schema` | Stores a PostgreSQL schema snapshot and typed graph. The DSN is never stored. | -| Write | `engraphis_consolidate` | Runs a dry-run or live consolidation sweep. A live call can write resolved facts and receipts. | -| Stateful read | `engraphis_recall_context` | Returns hard-budget context, compact sources, token usage, and optional diagnostics. Recommended for agent prompts. Compact-only: it never accepts `response_mode` and never returns full memory bodies. | -| Stateful read | `engraphis_recall` | Runs hybrid vector, lexical, and graph recall. It attempts a receipt without strengthening weak matches. | -| Stateful read | `engraphis_recall_grounded` | Returns a cited answer or abstains when the evidence is too weak. It attempts a receipt and reinforces cited memories. | -| Stateful read | `engraphis_answer` | Backward-compatible alias for `engraphis_recall_grounded`. | -| Pure read | `engraphis_recall_proactive` | Returns high-signal, queryless context and a last-session handoff. It does not reinforce or record a receipt. | -| Stateful read | `engraphis_proactive_context` | Builds task-aware cited context and attempts a receipt without reinforcement. | -| Read | `engraphis_why` | Returns the current answer and the memories it superseded. | -| Read | `engraphis_timeline` | Returns complete bi-temporal history, oldest first. | -| Code | `engraphis_index_repo` | Incrementally parses a repository into the code and memory graph. Each run attempts a receipt. | -| Code | `engraphis_search_code` | Finds symbols, callers, and linked memories. | -| Code | `engraphis_code_path` | Finds a path across definitions, calls, imports, and memories. | -| Code | `engraphis_code_impact` | Ranks changed-file impact using dependents, communities, memories, and hotspots. | -| Code | `engraphis_export_code_graph` | Exports graph JSON, Markdown, and HTML. | -| Code | `engraphis_link_symbol` | Manually links a code symbol to a memory (idempotent). | -| Audit | `engraphis_receipts` | Lists content-free hashed operation receipts. | -| Audit | `engraphis_context_savings` | Reports receipt-backed estimated context tokens saved across all visible workspaces by default, or one workspace when supplied; optional `from_ts`, `to_ts`, and `release_version` filters are supported. This is estimated prompt-context reduction, not provider billing. | -| Audit | `engraphis_verify_receipts` | Verifies the receipt chain, local tail anchor, and an optional saved head/count. | -| Audit | `engraphis_export_receipts` | Exports a shareable receipt-only audit bundle. | -| Governance | `engraphis_retire` | Retires a memory by closing its validity window. It does not delete history. | -| Governance | `engraphis_secure_erase` | Irreversibly removes one leaked memory and local indexes; reports local-backup and external-copy limitations. | -| Compatibility | `engraphis_forget` | Deprecated alias for `engraphis_retire`; preserves the legacy response shape. | -| Governance | `engraphis_pin` | Prevents future automatic decay or pruning. | -| Governance | `engraphis_correct` | Replaces memory content without losing the previous version. Changed content clears the old literal binding; `exact_value`, `exact_value_type`, and optional `exact_value_span` explicitly bind a replacement, or `clear_exact_value=true` removes it. Governed provenance remains pending unless separately approved. | -| Governance | `engraphis_promote` | Widens an explicitly approved memory's scope while preserving and linking its narrower history. | -| Session | `engraphis_start_session` | Starts a work session. Exact retries are safe; `force_new=true` creates another session. | -| Workspace | `engraphis_list_workspaces` | Lists visible workspaces for choosing a destination. | -| Workspace | `engraphis_get_workspace_routing` | Returns the current caller's saved destination for an exact `repo` name. | -| Workspace | `engraphis_set_workspace_routing` | Saves or removes a repo-to-workspace mapping; takes `workspace`, `repo`, and `enabled` (default `true`). | -| Session | `engraphis_end_session` | Closes a work session with a summary and open threads. | -| Operations | `engraphis_stats` | Returns memory counts for health checks. | -| Operations | `engraphis_check_update` | Refreshes the release cache and reports whether a newer version is available. Update checks are OFF unless `ENGRAPHIS_UPDATE_CHECK` is set to an affirmative value; `=0` keeps them off. | -| Decision | `engraphis_decide` | Advisory typed decisions with local fallback. Remote Jev requires an explicit backend and per-call `allow_remote=true`; missing, malformed, and uncertain answers stay visible. Smart discovery routes it through `engraphis_execute_action` because a remote call may consume allowance. | - -Classic `engraphis_start_session` accepts optional `resume_from_session_id` for the same explicit, -same-owner, exact-workspace/repository handoff. Its `handoff_usage` reports the deterministic output -limit; it is context accounting and does not measure provider billing or task-time savings. - -Decision inputs must be nonblank for the selected kind: `guard_command` needs `state`; -`classify_contradiction` needs `state` and `existing_content`; `verify_support` needs `state` -and `query`; `verify_completion` needs `state` and `goal`, with optional `recent_actions`. -`custom` accepts either `state` or `question`. Missing required input returns `invalid_request` -before backend lookup, with unknown/null conclusions and no remote allowance consumed. -Managed access admits only the four concrete workflows above with their fixed questions and -required context. A purpose label cannot authorize arbitrary question schemas. Managed `custom` -requests return `managed_operation_unsupported` without a credential refresh or network request; -custom remote questions require explicit BYOK. Local custom fallback remains available. -Managed Jev is currently `not_yet_available` pending release acceptance and -service-capacity qualification; client configuration does not enable it. After -enablement, every legitimate Pro user and each eligible Team named seat, including paid -viewers, with an active paid, trial, or test entitlement receives managed Jev at no -additional customer charge and without a personal provider key. - -Each individual receives all three independent rolling limits: **100 evaluated questions per rolling hour, 1,000 per rolling five hours, and 2,000 per rolling 24 hours**. Usage is per -person, not pooled across a Team and not monthly. Each evaluated question counts once; -if a batch is evaluated, every question counts. A `guard_command` review evaluates two -questions, and each other supported workflow evaluates one. Admitted attempts that fail -or are interrupted remain counted. No overage is charged. The account portal reports -that member's remaining usage across all three windows; usage returns as earlier -questions leave each rolling window. - -The existing production fleet guard remains 100 questions per day across the service. It -conflicts with the per-person rolling caps and may pause or reject requests earlier, so -resolve capacity and the fleet guard before launch. No latency, accuracy, or cost-saving -guarantee is established by configuration or a successful health check. - -Managed access admits only `guard_command`, `classify_contradiction`, `verify_support`, -and `verify_completion`, with fixed question schemas and required context. Managed -`custom` questions and `query_planning` fail closed before credential refresh or network -requests; explicit BYOK and local custom heuristics remain separate choices. - -Admitted errors remain counted. Managed transport callers can preserve an explicit `request_key` -for a retry; duplicate admitted keys return 409 without a second provider call or usage increment. -No answer is stored for replay, and no automatic retry occurs. The MCP tool does not expose a -`request_key` parameter. - -The direct Classic `engraphis_decide` tool permits authorized viewers and remains advisory. -It does not grant memory writes or administration. Smart discovery still classifies the action -as stateful because it can consume allowance, so `engraphis_execute_action` retains its admin -requirement. The private hosted Team tool catalog is unchanged. Managed service release -acceptance, service capacity, and live quality evaluation remain required. -Command decisions always return `allow_auto=false` and `escalate_to_user=true`, including -successful remote answers. Provider probability and category are advice, not shell authorization. -Local command labels are coarse: only one simple inspection command, without chaining, pipes, -substitution or file redirection, is `read_only`. Recognized destructive, history-rewriting, -exfiltrating or credential-file commands are `destructive_or_leak`; anything else, including a -command too long to screen completely, is `state_change`. - -The classic recall, grounded, and answer tools (`engraphis_recall`, -`engraphis_recall_grounded`, and the `engraphis_answer` alias) accept `planning="off"|"auto"`, -optional `mtype_limits` such as `{"working": 1, "semantic": 3}`, and optional -`max_response_tokens` from `2` through `1000000`. `response_mode="full"` retains complete -memory/citation bodies; `"compact"` omits those duplicate bodies. If the serialized response -cap cannot hold the packed context, it removes that context and its exact-binding metadata -together while preserving source/citation references when they fit. `engraphis_recall_context` -is always compact and does not accept `response_mode`; it shares the same -`max_response_tokens` floor. Responses include a stable `context_revision`. Planner -details, per-query rankings, type-limit drops, and fallback reasons are returned only when -`diagnostics=true`. Type limits are post-rank maxima and can intentionally return fewer than `k`; -they do not raise a memory type's relevance. Every planned query remains inside the caller's scope, -temporal, trust, and prompt-eligibility filters, and grounded recall still measures support against -the original query. - -Compact service and REST recall candidate rows contain identities and scores, without -`exact_value`. Literal bindings appear only in `packed_sources` for admitted evidence; -`engraphis_recall_context` exposes these admitted bindings in `sources`. A candidate's presence -alone does not establish that its complete source was included in the returned context. -An exact binding requires the complete memory source, with boundary whitespace trimming -permitted only outside the bound value. This retains conditions without inferring their -meaning or authorizing an action. Partial legacy excerpts remain ordinary context without -binding metadata; coverage withholds bound groups that cannot fit. - -For parameter details and return shapes, see the tool descriptions exposed by the MCP server. The -[agent connection guide](AGENT_CONNECT.md) explains local and hosted connections, and the -[Kilo Code guide](KILO_CODE_INTEGRATION.md) shows a complete editor integration. - -## Versioned integration contract - -[The generated MCP contract](MCP_CONTRACT.json) exports the registered Smart and -Classic input schemas, descriptions and annotations with a content digest. -Regenerate it with `python scripts/export_mcp_contract.py`; CI verifies the JSON -and the generated Pi/Prime Agent schemas with `--check` and the contract tests. -Host-specific session agent names and configured scope defaults are supplied by -integration adapters. Prime Agent also retains strict unknown-field validation -and documented action aliases and enum checks. Runtime authorization is unchanged. +# MCP tool reference + +`engraphis-mcp` is the zero-configuration Smart MCP gateway. It initially exposes nine concise +tools: `engraphis_session`, `engraphis_recall_context`, `engraphis_remember`, +`engraphis_discover_actions`, `engraphis_execute_read`, `engraphis_execute_action`, +`engraphis_get_memory`, `engraphis_update_memory`, and `engraphis_conflict_review`. Agents use +the routine tools directly; for any advanced capability, they discover the best action and execute +the returned, version-bound capability ID. Discovery returns the precise schema and side-effect +class, and execution revalidates availability, scope, authorization, and arguments. + +### Smart tool inventory + +| Tool | What it does | +|---|---| +| `engraphis_session` | Starts or resumes a session, or ends it with a next-session handoff. | +| `engraphis_recall_context` | Returns one compact, bounded context packet for routine agent work. | +| `engraphis_remember` | Stores a routine durable memory with safe default provenance and deduplication. | +| `engraphis_discover_actions` | Returns exact schemas for a small set of matching advanced actions. | +| `engraphis_execute_read` | Executes only a discovered action that is read-only and idempotent. | +| `engraphis_execute_action` | Executes a discovered write, admin, or destructive-capable action. | +| `engraphis_get_memory` | Returns one governed memory record, excluding non-prompt-eligible content. | +| `engraphis_update_memory` | Edits memory metadata; content changes use the governed correction path. | +| `engraphis_conflict_review` | Lists pending, quarantined, or conflicting memories for review. | + +The Smart gateway exposes these nine tools directly; advanced capabilities remain available through +discovery and the validated executors. + +Remember responses include `stored`, the resolved workspace/repo, `review_state`, and +`prompt_eligible`. A stored external or quarantined memory can still be excluded from prompt +recall. These fields describe the persisted record, including when deduplication returns an +existing ID. Keep the returned `session_id` from session start on later remember and recall +calls so that every desktop client uses the intended workspace and repository. + +For a deliberate cross-agent handoff, pass the source ID as `session_id` when starting +`engraphis_session`, or as `resume_from_session_id` to `engraphis_start_session`. The source must +be an ended session owned by the same authenticated user in the exact resolved workspace and +repository. Missing, active, deleted, or unauthorized sources fail closed; the tool never +substitutes a recent handoff. The returned `bootstrap` is +bounded to 512 regex-counted content tokens, at most six open threads, and per-field character +limits. `handoff_source` labels the source start/end times in UTC. Sessions have no automatic +age expiry; those timestamps show age but do not guarantee freshness. + +### Smart routine schemas are reduced by design + +The two routine Smart tools deliberately accept smaller allow-lists than their Classic +namesakes; advanced controls are discoverable rather than routine: + +| Smart tool | Accepted parameters | +|---|---| +| `engraphis_remember` | `content`, `workspace`, `repo`, `session_id`, `mtype`, `importance`, `subject_key`, `claim_kind`, optional source-bound `exact_value`/`exact_value_type`; safe provenance is fixed internally | +| `engraphis_recall_context` | `query`, `workspace`, `repo`, `session_id`, `k`, `token_budget`, `packing_mode`, `retrieval_recipe`, `format`, optional Jev controls `allow_remote`, `data_classification`; always compact, no `response_mode` | + +`format="gist"` is a compatibility option for the same budgeted, cited evidence as +`full`. It preserves the same selected text and whitespace; it does not apply an +additional summary or promise extra token savings. Source IDs remain in `sources`. + +Jev-assisted recall planning requires explicit BYOK and is opt-in. On the Smart context tool, +set `allow_remote=true` and +`data_classification="public"` or `"internal"`; that call both enables bounded route selection +and gives per-call consent for remote processing. The default stays deterministic and local. +Classic recall tools additionally require `planning="auto"` and `jev_assisted=true`. +The provider receives the query and bounded local routes, not recalled memory bodies. Jev cannot +change scope, time, type, or trust filters; uncertainty and failures retain deterministic route +order and appear in `planning_advisory`. Leaving the Jev controls at their defaults keeps local +deterministic behavior. +Managed access does not admit `query_planning`: it fails closed before credential refresh or +network requests, retaining deterministic route order with a visible fallback. Neither `managed` +nor `auto` silently switches to BYOK. + + +No user profile choice or tool switching is required. The dashboard `/mcp` endpoint and +`engraphis-mcp-http` use this Smart surface by default. `engraphis-mcp-classic` (or +`engraphis-mcp-http --classic`) preserves the 39 direct tools below for integrations that pin +their historical names and response shapes. + +Hosts which already own chat history should use `POST /api/adaptive-context`, not an MCP action. +The gateway works in general MCP clients without native deferred tool search; clients that +explicitly support OpenAI's deferred `tool_search` can apply it as an optional host optimization. + +### Workspace routing + +Session starts accept an omitted workspace, resolving an explicit value, then the saved repo +mapping, then `default`. Routine remember and recall calls also inherit an omitted workspace +from an authorized supplied session before consulting the repo mapping. Local recall with no +workspace, repo, or session stays broad. +Explicit workspace/repo values that conflict with a supplied session are rejected; invalid or +unauthorized sessions never fall back. An explicit `workspace="default"` overrides a mapping. + +Session and remember responses report the resolved workspace and `workspace_source` (`explicit`, +`project`, or `default`, plus `session` on inherited writes) so the agent can show its destination. +Pass the returned `session_id` on subsequent recall and write calls to retain that scope. +There is no implicit server-global current session, and changing the dashboard workspace +selector does not change agent arguments. See [workspace organization](WORKSPACE_ORGANIZATION.md) +for project setup, hook overrides, and organizing existing memories. + +Saved project mappings live in the shared database, per authenticated caller or standalone +local context. The Classic `engraphis_list_workspaces`, `engraphis_get_workspace_routing`, and +`engraphis_set_workspace_routing` capabilities are available through Smart discovery and the +validated executors; saving a mapping still requires workspace access. Classic batch remember, +record-event, ingest, and proactive recall retain their explicit workspace contract: pass the +resolved destination returned by the session. + +### Standalone semantic startup + +On Windows, the standalone stdio and HTTP launchers import optional semantic dependencies +on the launcher thread before serving MCP requests. This prevents the first semantic tool +call from stalling during a native dependency import in a worker thread. Models are still +loaded lazily, and exact-backend validation retains its normal failure policy. Dependency +import diagnostics go to stderr so stdout stays reserved for the stdio protocol. + +`ENGRAPHIS_MCP_PRELOAD_EMBEDDER=auto` enables this startup step on Windows when a semantic +embedding or reranking model is configured. Configured sources are validated before +optional imports, including the immutable revision policy. Set `0` to disable it or `1` to +enable it on other platforms. An unavailable optional dependency is left to the configured +backend fallback policy. + +## Classic direct-tool inventory + +The following inventory applies to the Classic compatibility server. Start with +`engraphis_recall_context` when an agent needs prompt-ready context, and use +`engraphis_remember` when it learns a durable fact. + +Retrieval responses (`engraphis_recall`, `engraphis_recall_context`, +`engraphis_recall_grounded`, and `engraphis_answer`) always declare +`degraded_mode`, `semantic_support`, `embedding_mode`, and `vector_search_ready`. A `true` +degraded flag means the active backend is not a declared semantic embedder (the bundled +deterministic fallback is feature hashing with lexical overlap), the persistent vector space is +rebuilding or does not match the configured embedder, or the vector index failed for that +request. `vector_search_ready` is the authoritative vector-arm status. When +`semantic_support=false`, vector retrieval and semantic-cosine evidence are both disabled; +recall remains lexical/graph/code based and grounded answers use lexical support only. A +request-local index failure instead reports `vector_search_ready=false` while semantic support +can remain available for exact support scoring from the configured embedder and stored vectors. + +Classic `engraphis_recall` and `engraphis_recall_context` also accept `jev_assisted` (default +`false`), `allow_remote` (default `false`), and `data_classification` (optional). Use the same +explicit remote-consent requirements described for the Smart context tool above. + +Trust boundary: normal local-agent memory creation is prompt-visible immediately after validation; +it does not require owner approval. The default `agent` source covers `engraphis_remember`, +`engraphis_ingest`, and dashboard intent writes. External sources remain `pending` regardless of +a caller-supplied `trusted` label, and detector matches are `quarantined` immediately. Pending +and quarantined records are available only to explicit inspection workflows and never appear in +prompt-ready MCP recall or context, `engraphis_why`, or `engraphis_timeline`, nor can they feed +resolution, links, graph/code backfill, or derived prompt context. `include_untrusted=True` is +inspection-only and must never be copied into a model prompt. + +MCP deliberately has no approval tool. Approval is only for external evidence: it +creates a fresh, audited `approved` successor while retaining the reviewed source and its +provenance. In the local product it is available only through the CSRF-bound dashboard review +action (with `ENGRAPHIS_API_TOKEN`) or the interactive TTY command +`python -m scripts.approve_memory MEM_ID --reason "..."`; the command rejects redirected input +and requires a typed confirmation. Local operators can use `engraphis-cli review list` and the +dry-run-first `engraphis-cli review approve` for scoped batches; quarantined records are excluded. +Hosted approval is an owner/admin action of the private hosted service. Direct in-process +`MemoryEngine` use is a trusted-code boundary for code that already has local database authority, +not a transport permission. + +For the full memory trust model, automatic schema-11 classification, and operator recovery, see +the [memory write trust model](WRITE_REVIEW.md) and [recall recovery guide](RECALL_RECOVERY.md). + +### Receipt-chain failure behavior + +Receipt recording is deliberately fail-closed: if verification finds a fork, cycle, or +disconnected chain with no safe predecessor, the Store refuses to append and +`engraphis_verify_receipts` continues to report the invalid chain. The completed service +operation is not rolled back or reported as failed solely because its follow-up receipt could +not be recorded. Its JSON result contains `"receipt": null` and a content-free +`"receipt_warning":{"code":"receipt_chain_integrity_failure",...}` marker. Repair or restore +the receipt chain before treating subsequent receipt continuity as audit evidence. + +Emitted recall receipts include the normalized `packing_mode` (`legacy` or `coverage`) +alongside the retrieval recipe and effective depth. Historical receipts remain valid; +an omitted mode means it was not recorded, and is not inferred from current defaults. + +| Category | Tool | What it does | +|---|---|---| +| Write | `engraphis_remember` | Stores a fact and resolves it as a new memory, reinforcement, safe supersession, or related memory. | +| Write | `engraphis_remember_many` | Stores a fan-out batch of facts in one transaction: within-batch dedup/supersession, plus evidence-labeled edges between siblings sharing a `subject_key` or declared `evidence_source`. | +| Write | `engraphis_record_event` | Appends one raw occurrence to the event ledger; event rows are not recalled, deduplicated, reinforced, or consolidated as memories. | +| Write | `engraphis_link` | Connects two related memories. | +| Write | `engraphis_ingest` | Applies the configured extractor (`chunk`, `llm`, or `llm_structured`). With `none`, it stores one verbatim memory. | +| Write | `engraphis_ingest_postgres_schema` | Stores a PostgreSQL schema snapshot and typed graph. The DSN is never stored. | +| Write | `engraphis_consolidate` | Runs a dry-run or live consolidation sweep. A live call can write resolved facts and receipts. | +| Stateful read | `engraphis_recall_context` | Returns hard-budget context, compact sources, token usage, and optional diagnostics. Recommended for agent prompts. Compact-only: it never accepts `response_mode` and never returns full memory bodies. | +| Stateful read | `engraphis_recall` | Runs hybrid vector, lexical, and graph recall. It attempts a receipt without strengthening weak matches. | +| Stateful read | `engraphis_recall_grounded` | Returns a cited answer or abstains when the evidence is too weak. It attempts a receipt and reinforces cited memories. | +| Stateful read | `engraphis_answer` | Backward-compatible alias for `engraphis_recall_grounded`. | +| Pure read | `engraphis_recall_proactive` | Returns high-signal, queryless context and a last-session handoff. It does not reinforce or record a receipt. | +| Stateful read | `engraphis_proactive_context` | Builds task-aware cited context and attempts a receipt without reinforcement. | +| Read | `engraphis_why` | Returns the current answer and the memories it superseded. | +| Read | `engraphis_timeline` | Returns complete bi-temporal history, oldest first. | +| Code | `engraphis_index_repo` | Incrementally parses a repository into the code and memory graph. Each run attempts a receipt. | +| Code | `engraphis_search_code` | Finds symbols, callers, and linked memories. | +| Code | `engraphis_code_path` | Finds a path across definitions, calls, imports, and memories. | +| Code | `engraphis_code_impact` | Ranks changed-file impact using dependents, communities, memories, and hotspots. | +| Code | `engraphis_export_code_graph` | Exports graph JSON, Markdown, and HTML. | +| Code | `engraphis_link_symbol` | Manually links a code symbol to a memory (idempotent). | +| Audit | `engraphis_receipts` | Lists content-free hashed operation receipts. | +| Audit | `engraphis_context_savings` | Reports receipt-backed estimated context tokens saved across all visible workspaces by default, or one workspace when supplied; optional `from_ts`, `to_ts`, and `release_version` filters are supported. This is estimated prompt-context reduction, not provider billing. | +| Audit | `engraphis_verify_receipts` | Verifies the receipt chain, local tail anchor, and an optional saved head/count. | +| Audit | `engraphis_export_receipts` | Exports a shareable receipt-only audit bundle. | +| Governance | `engraphis_retire` | Retires a memory by closing its validity window. It does not delete history. | +| Governance | `engraphis_secure_erase` | Irreversibly removes one leaked memory and local indexes; reports local-backup and external-copy limitations. | +| Compatibility | `engraphis_forget` | Deprecated alias for `engraphis_retire`; preserves the legacy response shape. | +| Governance | `engraphis_pin` | Prevents future automatic decay or pruning. | +| Governance | `engraphis_correct` | Replaces memory content without losing the previous version. Changed content clears the old literal binding; `exact_value`, `exact_value_type`, and optional `exact_value_span` explicitly bind a replacement, or `clear_exact_value=true` removes it. Governed provenance remains pending unless separately approved. | +| Governance | `engraphis_promote` | Widens an explicitly approved memory's scope while preserving and linking its narrower history. | +| Session | `engraphis_start_session` | Starts a work session. Exact retries are safe; `force_new=true` creates another session. | +| Workspace | `engraphis_list_workspaces` | Lists visible workspaces for choosing a destination. | +| Workspace | `engraphis_get_workspace_routing` | Returns the current caller's saved destination for an exact `repo` name. | +| Workspace | `engraphis_set_workspace_routing` | Saves or removes a repo-to-workspace mapping; takes `workspace`, `repo`, and `enabled` (default `true`). | +| Session | `engraphis_end_session` | Closes a work session with a summary and open threads. | +| Operations | `engraphis_stats` | Returns memory counts for health checks. | +| Operations | `engraphis_check_update` | Refreshes the release cache and reports whether a newer version is available. Update checks are OFF unless `ENGRAPHIS_UPDATE_CHECK` is set to an affirmative value; `=0` keeps them off. | +| Decision | `engraphis_decide` | Advisory typed decisions with local fallback. Remote Jev requires an explicit backend and per-call `allow_remote=true`; missing, malformed, and uncertain answers stay visible. Smart discovery routes it through `engraphis_execute_action` because a remote call may consume allowance. | + +Classic `engraphis_start_session` accepts optional `resume_from_session_id` for the same explicit, +same-owner, exact-workspace/repository handoff. Its `handoff_usage` reports the deterministic output +limit; it is context accounting and does not measure provider billing or task-time savings. + +Decision inputs must be nonblank for the selected kind: `guard_command` needs `state`; +`classify_contradiction` needs `state` and `existing_content`; `verify_support` needs `state` +and `query`; `verify_completion` needs `state` and `goal`, with optional `recent_actions`. +`custom` accepts either `state` or `question`. Missing required input returns `invalid_request` +before backend lookup, with unknown/null conclusions and no remote allowance consumed. +Managed access admits only the four concrete workflows above with their fixed questions and +required context. A purpose label cannot authorize arbitrary question schemas. Managed `custom` +requests return `managed_operation_unsupported` without a credential refresh or network request; +custom remote questions require explicit BYOK. Local custom fallback remains available. +Managed Jev is currently `not_yet_available` pending release acceptance and +service-capacity qualification; client configuration does not enable it. After +enablement, every legitimate Pro user and each eligible Team named seat, including paid +viewers, with an active paid, trial, or test entitlement receives managed Jev at no +additional customer charge and without a personal provider key. + +Each individual receives all three independent rolling limits: **100 evaluated questions per rolling hour, 1,000 per rolling five hours, and 2,000 per rolling 24 hours**. Usage is per +person, not pooled across a Team and not monthly. Each evaluated question counts once; +if a batch is evaluated, every question counts. A `guard_command` review evaluates two +questions, and each other supported workflow evaluates one. Admitted attempts that fail +or are interrupted remain counted. No overage is charged. The account portal reports +that member's remaining usage across all three windows; usage returns as earlier +questions leave each rolling window. + +The existing production fleet guard remains 100 questions per day across the service. It +conflicts with the per-person rolling caps and may pause or reject requests earlier, so +resolve capacity and the fleet guard before launch. No latency, accuracy, or cost-saving +guarantee is established by configuration or a successful health check. + +Managed access admits only `guard_command`, `classify_contradiction`, `verify_support`, +and `verify_completion`, with fixed question schemas and required context. Managed +`custom` questions and `query_planning` fail closed before credential refresh or network +requests; explicit BYOK and local custom heuristics remain separate choices. + +Admitted errors remain counted. Managed transport callers can preserve an explicit `request_key` +for a retry; duplicate admitted keys return 409 without a second provider call or usage increment. +No answer is stored for replay, and no automatic retry occurs. The MCP tool does not expose a +`request_key` parameter. + +The direct Classic `engraphis_decide` tool permits authorized viewers and remains advisory. +It does not grant memory writes or administration. Smart discovery still classifies the action +as stateful because it can consume allowance, so `engraphis_execute_action` retains its admin +requirement. The private hosted Team tool catalog is unchanged. Managed service release +acceptance, service capacity, and live quality evaluation remain required. +Command decisions always return `allow_auto=false` and `escalate_to_user=true`, including +successful remote answers. Provider probability and category are advice, not shell authorization. +Local command labels are coarse: only one simple inspection command, without chaining, pipes, +substitution or file redirection, is `read_only`. Recognized destructive, history-rewriting, +exfiltrating or credential-file commands are `destructive_or_leak`; anything else, including a +command too long to screen completely, is `state_change`. + +The classic recall, grounded, and answer tools (`engraphis_recall`, +`engraphis_recall_grounded`, and the `engraphis_answer` alias) accept `planning="off"|"auto"`, +optional `mtype_limits` such as `{"working": 1, "semantic": 3}`, and optional +`max_response_tokens` from `2` through `1000000`. `response_mode="full"` retains complete +memory/citation bodies; `"compact"` omits those duplicate bodies. If the serialized response +cap cannot hold the packed context, it removes that context and its exact-binding metadata +together while preserving source/citation references when they fit. `engraphis_recall_context` +is always compact and does not accept `response_mode`; it shares the same +`max_response_tokens` floor. Responses include a stable `context_revision`. Planner +details, per-query rankings, type-limit drops, and fallback reasons are returned only when +`diagnostics=true`. Type limits are post-rank maxima and can intentionally return fewer than `k`; +they do not raise a memory type's relevance. Every planned query remains inside the caller's scope, +temporal, trust, and prompt-eligibility filters, and grounded recall still measures support against +the original query. + +Compact service and REST recall candidate rows contain identities and scores, without +`exact_value`. Literal bindings appear only in `packed_sources` for admitted evidence; +`engraphis_recall_context` exposes these admitted bindings in `sources`. A candidate's presence +alone does not establish that its complete source was included in the returned context. +An exact binding requires the complete memory source, with boundary whitespace trimming +permitted only outside the bound value. This retains conditions without inferring their +meaning or authorizing an action. Partial legacy excerpts remain ordinary context without +binding metadata; coverage withholds bound groups that cannot fit. + +For parameter details and return shapes, see the tool descriptions exposed by the MCP server. The +[agent connection guide](AGENT_CONNECT.md) explains local and hosted connections, and the +[Kilo Code guide](KILO_CODE_INTEGRATION.md) shows a complete editor integration. + +## Versioned integration contract + +[The generated MCP contract](MCP_CONTRACT.json) exports the registered Smart and +Classic input schemas, descriptions and annotations with a content digest. +Regenerate it with `python scripts/export_mcp_contract.py`; CI verifies the JSON +and the generated Pi/Prime Agent schemas with `--check` and the contract tests. +Host-specific session agent names and configured scope defaults are supplied by +integration adapters. Prime Agent also retains strict unknown-field validation +and documented action aliases and enum checks. Runtime authorization is unchanged. diff --git a/docs/OBSIDIAN_IMPORT.md b/docs/OBSIDIAN_IMPORT.md index e67427240..a0ed10d70 100644 --- a/docs/OBSIDIAN_IMPORT.md +++ b/docs/OBSIDIAN_IMPORT.md @@ -1,125 +1,126 @@ -# Import an Obsidian vault - -Obsidian is Engraphis’s rich Markdown adapter inside the universal local-document -import architecture. Use the source-neutral [document import guide](DOCUMENT_IMPORT.md) -for the shared safety, privacy, resume, history, conflict, dashboard, and supported-format -contract. This guide covers what the Obsidian adapter adds: frontmatter, aliases, tags, -wikilinks, block/heading fragments, and attachment references. - -The source Markdown remains your canonical data; Engraphis stores normal readable memory -records and derives embeddings, full-text search, and graph indexes from them. The importer -makes no network requests. - -## Quick start - -Preview an import before opening or changing the target database: - -```bash -engraphis import obsidian /path/to/vault --dry-run -``` - -After reviewing the preview, confirm an unattended import explicitly: - -```bash -engraphis import obsidian /path/to/vault --workspace acme --yes -``` - -Choose the existing target scope explicitly when it is not supplied by the -active session: - -```bash -engraphis import obsidian /path/to/vault --workspace acme --repo product \ - --session ses_01EXAMPLE --scope session --yes -``` - -`engraphis-import obsidian ...` is an equivalent installed console entrypoint. -Unattended and JSON-mode writes require `--yes`; an interactive terminal asks for -confirmation. An unattended preview must also name `--workspace`. Use `--db` to -select a database other than the configured v2 database. - -The dashboard offers the same flow through **Import local documents**: select **Obsidian vault** -as the import mode, select the workspace/repository/session target, review the preview, then -start the import. The preview is strict: it performs zero database writes. -It shows discovered Markdown files, folders, tags, aliases, links, attachments, -warnings, and the files classified as new, changed, unchanged, skipped, or -rejected. - -A new browser vault requires a nonblank source label; folder selection prefills its root folder -name. Select the saved source identity when resuming or re-importing that vault. - -The trusted dashboard wizard runs behind the existing owner browser-session and -CSRF confirmation boundary. In token deployments that session is minted from -`ENGRAPHIS_API_TOKEN`; in zero-token loopback mode the same CSRF confirmation -applies to the loopback-only runtime, so the wizard works without -configuration. The CLI importer remains available for scripted flows. - -## What is imported - -Markdown is read recursively and retains its relative vault path, original -title, readable Markdown body, YAML frontmatter metadata, aliases, tags, -headings, common date fields, and source identity. Wikilinks such as -`[[Note]]`, `[[Note|label]]`, heading/block fragments, and `![[embed]]` are -discovered for graph linking. Referenced attachments are catalogued, not copied -into a hidden second vault. Fenced and inline code are retained in the note body -but ignored when discovering links, tags, and attachments. - -The import target follows Engraphis's existing scope hierarchy; vault folders -are source metadata, not invented scopes. Imported notes use the normal memory -write path and its normal indexing behavior. - -## Privacy and filesystem safety - -The shared source-neutral safety and privacy contract is maintained in the -[document import guide](DOCUMENT_IMPORT.md). The Obsidian-specific exclusions below are in -addition to that contract. - -Only files below the selected vault are considered. The importer does not follow -symlinks, skips hidden/VCS/configuration paths (including `.obsidian`), and -rejects common sensitive filenames and note contents that look like credentials -or private keys. It never prints secret-like contents in reports. Unsupported, -malformed, unreadable, or oversized files are reported per file and do not stop -a vault import. Invalid UTF-8 is replaced explicitly and reported as a warning. - -Review the dry-run report before importing a vault that contains personal or -work-sensitive material. The importer does not upload vault data or make API -calls. - -## Re-import, history, and recovery - -Each source file has a stable identity based on the local vault identity and its -relative path, plus recorded content hashes and importer version. A later run -skips unchanged notes, imports new notes, identifies changed and renamed notes, -and reports deleted source files. Deleted files never trigger automatic hard -deletion of Engraphis memories. - -Changed notes preserve Engraphis's temporal history rather than silently -overwriting it. When an existing target has a conflict, choose an explicit -conflict option in the CLI or dashboard to replace according to temporal rules -or create a distinct memory. The default is non-destructive reporting. - -Progress is recorded per note. If an import is interrupted, run the same command -again to resume safely; completed, unchanged source records are not duplicated. -The final report includes counts and paths for imported, updated, skipped, -rejected, conflicts, and warnings. - -The deprecated `python -m scripts.seed_from_obsidian` command remains available -for old local automation. Its `--namespace NAME` option maps directly to -`--workspace NAME`, and invoking the historical write command counts as explicit -local confirmation. Its `--limit N` option processes at most N notes and leaves a -resumable partial run; rerun without `--limit` to finish link reconciliation and -the missing-source report. The primary importer accepts the same compatibility -option, but a dry run always previews the entire vault because it processes no -notes. - -Exit status is `0` for a completed import or clean preview, `2` for invalid input, -`3` for a partial/conflicted/limit-paused run, and `130` for an operator cancellation. - -## Current limitations - -The frontmatter reader intentionally supports common top-level scalar and list -forms, not every YAML feature. Obsidian plugin-specific syntax (Dataview, -Canvas, queries, templates, and executable/plugin content) is not interpreted. -Attachments are referenced but not copied or OCRed; unresolved links remain -unresolved until a matching note is present. The importer preserves Markdown, -but does not attempt to reproduce Obsidian's rendered/transclusion behavior. -Automatic rename detection is limited to unique exact-content matches. +# Import an Obsidian vault + +Obsidian is Engraphis’s rich Markdown adapter inside the universal local-document +import architecture. Use the source-neutral [document import guide](DOCUMENT_IMPORT.md) +for the shared safety, privacy, resume, history, conflict, dashboard, and supported-format +contract. This guide covers what the Obsidian adapter adds: frontmatter, aliases, tags, +wikilinks, block/heading fragments, and attachment references. + +The source Markdown remains your canonical data; Engraphis stores normal readable memory +records and derives embeddings, full-text search, and graph indexes from them. The importer +makes no network requests. + +## Quick start + +Preview an import before opening or changing the target database: + +```bash +engraphis import obsidian /path/to/vault --dry-run +``` + +After reviewing the preview, confirm an unattended import explicitly: + +```bash +engraphis import obsidian /path/to/vault --workspace acme --yes +``` + +Choose the existing target scope explicitly when it is not supplied by the +active session: + +```bash +engraphis import obsidian /path/to/vault --workspace acme --repo product \ + --session ses_01EXAMPLE --scope session --yes +``` + +`engraphis-import obsidian ...` is an equivalent installed console entrypoint. +Unattended and JSON-mode writes require `--yes`; an interactive terminal asks for +confirmation. An unattended preview must also name `--workspace`. Use `--db` to +select a database other than the configured v2 database. + +The dashboard offers the same flow through **Import local documents**: select **Obsidian vault** +as the import mode, select the workspace/repository/session target, review the preview, then +start the import. The preview is strict: it performs zero database writes. +It shows discovered Markdown files, folders, tags, aliases, links, attachments, +warnings, and the files classified as new, changed, unchanged, skipped, or +rejected. + +A new browser vault requires a nonblank source label; folder selection prefills its root folder +name. Select the saved source identity when resuming or re-importing that vault. + +The trusted dashboard wizard runs behind the existing owner browser-session and +CSRF confirmation boundary. The session is signed with `ENGRAPHIS_API_TOKEN` when configured, +or a process-local secret otherwise. The browser on the same computer establishes it +automatically; users do not enter that token. Legacy zero-token loopback clients retain their +existing CSRF confirmation, so the wizard works without configuration. The CLI importer +remains available for scripted flows. + +## What is imported + +Markdown is read recursively and retains its relative vault path, original +title, readable Markdown body, YAML frontmatter metadata, aliases, tags, +headings, common date fields, and source identity. Wikilinks such as +`[[Note]]`, `[[Note|label]]`, heading/block fragments, and `![[embed]]` are +discovered for graph linking. Referenced attachments are catalogued, not copied +into a hidden second vault. Fenced and inline code are retained in the note body +but ignored when discovering links, tags, and attachments. + +The import target follows Engraphis's existing scope hierarchy; vault folders +are source metadata, not invented scopes. Imported notes use the normal memory +write path and its normal indexing behavior. + +## Privacy and filesystem safety + +The shared source-neutral safety and privacy contract is maintained in the +[document import guide](DOCUMENT_IMPORT.md). The Obsidian-specific exclusions below are in +addition to that contract. + +Only files below the selected vault are considered. The importer does not follow +symlinks, skips hidden/VCS/configuration paths (including `.obsidian`), and +rejects common sensitive filenames and note contents that look like credentials +or private keys. It never prints secret-like contents in reports. Unsupported, +malformed, unreadable, or oversized files are reported per file and do not stop +a vault import. Invalid UTF-8 is replaced explicitly and reported as a warning. + +Review the dry-run report before importing a vault that contains personal or +work-sensitive material. The importer does not upload vault data or make API +calls. + +## Re-import, history, and recovery + +Each source file has a stable identity based on the local vault identity and its +relative path, plus recorded content hashes and importer version. A later run +skips unchanged notes, imports new notes, identifies changed and renamed notes, +and reports deleted source files. Deleted files never trigger automatic hard +deletion of Engraphis memories. + +Changed notes preserve Engraphis's temporal history rather than silently +overwriting it. When an existing target has a conflict, choose an explicit +conflict option in the CLI or dashboard to replace according to temporal rules +or create a distinct memory. The default is non-destructive reporting. + +Progress is recorded per note. If an import is interrupted, run the same command +again to resume safely; completed, unchanged source records are not duplicated. +The final report includes counts and paths for imported, updated, skipped, +rejected, conflicts, and warnings. + +The deprecated `python -m scripts.seed_from_obsidian` command remains available +for old local automation. Its `--namespace NAME` option maps directly to +`--workspace NAME`, and invoking the historical write command counts as explicit +local confirmation. Its `--limit N` option processes at most N notes and leaves a +resumable partial run; rerun without `--limit` to finish link reconciliation and +the missing-source report. The primary importer accepts the same compatibility +option, but a dry run always previews the entire vault because it processes no +notes. + +Exit status is `0` for a completed import or clean preview, `2` for invalid input, +`3` for a partial/conflicted/limit-paused run, and `130` for an operator cancellation. + +## Current limitations + +The frontmatter reader intentionally supports common top-level scalar and list +forms, not every YAML feature. Obsidian plugin-specific syntax (Dataview, +Canvas, queries, templates, and executable/plugin content) is not interpreted. +Attachments are referenced but not copied or OCRed; unresolved links remain +unresolved until a matching note is present. The importer preserves Markdown, +but does not attempt to reproduce Obsidian's rendered/transclusion behavior. +Automatic rename detection is limited to unique exact-content matches. diff --git a/docs/RELEASE_1_7_9.md b/docs/RELEASE_1_7_9.md index 0c9e82a77..7199201b4 100644 --- a/docs/RELEASE_1_7_9.md +++ b/docs/RELEASE_1_7_9.md @@ -1,4 +1,8 @@ -# Engraphis 1.7.9 release preparation +# Engraphis 1.7.9 release record + +Version 1.7.9 was published to PyPI on October 1, 2026. The preparation and +qualification checklist below is retained as release history; publication did not +enable a disabled managed service. This candidate packages the managed Jev client, workspace routing, graph fixes, local decision hardening, Anthropic compatibility, and Pi dependency repairs from @@ -12,7 +16,7 @@ The final source is bound by immutable `offline-fixtures-v128.json`; those local fixtures establish their documented deterministic boundaries. They do not qualify the hosted provider or authorize publication. -## Customer upgrade after publication +## Customer upgrade Managed Jev requires both this client and an enabled, qualified Cloud deployment. The published 1.7.8 distribution contains an experimental injected adapter but @@ -20,7 +24,7 @@ does not ship the managed transport or the registered MCP decision tool. 1. Upgrade the Python environment that actually launches MCP. For an MCP installation, run `python -m pip install --upgrade "engraphis[mcp]==1.7.9"` - after this version is published. Preserve any other extras your installation + to install that release. Preserve any other extras your installation needs. Check the installation with `python -m pip show engraphis`. 2. Keep an existing valid saved Cloud session. If the installation is not connected, use the account portal's generated `engraphis connect` command in diff --git a/docs/SYNC.md b/docs/SYNC.md index 1b0d22294..7c29df352 100644 --- a/docs/SYNC.md +++ b/docs/SYNC.md @@ -190,12 +190,20 @@ when both endpoints remain in the export. Inbound legacy or untrusted bundles ca relabel, or overwrite session-scoped state because the sync format carries no authenticated session owner or lifecycle contract. -Bundle format v3 preserves durable claim identity and the system-time at which a world-time -invalidation was learned. It also carries a per-device `generation`, `previous_hash`, +Bundle format v4 preserves durable claim identity and the complete set of world-time closure +observations with their knowledge timestamps. A later shortening never replaces an earlier +observation: historical reads use the earliest endpoint known at their `known_at` anchor. +The set is merged by union, deterministically ordered, and included in snapshot hashes. +Each memory accepts at most 1,024 distinct observations; an oversized union is rejected +without truncating the existing history. Schema 19 upgrades seed the available scalar closure; +observations already lost by an earlier version cannot be reconstructed automatically. + +The format also carries a per-device `generation`, `previous_hash`, `state_hash`, and `tombstone_checkpoint`. Engraphis pulls its own device's remote snapshot before replacement and rejects an observed generation/hash-chain rollback. Current clients accept -inbound v1 and v2 bundles for compatibility but export v3; older clients reject unknown versions -instead of silently forwarding a downgraded snapshot. +inbound v1, v2, and v3 bundles for compatibility but export v4. Older clients reject v4; +upgrade all participating devices before syncing this release. Exporting history under an older +version number would allow older peers to discard observations silently. Erasure markers remain content-free and carry an `export_class`. Export includes only `remote_erasure` markers created for non-secret workspace/repo records that were eligible for diff --git a/docs/WRITE_REVIEW.md b/docs/WRITE_REVIEW.md index ef7503653..423161a18 100644 --- a/docs/WRITE_REVIEW.md +++ b/docs/WRITE_REVIEW.md @@ -1,72 +1,80 @@ -# Memory write trust model - -## MCP, REST, imports, and sync - -Normal local-agent memory creation is immediate. The `agent` and `intent_api` service sources are -stamped `trusted` + `approved` after normal validation, so an agent can create and recall a memory -without waiting for an owner. The write is still scoped, audited, deduplicated, and subject to the -deterministic poisoning guard. - -External/imported sources (`web`, `import`, `sync`, `tool`, `api`, `mcp`, and extractor/introspector -feeds) remain `pending`; detector matches are `quarantined` immediately. Pending and quarantined -records remain inspectable and auditable, but cannot enter model-ready recall/context, resolution, -links, graph/code backfill, derived prompt context, or public `why`/`timeline` history. -Corrections, promotions, and merges fail closed when their inputs are untrusted or quarantined. - -Approval is only for releasing external evidence. It creates a fresh `approved` -successor and preserves the reviewed source plus an audit link; it never relabels the source in -place. There is deliberately no MCP tool or general REST approval endpoint. A local owner can -approve through the dashboard's **Approve for prompt** action after configuring -`ENGRAPHIS_API_TOKEN` (short-lived browser session plus CSRF confirmation), or from an interactive -terminal: - -```bash -python -m scripts.approve_memory mem_... --reason "verified against the owner runbook" -``` - -The command rejects redirected input and requires typing its displayed confirmation. Hosted -owner/admin approval is performed by the hosted service, not this local package. The direct -in-process `MemoryEngine` remains a documented trusted-code boundary for code that already has -local database authority; do not expose it to untrusted transports. - -For a local batch, use the content-free review CLI. Listing never prints memory bodies, approval -is a dry run unless `--apply` is supplied, and one typed confirmation covers the selected batch: - -```bash -engraphis-cli review list --namespace vault -engraphis-cli review approve --all --namespace vault \ - --reason "verified local import" # dry run -engraphis-cli review approve --all --namespace vault \ - --reason "verified local import" --apply # typed confirmation -``` - -Use `--source web` (repeatable), `--repo NAME`, explicit `mem_...` ids, or -`--legacy-agent-only` to narrow the batch. `--yes` is available to an already-authorized local -automation. Bulk approval always excludes quarantined, retired, future-dated, and already-approved -records. Quarantined evidence requires individual inspection and is not eligible for the approval -primitive; preserve it for audit or create a separately governed replacement. - -## Upgrade classification - -Schema 11 automatically classifies rows created before explicit review state existed: - -- a non-quarantined row carrying the old explicit `trusted: true` authority receives the - equivalent `review_state: approved` stamp; -- the exact historical local-agent downgrade signature (`agent`/`intent_api`, - `service_review_gate`, `trust_downgraded: true`) is recovered as approved; -- every ambiguous or external row remains pending. - -The migration updates both provenance copies, records per-row and summary audit entries, is -idempotent, and runs behind the normal verified pre-migration backup. It does not approve -quarantined content. - -The poisoning rescan is a separate security operation: - -```bash -python -m scripts.rescan_poisoning --db engraphis.db -python -m scripts.rescan_poisoning --db engraphis.db --apply -``` - -The dry run opens the database read-only. The applying pass demotes historical non-approved -records to pending review, quarantines detected payloads, retires their derived bridges, and -records an audit event. It never bulk-approves records; use `engraphis-cli review` for that. +# Memory write trust model + +## MCP, REST, imports, and sync + +Normal local-agent memory creation is immediate. The `agent` and `intent_api` service sources are +stamped `trusted` + `approved` after normal validation, so an agent can create and recall a memory +without waiting for an owner. The write is still scoped, audited, deduplicated, and subject to the +deterministic poisoning guard. + +External/imported sources (`web`, `import`, `sync`, `tool`, `api`, `mcp`, and extractor/introspector +feeds) remain `pending`; detector matches are `quarantined` immediately. Pending and quarantined +records remain inspectable and auditable, but cannot enter model-ready recall/context, resolution, +links, graph/code backfill, derived prompt context, or public `why`/`timeline` history. +Corrections, promotions, and merges fail closed when their inputs are untrusted or quarantined. + +Repo and workspace memories keep their original session as provenance when a governed +correction, revision, or owner approval creates a successor after that session ends. The +session stays closed. Ordinary writes and session-scoped memories still require an active +session. Optimistic memory versions include additional closure observations, so a concurrent +history update requires refreshing the source before editing; reinforcement does not change +that version. + +Approval is only for releasing external evidence. It creates a fresh `approved` +successor and preserves the reviewed source plus an audit link; it never relabels the source in +place. There is deliberately no MCP tool or general REST approval endpoint. A local owner can +approve through the local dashboard's **Approve for prompt** action. Its short-lived browser +session is established automatically on the same computer, including zero-config free installs; +CSRF confirmation is still required. Remote operator sessions remain authenticated. An owner +can also approve from an interactive terminal: + +```bash +python -m scripts.approve_memory mem_... --reason "verified against the owner runbook" +``` + +The command rejects redirected input and requires typing its displayed confirmation. Hosted +owner/admin approval is performed by the hosted service, not this local package. The direct +in-process `MemoryEngine` remains a documented trusted-code boundary for code that already has +local database authority; do not expose it to untrusted transports. + +For a local batch, use the content-free review CLI. Listing never prints memory bodies, approval +is a dry run unless `--apply` is supplied, and one typed confirmation covers the selected batch: + +```bash +engraphis-cli review list --namespace vault +engraphis-cli review approve --all --namespace vault \ + --reason "verified local import" # dry run +engraphis-cli review approve --all --namespace vault \ + --reason "verified local import" --apply # typed confirmation +``` + +Use `--source web` (repeatable), `--repo NAME`, explicit `mem_...` ids, or +`--legacy-agent-only` to narrow the batch. `--yes` is available to an already-authorized local +automation. Bulk approval always excludes quarantined, retired, future-dated, and already-approved +records. Quarantined evidence requires individual inspection and is not eligible for the approval +primitive; preserve it for audit or create a separately governed replacement. + +## Upgrade classification + +Schema 11 automatically classifies rows created before explicit review state existed: + +- a non-quarantined row carrying the old explicit `trusted: true` authority receives the + equivalent `review_state: approved` stamp; +- the exact historical local-agent downgrade signature (`agent`/`intent_api`, + `service_review_gate`, `trust_downgraded: true`) is recovered as approved; +- every ambiguous or external row remains pending. + +The migration updates both provenance copies, records per-row and summary audit entries, is +idempotent, and runs behind the normal verified pre-migration backup. It does not approve +quarantined content. + +The poisoning rescan is a separate security operation: + +```bash +python -m scripts.rescan_poisoning --db engraphis.db +python -m scripts.rescan_poisoning --db engraphis.db --apply +``` + +The dry run opens the database read-only. The applying pass demotes historical non-approved +records to pending review, quarantines detected payloads, retires their derived bridges, and +records an audit event. It never bulk-approves records; use `engraphis-cli review` for that. diff --git a/docs/benchmark-evidence/offline-fixtures-desktop-memory-20261020.json b/docs/benchmark-evidence/offline-fixtures-desktop-memory-20261020.json new file mode 100644 index 000000000..2a9de64fa --- /dev/null +++ b/docs/benchmark-evidence/offline-fixtures-desktop-memory-20261020.json @@ -0,0 +1,699 @@ +{ + "environment": { + "embedding": "deterministic", + "numpy": "2.4.6", + "platform": "win32", + "python": "3.11.17", + "vector_backend": "numpy" + }, + "generated_on": "2026-10-20", + "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": "cbd20ca375589fcc71ccc344a9389c2b5ef6ebec67160d415a0a68de97dc052e", + "digest_method": "sha256(canonical compact JSON mapping each sorted path to its file SHA-256)", + "files": { + "engraphis/__init__.py": "680932bbb361e2a39f549e5ddb2369cd8728eb413925cccef1c58c44a8a7e608", + "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": "b8b9ad265453aba17ba7c27a355e12a793469b3e44cb217943c6fad9382a3006", + "engraphis/backends/vector_numpy.py": "c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72", + "engraphis/backends/vector_sqlitevec.py": "6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b", + "engraphis/build_info.py": "624c22471e56d4c4047160808c4245488292af611564d1a63ca437605bbb414f", + "engraphis/classic_assets/__init__.py": "a7c1d52b285e3faa670ce231814b5758754aa0fbd05e1428e74c20c3ec51a4f1", + "engraphis/cloud_authz.py": "e80500579cb3a1d5fbf30814dc94e3e3967e50b311e8ed2fa56afcc13f7eb565", + "engraphis/cloud_features.py": "90e876f8993d99f01760cd01ae9fb63140e3f65036706e8fb0c8224560416330", + "engraphis/cloud_session.py": "cc6ab27c6af69b5c676e721ab06c520730e205f5bbffe27818650edfb71d4b63", + "engraphis/commercial.py": "184f312066a9e682e51a0abeff042f1c0e8eed2d47470157b23930b5a17633aa", + "engraphis/config.py": "9af2072734f0e0fb409ce3cd422d5a4cb9a4854c9a92d2e03d7e680e119a8fb9", + "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": "c50341bf96585fae60e16fb2736dd4dbee83558b408b10bb10c096890859cc4c", + "engraphis/core/engine.py": "82d92b6c738027675d9d120eceafbd81bd424a67fc026d9b3d118b62e5422496", + "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": "c1cdfc207cc4d4ae3dafab052389c6279d6d51a8b78a8082283ecb56a88cd16b", + "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": "991267c153cb7c4c40f7fe8f50aa71688892e250aaaf383b22c9e5910dc263b7", + "engraphis/core/poisoning.py": "5bc67169ee8032f3777f2d3969dcf71821437bd473ce4f917c4b41a50e845fb6", + "engraphis/core/query_planner.py": "249062d67392ab7c203cc71e9040e99bee91bf570604e90949149a93cb652120", + "engraphis/core/read_snapshots.py": "be08e63a88bd38ed91d61b28657a65201c38994856db2798b209a73151dd202a", + "engraphis/core/recall.py": "3ed4f4078fd1fdbbf54ef5a0ac3f95d1aeed8b7183f434522b287f19dd2231d0", + "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": "a4835ba06e2616156528df6365ca1aba6cba0c97cdf834a709d2797a371099d2", + "engraphis/core/store.py": "775ba9d9d17c2eaebb2c91878d8c5a9fe55f32955fa4c658500005283d93d8cc", + "engraphis/core/sync.py": "6bf716a247fc1bc43e43fe2c1683d7a45fd242f4cb2ac2b4499bc48a608ef978", + "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": "1d4faffd5a4af356eb59497457d992efa5e0a106cfe57804b59e3b38d3af80c9", + "engraphis/dashboard_assets/__init__.py": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "engraphis/device_connect.py": "c8cd0a22e9fd2d92a65bd74047cc3a8b7159bfc9a297cb639c1c7f1699f8802e", + "engraphis/document_import.py": "94fa0ca340ad0ebd060b143f46657798a81c440a11b82aa46fefa83fb65295dc", + "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/factory.py": "a60847461e8124f1efd8db833facae95f5bcbec5b73b987e9a3c6bb515322ab0", + "engraphis/file_guard.py": "1084e3643da40267afd4812dfc9438fdfdcbfd38127ae94894d7760d765acf43", + "engraphis/graphdata.py": "f5dc93395f52203c66deaeec8bc58d5c1f990a35a535151e0b9a84cecf7d8430", + "engraphis/hosted_client.py": "5b56b0d2384cf24ecb409090af15f09e437ec8ab5a98bed931384daff46b4f48", + "engraphis/http_deadline.py": "8f41de086f36ab39d4e6461c6284c776e3b123afee845824e22f071ca128c918", + "engraphis/http_security.py": "596981e96741fd47064d03db409605bbb435f062c0c6ef69b040aacdd8763a20", + "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": "8b40a6b429217996f9adf630419824a1380f9bb5c859fc55b4ba4f073cee7e0d", + "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": "8935cda97ce697dc61f1231c39e968d85ad8b94bc67ecc066165c65623b3ba70", + "engraphis/mcp_server.py": "f5eefbaa3b4fc6a5881dd5235e12858a000af5788fd72e81afb61d75de1db118", + "engraphis/mcp_stdio_bridge.py": "6691c13affdd6f5180bebe99103df94d8d7ca5346982e585f85e99c74bf238bb", + "engraphis/models.py": "6e76e97db0aca3805c6f582ea78cc6e1c0ccd2665e16fd8ab81eb91b94141b51", + "engraphis/netutil.py": "2e0f8a9095f6f31dcb5b96d323f023b9214369d59e1c488125a7d443a55972ff", + "engraphis/observability.py": "a3a6945bf33a0d8e216da56ca7efec0031b82dc64a66cff36f0b2161f5db4981", + "engraphis/obsidian_import.py": "c7cf4b5e993afce3ffb45f01637804719cb9e7cd2461ffe629c055be5975721f", + "engraphis/private_state.py": "7485570efcaee8a1dc00b64ebfdc23178517235ae17609aa78db8bdda7e45fe3", + "engraphis/read_only_api.py": "aeea03282cfdc02074535b3ec6618b5baab0d26725be434911b777db6e7da158", + "engraphis/redirector.py": "5ba964b81f09008c9369180cb49d247519000763274aabc5e046215b12fa641d", + "engraphis/routes/__init__.py": "f0d59080212cfa0d9b50877bca28e5832d1ae917899500d52c68b1c8821231af", + "engraphis/routes/memory.py": "9ca066e1762eeeaafd9790ed57d730efcaceb57742dbe0c830ad8764500ff9e5", + "engraphis/routes/v2_api.py": "8694b9b05347c6249c594fa4a5a69c2768da3998e00835ac2c7854c2d98e1a7b", + "engraphis/routes/vault.py": "1a7eee7c1a7c7aa11091042756aa2739a3eb963647020f40584a6a38da1988af", + "engraphis/service.py": "897fb3727d457a27417c1c10a9c45bd2d60956eb7a3b9101fb1d03f89f9f01ad", + "engraphis/service_context.py": "3de9289f49a977cdc206285ac42a9953a104eb1d1b1bc8ea77febd75c7cd82ab", + "engraphis/static/__init__.py": "1fff4c4e2554e7f5fcf3eace269feba09827524917193a4dd65df95bae64ad1f", + "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": "a16544353940c0a8c40cea3b9932d3399b35ea5994b809b78f5dbe4a952c467f", + "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/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-desktop-memory-20261020.json.sha256 b/docs/benchmark-evidence/offline-fixtures-desktop-memory-20261020.json.sha256 new file mode 100644 index 000000000..02be2f41d --- /dev/null +++ b/docs/benchmark-evidence/offline-fixtures-desktop-memory-20261020.json.sha256 @@ -0,0 +1 @@ +8e2e2d74a4567d3b6ac0ba6d73aaabeef4827ea5d7ca0d8b6dc61a1d5b3f55ff offline-fixtures-desktop-memory-20261020.json diff --git a/docs/benchmark-evidence/offline-fixtures-governed-history-20261018.json b/docs/benchmark-evidence/offline-fixtures-governed-history-20261018.json new file mode 100644 index 000000000..877f1c19f --- /dev/null +++ b/docs/benchmark-evidence/offline-fixtures-governed-history-20261018.json @@ -0,0 +1,697 @@ +{ + "environment": { + "embedding": "deterministic", + "numpy": "2.4.6", + "platform": "win32", + "python": "3.11.17", + "vector_backend": "numpy" + }, + "generated_on": "2026-10-19", + "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": "2e0b6b93e1de27f54d11557de0a161a24a6086f44d77b788a28aefcd15f94c27", + "digest_method": "sha256(canonical compact JSON mapping each sorted path to its file SHA-256)", + "files": { + "engraphis/__init__.py": "680932bbb361e2a39f549e5ddb2369cd8728eb413925cccef1c58c44a8a7e608", + "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": "b8b9ad265453aba17ba7c27a355e12a793469b3e44cb217943c6fad9382a3006", + "engraphis/backends/vector_numpy.py": "c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72", + "engraphis/backends/vector_sqlitevec.py": "6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b", + "engraphis/build_info.py": "624c22471e56d4c4047160808c4245488292af611564d1a63ca437605bbb414f", + "engraphis/classic_assets/__init__.py": "a7c1d52b285e3faa670ce231814b5758754aa0fbd05e1428e74c20c3ec51a4f1", + "engraphis/cloud_authz.py": "e80500579cb3a1d5fbf30814dc94e3e3967e50b311e8ed2fa56afcc13f7eb565", + "engraphis/cloud_features.py": "90e876f8993d99f01760cd01ae9fb63140e3f65036706e8fb0c8224560416330", + "engraphis/cloud_session.py": "cc6ab27c6af69b5c676e721ab06c520730e205f5bbffe27818650edfb71d4b63", + "engraphis/commercial.py": "184f312066a9e682e51a0abeff042f1c0e8eed2d47470157b23930b5a17633aa", + "engraphis/config.py": "9af2072734f0e0fb409ce3cd422d5a4cb9a4854c9a92d2e03d7e680e119a8fb9", + "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": "c50341bf96585fae60e16fb2736dd4dbee83558b408b10bb10c096890859cc4c", + "engraphis/core/engine.py": "82d92b6c738027675d9d120eceafbd81bd424a67fc026d9b3d118b62e5422496", + "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": "c1cdfc207cc4d4ae3dafab052389c6279d6d51a8b78a8082283ecb56a88cd16b", + "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": "991267c153cb7c4c40f7fe8f50aa71688892e250aaaf383b22c9e5910dc263b7", + "engraphis/core/poisoning.py": "5bc67169ee8032f3777f2d3969dcf71821437bd473ce4f917c4b41a50e845fb6", + "engraphis/core/query_planner.py": "249062d67392ab7c203cc71e9040e99bee91bf570604e90949149a93cb652120", + "engraphis/core/read_snapshots.py": "be08e63a88bd38ed91d61b28657a65201c38994856db2798b209a73151dd202a", + "engraphis/core/recall.py": "79ca8b9fa7faac92eac2e0eb5262e29534a171104d426119c41c041f64c78db3", + "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": "a4835ba06e2616156528df6365ca1aba6cba0c97cdf834a709d2797a371099d2", + "engraphis/core/store.py": "775ba9d9d17c2eaebb2c91878d8c5a9fe55f32955fa4c658500005283d93d8cc", + "engraphis/core/sync.py": "6bf716a247fc1bc43e43fe2c1683d7a45fd242f4cb2ac2b4499bc48a608ef978", + "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": "1d4faffd5a4af356eb59497457d992efa5e0a106cfe57804b59e3b38d3af80c9", + "engraphis/dashboard_assets/__init__.py": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "engraphis/device_connect.py": "c8cd0a22e9fd2d92a65bd74047cc3a8b7159bfc9a297cb639c1c7f1699f8802e", + "engraphis/document_import.py": "94fa0ca340ad0ebd060b143f46657798a81c440a11b82aa46fefa83fb65295dc", + "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/factory.py": "a60847461e8124f1efd8db833facae95f5bcbec5b73b987e9a3c6bb515322ab0", + "engraphis/file_guard.py": "1084e3643da40267afd4812dfc9438fdfdcbfd38127ae94894d7760d765acf43", + "engraphis/graphdata.py": "f5dc93395f52203c66deaeec8bc58d5c1f990a35a535151e0b9a84cecf7d8430", + "engraphis/hosted_client.py": "5b56b0d2384cf24ecb409090af15f09e437ec8ab5a98bed931384daff46b4f48", + "engraphis/http_deadline.py": "8f41de086f36ab39d4e6461c6284c776e3b123afee845824e22f071ca128c918", + "engraphis/http_security.py": "596981e96741fd47064d03db409605bbb435f062c0c6ef69b040aacdd8763a20", + "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": "8b40a6b429217996f9adf630419824a1380f9bb5c859fc55b4ba4f073cee7e0d", + "engraphis/logging_setup.py": "f7d2edc756458a852e0401453e9c71b785aba08fa7859aacbdc23fb30dc7982a", + "engraphis/managed_processing.py": "6d33cdfd10800d9552fcfae3c2b071d2b39fe10fb69d1a8e5d9ed6026882197e", + "engraphis/mcp_classic_cli.py": "778122b121a1c654b8f85810fad05d9c8d7799cf13903d7dc274938b51008da0", + "engraphis/mcp_cli.py": "ed2d997438d727180842dc5fb3f6776f5a5d97974f690ee7229264be1ff61957", + "engraphis/mcp_http_cli.py": "8935cda97ce697dc61f1231c39e968d85ad8b94bc67ecc066165c65623b3ba70", + "engraphis/mcp_server.py": "d4b8a7dc5cb63d6e86656702f9fe70878aaa9944f1941782ebea401afb5cf6ea", + "engraphis/models.py": "6e76e97db0aca3805c6f582ea78cc6e1c0ccd2665e16fd8ab81eb91b94141b51", + "engraphis/netutil.py": "2e0f8a9095f6f31dcb5b96d323f023b9214369d59e1c488125a7d443a55972ff", + "engraphis/observability.py": "a3a6945bf33a0d8e216da56ca7efec0031b82dc64a66cff36f0b2161f5db4981", + "engraphis/obsidian_import.py": "c7cf4b5e993afce3ffb45f01637804719cb9e7cd2461ffe629c055be5975721f", + "engraphis/private_state.py": "7485570efcaee8a1dc00b64ebfdc23178517235ae17609aa78db8bdda7e45fe3", + "engraphis/read_only_api.py": "aeea03282cfdc02074535b3ec6618b5baab0d26725be434911b777db6e7da158", + "engraphis/redirector.py": "5ba964b81f09008c9369180cb49d247519000763274aabc5e046215b12fa641d", + "engraphis/routes/__init__.py": "f0d59080212cfa0d9b50877bca28e5832d1ae917899500d52c68b1c8821231af", + "engraphis/routes/memory.py": "9ca066e1762eeeaafd9790ed57d730efcaceb57742dbe0c830ad8764500ff9e5", + "engraphis/routes/v2_api.py": "8694b9b05347c6249c594fa4a5a69c2768da3998e00835ac2c7854c2d98e1a7b", + "engraphis/routes/vault.py": "1a7eee7c1a7c7aa11091042756aa2739a3eb963647020f40584a6a38da1988af", + "engraphis/service.py": "b9ed980610c8ca59523067eec62dc469419dd8d2afd20f023420f3c71f135ef0", + "engraphis/service_context.py": "3de9289f49a977cdc206285ac42a9953a104eb1d1b1bc8ea77febd75c7cd82ab", + "engraphis/static/__init__.py": "1fff4c4e2554e7f5fcf3eace269feba09827524917193a4dd65df95bae64ad1f", + "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": "a16544353940c0a8c40cea3b9932d3399b35ea5994b809b78f5dbe4a952c467f", + "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/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/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-governed-history-20261018.json.sha256 b/docs/benchmark-evidence/offline-fixtures-governed-history-20261018.json.sha256 new file mode 100644 index 000000000..279255d05 --- /dev/null +++ b/docs/benchmark-evidence/offline-fixtures-governed-history-20261018.json.sha256 @@ -0,0 +1 @@ +23203af13a30a78df9f2bf849f2cefdad1c426a9f494ebb0128abd5fa6e19ce9 offline-fixtures-governed-history-20261018.json diff --git a/docs/benchmark-evidence/offline-fixtures-memory-timeout-20261020.json b/docs/benchmark-evidence/offline-fixtures-memory-timeout-20261020.json new file mode 100644 index 000000000..fbc4226ca --- /dev/null +++ b/docs/benchmark-evidence/offline-fixtures-memory-timeout-20261020.json @@ -0,0 +1,699 @@ +{ + "environment": { + "embedding": "deterministic", + "numpy": "2.4.6", + "platform": "win32", + "python": "3.11.17", + "vector_backend": "numpy" + }, + "generated_on": "2026-10-20", + "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": "2fa807f78e18cd2b4fe59da9ea6f73c4a7e517cdfd42ff2a8d8e6c7844d2e891", + "digest_method": "sha256(canonical compact JSON mapping each sorted path to its file SHA-256)", + "files": { + "engraphis/__init__.py": "680932bbb361e2a39f549e5ddb2369cd8728eb413925cccef1c58c44a8a7e608", + "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": "b8b9ad265453aba17ba7c27a355e12a793469b3e44cb217943c6fad9382a3006", + "engraphis/backends/vector_numpy.py": "c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72", + "engraphis/backends/vector_sqlitevec.py": "6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b", + "engraphis/build_info.py": "624c22471e56d4c4047160808c4245488292af611564d1a63ca437605bbb414f", + "engraphis/classic_assets/__init__.py": "a7c1d52b285e3faa670ce231814b5758754aa0fbd05e1428e74c20c3ec51a4f1", + "engraphis/cloud_authz.py": "e80500579cb3a1d5fbf30814dc94e3e3967e50b311e8ed2fa56afcc13f7eb565", + "engraphis/cloud_features.py": "90e876f8993d99f01760cd01ae9fb63140e3f65036706e8fb0c8224560416330", + "engraphis/cloud_session.py": "cc6ab27c6af69b5c676e721ab06c520730e205f5bbffe27818650edfb71d4b63", + "engraphis/commercial.py": "184f312066a9e682e51a0abeff042f1c0e8eed2d47470157b23930b5a17633aa", + "engraphis/config.py": "9af2072734f0e0fb409ce3cd422d5a4cb9a4854c9a92d2e03d7e680e119a8fb9", + "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": "c50341bf96585fae60e16fb2736dd4dbee83558b408b10bb10c096890859cc4c", + "engraphis/core/engine.py": "82d92b6c738027675d9d120eceafbd81bd424a67fc026d9b3d118b62e5422496", + "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": "c1cdfc207cc4d4ae3dafab052389c6279d6d51a8b78a8082283ecb56a88cd16b", + "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": "991267c153cb7c4c40f7fe8f50aa71688892e250aaaf383b22c9e5910dc263b7", + "engraphis/core/poisoning.py": "5bc67169ee8032f3777f2d3969dcf71821437bd473ce4f917c4b41a50e845fb6", + "engraphis/core/query_planner.py": "249062d67392ab7c203cc71e9040e99bee91bf570604e90949149a93cb652120", + "engraphis/core/read_snapshots.py": "be08e63a88bd38ed91d61b28657a65201c38994856db2798b209a73151dd202a", + "engraphis/core/recall.py": "3ed4f4078fd1fdbbf54ef5a0ac3f95d1aeed8b7183f434522b287f19dd2231d0", + "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": "a4835ba06e2616156528df6365ca1aba6cba0c97cdf834a709d2797a371099d2", + "engraphis/core/store.py": "5287e54f03243d8f6f08359dc93559c08d30b858429f509f1a1274b4a34fc353", + "engraphis/core/sync.py": "6bf716a247fc1bc43e43fe2c1683d7a45fd242f4cb2ac2b4499bc48a608ef978", + "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": "1d4faffd5a4af356eb59497457d992efa5e0a106cfe57804b59e3b38d3af80c9", + "engraphis/dashboard_assets/__init__.py": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "engraphis/device_connect.py": "c8cd0a22e9fd2d92a65bd74047cc3a8b7159bfc9a297cb639c1c7f1699f8802e", + "engraphis/document_import.py": "94fa0ca340ad0ebd060b143f46657798a81c440a11b82aa46fefa83fb65295dc", + "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/factory.py": "a60847461e8124f1efd8db833facae95f5bcbec5b73b987e9a3c6bb515322ab0", + "engraphis/file_guard.py": "1084e3643da40267afd4812dfc9438fdfdcbfd38127ae94894d7760d765acf43", + "engraphis/graphdata.py": "f5dc93395f52203c66deaeec8bc58d5c1f990a35a535151e0b9a84cecf7d8430", + "engraphis/hosted_client.py": "5b56b0d2384cf24ecb409090af15f09e437ec8ab5a98bed931384daff46b4f48", + "engraphis/http_deadline.py": "8f41de086f36ab39d4e6461c6284c776e3b123afee845824e22f071ca128c918", + "engraphis/http_security.py": "596981e96741fd47064d03db409605bbb435f062c0c6ef69b040aacdd8763a20", + "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": "8b40a6b429217996f9adf630419824a1380f9bb5c859fc55b4ba4f073cee7e0d", + "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": "8935cda97ce697dc61f1231c39e968d85ad8b94bc67ecc066165c65623b3ba70", + "engraphis/mcp_server.py": "fced425f76530aa6ecf95411c67eece122294ad265370cd452998a016f0c1505", + "engraphis/mcp_stdio_bridge.py": "a2e64883f4737415c633e9a69c1f3bdafe1860e1ee97d579dfc16814dd36c480", + "engraphis/models.py": "6e76e97db0aca3805c6f582ea78cc6e1c0ccd2665e16fd8ab81eb91b94141b51", + "engraphis/netutil.py": "2e0f8a9095f6f31dcb5b96d323f023b9214369d59e1c488125a7d443a55972ff", + "engraphis/observability.py": "a3a6945bf33a0d8e216da56ca7efec0031b82dc64a66cff36f0b2161f5db4981", + "engraphis/obsidian_import.py": "c7cf4b5e993afce3ffb45f01637804719cb9e7cd2461ffe629c055be5975721f", + "engraphis/private_state.py": "7485570efcaee8a1dc00b64ebfdc23178517235ae17609aa78db8bdda7e45fe3", + "engraphis/read_only_api.py": "aeea03282cfdc02074535b3ec6618b5baab0d26725be434911b777db6e7da158", + "engraphis/redirector.py": "5ba964b81f09008c9369180cb49d247519000763274aabc5e046215b12fa641d", + "engraphis/routes/__init__.py": "f0d59080212cfa0d9b50877bca28e5832d1ae917899500d52c68b1c8821231af", + "engraphis/routes/memory.py": "9ca066e1762eeeaafd9790ed57d730efcaceb57742dbe0c830ad8764500ff9e5", + "engraphis/routes/v2_api.py": "8694b9b05347c6249c594fa4a5a69c2768da3998e00835ac2c7854c2d98e1a7b", + "engraphis/routes/vault.py": "1a7eee7c1a7c7aa11091042756aa2739a3eb963647020f40584a6a38da1988af", + "engraphis/service.py": "91afb8e85e83f97911d3269fd3073aca5a8a4d9e8e5dd6c54122655f4a3b5d7d", + "engraphis/service_context.py": "3de9289f49a977cdc206285ac42a9953a104eb1d1b1bc8ea77febd75c7cd82ab", + "engraphis/static/__init__.py": "1fff4c4e2554e7f5fcf3eace269feba09827524917193a4dd65df95bae64ad1f", + "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": "a16544353940c0a8c40cea3b9932d3399b35ea5994b809b78f5dbe4a952c467f", + "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/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-memory-timeout-20261020.json.sha256 b/docs/benchmark-evidence/offline-fixtures-memory-timeout-20261020.json.sha256 new file mode 100644 index 000000000..f2e523543 --- /dev/null +++ b/docs/benchmark-evidence/offline-fixtures-memory-timeout-20261020.json.sha256 @@ -0,0 +1 @@ +3b1d2127e290e0ae6fb6c411263d9c61bec98d1b27e408d3da4cb2d51fa81f9f offline-fixtures-memory-timeout-20261020.json diff --git a/docs/benchmark-evidence/offline-fixtures-release-review-20261018.json b/docs/benchmark-evidence/offline-fixtures-release-review-20261018.json new file mode 100644 index 000000000..c0bcaccca --- /dev/null +++ b/docs/benchmark-evidence/offline-fixtures-release-review-20261018.json @@ -0,0 +1,697 @@ +{ + "environment": { + "embedding": "deterministic", + "numpy": "2.4.6", + "platform": "win32", + "python": "3.11.17", + "vector_backend": "numpy" + }, + "generated_on": "2026-10-18", + "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": "609a63fc89fb0436c9f1990c215fa34638addd752315113abeb8f3ecd98eb753", + "digest_method": "sha256(canonical compact JSON mapping each sorted path to its file SHA-256)", + "files": { + "engraphis/__init__.py": "680932bbb361e2a39f549e5ddb2369cd8728eb413925cccef1c58c44a8a7e608", + "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": "b8b9ad265453aba17ba7c27a355e12a793469b3e44cb217943c6fad9382a3006", + "engraphis/backends/vector_numpy.py": "c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72", + "engraphis/backends/vector_sqlitevec.py": "6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b", + "engraphis/build_info.py": "624c22471e56d4c4047160808c4245488292af611564d1a63ca437605bbb414f", + "engraphis/classic_assets/__init__.py": "a7c1d52b285e3faa670ce231814b5758754aa0fbd05e1428e74c20c3ec51a4f1", + "engraphis/cloud_authz.py": "e80500579cb3a1d5fbf30814dc94e3e3967e50b311e8ed2fa56afcc13f7eb565", + "engraphis/cloud_features.py": "90e876f8993d99f01760cd01ae9fb63140e3f65036706e8fb0c8224560416330", + "engraphis/cloud_session.py": "cc6ab27c6af69b5c676e721ab06c520730e205f5bbffe27818650edfb71d4b63", + "engraphis/commercial.py": "184f312066a9e682e51a0abeff042f1c0e8eed2d47470157b23930b5a17633aa", + "engraphis/config.py": "9af2072734f0e0fb409ce3cd422d5a4cb9a4854c9a92d2e03d7e680e119a8fb9", + "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": "c50341bf96585fae60e16fb2736dd4dbee83558b408b10bb10c096890859cc4c", + "engraphis/core/engine.py": "915be10590f0c3e688a2afe13994f89d1f2b7c15c75c8133f8ed611ee5c05aa6", + "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": "5bfb4dadd90aff6be89dad0384aabec533c04c9a5225d3e9d43a468152569d75", + "engraphis/core/graphrank.py": "1279a58396104d3f906bfd5ec75b32efedfefe52201467bf19d80be3517017a5", + "engraphis/core/grounded.py": "0d6b60ef16f3cd65b7032bdb60ba853dac364d0df58a74c932ecf12aec98e4b7", + "engraphis/core/ids.py": "e47eeadfaf560bc7638e1879fc491fa82d981cd42b9e4b05ef87033b2d7dfa60", + "engraphis/core/interfaces.py": "05d65c1919f3c23592f48e5f2820bbe857923fb65f5146727636eba51941271c", + "engraphis/core/mutations.py": "55c99b5798eb52e8f010966016c0fc50cc0105ace0bad27cab88a0232ec34b84", + "engraphis/core/obsidian.py": "991267c153cb7c4c40f7fe8f50aa71688892e250aaaf383b22c9e5910dc263b7", + "engraphis/core/poisoning.py": "5bc67169ee8032f3777f2d3969dcf71821437bd473ce4f917c4b41a50e845fb6", + "engraphis/core/query_planner.py": "249062d67392ab7c203cc71e9040e99bee91bf570604e90949149a93cb652120", + "engraphis/core/read_snapshots.py": "be08e63a88bd38ed91d61b28657a65201c38994856db2798b209a73151dd202a", + "engraphis/core/recall.py": "79ca8b9fa7faac92eac2e0eb5262e29534a171104d426119c41c041f64c78db3", + "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": "ac273d3f0383995be815bbd866f2a36ea1398f30833aed57b8d4459afc87096b", + "engraphis/core/scoring.py": "f5b6ac291edf0968b3de83cb1951a97d5bfd8a8d079199cb2ef95bba0884c89a", + "engraphis/core/secrets.py": "a4835ba06e2616156528df6365ca1aba6cba0c97cdf834a709d2797a371099d2", + "engraphis/core/store.py": "818c6728d7c0743aba93c2c5331428f5fd0ebf271f024094dde662ee5a6bc991", + "engraphis/core/sync.py": "293a297a0897ec4042559fe12f42937708896681258937ce30f6fae70b0ea466", + "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": "1d4faffd5a4af356eb59497457d992efa5e0a106cfe57804b59e3b38d3af80c9", + "engraphis/dashboard_assets/__init__.py": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "engraphis/device_connect.py": "c8cd0a22e9fd2d92a65bd74047cc3a8b7159bfc9a297cb639c1c7f1699f8802e", + "engraphis/document_import.py": "94fa0ca340ad0ebd060b143f46657798a81c440a11b82aa46fefa83fb65295dc", + "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/factory.py": "a60847461e8124f1efd8db833facae95f5bcbec5b73b987e9a3c6bb515322ab0", + "engraphis/file_guard.py": "1084e3643da40267afd4812dfc9438fdfdcbfd38127ae94894d7760d765acf43", + "engraphis/graphdata.py": "f5dc93395f52203c66deaeec8bc58d5c1f990a35a535151e0b9a84cecf7d8430", + "engraphis/hosted_client.py": "5b56b0d2384cf24ecb409090af15f09e437ec8ab5a98bed931384daff46b4f48", + "engraphis/http_deadline.py": "8f41de086f36ab39d4e6461c6284c776e3b123afee845824e22f071ca128c918", + "engraphis/http_security.py": "596981e96741fd47064d03db409605bbb435f062c0c6ef69b040aacdd8763a20", + "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": "8b40a6b429217996f9adf630419824a1380f9bb5c859fc55b4ba4f073cee7e0d", + "engraphis/logging_setup.py": "f7d2edc756458a852e0401453e9c71b785aba08fa7859aacbdc23fb30dc7982a", + "engraphis/managed_processing.py": "6d33cdfd10800d9552fcfae3c2b071d2b39fe10fb69d1a8e5d9ed6026882197e", + "engraphis/mcp_classic_cli.py": "778122b121a1c654b8f85810fad05d9c8d7799cf13903d7dc274938b51008da0", + "engraphis/mcp_cli.py": "ed2d997438d727180842dc5fb3f6776f5a5d97974f690ee7229264be1ff61957", + "engraphis/mcp_http_cli.py": "8935cda97ce697dc61f1231c39e968d85ad8b94bc67ecc066165c65623b3ba70", + "engraphis/mcp_server.py": "d4b8a7dc5cb63d6e86656702f9fe70878aaa9944f1941782ebea401afb5cf6ea", + "engraphis/models.py": "6e76e97db0aca3805c6f582ea78cc6e1c0ccd2665e16fd8ab81eb91b94141b51", + "engraphis/netutil.py": "2e0f8a9095f6f31dcb5b96d323f023b9214369d59e1c488125a7d443a55972ff", + "engraphis/observability.py": "a3a6945bf33a0d8e216da56ca7efec0031b82dc64a66cff36f0b2161f5db4981", + "engraphis/obsidian_import.py": "c7cf4b5e993afce3ffb45f01637804719cb9e7cd2461ffe629c055be5975721f", + "engraphis/private_state.py": "7485570efcaee8a1dc00b64ebfdc23178517235ae17609aa78db8bdda7e45fe3", + "engraphis/read_only_api.py": "aeea03282cfdc02074535b3ec6618b5baab0d26725be434911b777db6e7da158", + "engraphis/redirector.py": "5ba964b81f09008c9369180cb49d247519000763274aabc5e046215b12fa641d", + "engraphis/routes/__init__.py": "f0d59080212cfa0d9b50877bca28e5832d1ae917899500d52c68b1c8821231af", + "engraphis/routes/memory.py": "9ca066e1762eeeaafd9790ed57d730efcaceb57742dbe0c830ad8764500ff9e5", + "engraphis/routes/v2_api.py": "8694b9b05347c6249c594fa4a5a69c2768da3998e00835ac2c7854c2d98e1a7b", + "engraphis/routes/vault.py": "1a7eee7c1a7c7aa11091042756aa2739a3eb963647020f40584a6a38da1988af", + "engraphis/service.py": "8f58cfbe88468b664b0b4ab7a0271b005377d6f2449a3bc1f3a20ad2c56d8ab7", + "engraphis/service_context.py": "3de9289f49a977cdc206285ac42a9953a104eb1d1b1bc8ea77febd75c7cd82ab", + "engraphis/static/__init__.py": "1fff4c4e2554e7f5fcf3eace269feba09827524917193a4dd65df95bae64ad1f", + "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": "a16544353940c0a8c40cea3b9932d3399b35ea5994b809b78f5dbe4a952c467f", + "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/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/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-release-review-20261018.json.sha256 b/docs/benchmark-evidence/offline-fixtures-release-review-20261018.json.sha256 new file mode 100644 index 000000000..4616238e7 --- /dev/null +++ b/docs/benchmark-evidence/offline-fixtures-release-review-20261018.json.sha256 @@ -0,0 +1 @@ +9f187640ef016dbcce4e4aad60506cf95e0807fb91cab06ca673ee04a71bbd41 offline-fixtures-release-review-20261018.json diff --git a/docs/benchmark-evidence/offline-fixtures-temporal-history-20261018.json b/docs/benchmark-evidence/offline-fixtures-temporal-history-20261018.json new file mode 100644 index 000000000..1f2d7dbf7 --- /dev/null +++ b/docs/benchmark-evidence/offline-fixtures-temporal-history-20261018.json @@ -0,0 +1,697 @@ +{ + "environment": { + "embedding": "deterministic", + "numpy": "2.4.6", + "platform": "win32", + "python": "3.11.17", + "vector_backend": "numpy" + }, + "generated_on": "2026-10-19", + "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": "d0d9d041297103d0bb3c698a5d0eac9c458eeacd1387983345f462fadaa40e9a", + "digest_method": "sha256(canonical compact JSON mapping each sorted path to its file SHA-256)", + "files": { + "engraphis/__init__.py": "680932bbb361e2a39f549e5ddb2369cd8728eb413925cccef1c58c44a8a7e608", + "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": "b8b9ad265453aba17ba7c27a355e12a793469b3e44cb217943c6fad9382a3006", + "engraphis/backends/vector_numpy.py": "c598831bea547824cfe08844816fa79857d3617cb0631238f95dad955a424f72", + "engraphis/backends/vector_sqlitevec.py": "6148e14ceaacc19239b64c642a3fba0e98797c78cec356210157afadf475a08b", + "engraphis/build_info.py": "624c22471e56d4c4047160808c4245488292af611564d1a63ca437605bbb414f", + "engraphis/classic_assets/__init__.py": "a7c1d52b285e3faa670ce231814b5758754aa0fbd05e1428e74c20c3ec51a4f1", + "engraphis/cloud_authz.py": "e80500579cb3a1d5fbf30814dc94e3e3967e50b311e8ed2fa56afcc13f7eb565", + "engraphis/cloud_features.py": "90e876f8993d99f01760cd01ae9fb63140e3f65036706e8fb0c8224560416330", + "engraphis/cloud_session.py": "cc6ab27c6af69b5c676e721ab06c520730e205f5bbffe27818650edfb71d4b63", + "engraphis/commercial.py": "184f312066a9e682e51a0abeff042f1c0e8eed2d47470157b23930b5a17633aa", + "engraphis/config.py": "9af2072734f0e0fb409ce3cd422d5a4cb9a4854c9a92d2e03d7e680e119a8fb9", + "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": "c50341bf96585fae60e16fb2736dd4dbee83558b408b10bb10c096890859cc4c", + "engraphis/core/engine.py": "915be10590f0c3e688a2afe13994f89d1f2b7c15c75c8133f8ed611ee5c05aa6", + "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": "c1cdfc207cc4d4ae3dafab052389c6279d6d51a8b78a8082283ecb56a88cd16b", + "engraphis/core/graphrank.py": "1279a58396104d3f906bfd5ec75b32efedfefe52201467bf19d80be3517017a5", + "engraphis/core/grounded.py": "0d6b60ef16f3cd65b7032bdb60ba853dac364d0df58a74c932ecf12aec98e4b7", + "engraphis/core/ids.py": "e47eeadfaf560bc7638e1879fc491fa82d981cd42b9e4b05ef87033b2d7dfa60", + "engraphis/core/interfaces.py": "bd5f7f4751a035348cf99cae3e8be64f205cb2cb8fb6eb3bd7e56f5099d81f0f", + "engraphis/core/mutations.py": "55c99b5798eb52e8f010966016c0fc50cc0105ace0bad27cab88a0232ec34b84", + "engraphis/core/obsidian.py": "991267c153cb7c4c40f7fe8f50aa71688892e250aaaf383b22c9e5910dc263b7", + "engraphis/core/poisoning.py": "5bc67169ee8032f3777f2d3969dcf71821437bd473ce4f917c4b41a50e845fb6", + "engraphis/core/query_planner.py": "249062d67392ab7c203cc71e9040e99bee91bf570604e90949149a93cb652120", + "engraphis/core/read_snapshots.py": "be08e63a88bd38ed91d61b28657a65201c38994856db2798b209a73151dd202a", + "engraphis/core/recall.py": "79ca8b9fa7faac92eac2e0eb5262e29534a171104d426119c41c041f64c78db3", + "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": "a4835ba06e2616156528df6365ca1aba6cba0c97cdf834a709d2797a371099d2", + "engraphis/core/store.py": "775ba9d9d17c2eaebb2c91878d8c5a9fe55f32955fa4c658500005283d93d8cc", + "engraphis/core/sync.py": "6bf716a247fc1bc43e43fe2c1683d7a45fd242f4cb2ac2b4499bc48a608ef978", + "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": "1d4faffd5a4af356eb59497457d992efa5e0a106cfe57804b59e3b38d3af80c9", + "engraphis/dashboard_assets/__init__.py": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "engraphis/device_connect.py": "c8cd0a22e9fd2d92a65bd74047cc3a8b7159bfc9a297cb639c1c7f1699f8802e", + "engraphis/document_import.py": "94fa0ca340ad0ebd060b143f46657798a81c440a11b82aa46fefa83fb65295dc", + "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/factory.py": "a60847461e8124f1efd8db833facae95f5bcbec5b73b987e9a3c6bb515322ab0", + "engraphis/file_guard.py": "1084e3643da40267afd4812dfc9438fdfdcbfd38127ae94894d7760d765acf43", + "engraphis/graphdata.py": "f5dc93395f52203c66deaeec8bc58d5c1f990a35a535151e0b9a84cecf7d8430", + "engraphis/hosted_client.py": "5b56b0d2384cf24ecb409090af15f09e437ec8ab5a98bed931384daff46b4f48", + "engraphis/http_deadline.py": "8f41de086f36ab39d4e6461c6284c776e3b123afee845824e22f071ca128c918", + "engraphis/http_security.py": "596981e96741fd47064d03db409605bbb435f062c0c6ef69b040aacdd8763a20", + "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": "8b40a6b429217996f9adf630419824a1380f9bb5c859fc55b4ba4f073cee7e0d", + "engraphis/logging_setup.py": "f7d2edc756458a852e0401453e9c71b785aba08fa7859aacbdc23fb30dc7982a", + "engraphis/managed_processing.py": "6d33cdfd10800d9552fcfae3c2b071d2b39fe10fb69d1a8e5d9ed6026882197e", + "engraphis/mcp_classic_cli.py": "778122b121a1c654b8f85810fad05d9c8d7799cf13903d7dc274938b51008da0", + "engraphis/mcp_cli.py": "ed2d997438d727180842dc5fb3f6776f5a5d97974f690ee7229264be1ff61957", + "engraphis/mcp_http_cli.py": "8935cda97ce697dc61f1231c39e968d85ad8b94bc67ecc066165c65623b3ba70", + "engraphis/mcp_server.py": "d4b8a7dc5cb63d6e86656702f9fe70878aaa9944f1941782ebea401afb5cf6ea", + "engraphis/models.py": "6e76e97db0aca3805c6f582ea78cc6e1c0ccd2665e16fd8ab81eb91b94141b51", + "engraphis/netutil.py": "2e0f8a9095f6f31dcb5b96d323f023b9214369d59e1c488125a7d443a55972ff", + "engraphis/observability.py": "a3a6945bf33a0d8e216da56ca7efec0031b82dc64a66cff36f0b2161f5db4981", + "engraphis/obsidian_import.py": "c7cf4b5e993afce3ffb45f01637804719cb9e7cd2461ffe629c055be5975721f", + "engraphis/private_state.py": "7485570efcaee8a1dc00b64ebfdc23178517235ae17609aa78db8bdda7e45fe3", + "engraphis/read_only_api.py": "aeea03282cfdc02074535b3ec6618b5baab0d26725be434911b777db6e7da158", + "engraphis/redirector.py": "5ba964b81f09008c9369180cb49d247519000763274aabc5e046215b12fa641d", + "engraphis/routes/__init__.py": "f0d59080212cfa0d9b50877bca28e5832d1ae917899500d52c68b1c8821231af", + "engraphis/routes/memory.py": "9ca066e1762eeeaafd9790ed57d730efcaceb57742dbe0c830ad8764500ff9e5", + "engraphis/routes/v2_api.py": "8694b9b05347c6249c594fa4a5a69c2768da3998e00835ac2c7854c2d98e1a7b", + "engraphis/routes/vault.py": "1a7eee7c1a7c7aa11091042756aa2739a3eb963647020f40584a6a38da1988af", + "engraphis/service.py": "b9ed980610c8ca59523067eec62dc469419dd8d2afd20f023420f3c71f135ef0", + "engraphis/service_context.py": "3de9289f49a977cdc206285ac42a9953a104eb1d1b1bc8ea77febd75c7cd82ab", + "engraphis/static/__init__.py": "1fff4c4e2554e7f5fcf3eace269feba09827524917193a4dd65df95bae64ad1f", + "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": "a16544353940c0a8c40cea3b9932d3399b35ea5994b809b78f5dbe4a952c467f", + "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/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/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-temporal-history-20261018.json.sha256 b/docs/benchmark-evidence/offline-fixtures-temporal-history-20261018.json.sha256 new file mode 100644 index 000000000..91245fff5 --- /dev/null +++ b/docs/benchmark-evidence/offline-fixtures-temporal-history-20261018.json.sha256 @@ -0,0 +1 @@ +04a7c10755907eb5b8aba77b6b061882ba71dcf6455aee5c54a1fa9f524e033f offline-fixtures-temporal-history-20261018.json diff --git a/docs/images/context-efficiency.png b/docs/images/context-efficiency.png index ea9bd1752d7fc6532f8d25da4fb0bd8d8a7dce66..72929ee517ae7f23e733a5d0291d222b8667e731 100644 GIT binary patch literal 61327 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

3y##t_uwEoW*i& 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

FO)uKK#*#$WEzI3qlMa13FK| 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 literal 90849 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[NY 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 YZMGfZeyedD6kwB;RFbF=HwyT_0O)1?`2YX_ diff --git a/docs/images/context-efficiency.svg b/docs/images/context-efficiency.svg index 68ba991ec..87351f52b 100644 --- a/docs/images/context-efficiency.svg +++ b/docs/images/context-efficiency.svg @@ -1,6 +1,6 @@ Offline context, retrieval, and grounded benchmark results -Artifact-driven offline benchmark report with context efficiency, retrieval quality, and grounded behavior reported separately. Structure-aware chunking reports 740.3 to 214.3 retrieved tokens per question. The JSON-shape payload proxy reports 24,590 full versus 11,138 compact tokens; MCP transport was not measured; provider billing was not measured. Retrieved candidate quality is Recall@5 1.000, Hit@5 1.000, and answer-token recall 1.000 across 26 questions. Packed context quality is Recall@5 1.000, Hit@5 1.000, and answer-token recall 1.000 across 26 questions. Grounded checks score 5 of 5 answerable queries grounded and 6 of 6 abstention queries rejected, including 1 of 1 quarantined-evidence checks. Decision accuracy is 1.000 across 11 decisions. Source artifact SHA-256 e423e99ba683c1928984903aef715ad97440c9717cecb3bd3f6fe4f6746ea7d0. +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 3b1d2127e290e0ae6fb6c411263d9c61bec98d1b27e408d3da4cb2d51fa81f9f. @@ -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 e423e99ba683 +SOURCE SHA-256 3b1d2127e290 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 d28ad3919aa21969091e283df3ee6dceae5e658b..52db49a24393dbb954dab1bd543ad16cdc43a3d5 100644 GIT binary patch literal 272888 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

JK|546KJZg)cuj5<(?gO!jyr#fovs=~qGO6M| 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�dp8^$I9DoC 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 pq>#Od2l}}}{F{;`$ur5_{{@l<%gct?xg!g# zEAJ8)*pdf%SRIZfv`aj86X(1F<+0YN_|9w&E(Jr*cl@OROxDx-8CLX88^kvO=G+9N zuj%dIX=M=h))56{rkZjycAl2V=osyNe6`J?;TCSX3jnnvw<%HwU&X(Bf<+{gXlYKY zex=3+6z`603SyE9TOZwXLeY!2J0u~kLC$*EF>-(r=U=D82lS>m?~o}(Vk4OH07y0L z>k;g7M2hQ=9gy4vF!=lG-33uWsHX?0M|EE8KnOs_Qm<-#YWiF9WlnYtpZAmLi)ZEo z>d3miEX}8_lvk-NdqW={3x57Z{QTMKvAtS}g_hw@>=5-6caSuZ;~6kxDMd{6GXU2s zbEBbBtpBM46N%&6Njlk;XXcGzy{I`es5th&iVFU?7AEebV#ww@eNH$usBbRXYP77iXbyt6u^A&L`o! zT+VK*hcw%64-dW^r*2AZ^daIdohdb?MSplaGw;+_zlk?72UKwZJ&S@+wtNKu;Z5oP z;Pz0ITGNG%^4ELtufsTA)U$Pt{B!cpPq%N-UQX<54r%0=BiIVznZ0cd91Z;*hWFGP zOK^5{hGv0Oum7%30k%i5ZmU_oaffcGYdpyG4=AZiC1qBa3gIz1z+$11Cs{65`MFB| z*ffh;;AOIK5s!A@%>^6Tiuzpp7co>L?yWxh-CN!%K!i$n@`j7bkjvA#aB>JZbfUvAA2o*?UZ2? zGXL?`8UO)_`S&l_v9E3viYgNfpv9pQx@6`S^$uL;73~1=6w_0t;ow=Ig?*Rz@F&`T zeX}{?WQC^NRlA;Q9d*gr~}6F@=|TXz5Z2zZ4VP|~$V#K<+R4>ZSc%*)>s9C+fddl}L_)-6EtgIf95m1c2HIT1Eo>&<=+LfC~hGYrP8-auG4Ffz(W;1Na4m z>#kfZPn&&5xAx0X8(97afun`#HxSGqfEM?g_lSgZspxjI^lTooWeUmCVa2-f>^Gu zdQj91lA?O&faDOP(G|m$j$Cus_360a)3m~E(+MXyzD$Z!)8lLSMq{(PCKGph66Toi z0f5K;5v4QVTlP|qITsGkNRhE*a&4JtziS`~^fW&f`PtVozZHWnLPwxkT$h?YZh&CWWMsFw2VMIC?HUjf8(LHi2ThC0 zUPtyodw6)3R4C%m0Hlf{e8hkZ#qe}!5$aJC`FsWkz%^A;9R64* z)osgPy9ywGA^0MhA@K+;O?S#A4#_`ry3&(~0p{l!WZXS$#Ip!RTz@CFK3nkek;I}M zA}#G4Y*x?K*O2|V%~z{XiClgrIi1h21e`p7sCA`Y!+KhSHQDYU&5m{pp&|f@2LKHy zRkd@NL&YIbQy%p?LX_Kh4Y$MGioh4N<6fjx!U_Wbut0imFYB8tvE|(RjFy^bv2!!m zmq{%M_)Z0)49u}q{$KK#;o^%2@JaaO;=Gz$-E?cy?>#jqRtIlX+)SjOzwKBT+?-ma z-f>91>!;D^XW4#m9)GwyuyEw%h)GIKwYNs;R!dEFrA)uPZe=TxQNM~3Rp z$Un(F#cHkI9e}qt@%Dm+aYfCqOWV1NPBI-r2pXuEF>6adFHvLehy=YwRdLmDI4;J%+F+KyP&>MtJmQ$41xNzAR>iO~lx z8vihI#)T=2p;?fxRP*zlLl~UX4FB2VXF`ysDfaNqwuGLEQP|@5N;)}=@An-)fib^^Hz?c#O>4mqJjY#<~B0?q90@_qF-?e zbr8_rJ-2-E7S&(6=9{W{sd|jc?As>m?8QJW_s#chWO9F600KFGVvtNvjp@_oFCe2o zw%?jwFE0LZznCROTUzMP0IPdL@Hjw$I%0Qn(dPaa4P5kp$Z@9sR2r4-b_r8|WgmA5 zxl%Y2g28N+g`KzNnOpLIiI}F0s?4!*RU%OtB(}ZZ zuy6NDu?12b4yP4#aHhW$TuycU$nQjU(jO)1W4A4_xxz3$(ZEyY6Xq%&l0S#Yb)Sq@ zlOT$y%l{!u!4%s()|lB_WZP_5c`(0H5!2F)Rl-(sX_I!EOis=dBqPT;>8n*fU!862 zP=61Nt1+2ow)*2P1uS3AEHkWT4^K{)KZRSVj4`F^T?n^!Bwo?hNKTdQt~ZStD|fLO z@}9+Wx%qFHe-rP}-lrpnIe!O)IZrCgT<=7=%)7Y4H@RLjrkkj|whv1R<(U|kEVKfk~<*Hqce0Nr2@@^P%*mAeAjh(Ra8g5cW@q z@#C?g6Bh(;%t0flDB1|QZXaXcU?QNIyd5^mpvl*$7pK z#_zdJk+{pUq|o}5O?h{-73h^IC5W@Co>#*wU6CeO{3i#w)D@8qZp8M;B3b%jQ#;_4 zVRfO-3`PCP{zfMc6o!k~xYjH92LEZcYh0>pHNDWh$)Zq7UE_R#Vw$Gd4W6d>_0^!> zrr7~Ga5nSA2ocgWKU~z$Nd%oqwY`g9g}Djq4kbtiK@OYWPx?+GdK9n0-#eI&)f+mP z^VIP=!Pnx#O4N7^9{2bq3*D%R%XGXZ-w-Lc@_~(N=W@7yDhSc<=I48ssfdH|!YNSJ zS2cA$!uXMiPowS>VSrpp;1~VPhs|C1bLr;Q!}hAIMhGB$Ed+HY$MlWovCUrcEzcmY zy2Cm`6pVed&+R2Xb)Eh%7LYMF5*L=_=J5BPS>qL&s^}sSvr)cqO8uocpjNjGhK*GG z@iNtv%5<#1jgx#~R?AJFR`Vn~#+m?MrmY4`PH*}O{KkF$X|2rC;TJ@Os>A7Wa9=_t{S zB3xolr~QgMEq?O&;ehM7xdZDk_>W-HM}Bpe_oykGKX;uRuG_;H9zUM37a`<7S{QYS z4-92W2Gd5Dvwh8T!p4XN^nA86H5C-6oi1!nre%g+-~iF|QBee!{u5eTZ%^?BM@vF~ zA>a|zCEROp@cfj&5G4ZC_7^HaT`+dh_UWkIuD4%-mc`q&U2m>Oc7EL2NW&AzEYN0a z;V;?_${l=TdM}7u5iQ87nRN20$GbX0LtGX!iv$=%;VYG3bokaBPLd>WwNLW9oMWS_O(i2gZFpm&sJt!~qVU0_NX>aXMM4{W33Q(} zf*WDq&{<&4YD|ynKE$2sh@$xajg$s~5_r5Eb*XAJIBX{1hg*Qp7S^kSgcQ~?2gKq~ zJ|1`@JS`w8=q0$!0n|Q)Wr^Rx5GA?G&ljMb39PJ<{(V9ExGygiw39ONX<8Dajp=nP zyOp3^w2e1ih(h!>5*d`JjW8;k$Z6%Zb~0r?;CF9}Wj}6So%(JA3D3hOd=t6JO2#Y=417-yl7YookBkzr-ri)&vg!$o?b48tFCVK`*zY$)0#gAsH?t0*-d+i-3L{E<(BL zOG^O8&-wV@m#XqDqU5pWm`|T(edC9B*lrg&KMm{+Y(M(ewN;naNjDq2!~LC^d`t7j z2nIv=pE1->UZ?c`n6J@?Wmyob1Qh`m zk?KkltAr1|-01Vw1EfvJ44OjCn)u<(|4B)o-R{*^pG*h)2Erp-B!D{%d?*x6Q$Xg- z|HLTK@Na@PAD0+h6ju08yV4-6z`r(_3&^@QL^rSCY@JAt^o$e2ax^GP7mO(W{zL|U z;P3PW7s|$SAJG5C8M}35Y=mKN&LJX^?Q3W-7>_d`(E>T zoq1*!)^3e62XO16GET{=s)&9?HY?83LHjmO^5uQ{Bmzg*fRX;kd909>Udm>{s zvk7+brkaV7qJ+s^(fFG$iIZB&ZSG=s)pI?Q@Yx>MxzFF<0m=whfsn_T0#_wzz!+C< zPOZ*DZ4!HosqNM^I@R@)%`^2K-%xytXmF$IkT0&C{xIq{ZNxC(p`RCz1ZImuS62QY zfveo`BR(z5h3<1jXn!?&1|K!y;$Gfru*Cj?OKh?1Qa5W+WEON;jd5cF?IaSdA`*Da zWwvyR1LX=Kq86E4RE*g<;pG%0oIl3<1HkRNgbZt-r=TEKb-8T$N_3@d?>ME=I z+@X+ujygy2}7o2 zH7-+Kjq%baLtv;fNqt`gII62%nu7rrhKQEhlfPUg3>S+7nlwuAhqY>q((==Q3lweV zOYVZpL}+u!rqcXd)}(x(7Oe=B|E?@EpPS3{c@iVDQ9$m=mq$-Ss|q&L|APkJ{(XT znAHD!QPyN?=kh4Q@2fax2Hx$X`g)REsWDUjsn)18cs_I^H;Db$;d3&=MOVMz-Se7c zl>UPT;g2nTT}vv)p#=k9G784WUV>}<6<)UrNeWA7>R`6XPQQaEE4MeLWtkJFP5^3u z=gU_ldvin&LG$5>G0n@b$fZ_4Ci-q(XpMj1qeGy3b4V#yJUI36YoC>;XqroDvcYA# zTyB0X7hr*O9HW%PbpFAV`BEIWk_?e*^*eKida^N^wp7>1AF$F2`E-uS>5gRxAZ04t ze;ug5&7;x50ZKP1D`Hr>nLnEGs>~C~@270?Bu0vO*FUM*fQO>~;kZo5iLSf}@f|m5 z9SB+%i=(#Pdwa3yaj663qzzOXJTQ0qEjbmMPIH<6?<0NA9(Y zq)%f%-u5N2Lg}1zjSf#t2P>vUYj9L$4VB!OnNr|ZOL+pmA2{Bk#(k+m{>iPx%w(<} z10VcpiecL1@b0~nE=f6NwziPm0MDOL>pCpkn)<&k9||o{0Y=2hS0NX}o)wzraBFb3 zaz-P+g#bWi@_GDr+pl8mUz71mk^#y(WFW2aoMY*wV7fzJV4&oIi_id%?ElaJHwWXU!LTwkIecH>h54_?A02jL>Ej<#J_jo@$?0!h zXd1in$Xm6My&HXWSO8n|%l>4oe}HD<)dztwBED>x>{{5I-7$#^wztwN=*IiUdx4^? zsHa^gU%Upt^QXW;YyIKDGvI2GmROuO!@xIh-Fol+Hs~|e(ZW}w!{OH6incr>7CYlb zpUtp3ImTkw3HxV{rUjNmh3PVvQg?C%aJI!yHiIhBE+T&v(ch)cB9pxgXF=L-H%*;I7MR2k6YL8cDs!=TVi^L&CMGVGHRYJ z0iP1<)Hw-?06$k+rnG-IOJve!is~S#u{$0x9M%$)Fgsk5VSAI~J%kQui954zsUHQ7 zA(&gk!Ym?*unvvr8}(a_DIH-v3U8sVaRps_?s9JlFVlR)PrHD*7maOReL4AZDhO8u zTtY1X<^iFMy%NMnaUqogtO`t2?Q}@Q$FPx*Xmc>PlddwR=}-l)2bNLM`PsbL(9@#i z(aDBxfZB3NoIBG^66q!c6TFkg%p7J2vEO8BkD zbslL3i3;{a`-_T^lb!r5sNW}|OW?KX8On8f6SOCAg2h29 zD^}JU(;+E>tmJ^l0Q&6yz1#R~i*?*USD+5zkrfgSRz=(fumn7j2C=RM7p&%n_t!&Y zDWfUBjirz|(u(Jw!V;MRJf>;SIh^3p1}I|lqb>%yx*OVVSOeKVc{;`jHS>$7Ku=MlPe_I z{*Ktqg`WIxbtw7ap`Ss!TbTot+*febU0z{c)8~(Z+O!rnGq7fCYa>62^$;vn94W|3 z6=)=k1EK;C*=-N1*zJn31@<);YBqjiDq-iu1}OiPdz82;t|Y6_cB>mUSS0CdyUkz| z4cM1V#CxJL3jTMGx2!%lttNGSbyeS8V18t(YtKt6A*Xcmz=@4%trl6P60;|&t2JY3 z=`0M2Z~gm|_#Tn6__e#;hBug{xlo&bnykG0-13oc}@Ut4M8;GAwsY<8=B z@Kseccq{a{k`q}33<=NNENOYrE2odE46tnI{-jCWea_D(|6E!-r7cAj zu1H`$Y)%KfJ~=A8$Z7lI?4OvvOzaqMp4M!)(z7umHPdm32w1n?jG}uY)uxFD z(T`RgS3PTWC88LxLRR4l$q-^ypv^0I+=e1$B4&FeXSES#o2bMp@7coF2+MZu^MA6l z`9DWfKhD{G%t!L*5!%M^u_(Z3>i_#^5Tvpy|3EZ!1qP>?7=N)qn6`an2pq{|z;5&H z`k)-~^piL6{vx`;sBJZ$=xsYcK%xNuE6P2VE9l9MKsO3SvyFm)&u9=Lh-VXCg-ytO z5jP;OdU9D@KF6c6n!PF(#^4Qmbe9ukF#LU=8}+uQsmFqZxXV+OFyA^SPG@MwQ8=Xs`ucvLKv9zi5%Cs~Z9RIO06A$O zz@<3?TX$_(Nde&K`Dy^&uCPgEkl^<#xFMYSR6aOlu)Xq0o#7`LzBt~R#>s)L*)nct z{`nx}ABro5AFCAMw=og|D1RTmOXI(2Yflu$l;}^xk3;;wX>jXgq!+uida+yzy1F-4 zR45;rfIq9~u=X*fuU+ZWil5*1L`Z^pga{_SxZN%;|i@SgzqP9S!SSVs*f`Oh)U z1Wq`+(90;-m5s-!WT9=e^po!TpWo`)9x!tS7fq)A|J`M=W5OSa{P1R%6L;HPexZ%| zCwU=xn=S|*xdUCg0Sb&>IVp&S0fgm({rXVgY2S$d5lQLfV~0n2YKm_mXm@4g)>WUD z=DbQ;VdckvUgxI)jYFZl+eZ)7pAl!2CXbE^+|jOs^5hs(^veBO$q-}}PXhFjmpmv+ z5Vn`sbEE{*1g3;u?$`7F=(ue|FxKyLO6_Az3C5|h)s?(jvZb=yOCZ-xi2 z$(DPoZ2nYPG6u=7`}6tm8S|m~#ovyox_yS=7SHz01|;QQ$Ei7B(5hZgp?^pMspF8p z$emE|)!q5h$vH99H%PPIeKQB#cHf6naq2MEq>}=2B6YbA4z%B!Q`F2L6%+}5xQqXt zeto=d{k+!D=s44}fW+zeUs}WIJiFPRYTNZxfz#pzfNJIC+d9{&D^Y*3pv92}^nwp1OPOjb?ynto!gAj31+{m(r0K5T}hzlI}Y=D3t;Z4YboVND^0W-SggV+P8vD z^38#W%l$5kt00fbVYVgaFQfX|%I3QvTw=^!w5qpmioNM>+jgv819zL-B(~?T+z^XD zcUXFV!67F#Ds8=31=()MB>9XxSHz(Sl${UcN-ize3s^TABN*@Og0<) zkIC8-fnM1=_IIkQyQw#cFN?y}8}mQ_BWyP-P`CB&bwTUZ?snKnE8nHcTU1=NiS@Eo z4L-WVFCFc}isnV?ocA#6aYvE0T{%boy-nWME@KjZ#_zA9?5E0piRuVx9yUAk!#^A8 zntp8BVRwzigK9Ox3TtXzrhtC`aQ^6A*VAsN*9#^ESj5&ZzcTXhI?dKOiCmqf;O&TH z9z~%X)lbF)*nTtR5-_5_H;WQ7@eika8`H~IXL&-QFTD)=p z!;$gOcHy|>8M5d|Y8sUQc{mPX?L}$c6loCRFqjF*a$?U>XpF@Al|D+n&+!G^IDLav z)4JsFk9Wprv`rm|moJ(iS=;OjDqM{=yM$nhY?*lmnog)G(koLD_7jGRSR8FBcl%Bl zeir*VnP>M~M7RabHtUDPa9V@&15SBH*f1Ze$~%sCM`tNE@Gg-#3l-$c;YN!E{e_<; zE}obrSS}`EAERB4HS+uJ+>rqd>*5Y{w=rx_AhKe@%;#}yl6Q9-bejyPa-F)pIO$qB z&szcky<+!!DLLRm_!f0Q84kxt&~``w&X(sT-I37cYKhYW?C;{u`Jt7ZZ53jU$Gqyi z759D*6O`a$5=KYrM6o^Q+~b0V$E;o(d(+~$3?MU4OB+ADjoD+?ZrZA;&qy*w{Lda;zjdu98DJ8-|S~A;*>D=CC;rJ7IP*+kRK~_woDRwOzPA z*XR9yybR6o(qi&>C;EH3Ce$!WbiTMZp&WBZDgfRE zP)Q$Q=sJNz987V88cBtCO@e$e)2_$(o>GDvnrHCtX6y?+Fx& zVEv}{GN6Qe-+66l+^j9BjJ7}0-PMe=Kk{xpFA8!jsQJlvKGocwV+M0u9gBSHW_7`z zSu#qH^w{T63#|~_dUJ8Vby~06^%r7M3G&G4bKX!SLs>e+R$VW%)6H^2(kg{?bVlwo z&D^N9q)IoF#{+ZSF1HDvlO;RW_=kcKrOG;fDY9V_e2t!W0>4Rt2|$rDVer$!jS8!f z=^4yf_)zud&nqva2&zYStyd{a0UPBJy-F#^RMXJS4(5SX%fAUOA^EHjs<{0^*bxRj zCF_*OkQt}YGE3YJx2c1VmkqWuG)5~gWyQyLC+8VLma!7Y1SGGq(i7ukbVB_D)C!jd zJPFUTZ2rFLvJoh^t{@6A#PR>4YU6Q<$V2vh}=k>z}uFPmM2J#8DM2@&At}G zUj7Sz&A?snq6P$zWS(F5;SftwpCmeIcosR}3e-)jv(9*WozL;W`gqAl!!WTXq zxdRofe#;Z68CH`X98G{Zr1j)Qi<<4{c*o9MidzNGVI_&w$l}Zb3unF`u-}O}3=mX+ znZ{(_gH*rwD69o@D_WP$IVz@-f)BtOoQDxYd!*5Lmj=mvX+0K>S5C*PHxBr4_O%_m*llb4=p1u9z^o_cfya+&O!BOLQ!YJoHZAXawHTiOt=I4>cDJgXVa zZDiy`{~Sk`$NP0tT)7xOmyx?*$;83v6sWK8Cn0c-9XVaPe|dO3xP6-yOn%Vqx^#$% zUQ>-@86vkW@cJFf*Opq~X2NxaImrEtms$#%VXJ|oaPSRBEcY^ETs#G&TzfGK8WR7| zAyoz2dw_*7`|sCrv*bOrMAD0(zaU{D%tL>jgBXiP@_@RHE}eHuA+1)14W#U=|9Mru zzmaEnsWnfQBqTv*K4z7`$RtwSZzEK(xi{m&wJkJ6aKAM3_845J0%Dvr*H5tVt z(nmF-TGmb)J{Aq{%u9jXGbf=t^MAxqs)BnE)7s;*B{go;(vwqV4njBJ8yLM>--c`( zj_nMojP)mQFaIU#9{beKUz-m?3=?Oxxgy&=p`methE$iD51%E<8o74QR{U(R`6;KOj47m+5&2k9q?A==Q^)?4+Q7&1_0?Q{Z1=WRX96h4X+p6NjbG=?gsar0>V~O=2Sly0XGaMP5ZR(kM5yVPL~SYm}gw& z?j))Hh_`%OffaUJ{ZMgv3WPr(cf7MeH~xPuU^?k?!W;Z)r}u70Z%~8N`^m=BZ?G}& zuC?C<(E^(AF|M~UI$t%Y2Y?|cXd zU>*D~sCxgOpkK1&OLWXKs8$$luY10&@YA zQ!n)N)3wMnCG@U9cb|zbS)7er`)3|%FQeY$7?@iJ@Y8f zQVEuD?uq@FXyBO(S++d<>BVjTf9Ia{%keFb(wo+Y?0bWIW~O_aqGxhd)}fJ`W-c@$ zAB+VTH;)bL6-&B_-;SUw5l|fW(7wj;rHQMJYim#Y^)V0Se-$sxb*V~b^+(`T##fSe zuL}LE;cO|WskTGU8?v^lP}bWj63;nN&W^c)lPX-%%sj4(hmqkP)HH;!K@&HA3My8f zc8VOVwMbCM@B0QBy#iZbPZuo~(${?>1v)ZOm*s_CJxw%UsyXI2yi~h?#&M-Ht=P&; z&N&??&9Yito`uQQ%{SJXq*16Q8FJ^3H?7Z!sgeJDu_dB?T9sqddD+OhYg=aj0&M@7a$EcE^;`D%xpcbBZ2Cji zw>ymi_EQfFFPqt zzajG9i9GGHCh;V0~xhI#iQ*ZPbWTvbB7 zN{&vtb{84E5aO}MB_9{4-mqtw-@11uO}TAgGPB`aO-)nF#oZ?mA?7nXpJm$UUo3;% z-nI%npcPv_J=}UdT^Gzwl=oz^zmJ{@9Qy_l5DpwzL#@|cMQKKy&a!+w_PnM8*w*6z zQ8c;NZmB+cipSH(uo2&n^cRYI4>yUvn%leao~7wMBqvKHoeI$eq7lh#;iH-h&+gyK zb1g$DY4}?Oto(GkTu86anB6|^>fguCWNIiWY5nbD@_}-T>)=g4Dh*-YqkD;sR7Icy(R@sjmMo&VuCLu0^x#ROG}*m z#ImlLTO^mYcc3&Plg`<+Se_%B+{L1qs;ZADchSpys|pkKq223@T1L7a&>>U(zh5q7 zwfDBUT=(FU#llN==d5uIUFcFBv*56w7u9&S!UpbTV~JKiYLO8}>5%7`fQ1f;luRld zuUiVEug@#k*F7|LIva75zVp)1HrEN4lt%vdey+l;JCN26mGLfgmGr%46k;LF>JQRv zm0Io$q_r&X>0q^Qr?}8<{S5rkbe}Adx(z1jt-IA#wFuXGg8VlvVn)y7#-$b1kdtfFthCZC(SX+XnW`mpImaG0asdV0$!pT? zF+;dvgbBChvD$&|UEX}ETB-J_ih`wLIQsD)YouZ56nkNdIhV}6E{&cV2-UA2ESKTw z`#@1eK10>%)Ni2MBt*MqK!%uaG=1VnXIf2vAz4x6Z0Hs#T28;6`gViwPvV>mtY;!C ziq10f+Zt(;Zpj|4Hu}2!=A^T!x2tIWzLd{;D}TP#N)|x-;EQs<4z=`??L^Z6{+ z26RB%g#R~aRxiAR@m#w{VfvOm?Lq%1B@9Nu{UjGMJ~DvuBlluYFI{mznZlUjshor` z#zrGjQ#zwC(%W2=yU7AW*^_uaehxQ5}*A!`=xkgQ4pQ7;{2^NEV+_*N52PKeJv$z-LRI|W{wifbSbCpE@Kj|sqg zU#j;Pb~i9-a@-*>RI*;^om*>l;`X^FPb!&;pj!jA@a;tt{HIx01JK0(y0YBwuxYj%jrh`x^K?&z>${{?5}Ru5gUX ziM*PcY#lQA77@{xKUC^yhGeR4!$iN55`=A-$}6uz4Qqs4IW*_+FwdYoO0b|Z_wJo- zzc&|*#dOMnS-a0I1pNzF)cpGMp3Bo^Lc6vO}#pki>5Y z=I;1I$vfc+oDe)}sQbkY&>K*!SWvxpzpuVo&_RhZYg@NOyzC^4F0oCLQt;5VN2np2 zG08{iYhsH)v(@kjs_UaZXB;jpJt>>5d%kV8Hy>WE=d=T$@|gzzc^xg8KCCkZXIgAp zWepvq;aB=6HZ+!o@ms7?89hdUXR=JAIUivK3Fk=S;40-LDEjN`d@uVt=^nM3+P2)s z4dK2L49KloBrNoni;j(Ab$j&^lcZM?z;|slQJX8{%T#Zh-}Vffy&t;iFWZ%nx!&pT zIbgDe`QJd^Eei~KY;yi4po{~WA5sR$UjOP5A1}Q-27B#P>QvLfU*nmEpDH_8!4?f9 zaBpv547-H3U68N_s(r#PU-tU(G+MKcy$jmWx=QIUUmn$UNx9m{>9Yx^1sa40BuORh z7xGfp7_R=brPoRzv{XD=V*V`1@b%3Q#KP-kHGQ(xCbKz&;GRdf)5raHyS0m@&(n1)wv{DFMHq}1So z_x=2An?P$so-#mjey3@6=Ne!hX&rMpWLQm*y#FZN37EL&O?b27ncmRg&9emx+HoPn zRkOvixBlH%`P^ZTc6Gx^;fS9uPms0FptmE~KbN0rwG%~OYviUKXv2azQWr+^E1G4@tP-|AyUT z>?yr@v5#;xab##>`?sdHQhatV{-ngj$W>nc)1&sc-6zslu1s3PENAxr^G*Q-H1;wY z-tNGXi8dS6`y7R<-xWR}WRLSX^6aw%W{YkS0W3|-Nb|T;V4u9tMu3+QR~rgtVYKlf zthq*2o-*x-b=a(dFdI1GMK>ZP^gC^IEVAY5Py#$jT^oJ#aHVQSyX)GwH6A8i!vIOR zyKa+gdS5avulEBMF*a<91_1x~RBhIukvHSQO9kVDo2kVvc0xfr>>0f$Q`a3A<58=* zhgjqgs?zh;^TCF!?XGZwuF+I;N{NT*+6#3u%o8JXYC=vIG$Ky7xs`-Qist$?>iMhV z;Z*;ncTM)$VVkjn#j(nspcFK!!}cd3<4@Ck8> zwd4sEU01>2@RhjcUWxg3wPQov{^HR$S^JK|sJ8=G+(v-2-MdZfnoJCz8-^HwwU3Ps z4Lh%uThWjcA8;uefasR?GU*&$I?U~vU~r*P12DmECY(Y7&0I9_CQF!BY-Z9})cth5 zk?fo>C%yTNUm2Q>lMfpaGt+o$tvEMiZuFxfI6aJV?)>oYOtpQH=I6X=7mJJknpSl~ zE|R9Sp1MWzQc2!3(0su7JEIK9tm|9qn@#w4bi2y!ZEVJSqCU28l?nn0EuZk?vh{9< zwhm^K#9zXvNXiUL&ruW(!egi$w@z-%+Yv(pgdgI(zzvU@+)cx*iB`AhVGm70J# zm}hRTvM!5*m7ISXP)n^tz$J703@@z?`58fYt7&m^5Bs*rO^Bh{XlZ^hn%qFNcq&v$ zZ~Gk3RR1#EyKjb?svVcOi<)_t8CRNL-cG|cp4uNk{oqjUnsad2o#%&Q8Ll*;$lj#l z)SxF;!)0uz>U6XEV~d^3zGMh~5AN&(&OmM3c0;$yd$YeoZhj~W>@gP{JBwojD$#Bw z7C41Q-CL;ZQahFICY$n_;@Xs&nrk+yVe@9(wYMv%*J-`GS)YD*tA0V@g(j8+6h1)m z^N#9b)%E>J7sBv8*UZ=*k0u%?s3c*@?IRUJRj?ga~D z@uin^UM)9TwK?JQEZ>TRBF!X8oZ)CSjIddSm@No!tiEsEB6Z?oj28c~alu?aO4 zUxAYzRQj9DZ3lDy|Fr-oLuXQ91kPaeo+O#NvH2YfYU_ps($*8U- z4auuEykmLS8Z(@y7hbGuwEqV=_qj)C4y2(PC(AZ&tqVx#iQrV#oMw3vIB{;b`(u}e z;^lRcRlj%dxPNMrS8t}bXaD@qyz579j}7aq7YW6~{&&g7vO9)kU|He=tqiZWleIU&DP+^s zJM;kCoZ9TUo-8}N`xV|oy>^XyGNrs7IJHXDG^#i1Ul*KlpSSagMxC^390WfQT66<3nYyk1Ka9F{jwGYndAI?voa#*f>0 zV|YC=z~&hASoWof=)2ITtc1>4TW+V5L>O#$`k!g$E+%@FT`n%Bc%`dxf>NjIK+d(o zBb(4wY2jZUsVJNbow~W%F0@H){mOLa$ulis`}qacgont=Wf z7&-@*krcpUICj6S`QkgYwmLs>Ej-n$>0HG5a_g)>(eiCw&7XCe^6b^&lW01JfK9~x z+svuyFL-a2$R4X?4`r=l7A&T>j;*SOl8NgZ1m#xPK!rr=)RNS^w7=2gzFWn;g-$Fw zYmKHoor?@BsSaHj%$v>yCq>*Op%B2nF@1D>(1OGl0r%_B-pNedXF|z5)ZKxvs$TvQ zMqSmxM0Liw$wKC;Z23EO(53#}Om;)dQJBm$mbAuBNd;u3Z(K}Ib#BcW--KrMk(w1f zoE2HGtg#r*E$g%L4h_dc1g3NpbvtW|oDyp>sq0!b6>V_%1$#2jj68cjt?C*7_2_nt zhTdtShTeJN0fje4!80i=1%A*?^VlBCrj@`GnFHTnv)y`YyRJGt&S*xyyjGi_kliZzK7Ee}*`p zP%l`o4r%?YYLG}Mqqr8X>^yeM-8%Yna#(&_+Og5B?98zY-q-939ORkj(Dd-DrfG_Y zV6PD;#R{gbe=&TQd|nG)g()4aFtl{)TIfm8nOi$8MZY33fbQ+T-LTF^43 z(owoUGwwkc&+5(Yt7(cQV!j9+bQW2L|>k%CwdJ!*70k!0pG`?&!GTjMMg z!uA&Lkl(`mV`ggv?{l1NQ19_l+oyKkB6L+fuPmGoNIn~|UpG%bP7R7u!%4+jBN+dIv&z~|Z*Y4dD#v1UFynJ#gy~VT>Xu(*F=NPMywk)fRGt8%8 zw|jD=U_R=nkAw|Ih7RvO$aJQOMRDf2^7-jXQcSoW>!aUlc#Gy)+)Cpb2M>szG5n|H z6r)Kmfl}Y`Z!)`o!xkxC2}#2kpIXD~qINh&el8%Ky|{KYHOac;Ju1M(+F|)AZ8!?h z5OwT^;z2(sm&29xZ1BAS7c7{9MIv>A?_y9vvdCTTC)-5CzI58|NcOh;%QmyBZMUkf zTP_-|BUEkXZGmcSB_r=YZ2;+rqwG%!kqC5S_M3NK28q6_bjK5hk@#7!C)h|)LvACh zYr;s%{fW{*R~|E&0Map7gt&B+6}1WhnPF3|LazHKe@a4|vpa2T40@gy^4A25Bdw5% z^*ONCVM2jJKWCif)B%-DDQIY&^8ZKvBgKza@+s{tC`P+u&WROD*aFyzWz&_))RQ$0 zGkbtn_o6{P(DdR$@11n5Le}KOJZA55ubLdvO>%lA5me?$y}_^#)_cF(8kLrP>X`O8 zfAg3PFlf}u_q#;Yn8#@g>4;{G9OY<5waD{-v$)38r27`9V%CGt97{N-_+xKC)AMr| z9AfKhX$xR$SD@3HGI6I&?}QTXn=!sQ9Ca9y^I4}CQeAWM zRKQwdy><>__;BU&3z5eNH`S|x3Xy3OICa)j z)d>H&$ z@J2D8AM7-{{s-t^(jF`JqwtMB3g&XK=kr4h@%3d44@=F9^cJbxLKdIK3$NU4qY3?Ly|C@5&Zo-f3c`*Db@(X&j62qI*7qf87 zTq2LJttmZl6zkgHPi;*f7tTzlzXyyOnVNW`ZUdOvA)=#!+^N$-|^6x(7J5T{Qt0{Y%CToUaf)#o6@Ads9 z)NQ>`h9dLo%^OeRO7P>gEwvQ)o9TLNMdQ^fG44t3b&asuZ%Tw}!TPr0?HWYapqe3| z7P#Mk@{YOt_>Qw}nCc^2XSKXP=4Nn4TY!!yAz^3K9)!5Ym(`rShR4<)nz&o8S84lz zp2|g^IDB*?1WfZZg7H?>z3~E)LimugL+yrxGCwKQq)uQ{Zdle4N2ylNB2F6>ube*> zT>BllDK_4*#veVjjLL}|%k|G>Nv$wZ%k1(tw7A&#u9445cUNG<$|HGh=bMgA#@bj6 z{;lxN{JSPF+~7f0k3u-JFA$G$J?Au3UmeizzFMos%X%j+t?RNMaxj_%N`}qcg2dI* z12?k*h|sLB)0)EG!s(Mxzy@&v*QgubcggkjS*Wt*q78F)v@ zhe=9e^-ljyf_D&e$Gd5)vv-OoCxZ_by?UiyUWn(Y{`EodC3&E0FV6!Q<5VtNY^|?5 zRiD@~bw{XGfL-mU!kR{d!Mtkp25_a{E9dR5ZqNfv_ijGox21zafV*$+E7!& zr?Sj5O#X19xxt07*(V^Eb$&X?S&mH=QUdN}8S1_{>8fZlsGu3;9p)reDB%sJ3rVuh zByVik_cu=d-_ZQbv{#H&z)o2n_~3~{H?kYud&ib=&xT5~5NFB{T%)frUONnl{)+9) z88caN58CBSFSF_OrB-F6MJK9%#eF#YkuEnDuc6#lRw~_OD-#CXKf6CgOZsQ>z25$w zj&>MPZk@Lu|2+d1!Ir{B*eQc!1Ff6H zfaPd9_alcE#p(le-L+{P!O8@QU=HfHTR6YdW=Bf$f>R5+r-CLPhM^^^y;g1wf>#EL z_jt&X;oF|j=9c2qqaY6tO^#;t$5@BFLb{_4Ro z$+0uvm5hsm1_1}@Z36etXjk%fT7N0~{V9z@L5ouplYWhgkUOjMF09`g7y7-%!?^}S z;%Gu`=~G2Ry^|HLGnL-7zZj(-7paY1o21BKaxW1k68S9;%2Cq1gHGxZi}glCUUT|3 z6~C8%y3g;=(%xr!e`B$2X{nQR`1M<#Ko@tV*X!EdZbdsbIad4A3K0n2b-gCeT%0&P z!vsY;&TvKyN0S}lGoYS3-n+=eRSw?>*8L9_Iu3%}Gm;&lUeBxDA83mP+)KTtMZMZ% zh9<_c!spu9uu^5p8TbAR%&g-4-}cqrb<=Ui?hphL`7b@Y?5hHVtJ}Cl!JM{NX}98N zV5dvho}OAtx8*c)w9yK))@m#$F0{ag0-;wY&|cN1ev)cs!L0h9S zn4cg@&_b7svQQd0az`8M?5>Ty)_=a#%1wR-%;^>7#)KS0RC+`%4p38I%Yx=O=UV!O z^plSM3Xdz%cZK7%EXtrdVf;g6&zmgxJ*oIh9=!MM*fVyJaUob0Lp1+=6S{+3~H#US< z;uY1X*ECP>1a=Y|o|lEWf5Uq|;}WRxAlmj5#h=@DNg{5H6-zkLsyBpfw^*NYn*M23>U2)cv;y?)-*Qfl;VG;^muVgX8gBN3D;|b%34>eOu_#8CW)was z7+ttdnQ?*frl@&Vm{UGgr^k;%pB%bfVlD}r0!$l}XI8Yn=^SgcR55vnFD0sf0|D;S z^UcArYAg^f5wfWk^Uku|`U7{m1c2O)vq&1B02UC={SU6=-jh{Ac01M4V2?8h9ThB@Q*%@}3CB zsg5K0_T#*nP~_DZ4=G1vgy zcGg_LNcD{D#S_FmduSaN>N#{F&PDK3d-}G7EIZvI%MNrE&;gTTG{c?+lu<*ttnngO z1kXkj=z5rE?8-h9rh8S&NwVN)#g8^h5=VSD3rk!~mp`guGET)xx7N-OxMGsMtL#DF zOvbNVoU0@o!vKy5+C0>x9x1wC+=ZH35b6=8nlF2$BP8nj+xJC%Th?|4nsQBY2JVDw8>$N%64S+Saql6#Kq=9n#6A;`-;Jq-~6I9qUIoW8FGjI&kJdc{cBvD zVwG1FC5nD|7@~3N<%3mKJZ!aFfu(gjg*Rjl+mBr98M=;Q5dpJTxx%Cw!y%ON8Ikr7 z!S_(jrQjX~4-9bzmVyiJG0?{#m!2DOLdU}18@x@kP1DU(YuPZAC>M?1s?8Q}OD}r> zz3L1!utuGdpv1+_jXlOqHzA@`R2Db9SJ?=9PR4*?F<~qdd&n6?Q#kbo;RVct)+224 z?FkLz47~Af-vdrJqDuue2-0NEw$bD`>^l7+**!0}F??j83q1o*0OaD@ zZO=dY4GW~h=X$Mb=^TmKkhN=qRTUp zoyYe7UI)-F9y-qqZ>$-MJYmA$-AG2Yt|Kq1XOHyP1adj)*{HFFuO&aij9_Q)-ysPC zYR2-j{E3s33W@0Tj$zSte_Fg$i7pd>u{exV;OW(7upWb43J#*@z{Kl7w0stSz?0l4 z+9M!K>I-usHWyp@;BpXSQ_bdyU0jySoZ7f!-Mvq5zv;djTA*FVEQ;4F-I6_83Cx$$81c|sA;#r=I_2{A0!8}W?O{k9dD!f9A#c_hQwpC88V51F{HbEfQ+dtN`z zO5FY{ViY}cGBRnga~s*L3K$32ww+c)S1r_(0d3@}(;VQh&?4h{F0YU;3GS$kAScT0 z8QCkKS~M_K6Vt8Z5{`y|hc(^;+>@E%(7A!UityRtQDkwmf@YOc7G(;97B%Bp*y_39 zc20e->6O}^Ko_uc*kEvbPSi&17#149*=g>)5Pcnp1OzrL?`E15mNf3ejlS_e5ijqc zp$4q&FX?@2Q%-k&@GWKwX5|^oFw4*jk0TCJoDXNcoIwLYT)FZ6c5Mx#s!L|#nNb$@ zc1E<7Af@Lz4aF620L8c<#sjkg440-;60h5&n5}P;XqTEe)p6@0ahV*5sT27R;>2SD z1?Hs@HSskkeC+JRU;v&fN*;Ba67wm4&BrLgU!eD(e4eXH_ILoBEKti>nxW~%W$0(} zh4FNXnYyH15bv!?BASCryY`+f;`(@N+_PBN#6lmt#R1G>ZRF|U+_!Gz?oZmtsqa8j zJ~zx9B``DES&=_68q=6~;l+?jF&L0h%k|13$StHx0>W}?8EG)t-;_aVjPs{tg>~BJ zBIXZjkAIs40tLZ9zn2|4MT1_jq8jKmrbt7r1;GtL3Ar-oqtQ&O_P$F;Nw?X&OSKLl||d~ z5SmrO+VUoO&bi37B_N~hFIdrYAbGaGycFoq$C%A$@ejz2XT5x)1%2*TCk2X5pvp5n z!9H6$A^7d>BD?pCfZE%pwp#hSVaC%PuH~t!gLRW@%-dT;b>U+nHx;>hi$BT+n+So& zuPmNsSMhJQ>ZhDIe))U3x%*64mu(p&Bjt^o)oDa7Ry%g-m<5V!c#WtgEW~O6r$Hb5BOfh;4ey(*`1w^=ZsLvqIVvJ)g?8;=tZX?`>yh(db;Cx5 zt4f#d=`}7z=oI*WEeEDeQ!>~^*V6@BMw(k4-^nyRNI(89s-kv}1{#yv`o9JmgotwR z&hsu8$r>)M5mQHNKM=K*0A1sibiW2@esI~GjTl!hj2|X!&C4AJ!-7Wh)xlS>p|9#0 zZ9JpVZ^1S3(lVu#k(6ksg}y6+Cakt%auBe)v{J)`)bOCm`gRtfUGVUt@;~7pT{)1t zXey|O(u@}8n`@$H-cI;>=Y)MfZO+zv6>vo{Qc$V$G3CdsyMent1a%JER9QVbud4|5 zSMia$n8Smoo=eUkTKiovie42J8L(8WRzl~M;1vJJ zplpT7*IPdkw6vOAX?KdsVI5>WKAP_ zDS15#m2TaVZ1%0GI%rT`KWcfLOUri&T9g42?~`-{dthkoHN~ARhR98Vxj(Zc_8nu< zEgdJ6LQ7O(a|b%o01@5y!Ug}W@S+!kj~0GF%s-R&RAz$4A9T*EoBuHsvi@d+6zLqb zChJ3L;VJX+p`aSs`U)8F@3@A`D7^9c^y!@_&e_L3bw}iO?}1d7S7AV0{NLI2N=i%1 z`W1r(8xItY1ZN$zW5MQ^3sKw#)pwmYb0Z+~btJl8@CSe>pr-!C?Jc*z2ij$&75Q3^ zJ6gwm3*xs6Ejtcr%4=qjY_f|@|eVk=PA zGwa+zum?geG&+iWdK9UBtmfrHq_$2w}X(x(T= zG+LD~VC#8CTo_;U1ei1ZwFJtXBgB9N4C*5~^!t?=b5c_{_xg1pIn*EuR&MGk*C*9+ z6Z;icYvGsT1d`Pjf@{>IqoLT{1^mg)&p1x*sB{Yo#(-e1Q#q8a9fUND_h@x1LR;FUM7 z#T*?BJ|wh0rU`Jr3IUSV4L6xe>D9Om%eW2;{MbpY~Go z#PulU$(K3w`IL5Q1^C_8cOwmh?Kwz(+*+uaU_4a9EH}-IG|1n-<3j;Cn;_rG;AJ_< zd0d+m;|CzXnf{Hmh~E^eq`2e%wSd48m8~UeZVtiyevT#m%V0ZfHKgO-8CG>l+Q_FO zJpq2UhIfJY@h0i=@Ihwa@kJJFf2SA!5K0(h50}o`+Oa=afa>nxp1hk_bAP@ktmbZ4 z*ngY~2a=vVE6u64Bba!js7$g(^&>^*X7n44On=)UoEcY`%r}2$6|`i8)K)nZrm5Rh z-?&%|cBuKxCYO#(?mtF6+E|q>abmdeHw(eZ z`_=~7-tApCZ|!ZAbcm!WQjidsG!pct$~kvj=TE>Ztr-`z$5)prSP>S#E}?iRj&HeX zGW-ByZcfU1U-4N!QLqMr-}CC3lRGvo99R9fxgqw(6`kh2k(7L@&%6!laGqJ}8{xmA z6Bl8vq;BM5x~`(y{q3+4?S+MzZgA1Wr#$=C>(W+nTs6;y6W_^@lO zEv!Ar=1(^6&oqAKAfVWxcN7 z2E&Qe-|ZA8lgMu5)g9)xxeNNmn5-6a&g?#OZjeK`a$LKGF_vjIj@i$yuT zfFmp!jmTH-Dg4q}IJPOc0!v`BXBsva%VlB33{-fr(8nT_GLp zfm>3j_T;}`g(+zj==-5cxnCjxDsk)y{hbG2KcwVsHeZ;D`f@kOw@IbysDifex$M&W zudPSF3t$}N^3O77a~9sE7x#7c??PuJDGXDc-&m^#_0J1?U&80XZ4eOAR_3z))6Y67 zl6F0lD2m<*mk(cgK{d$-^Q^!-mp|3LhAF4RK(N&rj?EBFQnlMBv-Byxz1}X(PV#>4>)*E z@BFWJu9is-gB<D#-V6rSpD4BmIVIp=Et8bn0s_b z&KBn|-VPI{RyX1wKzHx8wL5ck;mkm1j`?&5S{rpoOiyCyD3^`JtE zu95mV8L{~qVXtL{9+-y9s|3-Gk2`NWq<^pUpza1+$4*Xsck-SeH1idK!lJnWab zuPeIA7e9Tjs`^m4^o)XLhQTH)Tomb;TNeFCUtC)t3wrJ&!1Go|piy+|LUo)|@tavJ zu_zK(Fb>9JVJWcP#sx>-ITl3(xVF5hTMTl-FG*ORyz|1`ZrGGYdk(AD(doXwj4PR(TMW`{$sYTABGIJ zZG=QX_)8~iBHuW685y6@Mos`(ZA`_U`KJjfcjWK>e!zMU#$hT1pKRiWuuTti-wo-h zE+Yj4xyx@IG-|w^PwWbr@thHJZ<%#ek}siuO#|0IUr7aZ{E^}~+Ip0wNI{tkKmfaA zi%ulEvE-yH`THkEM|s>OgJ0_bB%U4K_Ps`X%fshpH+HtdxQZd!t<0QpghdQsAA;)a~_f%JEIP0H}8L3H~R^ENdGO9hwS|af{)vreIg`J>Gj`>KmQ=kw^Sv{2CIzC z0CZ3ON!l~dc(dE@?RvF>pajE*PkvWW)nEH@!%BU#L$>R$pC~Dj{ph|?>2%9CJtjWp znz8l7-_LrrylgXHC0zV?QO5^JAs)N65O3s8yr46JsJNoQxo>>hJy{gAdFDX*^n{|- zhhQC&#y?M_B5vy7%g{rU6H2oR$s5uDhTSdKnbge>PejA~zyD%Zrj_kmO|JP|q^>h# z`ouM8ORJsj6;z__r9BG3L-skQRjtyu@q*5WXD(~Ay$QVm@Z{2%%+_0%YwQdS|09Hz z?PJzdRqcyE2DBL;QvL(&{PxEzkcU-$@ps?P?77}2zpotXdOO?_4Ez}Z;#7i??PZO0 zu?z9)&41k#TkVv8GWe$^08D9Gm58VT{}F#|x@t#Hx`Ou~ZZBKonyE=*f|!k16YpG&iNU$pW!}d?ywL-` z@5y%m-{qe#J5q^SyREOFA!+j_M?m!}hsUn-m}1nk2~X>=1GDa1GwI$TRy%PPxh3zXZG7&&K=gOet2ZJQ^4xorO$1H#kjpyzi@c}0(iihq+$ouNI{j! zZX4@R#wVm?*9()}$7feHdq(_b7iVv}{m0#lckda_RCavg*sl?PtY#g#2({a?)p^Qb z1t@fS0GRKEM+%YqzVt7fjVU7M=htU{{Z{!!^pJ0Y!A5(Yyr?p-)#Wo+QyGI zVSZk7<*u84E$zpdc;s;%ur2;bE9ZP-@P?jB?c{aji#O}YkH0&H^>u6p@_T=4z3;cyx4yqDYi7>9_ndQ|v!8vQ zvv(15E91DNNKQlmHQh%*7PvS`+tm8<;G=H&7qgNTZfwe4mJZ^I8!*siG#+hy1=BHBU@4tZ_z9qv; zR3Oxueekv+eUOwD1g%n6YxM6v0u>)~Xz(wN$G zQUKm6>1c-XUZw3Z*=a^M*UeG|_Uuf0MUstI;YXhv!X6%1?}M)=_G@y->dmbmC?9HB z1JZ1AXct2XDpv~8d7Ig5w<#TzoJ$XdFWpiDPBKY$|Nr|IJX2bfj{~HiUBP1s=JDJ( zZ!DrcOJA%;ef+&qixF@D)G!K7$}`Ryy<{A#GvAZ~3Yaf?h0_EiKa+#Bt_82yc~6h( zRdEkz?s8>4-=Q#gM)39~pk^x77+H5`mJfXWC^msjAeataLpbCqH^<&T~y5yS0FMJ1{Nc|YOXyM6V)=Ac^5 zJSt{7;B2%11S%up+{yMt(8>sR(1lNGre3?U^cN-Vo!hj5!(p@PpKn)Y1EV7xDWX=B z+Lr54Yg=lgJyRSmTAM~Y%6@vFCeR8i{F<@Z&O80iP^(7L_#bLu7O*G~vuI%l;PPXU zUp=#)(T^*$hckinV%PvM9^g|AFDDXFenEUALTMEsP589oNtf>A{i)eInRTO#p9RjT z4M4=S=p{hSPc4~tx^1b%2uIf(Pvl+gwco+hMDm>LqZPM+$cJqiySTMKTtfXo7q5k$ z-P7vRMEFTc$!n#1qT3{x)b^OL?~uadN+{)9Dv_Q#5 zLqjQ9?X^};`}J}`rDND9`liN7(nWW9lZv>u{O-5H(skYO5)V&0)r<%O3Ujb2GRC3r zwHS(p+>k5gOL4w!RF`A~EURN|M;~1fXkwWsrgpx7m2_>M%<)cL9t#RIL zSsoDB`^QW1(-HNY?e1u zVrK#Y6sq$CCxcJp_hI>l!R@EpvTgOn);;5B6Udq(Z1B&|@iTNeL%T|~ZD2ce6L3PP z-6h~{?C9wK{{E|I)Y^?&KUaflm}t&)3xB~rSFH#)E=_*H*$TsC(eYhyEJUwRqL_Ks-J-<-KM_7&sLEsoX1{3vNZ1DS?CZ2_b2N?ts{0TBUHn2QmRWr2aP3 zJG?HsRa8XQ{-S`|K8sHVZvT!>$wNA&BA**y2z&BLp*FSrV?p^QvyX(8lIPLoJ3H&3 zk)3FT?4o@=lQh5{VferH2t*=rPe7$%#YE&#Ir=vLkZ7GOV@ zCg1_D#T^5*IFr)&RUWqgH|3fACd*&47`bZynw0fFnweq6(pZk@?-;CN5UST$%U3Tz z+UiKlosEt8g{hk%5;V4a{Pu zJ4pZn;md$(Pa%`gW6fOef%-@uR&eb2E9cfURdH4E;bn|#JFQ^9KTNI3>gJg_rfVGA z$)Rc1|7n%1HHJ+Q6$Y9YP56WM(IfaD)ya>qp^O!MEL@x~a(#)ai={KCQHx2pD*YT& z0Q7R^+StVndFr36^{dvBC8SDD>REatX4^LJi4`!EL~nmtu6G8pgv*FJ$2 ziCR2xb?NtTS@ua{+Ji9i)dt=t4@eK8o&f}uyM#As&h>a2o%Fh0fXZe^*U;`tiB3tsLvxnCn23?r=93pzV0?1x!~15lF@b!h zHH5&x5$ZZr8}F5_O@Rz5u0=k_pXItK&&+Z_RDY{+4>FG8d6aMe8hcbZY@7-x1p-q( zbB$NTXpe+<$OPh3f;CARl-TW)I?sD>R}r=0;d>@1L~}{2|J6`~=n77x$NS&kd^76M zzHty5y>`PYs%hwL<-D6pj3+-KQ>0^C zXT$wB2krM8dm>8)eJrj5%PqQ1FE38Kzyim}L&EiXnvi$0Z`4;QCj30iD)R*pk3)^w zhij=s_im&N@|MR(QALivASH#Lzos)Ua=vln% z!{Oq0ZP$R}i`t99>4~e_HxGq0xVfesM_=T=7qU8x`C{;P7i0q)MPYc5ze@RabZXM7 zF##cGMmHTvV1Go|U?F(U|Lv{Od|oAu(${$Mnf1>4k8IH$Y)~NSdRhcqEja?seaCNH zO4AdS&gEQm821y&IO9Fj``$V&+~F&0x$AL#zZ}ijWrryrp?9;DrPX<(47#ZP5<8nY z98PM`quT+nw=zNroe@`#gc?7$=W^k{otCAvTXAk?Yn(yju#t;x-Db5V?l)m0zx)hu zMdU2nTX2_)W7JQ+i7~MDOP;*~BJnq;NQM2yrT@*XZxo}kSc9!^8J^0bI=Xl2t2Z(d zy}x?f&f(0T>97Bl7Yle+(d$$tm-6VHozf1C9Cc5ja7wlhS%P-_qqOMzGI{6BR|1v)PDOwi z3ut}6sr|#&;QZ$0CV*K&;~BQ;p)_7{s(@B5V?HNuERo`xZ%y;hm0aZhmhm-!EFRP9 z3>B-l-N2tHa-}3Hso9i&Frcgs|H&5eY5>L}_Iln;zI>GzJ?RrTj`RhF0Ldu<`B5zW zEH`{Gsz;`$$!xiD?^j{X^*+n1Lgof1uVt4#*ZE)G#^3KT-8#URGRmHVCStaz+PG08 zL};T5j%KOVY+t>KKMetjOqY6dKK1BTs+)&?{cDn+7bUhBx2``5KXqVCwEMWJb!X~U zR4HinVFB!Sr@;9gszsZx;N;^hukBukNl{U=^U3l5`K_)w$S`ZNV%6oZ=kuZgJsK;; zO#(K4E2Wv+ii5SBsWd3}7`U9ggYmKbc3s9jTD^(Z!0VjrLFt1ZYdMZAW%Hqb9RK{z zZQtE~G$Le|nQ2BCf6K z?s%+p^9{SYlUZG=Ue*;I?EQLng$oO2xb9s0hdJlI z%V2(wR-YC7sW3WjRxmR&ptQ&aFgeLMWs_uK2Q@7lIX@9e}k)wT7nG8K;GWc2Btpx!SSO|9MOuj1x~ zY#+S|Snbf_x9PuC=VVvC0(*18q&?T!gK%{17+_0nzN>Ewh6Z+-r6l_PBN`I%ScB5=i&~N(^Jy^AkwL_y`O(d z>u`q7A%e>Nb{Rv((zjR#OHhX|^7q@)l3zIkekQ09kDD-k@Bwhb%fU&99`fuhf#UBq zvi5Fe%QTcQM&53{`0F}$seUsLOCD^6Yu0=BGt~g+kEfh-MZXfnj|k<8rl6Hq(CZDV zc$4z<8=3X(9+DghdA6sw1M}R+}yB}p$JED&-X{gM|4M3NmDI$ zf=1OkdESLo8H83Bx7SjHtyK2OYjEoB{B0k}v2$e$=v<@CI}Q^yt9hqXGeCGS*%W#I zq6+eGYgWaJ3IBR2QOl^f+{_5)^lQ6>Djff57-xq08%G~J22K3g|E&m1VL+^_AHHq8 zPu@tPB6jCE;zi8S^4_rR(f*xF-AiVc}OpR0nQ zBw3n1%~Sg7SSo*&8xZiCRc&j&vEqtyIWGGwpplmW+A^#DNP(F5`-cxA_sH3f@T=}U zyl7#qQGe#X1OLdNSiHQJv>4GAc}I-t(vFPHEIu%f=c8sW?x%xC97l$keF{xvId)L) zDKJwG`phZQr7=jvT+=DN)g<3sqcnHlyK_V#OGlUNW*%wPx{F9}j&OhhT@!BWSKjV} zJN#ScWySRSzg&R(Z|XObAmtkE)`5|$ zTK6`-S6a9?$fshe+~jgafLIpP|IcAt7_z$iQ^X-h88^Of@KNN4!wr!20ZF;4w8NWu zobOT;E&Yv7!^PIvt?%x|loBm8a1xsA4mUmOapI_g1vr|&5W45|(ar|fe#XaQ zoawRHkogxh@aCCcl-8L>M+Hf{IbCCPrE!e;6obcf5_+8WBp0VKF)OarezE`=Q&s$| zBFzv+I@LDP$SE^Cg??k~QUH65lp=p7WpxW+2DjDazgDtmXi10rOJT)iYQ^08%c=jJ zl)oEKrSn`TzImu5*y&|*=X}#*_^I|uE?q5fuBNW&XT{r|IJyf zq0aIVMD5_dfC_C-{#~z1lJRSy|zrH1zC_&|7f)L=6bhSQIZK?4*Jq2d|hWN*kI@KUt|$ zcgZL;+%^4YJ!dNFV1HEaG9+3tA}YO?(NNVYYCz|?Y?d<3fb@CmlKuDEIU#s6ymb@d z?>EaozH<-S7|T^5^VvT6WsXdkog&JH>&Gn!+iL`22%fbt?(u^h^aPhLEBK*_W!LNP zL58B-sWH(UZ)O9F<$nD8C0Q)}FFJfa_cZ_LUAVUqc5NqazJ44KnFp?NXR}d)5^=VU zL=$~AeDe}v`44%KEArzhxpUfB)P1Hf2{rC4Sc*0x>NnAo8o%FhSXTL`Ud%fDbaXYe zUp~61Z8{*EJ|9$K%U!ZITHKDzrtOAQDsF5Ho(*XJvmEB8eq;9sg*n>V(Q6z`@%%wR z=Vo9UmTm~%mNywi{x+4dO_i=HkT=_d$k@a4D#!`>!1PM>VtcuFs$5njj(%wBV0D;M z>??ixnwji&&p(@8kkENm&bhfJ=X?~NEpXBOc)elt<$y(W=G4pm@?7@Y)k4u^>qxcT zp+TeXHi>R;{J_CFh^1+>~Ay?V%%uFGO7sn#OO1QZkZq}SJ+4>M9;qWFx*>=7!#fUt9yB5 zy2Q_#BUV0VQ51D0Wji%{_xm|8wTeMc|D<1@H}=RPT~3z$C9(R8xYa&2lxg>60i6NS za9Z1V%e50xXK#@`nmaI91gN_Kuo7=_*0n`$hm=ftJp`pr;f9FH*&*Er zo5|aIo80_g(xuyj`xg>@bWpK|QgM6dhPvLZEk1LH<=&0hEu+yQ1)IGQs&5XiAFq+F z`)3aBjfqp@@rS~inY?fx;U@jH0s)PWUdA|Ulivxi4vn|uXukf;JARHRQ90EWgz=LC zh=U~_#!zeL-uUweD7Oi7*VNz=zxDB(P|IK2^&HwRjM(k^Fzsg9g1|q{{zl%rf4&-* zZQ9;U!Y(6|pH>576545ilr)-WuPp2Rw(nXG9V_0+#{krVK?EP zFs5@%2Uv$~n83Y$A1N~b`yJNUlNm+qFv@>=rRERU+Qq^pOF`$ojN@k2bI%v!K(Q#$ zxm|g+>Kh#;vr6OZ!Ln`nA1l~4>tx>9gvahYY9?f>VSJ1B@hMLen#nrCrID6I3{~14 zviIj>+^J^mJPey)P*b%CXvDzjT@yt{plVy-mNiH@<`t0;A2O=(dy^=@RZ5Smygy8y zFLMdBJDR|Ka1CK}U61U+nzOb2Q*`BZ`Is03!hp$_OvxGit|VuN3<3&t;qc zhCjGGV~n#@-bCZeRAFy9DErV|M*l5ql1BA52KcxQ9Gu=fLvR^5T9cD>);DcLp_{Q| zT}Y5dXev0O%6DR*92GNK6YEknTA|h5T=L!`)8}PpUXnEDzMVysw6=Ub1TsL| z1C2gEIE+b)>EV{$kELreUg8K?&BPXI(C80!sY*XnKtidByvUlUURl1}hvM)8NS~#~ z?0Kqm_;fIquKU^jGEHlRPVd2@xEz_?=8R>#a+4c;HBjRty7%20`-RQ-J_E7^tRxCh zN87H17&ERKp{kmVAh4};Ekrth1q1Dp9fWmeVOKLZ7`G*LXL}xF0xK@Ib_Qf+M|iTX zt)IdMYN~mCr?3o@Gu~Oc3}TMLmFS4YIXz8j?$_XQ>v*Yh3m23syM3ss^I$e=8l#9& zT0mxnna&M(v|D4v-B#g%cJ*e*Iahi!#)T-8&+L<_9v-*exQ*wJ7Pp1w{23bxm)7a}easOn9jA+26QYu6NREB?n48l%36X zF{`b|C~f>%mcFy$PxPJE><>Fk+?LoLk1{f&pB6C3 zr1BA!{?%*{p5ArB1D=36nD5c}?~{ax_foIj&y}dP^AgjWU`*Ev;7z+yPUT~FOii2| z{1PBxZ-VngXY90EWaQeYJX(ovh-++}15YAVsd%^woZ-8OhRgb@1*SPGf z?+07AXPOD)*Kni-awN_Y6n7-NA7Z5H1J&L;t1K$D6cTyH|r%Ns|}ho**dseeajC|^PX{5qV9Hr;-)y7{yiEKVE=1Aw9h{P_>VMYPG(5T*jo%SI1)6? zxX1QGNLK*(D+PpE2QU6QZ{B&3=sTCSllfO;&QB2sA&-$moU5`tFqB=94 zPTxQ@{aAdZbc+RQlFZ%9R6I8tb5S637$ZL$U3h5t1F7F##}va7fi5@ZZp-q$;&X5V zYc|KgU#9U}5i#=*L!^-hG`uio0x>Dmr-()|N%?jG}YxK(76CPbvnp~vWP6%)hy{^q=f%Qgej>9Ry@8g?3-V;(;}&5s|X zJAbgIVpxMavS$(Z*uTk4D3rQ`u87>(C5H{yHKS<4q)8UM@HF?q_oh(?Tpt8`;z=o> zIvY!ASQ$Sz7ICm#ESNYWc!suuTHm)lh?xzHKyJS1swZ-#032Z{)yvYBn8-GoGHpu+ z>Byll6JMPLy{~$87d(OeZ}H_0r|dGo4tT8|=YBb;^BmfhlJ}K|erFwlc@1gYt-Vij zr=EJcIM1ZR_)ChBC(1#fF}alZ5Fv(eGF)Hr&E66zRL&^xE%IiDup%v>7-~3bEs%#~ z>b~ zmNhfsPYnRs_j}i~w-l&nLi`whKB#OzLoZ$LzQLb#bGrwCV2E+5)-j&i2njpmckg{0 zyrpYX_phkhwgqFiEIv|;qbI6tKW_J^?{5~04Z>z&3hzYKvt8mgkLHp ztCz_ggX8xlf%v)E|J81iJZ<@ClVcCZ1R3=gr-q-rIU`fIQen6%d}d2LZ5g|9Re4~| zm5%Dh5LYgA=Of$I8&(|IBKeys5M@NK4r*zKCYuM!PPZ?+$Krdrt-H1i3uTr*D?;8% z;{T1*>ixIzGY`C+2ulW>;VJhv&>2ZuEJ!P>!4=jH>8$4F)s9^m-?BRl7yxvdcM)eu5!Sr4dn8p+C6 zym3ju6DE(#fS=#C1xS-W%piGo+%gvm6Me>PdK?o^gSHPqtcdl;?={0l!>ML_x6+Re z$99ckQaF5oT)j$u`eV`{0fdoiq}2Wx8sxzy^iu;LYUU9{CkZ&i$M z!a3uG-)qf7zj_NueyNZ*V-?#;+a|b};j$M#y=5+U2z(7=cGkO9GDO3fV=daEaNk*s zvWadr&{(=~u_Pq~(OuGiDk#Bu8>o=b4m_87)!Ew`XY|sAkx&jqZ3fz2^3b|6QEr#1 z)+w0sT#YL8!KBA_PCHkQ4Wpu{YY$z5pm+*?ni?x2{-E^U3~vduy@5)Vk{mX$`Ta%d z0$U)QPGYtU_i(=xI!p@0aqVFrs5i%Xa}6|v=@|o35pK{2Fo1LQrur>lGv(&iA`DbZ zEYoe~(yiAbxPHHXlIg0}i=89cd4kTY>@XNRWv-(8^b%`W#x*UsanwUKG%IR?W z;OAR4MCJXhYhkSNYoGe~R#ArxeP{}G3iTAX^S+L=nPf}s)qlAF}`Gu;57lrKUYCSDA#@5`qI30|CC?xdQc^!~MNtp~pTg8h!!HB7#v z$FK|zP_O*%96LQUP8*xWNeEr*DWW9!IT9CKzmI&z2g@vW$936#F~PW^vNlyG0AB5u zbY`7;8IOmVhQnREq~Oep%@x$4sZR-13XPDk4_sJcrNgs@@lk%e^13(!1>t<_TP%|C zlGcYO@;fT;@XrM**(Fl5QDe7}DgQM0sBaA|4^!pz!q-kag(1EiD{GYVj{Z!0a3iAD2Sy-sAL`xgxDAQj|ED7%Rh<_^5fGe#f4W1N@=7vWzmP4*RC z{wg^$+*>Pv1z5HXr1kpZATw^ct&Y~z*T2-iYH5O8`ptB9CmH;`=jL|V95zsy_GaWy zcosN5Z>r>5A4B2g4--pCgik|-&~lRaGuPCxm3E{+n5o5xH?xaIBAzc0nM-)3@sdJ) zr!3h=uy}j4eC(vhRv9jQ+}+$RTF~z|@tSMcREUw9lmKlH@0k?hMuj1b9hz->Mn2nl zl~rf^ED72)x=f7iQVpk?uS{$|rSCdDG)C)w|EgasU8qD=(RAv@Q^jM|A0Gyp^EF!N z#RSljFCN}$QlM`3Ct6(lu00a=dd1IBtJy#jF46vAq|Yuu?4}R{AXXtxMdb~7B+aaye91<-R z%7hq>OA1dSY_4I#)@2v|9bASU8R^kBd+_Y;DC8gbrTNiY{?MY?Nbk7pGOk)VhihpY zXxjg{_2u;FL@lbfnOlmBlyCTz7iQw+o>(MVC8vAq*pXRH<(F4^YS-{NiFl)T@asxc zZ%A`-F^HOLC{Fuc)^wSbQ-!M9n8ArVlJ0kQy}2M^JRXvUZn9YSuTe|)M1 z=epB4MtRiihcSa-p(2=Le@mgjR7`hBvo%lIfbKmZ=%bZKC?i;=#L^n<%`}?A>YYC| z-j-q;Tv!Y*KJVZ`En#sj_=VBM3a=pX+tr-Ect>w$^W_y`lkNN1hJ*FfBc?Ir-0*P5 zld?E=hj{&!Z>^^*COhCO*&;lLcrRkxt5=zYZh9s(pXBJ6`KVW?yOdW8c~1r2ndRXx zElD-c{m#K1De`oxSnC#?Z@ORfW2z;@!~oSaD`Mn>T*+cQY>#MhaUVH<7?QQOirt9H z-W#C(DLb$>Qn`S5$~)tr7d^dP)Ypyf!5%K_#euBu4;4o{hF6Ch1Kj@Z!3e_Bhp;+G z(H_#W7>#Prls-Z&%2;kNh-BetV#^$wej({yzy85g2&V(+SRWTa$4v%jX!_JE-qQN5 zXm&8n`wJh!0|n1bRUZFuA$NA_l(QVBgX#uaT`K_M`?|3g3P7fN|@AG z@+VthebMRs^XF?jURnT9&5yd|=Ik-dM>DxXtxUseQeRoP9Lt&I_7Eq#p11W>Dd~s@hPM;&D z-{WHw4k6D1YqO$xgI?kJ8PjS7(Cg}Pxku;b%k=+Q(%ztS*I+$nvf!252QG%1Bxn|% zhOM_t>S^qKv*sO-m>8TS)HNKH;AuCQwm%HJP}CBo77Ho9cxQVUhK&;)3a2hyAza(o z|E;gS4NZ1%m$q$N{I(c2Kj7jQB_Gr^pX7xavIPCbYdqclbB1Fy6F@ zx#L95e}z8MG!Y*zw!m`rQ9!nmY|KVmLK+})Vj9L}>gJ^^mWL(d%SFepVSHU=)LhI! z(d(V~uF$F$;UdQ8>$4VSp!t15GteeOQ-OMmXwu0TH@c~z*lzlFW`8)p)t>qGYkF;i zLh;}PK=vJ$IqJ7G$^aOE#M-xuM$0l8ftmbcTGI}Op8*&R#Ehn$% zEr2Orp|RV$4AttMDRjl_KegpCjQledPiMBrw0e%aC&A07j5$61WiwEh#YFn_)M6n1 z1o}&}pn7xgK3A;3TSmX9#O^UQ<rXDL$--LUlo7imLJ?*_V{%W zrB3$SN1rC)DFPABH}1Bge9#kVj!J4FF5wHq)j#!8+(ctqv0NvU zwF+y6M7s=9U)w^O;P3+!0Gv6wPG2WOcs$$^IHgZcDP8`W7_;a86aw9n)h0$awWK&A zy0u#z`VW39`&LZ5@JVdY*Psn?7jEek=eyh`v|?!W^Gj;~U@+h5Au-dJh@uloM|FgV zkY;c0Rv^rt?N<{Bcfi7d7)W-~uT;|v5;A{KrYw%}mKlkfhjF%z|-%=sWh z%G)168&LGd&oRlSy;>4FegEdIT2fvY|3Pg_!lzuM$AAgo@wm&klI*rKzP-{tu^p-u zYxq>E-+WwKySSz2q}GxejOpuW{mkXL8=2jU9FucT5@$%_VDl(f&(du(W8Xx$+hbtt z_Lf@)vwNZtVUK6yLy$P)4wCA*eo()EXysG;MC$6*M@QCZvPyq{PCQqhUVf(Bc|p^o zSes35f%KwzAd_zUD@AzXdi5s+zOl0!p{OT>1=49aq~b{`SJQ=GJEE);YT4}hm_EdV z={<}eCb1GF)oh@=T9N70Q z_(vi?DQem-_H^%x&DCvCaW&YzcFLnx_$R74??;@KBIBL`riC;!r=dF**$K6JgMIQC zl`n{JKjc_X_BY!-DaQx3dC+ylYU5Gepfk4&LFaEztqBC+wZuc((8f4I?+F4wkwC^F zbJSV~`QA!!D7L}uyK!`rs}5Je8086C{>O>KWh|?=5=sk$`z?dqhiHVrMbt#S=5fqW zcSCA2XC*@uqx0%QO(PM;9*#^=D$EJ&pepS%<>uB05zC0FdoaG!f`5CPZImXFS``D@$))vBz*RJycwkkcy3P^5oYGcF6*o z(xucaUW6fM<8R>N1{V;pm{F6z`GxT~wfsKG_|mpY>1WS&5Am$|2LZmuy}WwpeS>Xl>eQte5Yqa|A^QoLnB zAHLGuBu>e(jHFuWz0|u~TRNzhJ@xj1f7rva`@f%Af@Y!*4i21>R(pRyBUvM~do@vO zQ*rcw*sQ@?mXw+qclu3dIC}S|fGZ4VZ&wa{ON$)-D;pLs!#-EpLJC&O7)7X_%E|Kp z^`?wq3RjFr2y_Hwd`^>;=-LunTOQFglx}7~Lbd4&VqX{q5iBvgzCv}-Xb6%xYc_%A zIC)Aci@uoXjU$z;4lGEWHE}gVfbvYOg?3m3{av%SV5)1^bJAnxRVY_UC0-=w7n5e& z_s-`rFwfL=Q`?@t=jx#O(DmtC!&B4!Ie4Ov6c=Yf^$U(kWRhUw~j;iwho90tou&MtGn(x%+!-b3pD`@$>#`SPjJm=*fN)1b-o6 zXgu4uFTO|)_Hc?44@&{n*n|J*mkY|0aU(cBg=^iL7XG@S1VlgWkYqBF9*Pe5AIeTj z|I*PxsDIwy+g~pW(=xHfakcL1f1~N{=Xr2B>1_748>?v2ix4fXay;250~cgJai!>s z1PasHpW^NP$YJvSL^fn!kB4h^i?piAK_jy(gpgY8L-CwU<(hvg#WbQnG9Umb3YnPp zrIdL);nI!Ek1b>B)7DbMY_0wU2w2i5?GFDg;S+d(-gR10&qM5)eC46=2`I$`?YHO^ zHU~j|?32dl{u5u+N$nOwJ%>pqAON>Ne6qr+)+9DGG%GI{Loxu234ROC1K!}&=uq;MY?0pFoW4ACY6+wF0X<3B6v6U+@w26 zeFW;!mu2$L1K7@P^mUUV5HshN4Clb7`#j;F#1TQ^-M*hc9v|-OaaQu98NOug(^8P< zvrLD3KxSU7AerxbzBzB+g7SFt(NAAZyffpNJOaT3m% z64Dnpk%U5}ZMz~yJf?2vV&z|h{iGE!pWAe$*^kK(sFF}|vnF7Gd;)dtzv2O3h(8$2(!V3eyvbMqwCnI*I&@!fS)X$rrY4;T;ezZ)*+Jej zCaQEwY$k)C6vH8R(x6F9E)~*ALkf3t{RsMja>q+?_NcpeaT&K+(iMA0%ZKT zj}boUYt1&NAL3sqDt6z=`#X%$`{uRbsGVFSS^k#`NI=Kbv2BjN^NSxvsF%0~WQmdj z_wRO|t;fRRrvxy=Jtl}hP)-8gi2E3xQwP|fEhmJe0 z$BuV~+igA=VfP0L=E*(E^KB#c0FhOb;hXo`rZ*rUerk?s z!=cI5W&}8fW~1W_tM6VpJ+S4^AK78r=!l))8Fl{ zKS3Vu;S-2K+NE#MOZ2ImT=#Fau#2u>$g*&Eyl+4wA$3{?4&wrxc(JDhh&f;l9eDjq z8q$4b?yfZYj-zq!AR<^L+yd5xazw|;D5h_`S?vMbFo|=Rl@alGb}K5x;c-l@{ZVe;=xK4 zHV@Ws%d0vawfE%1w4dVjQF5A*1Z5{b*YuNMUuG^@UTbH3P~Vk&7xXz)JlkgrB3>wS zH(0`RvY&|vwKjtk(u`hmlSi7AJNN0KH#}6@q|)RQv*u~ zyNtd%jx0$4eZih*sv7;0M<*Re*CN?ayY||Lke`n=+DaM^R|gpVBvM?HKKy|xFD#1W zLy|sFUaFH(BrDuPq!qm;ArP+@Pn<%pF04%q2Az1gah!Q1985q;+hS|1_-`Y{aNTH! zSRQkdXzEDRZz(RcGcLJO40Qq*r-xDmjlZRz%Z9)g0qIK!{ha#oGD)8At@d-ba8Bq# zKS+0B3hLy}&S+s$l%d|!|o9vnH( zmo#O{c?vKm6WvSiRQwJg_Uc|^l-Q7oO&>{{|905fo{@6*%V%x2f&nWf4r^7?nv z4@+hmL-AF11Mqz+GvXB}hW)X3n&ND-cs3bSBX9c!S(;|gYA%lI)`!rLtdMMvJ(XZM zQcx8-q872qGS7HNJQRQcR1jg(Q?zj3c!v)#_)oulnLE@8Le&@W-J#GV)|3~2q#n_6 zgo_q3?nFf>7(y&NPqu}FvI*%@&fNB=wAO-wq#PEn7u>-4T9d*R1+OCXR#}mdelZXP zQR5&vkbGRU$oK$q8}y)UFd`U8JwDyif?r%1u8{S~bqv3)HSzG?=p}kZf$N-&T?4hk z)+tjhj)*qU6EoFjxs@sDw%3Ht;ztCQL~GkL>L*qE+)jH{_4tBEeY^JFX@6*Fk^A7u z>DXCa6=*d-q1en^9v@LC#+md$I?(Z18dPmKez-);i1SLL4OI16afJ|8xzTar$3rtz zyLg=VUy9}Ep{!*;^@=yZBbnE0q+pV`)6>f0=ewe^dT%ho(+Ge?<`(>lQs*iaYtdpR zMHhNSJT?y=*WPE!tni1K>%7{BjR(r^o<5P?-LpA8Dqgs(&*bC-2KAM^A27nHoGpxb z&{qqkj1;~RsK`6 zb8BQL1kF+6GPWxqL3g15$UsYLEqq0ozsYqWXs2iONqk-bdYV}jDLoA5EQQV7RVAC2 zezJcH*!t1G$B`CAz1oZ6zAD5l0^CC-2S*wcwYkPF5rCWYbw3r3M7fwf34lRs0`4RbywZ~iz=UW^crtbaH zE{|=>I^$K0Qw$pP8a6*_Ve+PSKN`>7&dCN-!MDUEa*LPBo&{dB4uwgrQjxuaxp>g( z5OZHg3et`*ODi^7J#?@4FMP7sF~L1@ef}~KL2#!8KKlF4XggtN|zdhs*HAofI zCo$9XgyY05VfsZE1EZCzYFiM#mivh0B0asQdnXQvcu6>YoZ0Lam>zgztBm|O-C(l4 z!16v!i5CXta1OdySvU$d=yr8=pB0H5B)=U5FjcLPGgA| zT+{-q7f*3rLIe$L;nRP1HNkeJg=FKX&Tp+G#Do;Yad~e?Xi2zDM*>JY0`OUj3&-=_ z?DcniJ>3W>%(n&k%yYXhHtVD)nANuo*n=t%;eGWjTaUVO74BE8ywD*bq+bDLh&?8f z7gv>_BGD2{YcA~|+AaA+iC5&TsiM&^C@opC>+2Wf zf~dz-{L_%XRpGDB+*G?*vgwddSV#czX_z1$eUpg&9nHUmgm|pqLWUs7WG@B@0isAdxe1NnFN6z*atJOW@%$F4e|ThKFeQN!)ec zFIo1VoCe@fMiC)s254!R^6CFx{fDWVsVdV-leihNTwI9QleB1H&j;YuZ1mhY=C#~w zw^ppu)LNE5wUe+`jIh&*rWd_@QRfHR{Ywq(68QsbHRQ!GxxszL?e5nZbjd2POjgq4 zI07D~-KXu6{e48J)^TH%jw6hu=?E~tAyv3!^-l<>sl(+9UvTk42#NXnW@C{> zP@t(v#x<#$NllfKmNxXXNPG@{rYoLXTn!(Owvm(gHwXZ7UvEX(7Oymu?uE=0k-N^$ zdrP9IX!NodO->fIM3mW@ErjLDB-!qex=U4(tceNHh?fZH&Y%-Mpn8n$C@txmKr_jA z=k`ChdBD$=fmQd9l;TSgW{$O5nJn#~aR?}ilRzA+xWep;kHSR@e+tRh6dsXQg~HS} z2U&tSahIzzyNVv8>Hil5z{RqMHmUR^!>8b94L5 zD6%1cwrzJkc^%U31F|-jjG`)OAm3ot9AAvQgM~?C(fh$q_%=%kC}7J;4fu)+6AOy_ z7ROCd+2Zwol27PW#lfxAq91(4UN7p*Kqlfte#Qxc7jzwzxmil4eJj%Epd{rTf61Ym z;3Rj%-E3AoC_}P#khT~e=B3Cd>NbVj0QA$WOk2QkK^$o5^E_rxg>I4ik>L?=24%A^ zC=NseXn=8>AW`L1X|OWw``t2g)p)U=)oIbP$cp@yYPC@y9Rb%bj@KzX*rbHgv0`oi zR?m`m<-+QSchMIUavj=m8~0W9{VWWhM7sKQnGk9$OItMx)+&-d2r69bJ;OXgzRfui zyNyw7&kWW-^3y09FPpgQ@vh!~B~|%`OHga>1{)mUp62LBqsHv_m*NrA(3bgoeeF>s z%Z~BXR1?3VvfAT&v1>nc>>(a$ksGH;!T%3i?;Qy$5+aNeB_X2s86tWJ z(ITQp7rn$VT8t=3v>^Hr34%n8-g`GhZ=;*2V=#L8w(GvH=X;-by=z&^Ka6efb05EU zoX6>ns>62`OGF^%auWcLt-$x?GoYLdm7#Y(k~dAu*oeI-!Wz9=LPhN?R_&IY`kbRG zfjjZ!IvxPem#9Ox=uuZU021^G8I4F->#oAPj{9=&5YT5ExgYfNRByqe7m^LfzkHfV6h; zH_3~DAMEppHlgKpfw4WCZa(`viX@Q$KQOJvSb@ z)F&U!-37=2D1zpl4!t;(BWm)SKhu(@I)Iwu`a9HHE%u)?y_3fQqbsv<)7^sTs7j;~ zxF1Z%9xG$CY6TqiE(@koi5m5u z@{$o9O+C3O`G|CVN~+h-#Q6icn;^}SHznRNpl*LcHa0cs-8+9gM;#7|8%~hFAB6*G zBChWbPA@5E3mJRW75RN^N`L9p8wwaLIUM1B8fwqy$N_aZCpWX6l|cWKR2E>{;gz35 zi2&86QaPV{B>`Xr|BF7nc42o(3FCgP`_)=Sb+=V+Zf>EqEjO69G$(RNRRTnlp3@HbX4S{KZ-$KgXN&@W>Mk)9J*Hlat+*zu3F?rcW6q+5QOSX~La zQREfPHM=WYxVkKKKGAvV)3wT~&G0MWow**>``{hjck~$?g0PpUacplbD9+Wc7Lt$v zifg~F#mfI1@Cf(ZPLzT6ojOds_J{ux2^l`=;0rIcauF;QC$<(A-oCp?yLD~O!27}Y z+$jg^75a)q7JA)8J{^_x66dy)`=(n*{p|F!DI}1kz6Tui;!qw7gUGnBqf!{;jM%t-bHs zK@+7Y`-ppOdVfVkhx)%n!U+F=5s7BVAG(bu4s3Ikt;#cge%ZowFrxA$4l_;2smcupDg-CO|jdl=O6EpBIqBf_2dc~oT;5v2u`JCB3A7CeG%e{{tv(Yw1gk#@h1Qm z5WG9@@(4gk5aQYbN1{CSm{;Z@G?QC$PPbp~T`1gfv31^z)|BcUP(zre=fgv3my=CU zy_cLM_e#`CcpNf;mY@5ai78cK!F#8zuMqTtWeJU}yDy8ni|41mj8;B%_xxPqIgQl* zWc<_*=)`6w4Ia7U-132^edWVfj%P}`5Ol{>PUcQf!f7xU1jvwbdn3B;X+FE<^lUpu z;F3us{f9|(gzl!b7=Y9_e%>o5x&o*kow2LHv9h*JIurdXnFrWDRB2?w$Nb)vE`5Wp zU)|Hrfq;HjR9s%>I?{5|P$YB3uIbPFW_GUt8R>5GktxvPHeGX1Y59##S61`Ln-JH& zZpJI(p!q=R^+WfXPyKx#nH2eB!O|P)3+R~^GMs(%@)++AfKqHn)b1VtBw|sO8c+lY zAjIduyFg*^_1ar(s&T&W<&)RA$;{3L-UnQqIg?8T2?QL|v@ys=wp9Aoq32YQQiBo) ztjigV7MG=mRFk~(n>Y3yxeTZ%_-^cj;T?r-|JpfJu#1;!94x%$dd!&aJm$J+nJTPK zyq6as7x;N1jMUsh2ne8i1r;RPC?rj$&;2hzCF+7kY|6ds*cJ)Xab^YsC69c1_T#+- zRwvpNWH7ewza|4I4lUOmj;sOv5I<;K+$Fj2(wZNx!hfHJ7G}G`s&^oTr(x`T&Lyz-PQ(K0h^R^ zfo!{Vp6x34l?qiTr0V;1K)xHLUJA0;`{gmf(v|@XCm*Q7;=g&9Jo=s=&|)EuIr?H2v2ISq1b_7F=|Me#FJ&iJX6n`F06b2`0U9{dy^4>7$%wsaAZ7mN_wA zoC`m6^PxUh8pL3~m-Hg$-+{58cRk>E8*Ug{-ylBbMe(>m>*6w2F zSD#%rD+=Pg#pqfI?j_hn79~FfH zi94)?3U4&o(pK@bKb^#TKA<4h9lb~h(+b*TOGUoSp-zPZDo+FInabqWG6U40|Ak*j zNm5sfeZUbckcz4L$>r*9@1B8?k^yjN={>C%v*71P!RxzhLO^l9guc(*T9O=dtXO+Y zB3)Qpu1lDYq^C<*Yl~m4mnr}#G`-=6CTj}{&33+!rSMcFIrmyBsmrx_Vo2b)T4dL+ zaS=uaM=X$h5X88Hk>WJb$8A@6T(4o-05u#YF96<;4pe)(4LGuPtT}E}B9S0d0GRH( z_`i6t_@w1_3bvV+>=EBlm6bp!5fB4sG0{ofcf0iZQ@8c*jmEefJpI&7{J8(drri~% zv|EN*fbIpBCaDWMLE)Vm5H&YJf+Vi5P`47&?#rNKxNl0tyFZ_A(n1VJ$qN8cF8k#% zP?#uQ@66tCbp9Z)-hS;)J^vp$d@3m+5#GAH3h;Per(K{>fsS^~_`9R9N~b+-i%o#L zwF9`}z}~*@TbkL;8xmGU=5;(9As>DL_*DSkfpzx8^0T2iW#z&z^)q0GPXcE6dqY}Z z4k6IGWzZ6GcXsvHV$0D#ZnzEeKSEe`>*am&c&c03SsIRA0O?|O8O6J~#!o<5(hLp0 zT(&PB@aWg-a>{o1<^EuOR6o?y2XvGEU+~qUcnM$eV4&~0v?S#5ducCM@rs_msiqVv zP1dS18%4lopDlz6r%zzNT1&G`ay1d@70PlUB z{8FO(Kf(Mx7j(WW;6&1Z%enn)2T&nyF2mvNRiFoe$MEfYJ=1s3)agAAGNUc4&^HWY zY8FE8$whNhbo5Z-nolZgIJ$w%J-G8to(nEGStvo{&vGP5=lY>bw2O;t3o;cL-Z~4f zW{RU8L0&7}g%eC;tkInzZ zE&fm!z@8z$JAyFTToR!|=SIDF!g?LA&>emSP`4_T((Iajw0eNS!*MBsG>{UB*dN8- zRDj~SOnyzlJVy74btQ3MEk!qX?TaHBv04`X{}sibNvCCaLlRRN8A~PFjWIIIEUrH z=j2rEr*$s;j7cIGm2}dL#b!wuRwZeO~Gz}MV=el3hb(c#!~}D7QC$HMG;oeZ-V+^_s?*n<1spgIOKBb-8xT~y;C`X(fv}_~r^(2KhOA&K+y%wd126z! znkoXSy7Ki9r6S?!+9JQxDU;X8%$bN?6T6hS(Y%0O8v|&nkR~l6*Nhf!Uj9YUefPmkZSPRl*@C29Xs$$18Mt$XQHW!g8KaB9iPBJ;vd zUHAbUXeIBy48G5y@38A_i$@yYR+@fOMLvoD;WovdVAOT1Z`WE#Kflo7g%w`>Q?hcB zP)<@My_Zr9$lKk08GUJ<=lB$4+{h8glc=@@UM#9!!V*h#p?RS+b ztEzn5aA{UA{bBz@2-cf=fE%^4DeCA#6N;n(Pcr*SP%ewuOk>yYrk<;?P;ln0#G<0E2aweDgH}L%(GWag=s#JxNLK}zv$y9-GNel3? z!W$iFqOd=|e*h9Qw&^{9A%p^+`183{Jaxo>746M%9a+G5;78Zx#7L-^#Jj7k5&Q>s z{Gpw(r{1nt9tA5u$V~}sWJ~0ZD*CvUSTfczEb_r}@5Ev*3+u!T4pA)dzqLK)eGl zrL2>N>;soG*_)S%c+T+4(sr8HZWfGV3s0DDp&SXOfBS{j;!}=*{vy57)ab=T~w^seP{G$&fa?Ic@maSb~wL7(#KdD zaGPseYrxl2?8iv2@ni=#sMrMh#n^LivQo}r8Mc)-VT<^#j6IRwuB3V$5m?S6GAx_$ zPkmv=^^li&Rf(|Zbsr}D%SkLgGYDTAi4L0aL?Gb2d)UCQ+h~$Iv;cEK9m4LzgzK3q zLiga*OYpj6xy^8KZeq>x81QE9B(i(gU@Z*g<3_W$!q$d*EkB@5gX;<1^CnJt2AK=* zO)*wjlZ*bgUX~b4pl(5quM0MqDDCy`u}L=4$ziy!wuH(9Kkt*bwQiq zZ1LR3D1~k6xwQ1=Dh%qz`li>wj~z$pBs4^3rqsEcJJLk|u~H#Dz2-RHiT7 zx*c@QUsDvS=fkuA_?v(V>=rz8HMdb(-QtiV(H5>q>K?pG;9xD4OeBZd!Uy&g67LZn zLnViC6Xcc<5$H9)_N4ah{U76zGQh6gJXr7!X~c&*l#0wUgwi?*J zE_i)DcN`h1-%zG(1sR6aiUx0Uk+4#&8>_#Di}uK73D&|QBR1O9pLn}?+fZ!m&h2n~ z02OKmsZvC>x3d?X`@>#jdI!WDS&~nMS=KdZecs^{Dfwf>%vs8;)`0A+yyZdqm@>7V z1UP1xIpI^0j6e7T&ML|7qeElN&kB$r$M2l}yKmWX`NRkWaB9H58JJ5&3ObcegnQT{ z^==B8)l~Md{z`_G>-F`ir-pXzG#BKUpJPKFnB@u2#5F%c(6{qPp9CFQH5<+t(m1$(8hz7@QAr(b^$4n*C2r>$xF0_JwVKG{Zya1tiohWroy6eGe;6rzxI*8D*MOvM8=_8O*gk zlrolw%XmI<%xQm%+3UnLFpkXn==9L7r9 z$IT$u^@!xNx{5fmSst1lb4SECNL^1|xQ8R;AS#s)4ACUTEPMh^GGpD2Lj=9tk^?sA zATf8{U7^39UrkU3Dg7~yLuDe@SY&Hp5eu|ddGl%47%}K+>iHan7TPRsMw6)U0uUwG z9`_+*|Fif?^D%{hWuP6?)eSAuYdFPkwnRZMEnAaQUCCRWai6CF|V_-SSk$Bs9y0Xl@uE_pWQZHDRrt~-*|-MeG1;f zjoo9UV3eVueSl143mLNt>!XesqGKZ;8dtw3G_p;sPS@+7w<0zPJlrb|5b~#C{6V*o zL2@lNapAr4?m!{17qo?k+Le~}JXhtqC3I83GqDp&xvwg86L_83xw-`K@W&=d;OAwL zpV%HEL$EhRYLmT5)A1*^z@Ig-)W6><#BY^+{pwe4GvnWhU$3yF*@Z}0le#ah7R}PC zTE7*j#5Bhq!PQ$Ss@2;tQ`3B(?HtNG3Nlqhd_B0H-u5M@-uMPw0!u8Mq7SXmVwhSl ze)9e1Fy!|gk=om|lq~5pAy0*J#dIiviPUpLklj2;z^zGH=qH+D@%}EKKuMFbl!W%ukh-9RHYwsc`+;C$*~cgY zac#uMrY8x`ypCD#zgQM_@;xe{iJY(!%3XOQ`fTD9q!cyvLGPy>S9_)K>%!OTE?H5o zaYrv>Mi@hqF!6#W*1NVSncF*({ugdc7>l}-lGgm18E4V? z5IIY`L~D_=T*siDvkCPf56-;pL+_aeEF!z4Sb zdmmKf5!d#b)L$REP<(|FP#<)c!zWMNZQGN4pmHIkhb~MHK94g!2nva6VznOFzKtBP zk$CbFjA3fHX=yJA)a@3MdFLN&;A)7GiBCZ-`?@VUNuZ@BT&xXaH*!wLxX?OX5a%Vnm_f6V z>`)-ahmsW>1N|O)x#W_s;;gUjv+mA)ik9o4P4L22%8pn?`((C>grFaD+`Qw!McN<; z3X9#Qj*30?hrKD?gEjQs_wBJ_irx7Tc31GHb5d`0$vBDqld6gi+2~x2cakI^sAI3w z{&o281MR4%D^c3KR&T-hfbP0N=QX&I%@vcT)0Mpa(Y@FZ@eBGti&BaZK}BbDlDy#C zv%wu-#t8ok8DxnPoYcUH^_t=1e({=CYYy(ER?{s-Wy{GJC|N9u6eyOx^^3Z9P~SlX)mXuZ`;X!c zifWv2cep|ckoMU^P?##6<|!l&btj9P^v|MP^diCjY8f{sm9&=q37zJdqzu|7_nyyM zm+AoRd(avhk0_i^Wpfsj)I+fETGPk)P%r)U;p0;E9@u8x_vtP4zFFta^BSFK?|p4~ zsC{@b94+wHUkdwBQ51F)&N#z6jv~_b&cvybf{7u5c=)yXi7CU#P!|~lgCzjq!9CUA zzJSt0=hXY5Pv{EUs9SEZAHqhqbysu8Ng|qA3A%Ht6gYurNN|fn3om=(J@qosdu)Qu za!!}PEnrn=tcD9HuK}1cTY9S#Y73`6{%M{!|I4+cnUPYw@9vWsrgBL>=7K%KjJ1w-E~m z{Vx*`G;PQ+?k0{9WsA9MkjBM{gZ1Kt>90S->KcLx^Ot}QBWzk~*T*XY7%#f`8AHYK z=-lbELx&w1=tW=U_ua>e_g5<|g9-Mdt8%ORc44QO&p&KFSva6`B7Bp^^Y199gAf98 zLaP)0$8O{EUWg)*$^?Q>06loUbZc_pMSnH`=O`=vamy`&gZuTHR_PHFlZi5lJ=Ovkm2kqxJQ7n8!-ZJ?IOvevS%2^=y2r9fR55)JZ;QWC*K z;jqC2#*U{oCr=aGw>3h;{*F+XXvXB&r<2ka9^94VvIv5)rrTvR%A6Yd`%pYd&;A$C zKWR!PfvT`N+;rY+1l_TdH(`AojXIQ?Fx25|2hA{vSX*H&?|Dn3V_2X72tag5D@riZ zyw2N?`gj1S{U7C)jnlc>GqwZA9s%e=Yoh|zxi!=)6h5C8Q#6JQsi?fePGH#f7U)Rd z0tTBl3b(*Z*~LnhQ=>y)H2sZMss8v!$3r5w7b&YeWfR zjLWso`L)InFsx-qBoTm}KsFNfMFBG~BV-9hUMF1LE?JFv(~E~hx7LYw`(9-q+_vXZ zIQ_!n8j&QoHUbdED7n9vRi<~;`shwryx+b{GCQyRY;~fS6&s1YTauUi$MtJI5pkTo zS81=L6LQuk8as;+Dz;*8u5GK9h`#^cb()>O$DTd>$}zLIP|ZPYgP53e{Efhy)i2Fv zn>AyL>%`rz-t>#?yN+4$318svBbW?smil`9TCi19T)sWfs+D}cVPWJ#??H9-Q5Tch z8ycQF8s%bjq9xfMI)7$cTh%K*Wob;3QY>8@&YFY1?e<-nxFc)R^2N5;`H+9tpxCh3 zOTV`k{N5`6VjckZFt$gu59B4TQWo}5r?4^R044uGFrrU{U5ApTQK2bge&!QJyye;< z^}~paHtKg@s{r^Gq2FCMeR!2^$iXsi=dTV;P;N``)f0}SV2c3&(pSxi-e{xot&5@z zujr7yz@1)Z8%+shH7DfEeY^s;TU=tVdBXO^`@xX& zlP}>RnqZyxUIBS%5m&BibtV#p1{DP+~1cZ0c2PG53fCoiirrcW{ zlL8=n2O0KveEH{P&hz-X_om(;H9_Kky#P<#Vx)OOS5Y5b`2+M$*j?j`=c_ClWwI$B zL?>Fu5u%OlX{w72<~dCg-M&9N?IT(o1r0SlKSuDh6#ZDQ5jIp+U%@xqLvR+P`-2S1 zNty*0%NfjJdx0@mt*03JLd|8$(zv3&_43v{K-B7bdyT1wM)kbFX&pe& zYRdXd4{ba1ilx!T7|6fctM6oA4`r+M?%T({tl!C!o}GSOKGC@v z)yK5euUw%m=jfoagg2Z`EdD)t`rT!;*{;R+=y0E9bXT}}S+WK?86r)uao35{z*&3z zsD{4pmDT1+ZBok&;o1(+4r=%W;U2J5FInldDGW|v7A?<0bOm)3JalpU`7zyVLI7g? zIUeKqn_Y2{Jhu>W17~a}7v?jZbjNB`eXzF0(+Z3({2@Ga`h}iKU^#>?p+sxNOCOCe6o0nlT2wo zmY9ZO{Xd2~#0oD8cVLN%v&BB0e1_=chCbHJ!dQpRw+L`70@5%tyHo|@bZt9xkLQVLnv&)7MgY?5Vd z_8n{2j_?(ay^s1)mtsf35@JmGuI{01aEI2i+}y92@1ivOa2j9}aQB2b&yS&$s7FWL z>c+rTT0ckR?ev!vuy^AHY~coJ>{ltH65-InZH`Q^B_`|`Wd@dm7-1rZmw{U{u56L) z;K+z+o7>5lXB#LjY>KTaF@QUT!GX7NU!>enWbYClp@y2uM>!p|{}u8S`WmY*0W*KUU{J5lnJH#T_uyH8vDTF0ugR{}zNG zj%6^p$@U@VoGD#jm~wkKHa3Hs96a3caNpKojlfDcPLjenxaI9D0OGH&poOy1Z2{mj z=72FkeFE?8SR{r7qcU}dak)M6_kmHpPXJ3mqUV@0aO?(`>voBG-G}gTlA9}mN|L(x zl=zc60yVn2D3qwsjA&1|nn)A+&r`Vj?^C#z;Hk_9e(GG2bbq!Ve)bczXc#l&^*D7X70}noXII^Tq0Ozr1 zY0a3YeNEFmL&{6a85?iQ?6jQK=u!r2t_yRhqUdpjE}o3s;P z5^SF#lg$IxO--@}n`v$uv~8wYHK9~I`PT`bTq)Kk#KL@q6`uR#hwc7iN{{h2;y8K~ z0@yBds}!jQz9-EKQ)HNU&04Jv5-mX$m};-i6zx}KdX*mO6EmCY-CX`|-!BS{pZ2N3 zJg@Izb;e3wFCGl>mW`QZ&Zj)k z{)(m5eySOdu_97lGayH-?TC!azk-Ij8=3TF>?@?+0sMOPW~9K-<_HB3n6+QMU&lOh zoc0~)T@LiMW}MRIFT8-I{C7r^KDfifUETRhiXjm+iwptrt`wbHKuk~GeZ9i~N9!N0 zn8^%3_X)L7I)RQ@zdg?N%TC%6>vFOd*T|ol=Gs;N)vf^_ zSuAYcCRTDzML6f%3Weg{!1B3B&iX<7tWn9qyxVS1PO?V8;_==gx78c}1CwWtNWsm7 z*aTCN+ND`8%Jmqe+XhbPu1CvfyNl=L8#AJM1SKDh9O4!=8;$z!Hqtfa#-n3No;Bw= zvC;)5=QVk;&!UIEp~I$F7-R#-ytAElYZ?%RZn&>QD?C&5dpyY!i<2>RZoVh)%lUHr z8s6!vC%t$wu$ULWdUjsPs`)pm!O@_~zOUd|s(x(d5rg)QGp+Le`i9d;_VF`bj8V^! z+fFqWyc?iA|Ft9?*5B~f{?EOG2;41?@57#uT=?l=iJy`qnGU4p+iz)HY2_h6BGODpPg&z}|4p>yyoIc0GK$9J-LoOitV zthTv{(<0{+e?Pj+Z93|(L*{%gV&qm$#J8lOEdBX7L+*&Jnf{*>_d$5CYM8&3bGUf@ zdf(BbTBk8|eZ^9k@vmaaZ(P#?Ro^r2&0Zf98#gj7_UP|Uz8ZTHkCE|u@^0>gpl#Dp zF79M~R)y#~?NDL~07je%jb21-ri=K!R-L69PU;jcXzv)5St0P$;zw1#Wt?J9*W1Z$ z_-bA6-~C$Lp#hVr@0gJLhA@2zJvS{7LSMOujpdAzt(!UIa2_9~xAvvyL-_2LHoSCj z^2Zd}-N>OAn$@k?^-`Oib^B{ef&=|*W{Kpn4gJz~_PE?}^!>n|X~9w724%ZB&;7Kr zxoQ+JT0msqNw00I!2mJV06$$~iQI_qkvX$`fT?+BI}y?c9BsWhGyHxCW1oUTakM7_$i^fSprthvm(Xr0O8;?yjxvpT< z*c0UX>Eol0ecgw&ObJY3THImP8ZU3p$Xm%E44CjL+AG57zM8ieUo2gW(i~I6T`Nb{ z|8xjiaG^(g(@ywgN{n?+zBFz4!QOU!<;m8*O3BihF`t5z+rV2g&0*iajG-Kq=&Ah} z9)cE$6;QtgcvnJG7JL=gdqf~4HbD|UHNnVqK$XYk6I>h=fh+C!tnmy1;EoMrqL@ypBHM4YGs z=_2>R#duttx}WXNd*%s&L5ac!ql?2#z0lO(lQ1hy+3Ihm^uN>hi)gCMOiwVWrh3C- z^K~-S&b!r;Q)18RFA8{1UiCoE|Bh)ER@=d(G1HW_Ej#*^KZJB zRo8@}(U%VFsp-vib+Rtztd9Dx$lHhM z!*9mRH{NB9P{R4W--~X%e5wC(+d)`y${Gu=H71qZvOT)mgwgv}chBYKWR0WnRjP`0 zNj*Ey~-*Bn+R;sD*9ej$EsjdlHOEs_ham(Wymy=IqGg_O;h&W$7jvV+wN3 zML~s25&6Zh0TmSON0Y-yXuO zmawP_%MsZKSe&>(@E5NY`%xwjyMI1-7!kJhf z4ZFNw@Uz@9!l(y_Z^<4(@7(cCULh=Ke<*3e!zl1#Wl<4b#YJ#g<~4=aeBn2&Sd)Yj zhjxc|=k%Rf5fW!BPX@lDO0U;+^G2%KkEc(%$^tiJjxt`;=bzOLJ%6TkCUZ4Pw#;tCd3%ri6@fe7)O5etd$1b5 zm)NiVBEi|-tR*(idHRJxmj^Zz7#5T7GLphOQSWuvz2mG9ChLr^4Nq7xn5uePA^Mw8 zK!G63-wj8L+ioz+Ar5keF$SU{U+eCQ29BJ19tnx%Cg}K{{ZvGw4R?C^^wTohs#Pk&A0{`L~5_Tu?qU23JqDY$(-lQejRcMcp>|MTk?RbuU$7K zx6eP8?lL;KT*zPtBj$}1q^hqV$Jc+o0BDFU#r}ZamSQTBTZWbP6Z&S-nANuM$aa{a z9EZ?VN^pM;TcUF2PR4$5;+jy5_iT=;vdZ0*92a#b&cU_O-oOEn;5C4r8v}0J3yw)9 zPHjq-h}@6^#Sbhu4{9YXMpIZHZ7N5y^am`LTDRc#i;h0*x5ehn>S+%|iZGw`umtQb zyjXlt{dFe|T=<~+CkdfeEq*MZeMYD7@7R|5S&o&O{;xbiY&@0k^B3+D-_KFGzxUfN z+YyeUtN>L{~vcCK4KZZIOJY-c}h|-HQ)QQdQt644J^{IFxRc$j; zk~2Yz3;%_4sC&EOBC$KqGPZlY_4JQ)wnp{gw2>|JuxaPWfswbYAw3G^@wMuUNtWir zhh<~03&&VQ*L#`WR0W3_O7C*-I~|=)-WY#%zGF_7BG>r+{*&<%wI=_kh8x^H? z_0^hrf5+$R8TS+J@SBzNtzNWDc-{6=5TzAFRIpIt{1^4}#Y&W9p4WjN-L+m4cX(84 z^IAG0Rf@1OSm%DX0aN4SZNEmYxqnl)A*yNf%~)YsHY>X8a***g=7Bu`a zWbwCEYni2ZwnkQ0U-{~R7(a0Kgqf*Nu8`CTL!4dvq}Koj=G&2l#Wp9k=;hBK5*DXw zm}W5q3(rwUK54VOt#8~>n-h=9maERVj13MumRAsd%GwA}a~;WVJ+nyXI|0RCU$dZv zj>jEyWxlK}v;6cuJ{ajx6qSHwZ6{ZGnIxz8$U05eI&oGf&q(|IaAkVqRlC2C9CbM2 zF!SZl@Ce0tOQMeO^3*-T4+ha$k>6f5?fj#)$9o}CrRFL7{>QhBriIHWF@9C{W@B$7 zI?i{tg!vs63&l%9i_a>@V7@0=byIQ{ej|AaUz`P^deSk%h_tt97!vSjD&pchx~SHb z+w@14u>q}-uukZ$M~YSys)8fiuWr6}Z&mrzu63__+dWoww9ms| zoBLo8=wmRol#M6n*$h4J>7C+VZQN;?e$^LH=suu#a0h@PAGc~hTy>H~wV-j;dSQLC zbUa27CYqbDv$@kCeSWT*fE5UEJ4wH=*8GA0le2VNBv-j}^;wGjp)yHf+JS|IHu^`- zY*A8_kxsAd4`9(;zjrYNJ1t}W`}S-`N9pbx(mz|LJ-G5Q+kNr%oB&%E?>B+t>C^QQ z4Oa2ury(cWz$GxcRZ}LrjU@J`U8Q(XwYi0Gx?}!Ouj*Gw)~!!Tdxf*>P8zwt*Ztb| z46owPX6n{?Yj1msIHw0m$-T4*{kj-hoCdA(cIPwPkL^Po2_HqPT{ZBes&U8sp@$fj zxL|tc_p71&U#?F!u|1j2w(ogVe;Bx)y~EeLX)IK>lv#Va@Yxl7zPHbN16tOkIQFdW z_%(3;l)u;0+_U`LXAxVT6Zxd(_@i3$SV(eovJ0EY!L)g72i?i-$7c=~Z}x8_&M3?J zURc%rlH#->-Iu(OI2+B6xc;JZQ_ynq@O&TiLZdH|M}sU&ypPGR-qp+v&6n)k(?>Gn zsm-r*2Uj@VZ@R;i>gr{$joz+;UDZIzR0po+)vf4P$M`QQf#qVB9o3(7C^J z9=jtIs)=x%ujRbKg4m2~YAh^w$sU}LK-`@^3p9(msu0iLm+Zd#Q&PCMv*CEQf5)6uEn+k!9ULrKhYd z754GMW?!k@b8JS~6f$oz6YZ~D8}%hRT#MB_%Fpmd7E~QnznjtSK&_wx+I}PWalt$p zZz*3qNEF!obIQ~c8^YH=vXNX>Vd7S=1^&G*?y=&4wSQ5*O8SOty6L*TQ*nh_x+{1# zxmqLJx1Wr*u&P#g5@un5uBUq|Y6GkB!%ZRzc^gk&FCij7#;CgQf;5Vw)Cicm9jO}f z^;Tn$i+CnPGpz-P`c~vNg*$Zr%5CQsM70KMQoMs5Q!a+X-1bvmQrK&X)V{D>RNpl! zm-rsqD`b0Escr#>Dc!U*Y0_eZ z?fqm%lDb7+0cX99U>rquARBj8pr}d66+bnzfo&?V0((7`F)d&} z5dG1qL7rq!-e1doxC-sZ4sV~av-yP&kLv**?ty>%b4TyFj^BpX@W5`EN0Y13&* zTXMwIT_uwkFJD8>fCT%wuwvNrv=FHwmkjHJ4g~MP@W%P{@d=HCX)2SKG;7pLZ_b;; z;Q@!BC1+dQ*5{2GHla%T!-K8yw>G%;C27xOpv{E&$QRu)BK=k`K)dB)ni509TA*Pm zt4q|BnwR50I}6tsENa>u$e}mVGAbH8pRr$e?JOupTr5Dw$2Q8T{P9Jv#}58_K7<%l z7W_H&FeUgiNO$UixB1Nbu#?s{Ht?=>y@B}gdcQHFIQxf&2X`hDj9w%DN_{Vy`z3`i zr6}|?GIQbgN*k?y{F0kEKOCkGMcKk>20=oy0ypbg;;z9eYngLnudX?KX{zS|j(Za>P0O_i&D0~yNv`=Gq70SsLhu{_W@{YK zv@-`ZW4VD%!2bMZVe`nuGnml{u~Mw7dIG2!&8nFl30iLlYx#XqSTkz+Zqm0^I)%$c z$kZ6rCcCPq(_59KWO+@%F|c8kKNTgmv$}DRpKRZHK5AU=0Hx9Jq~mR8m*_CFEiK2l zZ*PKr%ZWd%DoqqgJ6{iVJ`Xu_Gte=f$cqXw_BBs1(yFiBMns?C9?eu&j(v2WxYhEY z3f16z;&l>B#Vl1>(cq_yP|7MEWP(g^b}*?)6H;d@?rzKrf3|%!^oTUDyRMY7s;YZG zc&_}q_a|0`wAmU52W&Tin@D5h;jH^igh$6)v8chSzdJW3?)^yeUilcoUw9k^Q%~53UE#MtCuK z%wk7Z4&sMOABv?_zja;nAXuYtPPG1HBD*uOiIEkd91hBgCZ*3Myf((qc@xIISC+iS z;(0%a89Jxjj^eQn*czvZ2skT@|2Ugh5W7dhW9PQYd0V9XnZNhw1M6%rTzN)akD@2! zYE>dSiiI9pig~gu=wQ?=?-}|Nx`FO z0bhe>)HCI+zo#|-3OMELyR7|qpj9FehF^Lq_&L-T(>A;wUY~>cfqQo>ELZ6O1=s)G zr=BpR5cCyRG^eirBpM=U(J zWupy78mp%doljkOEYdP#<^t~TDlF?XtnIgc)8F4;#!{vnFZ9jo&CqRrh#F?$8kiF0 zP<*pIh3+BqLzk|ZA6=N$Qd!ARrAv8!cno=^-y*(m;pU{#{hMU{P7U58)&OaIp}+qo zWDZ_?s}&DSOSt)Lm+xdY*OAEm?lghA|HWkBIxZbnE*$p3c_M=$v%mgc!>nB$e$(RF z)WvCAr_O1D%MGQd`o`07iX0vjU(av?xuu4Q|6p`xOv?>>{Q@581zj7Qv=eBb(k&6r-Wcl{O;dS)W^U&rNcYW+BFXy*@wmz;+T=2b7EU*53MkFJKYuI2o05#)r5K zHIm7zi$n6Bo)|wMh9b!|QMLlL8|Do87D#E89!nG33jcs=At<>R>yy}lgLT?y`%*vy zkn1Nvwn=!njB5N0SNRA8SYJSf?nxp0*9$-t zfc2pDhl@(Q4a(qiKW$1q#D)Dx2zwY8E>HY|%=xUc^=FA5V=Bc?~HhTc0w7|JM}*m+MeO`B>uHstv5cZ&)msEW$x zOV>z#gbdw6!_FJ|U(}Y`&CU-)**(@qkvW@FWfjD~IF;yzuftDfeB~Zo>=!Qvn3M(u zY#KKue%?(@t^Oc{v6Yn1k=fqi#~pV?=zlRX8X$AQmxISE1RMDO_B13>Y52b&lo9NB zMhoD3^zJCcNnI3}k@j{$mJ1n;bHY6WLBsUe$7@8Z_F??6Dkrae{SFKJR{^IboSH`LMTCHU)7IRZzcBL$+yr^4d6N@OBN%hG!e& zGmJO-?R65g!-)`QO8pvOl_Z)w=GQuH1Mxo2(Q&SH8wlL`J0s=p@zhv$T;PBXA%oFy zJ@-1C{qxND{l1ts_wGk9&tUB>gos;3VwK!5Idkti2wDN1>gQ=C^}h$D=J{1=uIE)ym`0b?Cjhq zef)UXoyE_)eLJGe(0{ivb=6Bo%3?IN<0Adu%2C)Gr^(8_c+UXI&&f8KHucko(|Hl6 z%n2IEh5!_HkdY|0OBpFm$>QjjWH2)2Zh}dAWkHS}b~4>Nlv*#e+-q?2puZPj7DJfX zWp5r2 z)&Kwfr-jN^X^~}A3T4Z_lM0o!$dWC|zLlL}MwER?vNK}|*-7>>vS%mzHiNOwU@&IJ z7|V6Mm(S;W{eIW=$Mt{Rs5x_<&+~Xa?)P(Oi*uM~uWF#AhS!O;S4lu9MsRp*vC9=T>#(Tlc=~7IdAhF zh~Jkpq=w3UFR09$VYK&Ke3yikn%0=UlPP6B3$2%SV(dlD$#VQmcS_c$hCy(kKSyH8y&I*0kGF@-k$2Ylu@97}QDCSNd*NIivtvd&?vP__9(pJB{Lt$D7)GAiNGUczPMRtkQ=pGA{eEmsHLeWe%x%(`3Bsq2OlJ|32SI zTxv{F$jxO5h_O^oo$&x&9)0SaeRhIPjO9X1&Qo8Y^WCDi^ptfePO~Ry+=iJxQb0F` zCJG+rC6InP?t!+c%$&Tq@X1GYT!>O`;cXg3nlp<4;cc zN?~%2=nLqqntiO^4mD$Z|~D9QVkl4Cp=Y7W)72|fEvkB&SXDg9_EE+TtDqPz_B+}ZG# z1WLQHx1(ZVrtel7B|@cksVvgH*01L`S>IV?a^8KhE793_^=UWUyK;-abLOzty=TvA z(K>EKU!bwNIejG0{m%`DMyB|KwXO4n*y^coY8&imb>GN99D=plaGC$G_UMO|1Zla= zQg~IhNK7_1wCUS>v+|uz4hxH%lR&YRLws+7~Ir)hek~ny^7fFAXS2}@!K529iOv|PXFIV%2IgN5agS)w;-#=`Ee+7?SS*DVmFQDXB zk~*Mh;%*fqdxu5?;u;4Uxg=#pDzX~NcL{Jmtu0lJJ;J7|T`Q3+WjK>lan;0T-vSO9 zi@sa(TGu5{>8>%v{Q6n?QW|zIdUsB@ULx>06AC9%CdkEG!}n+R!ng@V?-Qv4Nk&2@ zca$Vl3NKUXOE{(6hf+9I3F0HpCJ?mh^Gd$~b7>l`#5_dDB3=7?5gfvKdZ0JOqiSYx z==zlEm>tL<)sj6YroH5Oa%;q`#)^(v{R>kiCv2=vlf@ziI;==YkkzBYCq3}kc(cSN zeEwJvx|{d6c;25M_r8=dnDIOniD|XjSG=mrSo)UZL%2WUtevIXj(tT>VQu{5KrZKr z71VMN>qiZ7gb+|Q2(2*DYW!o&#WvN8 zcyXO_W=tEl1e|Si$$S?s;p*9+V^QCu6!!3QOrQ_x`ad!SS))w<2&>gFu16eM+1m*x z7}R+Or{%dK5T>D-K+%vBZB2Zr0~Q2F=o=n84c&3t=)%AMbo4>4yC zXs=xTZvU%&rn(_HS~z9@(cz0f9~k+oB%UOkG2mj{xn>47w?le4$xwg*Gf)a$w@bl3 zM$ML3x}-=v9^hFrdN&kxEwx`-3(^SXnCb0X**Z{1I*nK^eD1U4HECv^KvyXq5M-p# z63`7}D*GDbT^6$HgO%^pSb_4Dez-T@fqq%gNZO_8BTSZ(Kdw^iH6%n@ppuyUQ|tpH zcp~dV*Pq%;+8^!%$z%P`?fc`_#{Nx2)y=zdpOGv6uAPsnaFs=B)H-OaH8 z#!Bo$IeC|wd-%XtqJFVSWcgL{N7B7o#Z7^XW+$F|==Up;#u4CM-1g^$Az)n`8*M71gTO;-E6HX&=oR797+5 zxFzD&aa{k#Tj8&`{u}Ik%^4N8)QR0R-$=9?aE4sYElu}1266{joI0KZ_vd|>?XIoH zK7f!t-pW{1UTI?636*Eu5f|xalaBG33lC4`*-*w3S=Hvn z?2iNfn*+M}%x|*h;w*qTP#2&7{0cMgqOblUTzXFaNB1-Q@;JZU8F?P8?L!K) ziv!m8avrT7L4@Z9i2$KN%C5VB5gPA!VdIppH$4vGZh_vTWfGBsnm zL1_wuij`3geV^Ym3wSRmLhRF~cOh4x!|&JAC0?|JFY2sFznK)P;5F+r7U0DrvZk!j zN^W|*?hkx&$qQ8}VWs7mU+nOthS%88dBVU*(Yscs3aBi(4zg}S@oCveA_goA)Epf8 zj9oj6^>T6xz;8CN!(eh3P4{}Tm8c=7(j0#>Rv+$2&0}7?=7OZl-W?C2b4nGn*7rHj z2HIEj$$iKisEzTx!;;V2%u&s2`;p@dx6mh^*^_?<>nS&g89Seif?3TH?HEcs_yT)^ zrgfF0$NtXN2j3I`5EH}WOR-JXiX}?|XwrGVLB$qYO@k6qWYLODIiJ0GA?N;Nl-izwtr~)77Bc?BnpFGc_Ai z5mLfojPF|09LLgh%ww5f1^*NnA%4EtXAKiYg3i6Ua6bwt#fQyZiH*8H0=leds`2S? zHpRvnCb4Wyi1F@$;tP4)DpHsXiVdv}lG8UR4V0vbd>S3bd)gmm9GA=UPHMNJru-}v zPJ{xDR)Ej;KIXGG^IrU?-y9cU!|+^Q8D;h0%PO0g+Rn}7+q3`B*)|*rd>{^P;EKM{ z2T(I{;=A6*1?l`?W@~W}$^Sf1QwD4lHS*^chdKeUj5Sap0R4!8`643REe+YrO=M$h z(d`I2lFzCGeV2N{)@SqdwkUHGZ1y{bMs2<`Y*BgLRHFg50>(yVDiFZ~_xH?tmsYP1 z{wLZTGXSDZe73a5;)O&yLG3d#tJ@_{0D$#rMqZ#sdETT(w)eBPWLel=Ru$d7zTwT; z%XW$Zjz*buo67+mZnWh9Vtj*ObXmFORP=?mplCWSAg&5MK3rxwE{y9jq$oD3@tzus%}_Rc&I>og}g**HS6N^KtwbFku6z5*B0J-bI?-9CktQ&TCZ6t z7T$t;hcC=zJ1o!ub>^!;omp@7PVyGeXZ|U_`{t~6Yw|0z-rHGp7P(V}*Y!XT(vl(C zpJX5|D1h%eFJFF4O2XeZ+?0@!#Hegt6Yfto)j z(C`Y?YpGAR_a4o#+w_y4C0_Pn0}z)>En+<5r?xMwosyn>!8G)Mxg_l!OysPB?e1CN z2>s*hFUnleDtU#|x}|Em=OWL&N$!`9JAr?k)q6Xf_i+}+Y+SvjdJ3L z)hEy|_$g5T^;VEL!iV$1UhA!oR<=U4pQ@FZy2V9&Dpt?`Hs+o#qL&2+a zGNlX1y9WVEqB82|uD`mRotxF%SBiryZS`u+vM_H%YS+DW zc~&{i(x+hib<}fi>gnsEz;B~|-CaKX#9V{zKEs0xh4N?RpZi{FVae>b1!7RKSHTaM zAG)mnoDSmUjT{5;T6VETr!^wUPBT~^% zc!Xs)u!Kvt_-oP!-rL@`?dE!9two%8<3bDmod-J>fTDvdv+^U+qG@BYW%9P?OPjV4lT!l z*;2-i9R}g?R8GettH%7B+b<+>&!mhQv$vmLS4(z@2vhOgSn+oJ)4cZC;l3i;6nRFa z3U6P!G|GaB$+DWh+GKB28Pg7YHm~s8f-+e6YVpzecUM(BPh?YmmTw_pjb&SjlWLY6 zR)+#HdqWi#wYA+wC{4q2;EW%uCsg@X>%ZP^E6yq{pZf%`+Hc`|bV8z~9Mf-`Fq$bY zB}Jxw0G9>|`Xq$bpL|9y*l89E{NgG-euvLi`{#{~+&I2?PwP*&?a|O$PG2v#VR=z> z^QGdsc3QRMm@7ubvOq>r-RAu_cW(RHpnQnffC>|ZkBF?DQmg4WTWIlR`6sc$HZAuS z1)>1<%iJ7H+6BB1Ve`t}g?0I@Ll3lB2t}FMVlTlDKnTTC8my<@gz>V$9-PhohC)2&x5-4rU z0NC?-Mw^1UHh`1}%niQ4)U6wAa(EyaFD*|PaHFr$&oNNfs57u_L&1Tm6xIp z80+uP0!BEVH0v3?{AF49pZ$njLJvk1{pL?Hw=wL!r~q%TROlMqnrPYGPw*+5Y%+d} z^6(GGOg&3T7=6#XY5LRyzpVwWq&v9O4}9+uwzPtY^VVM^jG8sme!A2iZ#=su47l_x z3}+Qjs-7Zv9DDXT;`gS>{~*p8-CT3=mo@wI)th_}3^-%1)a*C#j@TIQa%D?9ct`xX z_I_E#Ai09&^*JZo7^?0IMOpw@^%OymiB*IG$sbLHOgONujEM!?!o1xcdaHco1AHsr z<>3v{EBU;<$}XT6*#0KiX)qSpaDW|Xx1sig@O|sCf2nf|0D$_7Iya7$SIPU1))i!a z2!9VSS|%`Gg(jG<{4sJKksPXYhEsb-;W^OcI8S>+tM_69u|l}}ku+Sv(ai0n{BP9h zugIIUTU2j>#gXt&^^3GI?Z)%y$Pi8+16r>&gkih=`v0 zdrtgLcjTT~mkb+$j4pz4H))o46`Z9z^6*#3?6V41vyXq?F?K#yeC}?%msC}B-XVEO zJn0##O&46OK@v8PP9S^LlpzIQ&D={zJs4d4g`-KoAg#8M&WftwIw$Tx=zz|?^Sp&1 zE2@5H=eY~FRU!Z+uC6k=)FwuFce=I>I5}hJ2ShISJMg2Ia@k-Kwruj2o4(=lQA=?E zY6Wb2Yu>rab&brX)+NDEvJS4T?QX zvOBVsz2H#TW(a#M z-PD6-RL6xDvEvD^)9Kbt1^9Ej@S-%$p8E99#lTISbAfRvvWJ%OWDLo8X-XQ?v%&iJ zfamS>yVdD}=y54|#ua?GQu6k{KAAE8e|$3K|MAH*{Ns};#@Mrq{Rkh=NV`(r?;MSK zx0f8Iwkb}Y=$#K_4(?I2akx}qRWXveb3G;}HId?Zw_$dnb2!z%=)1plav(wa9>In= zm3!_4J~F|oU8$s95$7p>l7P(GxmlCk_u+Tj(Sh%C);D60G2$wr%|L%Tn7Ku-h3@|I z`>nuTo#izlz)*m2WB2l9fT0o8cnb>juVeiPyT#RdA>2+<^a}8)T-6OZcdU?sgSOd) zgMhz&`#K;V{_OxD0c$p&MLq%sr;@Yf$!eR|8)Mot=CV~iKRpy6yvR*{eZlwSsFeVx zhFGfzciB17R4c{$>n1MCoru&CxUblm|0rYtA57&nwXC^pv9R{flE5Lc<)xIcw zJe^p0qq(qRwTj%2FHFuGt_fZ&`meTWB6T)J7;s`Oz)CQx)&&pIQ)KJb`2A!M3?2O6 zRWg4p%LW#YT~C>JCiq?Hql!Y4O)rle3LxVpI&Ow_IJKD;b^a@o$@oho6B;{m?Y|-! z+s`r0zz>OtwBQnGMcF4U$T%Gcu-QD7330iYnyL|;?l}*;y{g;-0OiM-n2YFRn*vgR zythAX()a6uKq&SgP@@t7$^HFbflLnQq(c1O4IU9c>0lk0ueM6T_T%+bcsl%dr@weY z^5@vU>$LwJ;Eeb;p>vh2w*W5s#%5anK!jq>O3;c~llWf{FHo5kr2WMETaKrW?gMkE zcrNgafC=l+{lGh_AO-v{3tMdZeD<3}FwDt2`)+c9rU0je6`-qV)P~vd0f~o)|7}Tp z@V^q7mQKI#Pa0@H(^YDt0h88W1Dp+!dUG5Q5{OCC3Idbly#HldQt0=cd%0{#qg#X4 z0GTTKTI_~a^P?{e<=5qb?SeK)AddogoI1YASCXZlqeYXw)HQI_SgtOgmm_egAum^Y>8KM!QpoUUFy3oZU4q_sm1g@Cg4B&!RUhKuW@M+(+#M^2-^ahL+m=Txt=gD+{pC*q6 z_4Y)oMMK0>2Cf)82A3G?Q~uIC4}li=J}gv05(&8W{^NxEYofpSXQJnm*#<}j_^}p| zyT`M95UAOm!^QFK-tR)B7`uDDzBBXhG><`h{aGKnI+4zsVjvd1hqBpq|Keh_wjL&b zX);J5>Bj1&BlIMqLO^ccnK^-${KE{7pSSQgw&U+LoHuVuB|b~hc&oBj9C8}iVro8c zz>&r#g<%D`G1!AUchNs0cGIeOzUhI4SIK~)Xb=5u=rzaC`g;B&j2mFvi++wSlViWd zzq&&kE<`7bnrQ=`2j�u@Ce?R}bizYNc> z8B&6Hg4x=5yMTuAKf)L`7$4v@!n6DMYPbW+03GlVa2L@I0@f2+R?%PLOs7bECqI() zR?^=~^bHa|&NTk*fvdQ6i4h+eelv@Ae@=CwN%C_US)_F{Qh6xoS5QaC>w#H;-M>b+ zUrhh4r5UZ?`Xya9bT+ePHciTwLFYd7cUiiQ-mka(-Vo5+a{_+u_#Wbje$$|uaM$Wt z@fDt~{kq5Bml^9n_e>&l?i*4!76-v0EZM11~}g6fDXyIGM@(m2|hAGfd|P0 zCcwB(iJ%a*Bb0%zc13Ld=dj@FBv}pHvi{R7B0&I}CGtb#ZrslO48h2zfh^Xy3>LtN zJpV(-rHg*;ctF#(`#sh)^FlY`s~rz8^8-AtTLz!^bI-j#pLB7JjLqVYWqM%_Z zrkMsjUW&eF*i{`~(f$2`2l&U8|1+!S#v?OGm-PJ`K}%J(YWl;9Y`~MvssL6fqosK1 zmnueIZ?im}(sMuB!vA<;$9-A;9IGwXHv8n_+T0WG(xxI6;3+&}(wO0DoxD)-&iMiT zS2kf#E0;5|S41izvL=Cr+G|vGo_5!#0;fC~Q3j z_EhBV{R$K|^CxY>!-9E7@0hQg+l~&~P@X+?D{7U-XVZIrOK&#J!(8j*)BAuaMeOmb zdv}4fYF0_UDvu4;bzHuK%Ma;*jtRRbj;IR91fMH$J0h@P`pt{n_uF0iKLH06$Ka

t)cx{jte?!{q{bTnsy2 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 diff --git a/docs/images/evidence-backed-agent-examples.svg b/docs/images/evidence-backed-agent-examples.svg index 88138d129..9ee101939 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: e423e99ba683c1928984903aef715ad97440c9717cecb3bd3f6fe4f6746ea7d0. + 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: 3b1d2127e290e0ae6fb6c411263d9c61bec98d1b27e408d3da4cb2d51fa81f9f. @@ -12,7 +12,7 @@ - + FOCUSED CONTEXT Find the relevant passage @@ -24,7 +24,7 @@ - + CITED ANSWER Answer only with support @@ -36,7 +36,7 @@ - + SAFE ABSTAIN No support, no answer @@ -47,5 +47,5 @@ Reproduce: eval.chunking_eval + eval.grounded - SHA256 e423e99ba683c1928984903aef715ad97440c9717cecb3bd3f6fe4f6746ea7d0 + SHA256 3b1d2127e290e0ae6fb6c411263d9c61bec98d1b27e408d3da4cb2d51fa81f9f diff --git a/engraphis/__init__.py b/engraphis/__init__.py index 0b06b4c42..c4cf559d9 100644 --- a/engraphis/__init__.py +++ b/engraphis/__init__.py @@ -1,40 +1,40 @@ -"""Engraphis — self-hosted AI memory system.""" - -from importlib.metadata import PackageNotFoundError, version as _dist_version - -_SOURCE_VERSION = "1.7.9" - -try: - __version__ = _dist_version("engraphis") - # Editable checkouts can retain stale dist-info until their next reinstall. The - # checked-in source version is authoritative for this runtime and must not advertise - # the prior MCP contract merely because metadata has not been refreshed yet. - if __version__ != _SOURCE_VERSION: - __version__ = _SOURCE_VERSION -except PackageNotFoundError: # source tree without an installed distribution - # Keep in step with [project] version in pyproject.toml — tests/test_packaging.py - # pins the two together so a release cannot ship them out of sync. - __version__ = "1.7.9" - - -def _default_memory_engine_factory(**kwargs): - from engraphis.factory import create_memory_engine as factory - - return factory(**kwargs) - - -def create_memory_engine(*args, **kwargs): - """Public lazy wrapper around the v2 outer composition root.""" - from engraphis.factory import create_memory_engine as factory - - return factory(*args, **kwargs) - - -from engraphis.core.engine import ( # noqa: E402 - MemoryEngine, - configure_engine_factory, -) - -configure_engine_factory(_default_memory_engine_factory) - -__all__ = ["MemoryEngine", "create_memory_engine", "__version__"] +"""Engraphis — self-hosted AI memory system.""" + +from importlib.metadata import PackageNotFoundError, version as _dist_version + +_SOURCE_VERSION = "1.7.10" + +try: + __version__ = _dist_version("engraphis") + # Editable checkouts can retain stale dist-info until their next reinstall. The + # checked-in source version is authoritative for this runtime and must not advertise + # the prior MCP contract merely because metadata has not been refreshed yet. + if __version__ != _SOURCE_VERSION: + __version__ = _SOURCE_VERSION +except PackageNotFoundError: # source tree without an installed distribution + # Keep in step with [project] version in pyproject.toml — tests/test_packaging.py + # pins the two together so a release cannot ship them out of sync. + __version__ = "1.7.10" + + +def _default_memory_engine_factory(**kwargs): + from engraphis.factory import create_memory_engine as factory + + return factory(**kwargs) + + +def create_memory_engine(*args, **kwargs): + """Public lazy wrapper around the v2 outer composition root.""" + from engraphis.factory import create_memory_engine as factory + + return factory(*args, **kwargs) + + +from engraphis.core.engine import ( # noqa: E402 + MemoryEngine, + configure_engine_factory, +) + +configure_engine_factory(_default_memory_engine_factory) + +__all__ = ["MemoryEngine", "create_memory_engine", "__version__"] diff --git a/engraphis/backends/embedder_deterministic.py b/engraphis/backends/embedder_deterministic.py index 14bfc3c53..c03858ca1 100644 --- a/engraphis/backends/embedder_deterministic.py +++ b/engraphis/backends/embedder_deterministic.py @@ -195,13 +195,13 @@ def _variant_features(text: str, tokens: list[str]) -> list[str]: elif token in _NUMBER_WORDS: values.append((index, _NUMBER_WORDS[token])) - unit_positions = [(index, _UNITS[token]) for index, token in enumerate(tokens) - if token in _UNITS] for number_index, number in values: - for unit_index, (dimension, multiplier) in unit_positions: - # A short window handles both "60 seconds" and "60 per second" - # without making unrelated numbers in a long sentence collide. - if abs(number_index - unit_index) <= 3: + # Inspect only the qualifying window. Scanning every unit for every + # number made dense measurement documents quadratic in input length. + for unit_index in range(max(0, number_index - 3), min(len(tokens), number_index + 4)): + unit = _UNITS.get(tokens[unit_index]) + if unit is not None: + dimension, multiplier = unit normalized = number * multiplier features.append(f"measure:{dimension}:{normalized:g}") @@ -210,8 +210,10 @@ def _variant_features(text: str, tokens: list[str]) -> list[str]: # canonical unit feature above. if any(token in {"per", "each"} for token in tokens) or "/" in text: for number_index, number in values: - for unit_index, (dimension, _) in unit_positions: - if abs(number_index - unit_index) <= 3: + for unit_index in range(max(0, number_index - 3), min(len(tokens), number_index + 4)): + unit = _UNITS.get(tokens[unit_index]) + if unit is not None: + dimension, _ = unit features.append(f"rate:{dimension}:{number:g}") break return features diff --git a/engraphis/backends/embedder_st.py b/engraphis/backends/embedder_st.py index bd546d0e4..9c8237d42 100644 --- a/engraphis/backends/embedder_st.py +++ b/engraphis/backends/embedder_st.py @@ -33,6 +33,11 @@ _IMMUTABLE_COMMIT = re.compile(r"[0-9a-f]{40}\Z") +# The shipped default must retain a durable identity even when a newer loader +# omits its resolved Hub commit. Operators can still select an explicit revision. +_DEFAULT_MINILM_COMMIT = "1110a243fdf4706b3f48f1d95db1a4f5529b4d41" +_DEFAULT_MINILM_NAMES = {"all-MiniLM-L6-v2", "sentence-transformers/all-MiniLM-L6-v2"} + _LOCAL_ARTIFACT_HASH_CHUNK = 1_048_576 @@ -186,6 +191,8 @@ def __init__( loader="sentence-transformers model", ) local_source = is_local_model_source(model_name) + if not revision and not local_source and model_name in _DEFAULT_MINILM_NAMES: + revision = _DEFAULT_MINILM_COMMIT local_before = _local_artifact_state(model_name) if local_source else None from sentence_transformers import SentenceTransformer # pyright: ignore[reportMissingImports] # lazy: optional dependency kwargs: dict[str, Any] = {"trust_remote_code": False} diff --git a/engraphis/classic_assets/dashboard.js b/engraphis/classic_assets/dashboard.js index 03f54e327..7992b2c05 100644 --- a/engraphis/classic_assets/dashboard.js +++ b/engraphis/classic_assets/dashboard.js @@ -1,1832 +1,1854 @@ -const API=location.origin+'/api'; -let WS=null, WORKSPACES=[], LIC=null, RELEASE_VERSION=''; -const TITLES={overview:'Overview',recall:'Recall',memories:'Memories','mem-editor':'Memory',proactive:'Proactive recall',why:'Why',timeline:'Timeline',audit:'Audit trail',graph:'Knowledge Graph',analytics:'Hosted Analytics',health:'Memory Health',consolidate:'Consolidate',automation:'Hosted Automation',workspaces:'Workspaces',team:'Team Cloud',settings:'Settings'}; -const ROUTE_SECTIONS={overview:'Operate',recall:'Operate',memories:'Operate','mem-editor':'Operate',proactive:'Operate',why:'History',timeline:'History',audit:'History',graph:'Relations',analytics:'Relations',health:'Relations',consolidate:'Engine',automation:'Engine',workspaces:'Operate',team:'Engine',settings:'Engine'}; -/* Per-view subtitle rendered in the topbar next to the view name. The body no longer - repeats the view title/description — the topbar is the single source for both. */ -const DESCS={overview:'',recall:'Hybrid semantic + retention search over this workspace.',memories:'Browse and curate the memories in this workspace.','mem-editor':'',proactive:'What matters right now: importance × recency × retention, plus the last session handoff.',why:'The current answer to a question, with the facts it superseded.',timeline:'Bi-temporal history: what was believed, when it was valid, and when it was recorded.',audit:'Local governance history or content-free, tamper-evident receipts for sharing.',graph:"Explore entities and their sourced relationships from this workspace's memories.",analytics:'Hosted growth, retention, decay, and entity insights for this workspace.',health:'Memory lifecycle metrics: age distribution, decay rates, and staleness.',consolidate:'Run the free local consolidation tool manually; dry-run first to preview changes.',automation:'Hosted maintenance policy: consolidate, dream, and review on a schedule.',workspaces:'Switch between workspaces or create a new one.',team:'Hosted organizations, roles, and seats for Engraphis Cloud.',settings:'Theme, update, and connection settings.'}; -let CURRENT_VIEW='overview'; -/* Loaders that compute a live subtitle (e.g. Overview's counts) call this instead of - writing to a body element, so the topbar stays authoritative. */ -function setViewDesc(v,text){DESCS[v]=text;if(CURRENT_VIEW===v){const s=document.getElementById('topbar-sub');if(s)s.textContent=text}} -/* Plan pills are async: a slow /analytics response must not repaint the topbar after - the user has already navigated elsewhere, so writes are dropped once the view changes. */ -function setPlanPill(el,text,cls){if(!el)return;const owner=el.id==='an-lock'?'analytics':'automation';if(CURRENT_VIEW!==owner)return;el.textContent=text;el.className=cls+' topbar-lock'} -function esc(s){if(s===undefined||s===null)return '';return (''+s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,''')} -/* Scheme-sanitize URLs interpolated into href attributes. esc() entity-encodes but - does not block javascript:/data:/vbscript: URIs; a compromised or misconfigured - license server could otherwise push a crafted upgrade_url that executes script - when clicked. Non-http(s) URLs collapse to '#' so the link stays inert. - Control characters are stripped before the scheme test: the WHATWG URL parser - removes tab/newline/C0 before resolving a scheme, so a split scheme would re-form - as javascript: after a naive scheme match had already failed. A scheme-less string - that still contains ':' therefore fails closed rather than passing through. */ -function safeUrl(u){if(!u||typeof u!=='string')return '#';const s=u.replace(/[\u0000-\u001F\u007F]/g,'').trim();if(/^#/.test(s))return s;if((s[0]==='/'||s[0]==='\\')&&(s[1]==='/'||s[1]==='\\'))return '#';const m=s.match(/^([a-z][a-z0-9+.-]*):/i);if(!m)return /:/.test(s)?'#':s;if(/^(https?|mailto|ftps?)$/i.test(m[1]))return s;return '#'} -function showAs(el,visible,mode){if(!el)return;el.classList.toggle('is-hidden',!visible);for(const name of ['is-flex','is-block','is-inline-flex'])el.classList.remove(name);if(visible&&mode)el.classList.add('is-'+mode)} -function setTone(el,tone){if(!el)return;for(const name of ['tone-red','tone-green','tone-muted'])el.classList.remove(name);if(tone)el.classList.add('tone-'+tone)} -function renderMd(md){try{return DOMPurify.sanitize(marked.parse(md||''))}catch(e){return esc(md)}} -let TOAST_TIMER=null; -function toast(m,t){const e=document.getElementById('toast');const kind=t||'ok';e.textContent=m;e.className='toast toast-'+kind+' show';e.setAttribute('role',kind==='err'?'alert':'status');e.setAttribute('aria-live',kind==='err'?'assertive':'polite');clearTimeout(TOAST_TIMER);TOAST_TIMER=setTimeout(()=>e.classList.remove('show'),3200)} -function fmtRel(ts){if(!ts)return '';if(typeof ts==='string'){const parsed=Date.parse(ts);if(!Number.isFinite(parsed))return '';ts=parsed/1000}const s=Math.max(0,Date.now()/1000-ts);if(s<60)return 'just now';if(s<3600)return Math.floor(s/60)+'m ago';if(s<86400)return Math.floor(s/3600)+'h ago';if(s<2592000)return Math.floor(s/86400)+'d ago';return new Date(ts*1000).toISOString().slice(0,10)} -async function api(p,o){o=o||{};o={...o,headers:{...(o.headers||{}),'X-Engraphis-Browser-Session':'1'}};const r=await fetch(p.startsWith('http')?p:API+p,o);const txt=await r.text();let d=null;try{d=txt?JSON.parse(txt):null}catch(e){} - if(!r.ok){let msg=(d&&d.detail&&(d.detail.error||d.detail))||(d&&d.error)||(''+r.status);if(typeof msg!=='string')msg=JSON.stringify(msg);const err=new Error(msg);err.status=r.status;err.detail=d&&d.detail;throw err}return d} -async function authenticateBrowser(){let token='';try{const fragment=new URLSearchParams(location.hash.slice(1));token=fragment.get('token')||'';if(token)history.replaceState(null,'',location.pathname+location.search)}catch(e){}if(!token)token=window.prompt('Enter this deployment’s ENGRAPHIS_API_TOKEN:')||'';if(!token)return false;try{await api('/auth/session',{method:'POST',body:JSON.stringify({token}),headers:{'Content-Type':'application/json'}});token='';return true}catch(e){token='';toast('Authentication failed: '+e.message,'err');return false}} -let ACTION_RESOLVE=null,ACTION_SPEC=null; -function actionDialog(spec){if(ACTION_RESOLVE)closeActionDialog(null);ACTION_SPEC=spec||{};document.getElementById('action-title').textContent=ACTION_SPEC.title||'Confirm action';document.getElementById('action-message').textContent=ACTION_SPEC.message||'';document.getElementById('action-error').textContent='';const fields=document.getElementById('action-fields');fields.replaceChildren();(ACTION_SPEC.fields||[]).forEach((f,i)=>{const wrap=document.createElement('div');wrap.className='field';const label=document.createElement('label');label.className='field-lbl';label.htmlFor='action-field-'+i;label.textContent=f.label||'Value';const input=f.options?document.createElement('select'):document.createElement(f.multiline?'textarea':'input');input.className=f.options?'select':'input';input.id='action-field-'+i;input.dataset.name=f.name||('field'+i);if(f.type==='password'){input.type='password';input.autocomplete='off'}else if(input.tagName==='INPUT'){input.type=f.type||'text'}if(f.placeholder)input.placeholder=f.placeholder;if(f.value!=null)input.value=f.value;if(f.required)input.required=true;(f.options||[]).forEach(opt=>{const option=document.createElement('option');option.value=opt.value==null?opt:opt.value;option.textContent=opt.label==null?opt:opt.label;input.appendChild(option)});wrap.append(label,input);fields.appendChild(wrap)});const submit=document.getElementById('action-submit');submit.textContent=ACTION_SPEC.submit||'Continue';submit.className='btn '+(ACTION_SPEC.danger?'btn-danger':'btn-primary');document.getElementById('action-overlay').classList.add('show');return new Promise(resolve=>{ACTION_RESOLVE=resolve})} -function closeActionDialog(value){const resolve=ACTION_RESOLVE;ACTION_RESOLVE=null;ACTION_SPEC=null;document.getElementById('action-overlay').classList.remove('show');if(resolve)resolve(value)} -function submitActionDialog(){const values={};for(const input of document.querySelectorAll('#action-fields input,#action-fields select,#action-fields textarea')){if(input.required&&!input.value.trim()){document.getElementById('action-error').textContent='Complete all required fields.';input.focus();return}values[input.dataset.name]=input.value}closeActionDialog(values)} -async function confirmAction(title,message,submit,danger){const result=await actionDialog({title,message,submit:submit||'Continue',danger:!!danger});return result!==null} -async function textAction(title,message,label,value,options){const result=await actionDialog({title,message,submit:(options&&options.submit)||'Save',danger:!!(options&&options.danger),fields:[{name:'value',label,value:value||'',required:!(options&&options.optional),multiline:!!(options&&options.multiline),options:(options&&options.options)||[]} ]});return result===null?null:result.value} -document.getElementById('action-close').addEventListener('click',()=>closeActionDialog(null)); -document.getElementById('action-cancel').addEventListener('click',()=>closeActionDialog(null)); -document.getElementById('action-submit').addEventListener('click',submitActionDialog); -document.getElementById('action-overlay').addEventListener('click',event=>{if(event.target===event.currentTarget)closeActionDialog(null)}); -let DIALOG_ACTIVE=null; -const DIALOG_RETURN=new WeakMap(); -function toggleMobileNav(force){const app=document.querySelector('.app'),btn=document.getElementById('mobile-nav-toggle'),side=document.getElementById('app-sidebar'),main=document.getElementById('main-content');if(!app||!btn||!side||!main)return;const mobile=matchMedia('(max-width:768px)').matches;const open=mobile&&(force===undefined?!app.classList.contains('mobile-nav-open'):!!force);app.classList.toggle('mobile-nav-open',open);btn.setAttribute('aria-expanded',String(open));btn.setAttribute('aria-label',open?'Close navigation':'Open navigation');side.inert=mobile&&!open;side.setAttribute('aria-hidden',String(mobile&&!open));main.inert=mobile&&open;if(open){const active=document.querySelector('.nav-item.active');setTimeout(()=>{if(active)active.focus()},0)}} -function closeMobileNav(returnFocus){toggleMobileNav(false);if(returnFocus)setTimeout(()=>document.getElementById('mobile-nav-toggle').focus(),0)} -function syncMobileNavMode(){toggleMobileNav(document.querySelector('.app').classList.contains('mobile-nav-open'))} -window.addEventListener('resize',syncMobileNavMode); -syncMobileNavMode(); -function dialogChanged(ov){const open=ov.classList.contains('show');ov.setAttribute('aria-hidden',String(!open));if(open){if(DIALOG_ACTIVE!==ov){DIALOG_RETURN.set(ov,document.activeElement);DIALOG_ACTIVE=ov;setTimeout(()=>{const first=ov.querySelector('.mm-body input:not([disabled]),.mm-body select:not([disabled]),.mm-body textarea:not([disabled])')||ov.querySelector('button:not([disabled]),[href],[tabindex="0"]');if(first)first.focus()},0)}}else if(DIALOG_ACTIVE===ov){DIALOG_ACTIVE=null;const back=DIALOG_RETURN.get(ov);if(back&&document.contains(back)&&back!==document.body)back.focus();else{const heading=document.getElementById('topbar-title');if(heading){heading.tabIndex=-1;heading.focus()}}}} -function trapDialog(e){const ov=DIALOG_ACTIVE;if(!ov||e.key!=='Tab')return;const els=Array.from(ov.querySelectorAll('input:not([disabled]),select:not([disabled]),textarea:not([disabled]),button:not([disabled]),[href],[tabindex="0"]')).filter(x=>x.offsetParent!==null);if(!els.length){e.preventDefault();return}const first=els[0],last=els[els.length-1];if(e.shiftKey&&document.activeElement===first){e.preventDefault();last.focus()}else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();first.focus()}} -function ensureDialogFocus(){if(!DIALOG_ACTIVE||DIALOG_ACTIVE.contains(document.activeElement))return;requestAnimationFrame(()=>{if(!DIALOG_ACTIVE)return;const first=DIALOG_ACTIVE.querySelector('.mm-body input:not([disabled]),.mm-body select:not([disabled]),.mm-body textarea:not([disabled])')||DIALOG_ACTIVE.querySelector('button:not([disabled]),[href],[tabindex="0"]');if(first)first.focus()})} -function controlName(el){const named={recall_q:'Recall query',recall_k:'Number of recall results',mem_q:'Memory filter',why_q:'Question to explain',tl_q:'Timeline topic',graph_repo_filter:'Repository filter',graph_search:'Find graph entity',import_path:'Local import path',import_pattern:'File pattern',code_repo:'Repository name',code_root:'Repository path',postgres_dsn:'PostgreSQL DSN',postgres_repo:'PostgreSQL repository scope'};return named[(el.id||'').replace(/-/g,'_')]||(el.placeholder||'').replace(/[…*]+$/,'').trim()||(el.id||el.type||'control').replace(/[-_]+/g,' ')} -function enhanceUi(root){const scope=root&&root.querySelectorAll?root:document;scope.querySelectorAll('.nav-item').forEach(el=>{el.setAttribute('role','link');el.tabIndex=0;el.setAttribute('aria-current',el.classList.contains('active')?'page':'false')});scope.querySelectorAll('button:not([type])').forEach(el=>el.type='button');scope.querySelectorAll('input,select,textarea').forEach((el,i)=>{if(el.labels&&el.labels.length)return;const field=el.closest('.field'),slider=el.closest('.gslider');const label=(field&&field.querySelector('label'))||(slider&&slider.querySelector('label'));if(label){if(!el.id)el.id='ui-control-'+i+'-'+Date.now();label.htmlFor=el.id}else if(!el.getAttribute('aria-label')&&!el.getAttribute('aria-labelledby'))el.setAttribute('aria-label',controlName(el))});scope.querySelectorAll('.recall-card,.gtop-row,.ep-edge,.mem-card').forEach(el=>{if(!el.hasAttribute('tabindex'))el.tabIndex=0;if(!el.hasAttribute('role'))el.setAttribute('role','button');if(!el.getAttribute('aria-label')){const txt=el.querySelector('.recall-title,.gtop-name,.mem-card-title')||el;el.setAttribute('aria-label',(txt.textContent||'Open item').trim())}});scope.querySelectorAll('.vault-card').forEach(el=>{el.setAttribute('role','group');const name=el.querySelector('.vault-card-name');if(name&&!el.getAttribute('aria-label'))el.setAttribute('aria-label','Workspace '+name.textContent.trim())});scope.querySelectorAll('.spinner').forEach(el=>{el.setAttribute('role','status');el.setAttribute('aria-label','Loading')});document.querySelectorAll('.status-region').forEach(el=>{const busy=!!el.querySelector('.spinner');el.setAttribute('aria-busy',String(busy));const text=(el.textContent||'').toLowerCase();el.setAttribute('role',/\b(error|failed|invalid|unavailable|offline)\b/.test(text)?'alert':'status')});document.querySelectorAll('.mm-overlay').forEach(dialogChanged)} -const enhanceUiBase=enhanceUi; -enhanceUi=function(root){ - enhanceUiBase(root); - const scope=root&&root.querySelectorAll?root:document; - scope.querySelectorAll('.nav-item').forEach(el=>{ - if(el.tagName==='BUTTON')el.removeAttribute('role'); - else{el.setAttribute('role','button');el.tabIndex=0} - }); -}; -function enhanceDynamicUi(){document.querySelectorAll('.vault-card-name,.tl-item.clickable,#mm-body>div[data-onclick]').forEach(el=>{el.setAttribute('role','button');el.tabIndex=0});document.querySelectorAll('.mem-card').forEach(el=>el.setAttribute('aria-describedby','memory-reorder-help'));document.querySelectorAll('.card-head:not(h1):not(h2):not(h3)').forEach(el=>{el.setAttribute('role','heading');el.setAttribute('aria-level','2')});['sync-status','llm-test-result','au-result','tok-created'].forEach(id=>{const el=document.getElementById(id);if(el){el.setAttribute('role','status');el.setAttribute('aria-live','polite')}})} -function enhanceAdjacentLabels(){document.querySelectorAll('input,select,textarea').forEach((el,i)=>{if(el.labels&&el.labels.length)return;const direct=el.previousElementSibling,parent=el.parentElement&&el.parentElement.previousElementSibling,label=(direct&&direct.matches('label')&&direct)||(parent&&parent.matches('label')&&parent);if(!label)return;if(!el.id)el.id='ui-adjacent-'+i+'-'+Date.now();label.htmlFor=el.id;el.removeAttribute('aria-label')})} -function enhanceDecorativeIcons(){document.querySelectorAll('.nav-icon,.brand-mark,.dropzone-icon,.empty-icon').forEach(el=>el.setAttribute('aria-hidden','true'))} -function enhanceExplorerSemantics(){document.querySelectorAll('#graph-entity-list [role="listitem"],#graph-relation-list [role="listitem"]').forEach(el=>el.removeAttribute('role'))} -function enhanceStatusContainers(){document.querySelectorAll('.status-region,#ov-analytics,#ed-history,#sync-body,#lic-body,#tokens-body,#llm-body,#au-result').forEach(el=>{const visible=el.getClientRects().length>0;el.setAttribute('aria-live','polite');el.setAttribute('aria-busy',String(visible&&(!!el.querySelector('.spinner')||/^\s*Loading/.test(el.textContent||''))))})} -// Coalesce the enhancement sweeps to at most one per frame. Each one re-queries the -// whole document, and the observer fires on every keystroke in graph search and every -// drag-over boundary crossing, so running them per mutation batch was quadratic-ish on -// exactly the interactions that need to stay responsive. Dialog bookkeeping stays -// immediate — it drives focus, which cannot wait a frame. -let UI_SWEEP=0; -function scheduleUiSweep(){if(UI_SWEEP)return;UI_SWEEP=requestAnimationFrame(()=>{UI_SWEEP=0;enhanceUi(document);enhanceAdjacentLabels();enhanceDynamicUi();enhanceDecorativeIcons();enhanceStatusContainers();enhanceExplorerSemantics();ensureDialogFocus()})} -const UI_OBSERVER=new MutationObserver(records=>{records.forEach(r=>{if(r.type==='attributes'&&r.target.classList.contains('mm-overlay'))dialogChanged(r.target)});scheduleUiSweep()}); -enhanceUi(document); -enhanceAdjacentLabels(); -enhanceDynamicUi(); -enhanceDecorativeIcons(); -enhanceStatusContainers(); -enhanceExplorerSemantics(); -document.querySelectorAll('.view').forEach(el=>el.setAttribute('aria-hidden',String(!el.classList.contains('active')))); -UI_OBSERVER.observe(document.body,{subtree:true,childList:true,attributes:true,attributeFilter:['class']}); -document.addEventListener('keydown',e=>{trapDialog(e);const t=e.target;if(t.matches('.mem-card')){if(e.altKey&&(e.key==='ArrowUp'||e.key==='ArrowDown')){e.preventDefault();memKeyboardMove(t.dataset.id,e.key==='ArrowUp'?-1:1);return}if(e.key==='Enter'||e.key===' '){e.preventDefault();openMem(t.dataset.id);return}}if((e.key==='Enter'||e.key===' ')&&t.matches('[role=button],.nav-item')){if(e.key===' '||t.matches('.nav-item'))e.preventDefault();t.click()}}); - -/* theme */ -const THEMES=[['dark','Dark','#15181e','#8c83e8'],['light','Light','#fbfbfc','#5547b8'],['midnight','Midnight','#111b2d','#79a6ef'],['solarized','Solarized','#073642','#58a7d8'],['sepia','Sepia','#f8f2e4','#925420'],['matrix','Matrix','#000403','#3ce072']]; -const THEME_GLYPH={dark:'◑',light:'☀',midnight:'☾',solarized:'◐',sepia:'❂',matrix:'▦'}; -function applyTheme(t){if(!THEME_GLYPH[t])t='dark';document.body.setAttribute('data-theme',t);window.__theme=t;try{localStorage.setItem('engraphis-theme',t)}catch(e){}const b=document.getElementById('theme-btn');if(b){b.textContent=THEME_GLYPH[t];b.setAttribute('aria-label','Change theme. Current theme: '+t)}const sel=document.getElementById('theme-select');if(sel&&sel.value!==t)sel.value=t;renderThemeMenu();if(typeof graphRecolor==='function')graphRecolor()} -function renderThemeMenu(){const m=document.getElementById('theme-menu');if(!m)return;m.innerHTML=THEMES.map(x=>{const on=(x[0]===window.__theme);return ``}).join('')} -function toggleThemeMenu(e){if(e)e.stopPropagation();const m=document.getElementById('theme-menu'),b=document.getElementById('theme-btn');if(!m)return;const show=!m.classList.contains('is-open');m.classList.toggle('is-open',show);if(b)b.setAttribute('aria-expanded',String(show));if(show){renderThemeMenu();setTimeout(()=>{const first=m.querySelector('button');if(first)first.focus();document.addEventListener('click',closeThemeMenu)},0)}} -function closeThemeMenu(){const m=document.getElementById('theme-menu'),b=document.getElementById('theme-btn');if(m)m.classList.remove('is-open');if(b)b.setAttribute('aria-expanded','false');document.removeEventListener('click',closeThemeMenu)} -function pickTheme(t){applyTheme(t);closeThemeMenu()} -function toggleTheme(){const ids=THEMES.map(x=>x[0]);const i=ids.indexOf(window.__theme);pickTheme(ids[(i+1)%ids.length])} -function initTheme(){let saved=null;try{saved=localStorage.getItem('engraphis-theme')}catch(e){}applyTheme(saved||window.__theme||'dark')} -function selectDashboard(value){window.location.assign(value==='classic'?'/classic':'/')} -function initDashboard(){const select=document.getElementById('dashboard-select');if(select)select.value=location.pathname==='/classic'?'classic':'primary'} - -/* nav */ -function selectView(v){ - document.querySelectorAll('.nav-item').forEach(n=>{const active=n.dataset.view===v;n.classList.toggle('active',active);n.setAttribute('aria-current',active?'page':'false')}); - document.querySelectorAll('.view').forEach(el=>{const active=el.id==='view-'+v;el.classList.toggle('active',active);el.setAttribute('aria-hidden',String(!active))}); - CURRENT_VIEW=v; - const heading=document.getElementById('topbar-title'); - heading.textContent=TITLES[v]||v; - const section=document.getElementById('route-section'); - if(section)section.textContent=ROUTE_SECTIONS[v]||'Operate'; - const sub=document.getElementById('topbar-sub'); - if(sub)sub.textContent=DESCS[v]||''; - /* Plan pills live in the topbar; clear them on navigation so only the active - view's loader can repopulate one. */ - ['an-lock','au-lock'].forEach(id=>{const p=document.getElementById(id);if(p){p.textContent='';p.className='pill pill-muted topbar-lock'}}); - if(v==='graph')graphEngineResume();else graphEnginePause(); - closeMobileNav(); - (LOADERS[v]||function(){})(); - heading.tabIndex=-1; - setTimeout(()=>heading.focus(),0); -} -function navTo(v){selectView(v)} -document.querySelectorAll('.nav-item').forEach(it=>it.addEventListener('click',()=>selectView(it.dataset.view))); - -/* workspace */ -function setWS(name){ - WS=name; - const shown=name||'—',sw=document.getElementById('vault-switcher'),top=document.getElementById('topbar-workspace'); - document.getElementById('ws-name').textContent=shown; - if(top)top.textContent=shown; - if(sw)sw.setAttribute('aria-label',name?'Choose active workspace. Current workspace: '+name:'Choose active workspace'); -} -async function loadWorkspaceList(){const d=await api('/workspaces');WORKSPACES=d.workspaces||[];if(!WS&&WORKSPACES.length){WORKSPACES.sort((a,b)=>(b.memories||0)-(a.memories||0));setWS(WORKSPACES[0].name)}} - -/* overview */ -function formatTokenCount(value){return Math.max(0,Math.round(Number(value)||0)).toLocaleString()} -async function loadOverview(){ - try{ - const st=await api('/stats?workspace='+encodeURIComponent(WS||'')); - setViewDesc('overview',(st.memories||0)+' memories · '+(st.workspaces||0)+' workspaces'); - const cards=[['Memories',st.memories],['Live rows',st.total_rows],['Workspaces',st.workspaces],['Sessions',st.sessions]]; - document.getElementById('stat-grid').innerHTML=cards.map(c=>`

`).join(''); - document.getElementById('nav-mem-count').textContent=st.memories||''; - const bt=st.by_type||{},tot=Object.values(bt).reduce((a,b)=>a+b,0)||1; - document.getElementById('ov-types').innerHTML=Object.keys(bt).length?Object.entries(bt).map(([k,v])=>`
${esc(k)}
${v}
`).join(''):'
No memories
'; - loadOverviewAnalytics(); - }catch(e){ - const msg='Overview unavailable: '+e.message; - setViewDesc('overview',msg); - document.getElementById('stat-grid').innerHTML='
'+esc(msg)+'
'; - document.getElementById('ov-types').innerHTML='
Memory types could not be loaded.
'; - document.getElementById('ov-analytics').innerHTML='
Analytics could not be loaded.
'; - toast(msg,'err'); - } -} -async function loadOverviewAnalytics(){ - const el=document.getElementById('ov-analytics'),lock=document.getElementById('ov-lock'); - try{ - const a=await api('/analytics?workspace='+encodeURIComponent(WS||'')); - lock.textContent=licTrialActive()?'TRIAL':''; - lock.className='pill pill-muted'; - const t=a.totals||{},f=a.decay_forecast||{}; - if(t.live==null){ - const nd=a.namespace_distribution||[],mx=Math.max(...nd.map(x=>x.count),1); - el.innerHTML='
Memories by workspace
'+nd.map(x=>barRow(x.namespace,x.count,mx,'var(--accent-dim)')).join(''); - return; - } - const weeks=a.growth_weekly||[],thisWeek=weeks.length?weeks[weeks.length-1]:0,avg=Math.round((t.avg_retention||0)*100); - const cell=(v,l,c)=>{const tone=c==='var(--red)'?' tone-red':(c==='var(--amber)'?' tone-amber':(c==='var(--green)'?' tone-green':''));return `
${v}
${l}
`}; - el.innerHTML=`
${cell(avg+'%','Avg retention',avg<40?'var(--red)':(avg<70?'var(--amber)':'var(--green)'))}${cell(f.at_risk_7d||0,'Fading ≤ 7 days',f.at_risk_7d>0?'var(--amber)':'')}${cell(thisWeek,'Written this week')}${cell(t.pinned||0,'Pinned')}
`; - }catch(e){ - // Same routing as loadAnalytics: consent first, entitlement second, everything else - // reported as the error it is. See hostedFeatureUnavailable for why 409 is not an - // entitlement status. - if(managedConsentRequired(e)){ - lock.textContent='CLOUD'; - lock.className='pill pill-accent'; - el.innerHTML=managedConsentHtml('Analytics'); - }else if(hostedFeatureUnavailable(e)){ - lock.textContent='PRO'; - lock.className='pill pill-muted'; - const offerTrial=licTrialAvailable(); - el.innerHTML='
Hosted growth, retention distribution, and decay forecast.
'+esc(lockReason(false))+'
'+(offerTrial?' ':'')+'
'; - }else el.innerHTML='
'+esc(e.message)+'
'; - } -} - -/* ── hosted access state ── - /api/license reports what the control plane said, never what this client guessed: - access_state is one of active | trial | trial_expired | lapsed | inactive, and each one - is a different thing to tell the customer. Reading it here is what stopped a customer - whose trial had ended, and a customer whose card had failed, both being shown a - confident PRO badge over rows of locks with no reason given. */ -function licAccessState(){const s=LIC&&LIC.access_state;return s==='active'||s==='trial'||s==='trial_expired'||s==='lapsed'?s:'inactive'} -function licAccessLive(){const s=licAccessState();return s==='active'||s==='trial'} -function licTrialActive(){return licAccessState()==='trial'} -/* The server refuses a second trial for any organization that already holds an - entitlement, so this is false for every connected customer — trialling, lapsed, or - paying — and the CTA that could only ever return 409 is not drawn. */ -function licTrialAvailable(){return !!(LIC&&LIC.trial&&LIC.trial.available)} -function licPlanName(){return licPlanKey()?licPlanKey().toUpperCase():'LOCAL'} -/* The plan the customer actually holds, as the wire spells it: '' when they hold none. - Renewal and billing actions must follow this, not a hardcoded 'pro'. */ -function licPlanKey(){const raw=String((LIC&&LIC.plan)||'local').toLowerCase();return raw==='pro'||raw==='team'?raw:''} -function licTrialEnds(){return fmtDay(LIC&&LIC.trial&&LIC.trial.ends_at)} -function fmtDay(epoch){const n=Number(epoch)||0;if(!(n>0))return '';try{const d=new Date(n*1000);return Number.isFinite(d.getTime())?d.toISOString().slice(0,10):''}catch(e){return ''}} - -/* ── shared hosted upgrade / trial CTA ── */ -/* The plan-neutral hosted entry point. An account portal is not a checkout for either - plan, so it must not carry the ?plan= that would reframe it as one — and it cannot be - LIC.upgrade_url either: that is licensing.upgrade_url(), which resolves plan="pro" and - prefers ENGRAPHIS_PRO_UPGRADE_URL, so where the portal and the checkout are configured - separately it is the Pro checkout under a neutral name. LIC.account_url resolves the - generic value directly; the fallback only matters against a build that predates it. */ -function withCtaAttribution(raw,content,medium){const safe=safeUrl(raw);if(!safe||safe==='#')return '#';try{const url=new URL(safe,location.href);url.searchParams.set('utm_source','engraphis');url.searchParams.set('utm_medium',medium||'product');url.searchParams.set('utm_campaign','pro_conversion');url.searchParams.set('utm_content',content||'plans');return url.href}catch(e){return safe}} -function hostedAccountUrl(content){return withCtaAttribution((LIC&&LIC.account_url)||(LIC&&LIC.upgrade_url),content||'account','product')} -function hostedPlanUrl(plan,trial,interval,content){const cadence=interval==='annual'?'annual':'monthly',key=plan+'_'+cadence+'_upgrade_url',raw=(LIC&&(LIC[key]||(plan==='team'?LIC.team_upgrade_url:LIC.pro_upgrade_url)))||(LIC&&LIC.upgrade_url);const safe=safeUrl(raw);if(!safe||safe==='#')return '#';try{const url=new URL(safe,location.href);if(plan==='pro'||plan==='team')url.searchParams.set('plan',plan);url.searchParams.set('interval',cadence);if(!url.hash)url.hash='billing';if(trial)url.searchParams.set('trial',plan);return withCtaAttribution(url.href,content||plan,'product')}catch(e){return safe}} -/* Why is this feature locked? One sentence per access state, so the panel never claims a - trial the customer cannot start nor blames billing for a trial that simply ran out. - This is DENIAL copy: every caller reaches it because a hosted request was refused. The - one surface that is not a denial — the Team tab, which renders for everyone on every - visit — must go through teamTeaserNote() instead. */ -function lockReason(team){const st=licAccessState(),ends=licTrialEnds(); - if(st==='trial')return `Your free trial is live${ends?` until ${esc(ends)}`:''}. ${team?'Team':'Pro'} needs a subscription of its own.`; - if(st==='trial_expired')return `Your free trial has ended${ends?` (${esc(ends)})`:''}, so hosted features are locked. The trial cannot be started again.`; - if(st==='lapsed')return `Your ${esc(licPlanName())} subscription is no longer active, so hosted features are locked until billing is up to date.`; - if(st==='active')return `Your ${esc(licPlanName())} subscription does not include this.`; - if(licTrialAvailable()){const days=licTrialDays(team?'team':'pro');return days?`The email-confirmed, no-card trial lasts exactly ${days} active days.`:'An email-confirmed, no-card trial is available; review its duration in Cloud.'} - return 'Your free trial has already been used.'} -/* The Team tab describes the hosted service; it is not an answer to a refused request, and - it renders for every customer including the ones who are paying for Team. Handing it - lockReason(true) told a live Team subscriber "Your TEAM subscription does not include - this" directly above an unlocked Team nav item and an Open Team Cloud button. A customer - whose live plan already grants Team gets the truth; everyone else still gets the denial - copy, which for them is accurate. */ -function teamTeaserNote(){const ends=licTrialEnds(); - if(licPlanKey()!=='team'||!licAccessLive())return lockReason(true); - if(licAccessState()==='trial')return `Your free trial includes Team${ends?` until ${esc(ends)}`:''}. Organizations, roles, and seats are managed in Engraphis Cloud.`; - return 'Your TEAM subscription includes this. Organizations, roles, and seats are managed in Engraphis Cloud.'} -function licTrialDays(plan){const trial=(LIC&&LIC.trial)||{},days=(trial.days_by_plan||{})[plan]??(plan==='pro'?trial.trial_days:null);return Number.isSafeInteger(days)&&days>0?days:null} -function hostedCta(plan,content,interval){const team=plan==='team',name=team?'Team':'Pro',state=licAccessState(),current=licPlanKey();if(state==='lapsed')return {label:'Update billing',href:hostedAccountUrl(content||'account'),kind:'account'};if(licAccessLive()&&(current===plan||(current==='team'&&plan==='pro')))return {label:current==='team'&&team?'Open Team Cloud':'Open Engraphis Cloud',href:hostedAccountUrl(content||'account'),kind:'account'};const trial=licTrialAvailable()&&state==='inactive',days=licTrialDays(plan);return {label:trial?`Start ${days?`${days}-day `:''}${name} trial`:`Subscribe to ${name}`,href:hostedPlanUrl(plan,trial,interval||'monthly',content||plan),kind:trial?'trial':'subscribe'} } -function ctaLinkHtml(cta,className,content){return `
${esc(cta.label)}`} -function unlockHtml(feature,plan){const team=plan==='team',name=team?'Team':'Pro',featureKey=`feature_${String(feature).toLowerCase().replace(/[^a-z0-9]+/g,'_')}`,primary=hostedCta(plan,featureKey),annual=primary.kind==='account'?'':{label:`Annual ${name} option`,href:hostedPlanUrl(plan,false,'annual',`${featureKey}_annual`),kind:'subscribe'},price=team?'$20 per seat/month or $200 per seat/year':'$10/month or $100/year',detail=lockReason(team),benefits=team?['Everything in Pro','Hosted organizations, invitations, and named seats','Roles, scoped credentials, and Team audit history']:['Hosted Cloud Sync across your installations','Growth, retention, decay, and entity Analytics','Auto Consolidation with hosted retention policies','Auto Dreaming with reviewable managed proposals','Priority support'],lede=team?'Team adds shared workspaces, named seats, roles, and remote agent access.':'Support continued Engraphis development with Pro. Your subscription helps cover hosted infrastructure and ongoing development while unlocking Cloud Sync, Analytics, Auto Consolidation, and Auto Dreaming across your installations.';return `
ENGRAPHIS ${name.toUpperCase()}

Unlock ${esc(feature)} and more

${lede}

${price}
Your license unlocks
    ${benefits.map(item=>`
  • ${esc(item)}
  • `).join('')}

${detail}

${ctaLinkHtml(primary,'btn btn-primary',name.toLowerCase())}${annual.href&&annual.href!=='#'?ctaLinkHtml(annual,'btn btn-ghost',`${featureKey}_annual`):''}
`} -function startTrialPlan(plan){const url=hostedPlanUrl(plan,true);if(url==='#'){toast('Hosted signup URL is not configured','err');return}const link=document.createElement('a');link.href=url;link.target='_blank';link.rel='noopener';link.click()} -function startTrial(){return startTrialPlan('pro')} -function startTeamTrial(){return startTrialPlan('team')} -/* The badge follows the access state, not the plan name. A plan name alone told a trialist - they were a subscriber, and told a lapsed or expired customer nothing was wrong. */ -function updateLicBadge(){/* The side-menu header intentionally has no plan or upgrade badge. */} -function updateFeatureLocks(){ - const has=f=>LIC&&(LIC.features||[]).includes(f); - const apply=(id,feature,label,plan)=>{ - const badge=document.getElementById(id),item=badge&&badge.closest('.nav-item'),locked=!has(feature); - if(badge)badge.textContent=locked?plan:''; - if(item){ - item.setAttribute('aria-label',locked?`${label} — ${plan} plan; opens upgrade options`:label); - item.title=locked?`${plan} plan required; open for trial and upgrade options`:''; - } - }; - apply('nav-analytics-lock','analytics','Analytics','PRO'); - apply('nav-automation-lock','automation','Automation','PRO'); - apply('nav-team-lock','team','Team','TEAM'); -} - -/* ── analytics (Pro) ── */ -function barRow(label,val,peak,color){const tone=color==='var(--green)'?' analytics-bar-green':(color==='var(--blue)'?' analytics-bar-blue':(color==='var(--cyan)'?' analytics-bar-cyan':(color==='var(--accent-dim)'?' analytics-bar-dim':'')));return `
${esc(label)}
${val}
`} -function statMini(v,l,color){const tone=color==='var(--red)'?' tone-red':(color==='var(--amber)'?' tone-amber':(color==='var(--green)'?' tone-green':''));return `
${v}
${esc(l)}
`} -function renderAnalytics(a,isPortfolio){const t=a.totals||{},f=a.decay_forecast||{};const weeks=a.growth_weekly||[];const gp=Math.max(...weeks,1);const gitems=weeks.map((n,i)=>{const back=weeks.length-1-i;return barRow(back===0?'now':back+'w ago',n,gp,'var(--accent-dim)')}).join('')||'
No data
';const hist=a.retention_histogram||{};const hc=hist.counts||[],hb=hist.buckets||[];const hp=Math.max(...hc,1);const hitems=hb.map((b,i)=>barRow(b,hc[i]||0,hp,'var(--green)')).join('');const mix=a.resolver_mix||{};const mk=Object.keys(mix);const mp=Math.max(...Object.values(mix),1);const mitems=mk.length?mk.map(k=>barRow(k,mix[k],mp,'var(--blue)')).join(''):'
No resolver events yet.
';const bt=a.by_type||{};const btk=Object.keys(bt);const bp=Math.max(...Object.values(bt),1);const btitems=btk.length?btk.map(k=>barRow(k,bt[k],bp,'var(--accent)')).join(''):'
No memories yet.
';const ents=a.top_entities||[];const ep=Math.max(...ents.map(e=>e.n),1);const eitems=ents.length?ents.map(e=>barRow(e.name+(isPortfolio&&e.workspace?' · '+e.workspace:''),e.n,ep,'var(--cyan)')).join(''):'
No entities yet — they appear as the graph grows.
';const avg=Math.round((t.avg_retention||0)*100);let wsTable='';if(isPortfolio&&a.workspaces){wsTable=`
Per-workspace breakdown
${a.workspaces.map(w=>``).join('')}
WorkspaceLivePinnedAvg ret.Fading 7d
${esc(w.workspace)}${w.live}${w.pinned}${Math.round((w.avg_retention||0)*100)}%${w.at_risk_7d}
`}return `
${statMini(t.live!=null?t.live:'—','Live memories')}${statMini(avg+'%','Avg retention',avg<40?'var(--red)':(avg<70?'var(--amber)':'var(--green)'))}${statMini(f.at_risk_7d!=null?f.at_risk_7d:'—','Fading ≤ 7 days',f.at_risk_7d>0?'var(--amber)':'')}${statMini(f.at_risk_30d!=null?f.at_risk_30d:'—','Fading ≤ 30 days')}${statMini(t.pinned!=null?t.pinned:'—','Pinned (protected)')}${isPortfolio?statMini(t.workspaces||0,'Workspaces'):statMini(t.superseded!=null?t.superseded:'—','Superseded (history)')}
Memories written per week
${gitems}
Retention distribution
${hitems}
By type
${btitems}
Write-path resolver activity
${mitems}
Most connected entities
${eitems}
${wsTable}`} -/* A consent-required response is a valuable moment to show the job Pro can do, not a - dead end about configuration. A customer with live access must never be offered their - own plan again. Readable processing requires a separate workspace choice. */ -function managedConsentHtml(feature){const automation=/automation/i.test(feature),featureKey=`managed_${String(feature).toLowerCase().replace(/[^a-z0-9]+/g,'_')}`,live=licAccessLive(),trial=licTrialAvailable(),copy=automation?{eyebrow:'MEMORY MAINTENANCE',title:'Let your memory improve after you log off.',lede:'Turn repetitive cleanup into a steady, reviewable habit. Pro watches the rhythm of your workspace and brings the useful changes back for approval.',cards:[['CONSOLIDATE','Distill recurring work into durable knowledge on a cadence you control.'],['DREAM','Surface useful links after accumulation and idle time, before fresh context gets buried.'],['REVIEW','Every managed result is a proposal. Nothing silently rewrites your local memory.']]}:{eyebrow:'MEMORY INTELLIGENCE',title:'See the memory your team is about to lose.',lede:'Pro turns your local memory into an operating signal—so you can see what is growing, what is fading, and what is quietly shaping recall.',cards:[['GROWTH','Separate knowledge that compounds from activity that only accumulates.'],['RETENTION','Catch fading context before an important answer disappears from reach.'],['ENTITY SIGNAL','See the people, projects, and ideas organizing your workspace.']]};const primary=hostedCta('pro',featureKey),annual=primary.kind==='account'?'':{label:'Annual Pro option',href:hostedPlanUrl('pro',false,'annual',`${featureKey}_annual`),kind:'subscribe'},settings=`Review workspace processing`,actions=`${settings}${ctaLinkHtml(primary,'btn btn-ghost',featureKey)}${annual.href?ctaLinkHtml(annual,'btn btn-ghost',`${featureKey}_annual`):''}`,next=live?'Included in your plan. Readable managed processing is paused until you approve this workspace in Manage > Settings.':licAccessState()==='lapsed'?'Your subscription needs billing attention. Restore access, then confirm workspace processing before new readable uploads.':trial?`Start ${licTrialDays('pro')?`with ${licTrialDays('pro')} days of Pro`:'a Pro trial'}. After connecting, explicitly approve each workspace in Manage > Settings.`:'Subscribe to Pro, then explicitly approve each workspace in Manage > Settings.';return `
ENGRAPHIS PRO /${copy.eyebrow}

${copy.title}

${copy.lede}

${esc(next)}

${actions}
WHAT PRO IS WATCHING
${copy.cards.map(card=>`
${card[0]}

${card[1]}

`).join('')}
Your memory stays yours. Readable processing requires your workspace approval. Encrypted Cloud Sync is a separate choice. Secret and session-scoped memories stay local.
`} -function managedConsentRequired(error){return error&&error.status===409&&error.detail&&error.detail.code==='consent_required'} -const CLOUD_SYNC_PRIVACY_COPY='Cloud Sync encrypts eligible shared-workspace changes end-to-end before they leave this device. Engraphis Cloud cannot read their contents; secret and session-scoped memories stay local.'; -const EXTERNAL_LLM_PRIVACY_COPY='Memory text is sent to your configured LLM provider for processing under that provider’s terms. The provider must read that text to return extracted facts.'; -async function confirmCloudTransfer(title,summary,submit,privacyCopy){return confirmAction(title,summary+(privacyCopy?'\n\nPrivacy: '+privacyCopy:''),submit||'Continue')} -const managedConsentHtmlBase=managedConsentHtml; -managedConsentHtml=function(feature){return managedConsentHtmlBase(feature)}; -/* Only an unconfigured local installation may turn a 401 into trial signup. A revoked - or expired Cloud session is also a 401, but ``trial.available`` is false there and it - must remain a reconnect error instead of offering a trial the control plane rejects. */ -function cloudTrialSignupRequired(error){return !!(error&&error.status===401&&error.detail&&error.detail.code==='cloud_unconfigured'&&licTrialAvailable())} -/* Entitlement failure — and only that. 402 subscription not active and 501 feature not - offered here: each means the customer's plan does not include what they just opened, - so each may draw the purchase panel. - 401 is deliberately NOT here either. The cloud maps it to "the cloud session expired or - was revoked; connect again" — a credential problem, not a billing one. An already-paying - customer whose refresh credential lapsed was being sold a plan they own; 401 falls - through to the error branch so the reconnect instruction is what they actually read. - 409 is deliberately NOT here. Every 409 this API emits is a *conflict*, never a billing - answer: detail.code==='consent_required' means the customer IS entitled but managed - compute is off for this installation (managedConsentRequired, above), and the rest are - transient state conflicts (snapshot generation, cloud session state, rebuilding index). - Counting any of them as "unpaid" showed a paying Pro customer a panel selling Pro, so - every caller routes managedConsentRequired first and this second. - Neither predicate may branch on CURRENT_VIEW: doing so made both the consent branch and - the plain-error branch unreachable on the analytics and automation views, which is the - regression this comment previously described but the code did not implement. */ -function hostedFeatureUnavailable(error){return !!error&&(error.status===402||error.status===501)} -async function loadAnalytics(){const el=document.getElementById('analytics-body'),lock=document.getElementById('an-lock'),acts=document.getElementById('an-actions');el.innerHTML='
';try{const a=await api('/analytics?workspace='+encodeURIComponent(WS||''));setPlanPill(lock,licTrialActive()?'TRIAL':'CLOUD','pill pill-accent');showAs(acts,true,'flex');el.innerHTML=renderAnalytics(a,false)}catch(e){if(managedConsentRequired(e)||cloudTrialSignupRequired(e)){setPlanPill(lock,'CLOUD','pill pill-accent');showAs(acts,false);el.innerHTML=managedConsentHtml('Analytics')}else if(hostedFeatureUnavailable(e)){setPlanPill(lock,'PRO','pill pill-muted');showAs(acts,false);el.innerHTML=unlockHtml('Analytics','pro')}else{el.innerHTML='
'+esc(e.message)+'
'}}} - -/* ── hosted automation policy (Pro / Team) ── */ -async function loadAutomation(){const el=document.getElementById('automation-body'),lock=document.getElementById('au-lock'),ws='?workspace='+encodeURIComponent(WS||'');el.innerHTML='
';try{const p=await api('/automation'+ws);setPlanPill(lock,licTrialActive()?'TRIAL':'CLOUD','pill pill-accent');const last=p.last_run?fmtRel(p.last_run):'never',dream=p.dream_enabled!=null?p.dream_enabled:p.dream;el.innerHTML=`
Hosted maintenance policy
The cloud returns reviewable proposals. Pinned memories remain protected.
Cloud worker status
Status${p.enabled?'ENABLED':'OFF'}
Last run${esc(last)}
Requesting managed work uploads the selected workspace’s normal and sensitive memory content, excluding secret and session-scoped rows, capped at 16 MiB, over HTTPS without end-to-end encryption. Results are proposals and never automatically write the local database.
`}catch(e){if(managedConsentRequired(e)||cloudTrialSignupRequired(e)){setPlanPill(lock,'CLOUD','pill pill-accent');el.innerHTML=managedConsentHtml('Hosted Automation')}else if(hostedFeatureUnavailable(e)){setPlanPill(lock,'PRO','pill pill-muted');el.innerHTML=unlockHtml('Automation, Auto Consolidation, and Auto Dreaming','pro')}else{el.innerHTML='
'+esc(e.message)+'
'}}} -async function saveAutomation(){const body={enabled:document.getElementById('au-enabled').checked,cadence_hours:Number(document.getElementById('au-cadence').value)||24,consolidate:document.getElementById('au-consolidate').checked,min_cluster:Number(document.getElementById('au-mincluster').value)||3,archive_below:Number(document.getElementById('au-archive').value)||0.05,dream_enabled:document.getElementById('au-dream').checked,dream_min_new:Number(document.getElementById('au-dream-min').value)||20,dream_idle_minutes:Number(document.getElementById('au-dream-idle').value)};try{await api('/automation?workspace='+encodeURIComponent(WS||''),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});toast('Hosted policy saved','ok');loadAutomation()}catch(e){if(managedConsentRequired(e)){const result=document.getElementById('au-result');if(result)result.innerHTML=managedConsentHtml('Hosted Automation');toast('Approve workspace processing in Manage > Settings','err');return}toast((e.status===402||e.status===501)?'Hosted Automation requires Pro or Team':e.message,'err')}} -async function runMaintenance(){const el=document.getElementById('au-result');if(el)el.innerHTML='
';try{const d=await api('/maintenance/run?workspace='+encodeURIComponent(WS||''),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({dry_run:true})});if(el)el.innerHTML=`PROPOSAL Hosted work was submitted for review.
${esc(JSON.stringify(d,null,2))}
`;toast('Managed proposal requested','ok')}catch(e){if(el)el.innerHTML=managedConsentRequired(e)?managedConsentHtml('Hosted Automation'):'
'+esc(e.message)+'
';toast(managedConsentRequired(e)?'Approve workspace processing in Manage > Settings':((e.status===402||e.status===501)?'Hosted Automation requires Pro or Team':e.message),'err')}} - -const runMaintenanceBase=runMaintenance; -const saveAutomationBase=saveAutomation; -saveAutomation=async function(){const enabled=document.getElementById('au-enabled');if(enabled&&enabled.checked&&!await confirmCloudTransfer('Save hosted policy','Saving this enabled policy uploads this workspace’s normal and sensitive memory content to Engraphis Cloud; secret and session-scoped rows stay local.','Save policy'))return;return saveAutomationBase()} -let MAINTENANCE_PENDING=false; -runMaintenance=async function(dry){ - if(MAINTENANCE_PENDING)return; - if(!await confirmCloudTransfer('Request hosted proposal','This sends this workspace’s normal and sensitive memory content to Engraphis Cloud for a reviewable proposal; secret and session-scoped rows stay local.','Request proposal'))return; - const buttons=Array.from(document.querySelectorAll('#automation-body button[data-onclick="h90"]')),labels=buttons.map(button=>button.textContent); - MAINTENANCE_PENDING=true; - buttons.forEach(button=>{button.disabled=true}); - try{return await runMaintenanceBase(dry)} - finally{ - MAINTENANCE_PENDING=false; - buttons.forEach((button,index)=>{button.disabled=false;button.textContent=labels[index]}); - } -} -/* ── version-chain word diff (memory detail) ── */ -function tokenizeWords(s){return (s||'').split(/(\s+)/)} -function wordDiff(oldS,newS){const a=tokenizeWords(oldS),b=tokenizeWords(newS),n=a.length,m=b.length;if(n*m>200000)return ''+esc(newS)+'';const dp=Array.from({length:n+1},()=>new Uint32Array(m+1));for(let i=n-1;i>=0;i--)for(let j=m-1;j>=0;j--)dp[i][j]=a[i]===b[j]?dp[i+1][j+1]+1:Math.max(dp[i+1][j],dp[i][j+1]);let i=0,j=0,out='';while(i=dp[i][j+1]){out+=''+esc(a[i])+'';i++}else{out+=''+esc(b[j])+'';j++}}while(i'+esc(a[i])+'';i++}while(j'+esc(b[j])+'';j++}return out} -function renderChainDiff(chain){if(!chain||chain.length<2)return '';const c=chain.slice().sort((x,y)=>(x.valid_from||0)-(y.valid_from||0));let h='

Version history '+c.length+' versions · additions green, removals struck

';for(let k=0;k0?c[k-1]:null;const body=prev?wordDiff(prev.content||'',m.content||''):esc(m.content||'');const when=m.valid_from?new Date(m.valid_from*1000).toISOString().slice(0,10):'—';h+=`
${k===0?'original':'revision '+k} · ${when}${cur?' · current':''}
${body}
`}h+='
';return h} -function renderAuditMini(audit){const rows=(audit||[]).slice(0,20);if(!rows.length)return '';return '

Audit trail

'+rows.map(r=>`
${esc(r.action||r.op||r.kind||'edit')}${esc(r.reason||r.detail||'')}${esc(r.actor||'')}${(r.ts||r.at)?fmtRel(r.ts||r.at):''}
`).join('')+'
'} - -/* recall */ -function memCardHtml(m){const sc=m.score!=null?Math.round(Math.min(m.score>1?m.score/5:m.score,1)*100):null;const rc=m.retention!=null?Math.round(Math.min(m.retention,1)*100):null;return `
${esc(m.title||m.id)} ${m.pinned?'PINNED':''} ${esc(m.memory_type)}
${sc!=null?'Score '+sc+'%':''}${rc!=null?'Retention '+rc+'%':''}
${esc((m.content||'').slice(0,320))}
`} -let RECALL_PENDING=false; -async function doRecall(){ - if(RECALL_PENDING)return; - const q=document.getElementById('recall-q').value.trim(); - if(!q){toast('Enter a query','err');return} - if(!WS){workspaceRequired('recall-results','recall memories');return} - const k=document.getElementById('recall-k').value,el=document.getElementById('recall-results'),button=document.querySelector('#view-recall button[data-onclick="h8"]'); - RECALL_PENDING=true; - if(button){button.disabled=true;button.textContent='Recalling…'} - el.innerHTML='
'; - try{ - const d=await api(`/recall?q=${encodeURIComponent(q)}&workspace=${encodeURIComponent(WS||'')}&k=${k}`); - if(!d.count){el.innerHTML='
No matching memories.
';return} - el.innerHTML=`
${d.count} recalled
`+d.memories.map(memCardHtml).join(''); - }catch(e){ - el.innerHTML='
'+esc(e.message)+'
'; - }finally{ - RECALL_PENDING=false; - if(button){button.disabled=false;button.textContent='Recall'} - } -} - -/* memories */ -let MEM_LIST=[], MEM_DRAG=null; -async function loadMemories(){const q=document.getElementById('mem-q').value.trim();const el=document.getElementById('mem-cards');el.innerHTML='
';try{const d=await api(`/memories?q=${encodeURIComponent(q)}&workspace=${encodeURIComponent(WS||'')}&limit=200`);MEM_LIST=d.memories||[];if(!d.count){el.innerHTML='
No memories found.
';return}el.innerHTML=MEM_LIST.map(memRowHtml).join('')}catch(e){el.innerHTML='
'+esc(e.message)+'
'}} -function memRowHtml(m){return `
⠿
${esc(m.title||m.id)}
${esc(m.memory_type)}${m.pinned?'pin':''}${esc((m.content||'').slice(0,60))}
`} -/* drag-to-reorder: whole-list resend on drop keeps this simple and robust — see - MemoryService.reorder_memories for how sort_order is assigned server-side. */ -function memDragStart(e,id){MEM_DRAG=id;e.dataTransfer.effectAllowed='move';e.currentTarget.classList.add('dragging')} -function memDragOver(e,id){if(!MEM_DRAG||MEM_DRAG===id)return;e.preventDefault();const el=e.currentTarget,r=el.getBoundingClientRect(),before=(e.clientY-r.top)m.id===dragId);let to=MEM_LIST.findIndex(m=>m.id===id);if(from<0||to<0)return;const[moved]=MEM_LIST.splice(from,1);to=MEM_LIST.findIndex(m=>m.id===id);MEM_LIST.splice(before?to:to+1,0,moved);document.getElementById('mem-cards').innerHTML=MEM_LIST.map(memRowHtml).join('');try{await api('/memories/reorder',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({workspace:WS,ids:MEM_LIST.map(m=>m.id)})})}catch(err){toast('Reorder failed: '+err.message,'err');loadMemories()}} -function memDragEnd(e){e.currentTarget.classList.remove('dragging');document.querySelectorAll('#mem-cards .mem-card').forEach(c=>c.classList.remove('drag-over-top','drag-over-bottom'))} -async function memKeyboardMove(id,delta){const from=MEM_LIST.findIndex(m=>m.id===id),to=Math.max(0,Math.min(MEM_LIST.length-1,from+delta));if(from<0||from===to){toast(delta<0?'Memory is already first':'Memory is already last','ok');return}const[moved]=MEM_LIST.splice(from,1);MEM_LIST.splice(to,0,moved);document.getElementById('mem-cards').innerHTML=MEM_LIST.map(memRowHtml).join('');try{await api('/memories/reorder',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({workspace:WS,ids:MEM_LIST.map(m=>m.id)})});toast('Moved '+(moved.title||moved.id)+' to position '+(to+1),'ok');setTimeout(()=>{const card=document.querySelector('#mem-cards .mem-card[data-id="'+CSS.escape(id)+'"]');if(card)card.focus()},0)}catch(err){toast('Reorder failed: '+err.message,'err');loadMemories()}} - -/* proactive */ -async function loadProactive(){const el=document.getElementById('proactive-body');el.innerHTML='
';try{const d=await api('/proactive?workspace='+encodeURIComponent(WS||''));let h='';if(d.handoff)h+=`
Last session handoff
${esc(typeof d.handoff==='string'?d.handoff:JSON.stringify(d.handoff))}
`;h+=(d.memories&&d.memories.length)?d.memories.map(memCardHtml).join(''):'
Nothing pressing right now.
';el.innerHTML=h}catch(e){el.innerHTML='
'+esc(e.message)+'
'}} - -/* why */ -let WHY_PENDING=false; -async function doWhy(){ - if(WHY_PENDING)return; - const q=document.getElementById('why-q').value.trim(); - if(!q){toast('Enter a question','err');return} - if(!WS){workspaceRequired('why-body','explain current beliefs and their history');return} - const el=document.getElementById('why-body'),button=document.querySelector('#view-why button[data-onclick="h17"]'); - WHY_PENDING=true; - if(button){button.disabled=true;button.textContent='Explaining…'} - el.innerHTML='
'; - try{ - const d=await api(`/why?q=${encodeURIComponent(q)}&workspace=${encodeURIComponent(WS||'')}`); - let h=''; - if(d.answer.length){ - h+='
Current answer
'+d.answer.map(m=>`
${esc(m.title||m.id)}
${esc(m.content)}
`).join('')+'
'; - }else{ - h+='
No current answer found.
'; - } - if(d.supersedes.length){ - h+='
Superseded (no longer true)
'+d.supersedes.map(m=>`
${esc(m.title||m.id)}
${esc(m.content)}
until ${m.valid_to?new Date(m.valid_to*1000).toISOString().slice(0,10):'—'}
`).join('')+'
'; - } - el.innerHTML=h; - }catch(e){ - el.innerHTML='
'+esc(e.message)+'
'; - }finally{ - WHY_PENDING=false; - if(button){button.disabled=false;button.textContent='Explain'} - } -} - -/* timeline */ -let TIMELINE_PENDING=false; -async function doTimeline(){ - const q=document.getElementById('tl-q').value.trim(); - if(!q){toast('Enter a topic','err');return} - if(!WS){workspaceRequired('tl-body','trace memory history');return} - if(TIMELINE_PENDING)return; - const el=document.getElementById('tl-body'),button=document.querySelector('#view-timeline button[data-onclick="h19"]'); - TIMELINE_PENDING=true; - if(button){button.disabled=true;button.textContent='Tracing…'} - el.innerHTML='
'; - try{ - const d=await api(`/timeline?q=${encodeURIComponent(q)}&workspace=${encodeURIComponent(WS||'')}&limit=40`); - if(!d.history.length){el.innerHTML='
No history for this topic.
';return} - const exact=value=>value?new Date(value*1000).toISOString():'Unavailable'; - el.innerHTML='
'+d.history.map(m=>{ - const cur=!m.valid_to&&!m.expired_at,validEnd=m.valid_to||m.expired_at; - const valid=exact(m.valid_from)+(validEnd?' — '+exact(validEnd):' — current'); - const recorded=exact(m.ingested_at),source=(m.provenance&&m.provenance.source)||'Unknown source'; - return `
${esc(m.title||m.id)} ${cur?'current':'past'}
${esc(m.content)}
Valid time
${esc(valid)}
Recorded time
${esc(recorded)}
Source
${esc(source)}
`; - }).join('')+'
'; - }catch(e){ - el.innerHTML='
'+esc(e.message)+'
'; - }finally{ - TIMELINE_PENDING=false; - if(button){button.disabled=false;button.textContent='Trace'} - } -} - -/* audit */ -async function loadAudit(){const el=document.getElementById('audit-body');el.innerHTML='
';try{const d=await api('/audit?workspace='+encodeURIComponent(WS||'')+'&limit=200');const rows=d.entries||d.audit||[];if(!rows.length){el.innerHTML='
No governance actions recorded.
';return}el.innerHTML='
'+rows.map(r=>`
${esc(r.action||r.op||r.kind||'edit')}${esc(r.memory_id||r.target||r.detail||'')}${esc(r.actor||'')}${r.ts||r.at?fmtRel(r.ts||r.at):''}
`).join('')+'
'}catch(e){el.innerHTML='
'+esc(e.message)+'
'}} -async function downloadReceipts(){try{const d=await api('/receipts/export?workspace='+encodeURIComponent(WS||''));const blob=new Blob([JSON.stringify(d,null,2)],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='engraphis-receipts-'+(WS||'workspace')+'.json';a.click();URL.revokeObjectURL(a.href);toast('Privacy-safe receipts exported','ok')}catch(e){toast(e.message,'err')}} - -let SAVINGS_PRESET='all'; -function savingsPresetQuery(){const p=new URLSearchParams({workspace:WS||''});if(SAVINGS_PRESET==='current'&&RELEASE_VERSION)p.set('release_version',RELEASE_VERSION);if(SAVINGS_PRESET==='7d')p.set('from_ts',String(Date.now()/1000-604800));return p.toString()} -function renderSavingsDetail(s){const e=(s&&s.estimated)||{},eligible=Number(e.eligible_receipt_count)||0,excluded=(Number(e.excluded_receipt_count)||0)+(Number(e.unclassified_receipt_count)||0)+(Number(e.invalid_estimate_count)||0),basisRows=(e.by_basis||[]).map(x=>'
'+esc((x.basis||'unclassified').replaceAll('_',' '))+' · '+esc(x.confidence||'unknown')+''+formatTokenCount(x.baseline_tokens)+' → '+formatTokenCount(x.emitted_tokens)+' · '+formatTokenCount(x.saved_tokens)+' saved ('+(x.receipt_count||0)+' delivery)
').join(''),counterRows=(e.by_token_counter||[]).map(x=>'
'+esc(x.token_counter||'unknown')+''+formatTokenCount(x.saved_tokens)+' saved · '+(x.receipt_count||0)+' eligible delivery
').join(''),preset=SAVINGS_PRESET==='current'?'Current release':SAVINGS_PRESET==='7d'?'Last 7 days':SAVINGS_PRESET==='since'?'Since tracking started':'All time';const buttons=['since','current','7d','all'].map(x=>'').join('');return '
Estimated context saved
View'+buttons+'
'+(eligible?'
'+formatTokenCount(e.saved_tokens)+' tokens
Baseline '+formatTokenCount(e.baseline_tokens)+' → emitted '+formatTokenCount(e.emitted_tokens)+' · '+(Number(e.savings_ratio||0)*100).toFixed(1)+'% estimated reduction
'+eligible+' eligible deliveries · confidence: '+esc(e.confidence||'unknown')+' · range: '+preset+'
'+(basisRows||'
No basis breakdown available.
')+(counterRows?'
Token counters
'+counterRows:''):'
No eligible estimates in this range.
')+'
'+excluded+' excluded or unclassified delivery(s). Measures estimated prompt-context reduction; it does not measure provider billing.
'} -async function loadReceipts(){const el=document.getElementById('audit-body');el.innerHTML='
';try{if(!window.__savingsPresetBound){window.__savingsPresetBound=true;document.addEventListener('click',function(ev){const button=ev.target.closest('[data-savings-preset]');if(!button)return;SAVINGS_PRESET=button.getAttribute('data-savings-preset')||'all';loadReceipts()})}const q='workspace='+encodeURIComponent(WS||''),sq=savingsPresetQuery();const [d,v,s]=await Promise.all([api('/receipts?'+q+'&limit=500'),api('/receipts/verify?'+q),api('/context-savings?'+sq)]);const rows=d.entries||[],packed=(s.by_token_counter||[]).map(x=>'
'+esc(x.token_counter||'unknown')+''+formatTokenCount(x.context_tokens)+' packed / '+formatTokenCount(x.source_tokens)+' source · '+formatTokenCount(x.saved_tokens)+' legacy saved
').join('');const packedCard='
Packed context accounting
Packing savings compare retrieved source tokens with emitted context. They are not added again to adaptive history savings.
'+(packed||'
No complete context-usage receipts yet.
')+'
';el.innerHTML=renderSavingsDetail(s)+packedCard+'
Receipt chain '+(v.valid?'verified':'invalid')+'
'+(v.count||0)+' receipts · head '+esc((v.head||'').slice(0,24))+'
'+(rows.length?'
'+rows.map(r=>'
'+esc(r.operation||'operation')+''+esc((r.hash||'').slice(0,20))+' · '+esc(r.status||'ok')+' · '+(r.target_count||0)+' target(s)'+(r.ts_ms?fmtRel(r.ts_ms/1000):'')+'
').join('')+'
':'
No receipts yet.
')}catch(e){el.innerHTML='
'+esc(e.message)+'
'}} - -/* consolidate */ -async function runConsolidate(dry){const el=document.getElementById('consolidate-body');el.innerHTML='
';try{const d=await api('/consolidate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({workspace:WS,dry_run:dry})});el.innerHTML=`
${dry?'Dry run (nothing changed)':'Consolidation complete'}
${esc(JSON.stringify(d,null,2))}
`;if(!dry)toast('Consolidation done','ok')}catch(e){el.innerHTML='
'+esc(e.message)+'
'}} - -const runConsolidateBase=runConsolidate; -let CONSOLIDATE_PENDING=false; -runConsolidate=async function(dry){ - if(CONSOLIDATE_PENDING)return; - if(!WS){workspaceRequired('consolidate-body','preview or commit consolidation');return} - if(!dry&&!await confirmAction('Commit consolidation','This may create distilled semantic memories and archive decayed transients in workspace "'+(WS||'')+'". Choose the dry run first if you only want a preview.','Commit consolidation',true))return; - const buttons=Array.from(document.querySelectorAll('#view-consolidate button[data-onclick="h61"],#view-consolidate button[data-onclick="h62"]')),labels=buttons.map(button=>button.textContent); - CONSOLIDATE_PENDING=true; - buttons.forEach(button=>{button.disabled=true}); - try{return await runConsolidateBase(dry)} - finally{ - CONSOLIDATE_PENDING=false; - buttons.forEach((button,index)=>{button.disabled=false;button.textContent=labels[index]}); - } -} -/* local single-user workspaces */ -function canCreateWs(){return true} -function folderCardName(el){const card=el.closest('.vault-card');return card?card.dataset.workspace:''} -function folderOpen(el){const card=el.closest('.vault-card');if(card)wsSwitch(card.dataset.workspace)} -function folderCardHtml(w,opts){opts=opts||{};const manage=opts.manage!==false,a=w.name===WS,count=Number(w.memories)||0;const actions=manage?`
`:'';return `
${actions}
${esc(w.name)}${a?' active':''}
${w.description?`
${esc(w.description)}
`:''}
${count} memories${w.repos&&(Array.isArray(w.repos)?w.repos.length:w.repos)?''+(Array.isArray(w.repos)?w.repos.length:w.repos)+' repos':''}View memories →
`} -function tfMemories(name){setWS(name);navTo('memories')} -async function refreshFolders(){await loadWorkspaces()} -async function loadWorkspaces(){ - const el=document.getElementById('ws-cards'),canNew=true,canSources=true; - const nb=document.getElementById('ws-new-btn'),ic=document.getElementById('import-card'),cc=document.getElementById('code-import-card'),iwn=document.getElementById('import-ws-name'); - if(iwn)iwn.textContent=WS?('"'+WS+'"'):'the active folder'; - if(nb){nb.textContent='New folder';showAs(nb,canNew,'inline-flex')} - showAs(ic,canNew,'block'); - showAs(cc,canSources,'block'); - try{ - await loadWorkspaceList(); - if(!WORKSPACES.length){ - el.innerHTML='
No folders yet.
'; - return; - } - el.innerHTML='
'+WORKSPACES.map(w=>folderCardHtml(w,{manage:canNew})).join('')+'
'; - }catch(e){el.innerHTML='
'+esc(e.message)+'
'} -} -async function wsCreate(){ - const result=await actionDialog({title:'New folder',message:'Create a local workspace for related memories.',submit:'Create folder',fields:[{name:'name',label:'Folder name',required:true},{name:'description',label:'Description (optional)',optional:true}]}); - if(result===null)return; - const name=result.name.trim(); - if(!name){toast('Enter a folder name','err');return} - if(WORKSPACES.some(w=>w.name===name)){toast('A folder named "'+name+'" already exists','err');return} - try{ - await api('/workspaces/create',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({workspace:name,description:(result.description||'').trim()})}); - await loadWorkspaceList();setWS(name);toast('Folder "'+name+'" created — now the active folder','ok');refreshFolders(); - }catch(e){toast('Create failed: '+e.message,'err')} -} -function wsSwitch(name){setWS(name);toast('Switched to '+name,'ok');navTo('overview')} - -/* import (files/folders from this PC — see MemoryService.import_folder/import_files) */ -async function importUpload(items){if(!canCreateWs()){toast('Viewers can’t import','err');return}if(!WS){toast('Create or select a folder first','err');return}if(!items||!items.length)return;const fd=new FormData();fd.append('workspace',WS);fd.append('memory_type','semantic');fd.append('derive_facts',document.getElementById('import-derive').checked?'true':'false');for(const it of items)fd.append('files',it.file,it.name);const el=document.getElementById('import-status');el.textContent='Extracting and importing '+items.length+' file(s)…';try{const r=await api('/workspaces/import-files',{method:'POST',body:fd});const wc=(r.warnings||[]).length;el.textContent=r.imported+' imported, '+r.skipped+' skipped, '+r.errors+' error(s), '+(r.derived_facts||0)+' derived fact(s)'+(wc?', '+wc+' warning(s)':'')+(r.truncated?', TRUNCATED at '+r.scanned+' of '+r.matched_total+' matching files':'')+(r.unreadable?', '+r.unreadable+' unreadable':'');toast(r.imported+' resource'+(r.imported===1?'':'s')+' imported into "'+WS+'"'+(r.truncated?' (truncated at max '+r.scanned+')':''),'ok');refreshFolders()}catch(e){el.textContent='';toast('Import failed: '+e.message,'err')}} -function importFilesPicked(fileList,el){const items=Array.from(fileList||[]).map(f=>({file:f,name:f.webkitRelativePath||f.name}));if(el)el.value='';importUpload(items)} -async function importWalkEntry(entry,path,out){if(entry.isFile){await new Promise(res=>entry.file(f=>{out.push({file:f,name:(path?path+'/':'')+f.name});res()},()=>res()))}else if(entry.isDirectory){const reader=entry.createReader();const readBatch=()=>new Promise(res=>reader.readEntries(res,()=>res([])));let batch;do{batch=await readBatch();for(const e of batch)await importWalkEntry(e,(path?path+'/':'')+entry.name,out)}while(batch.length)}} -async function importDrop(e){e.preventDefault();e.currentTarget.classList.remove('drag');const items=e.dataTransfer.items;const out=[];if(items&&items.length&&items[0].webkitGetAsEntry){for(const it of items){const entry=it.webkitGetAsEntry&&it.webkitGetAsEntry();if(entry)await importWalkEntry(entry,'',out)}}else{for(const f of e.dataTransfer.files)out.push({file:f,name:f.name})}importUpload(out)} -async function importFromPath(){if(!canCreateWs()){toast('Viewers can’t import','err');return}if(!WS){toast('Create or select a folder first','err');return}const path=(document.getElementById('import-path').value||'').trim();const pattern=(document.getElementById('import-pattern').value||'*').trim()||'*';if(!path){toast('Enter a path','err');return}const el=document.getElementById('import-status');el.textContent='Extracting and importing…';try{const r=await api('/workspaces/import-folder',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({workspace:WS,path,file_pattern:pattern,memory_type:'semantic',derive_facts:document.getElementById('import-derive').checked})});const wc=(r.warnings||[]).length;el.textContent=r.imported+' imported, '+r.skipped+' skipped, '+r.errors+' error(s), '+(r.derived_facts||0)+' derived fact(s), scanned '+r.scanned+(wc?', '+wc+' warning(s)':'')+(r.truncated?', TRUNCATED at '+r.scanned+' of '+r.matched_total+' matching files':'')+(r.unreadable?', '+r.unreadable+' unreadable':'');toast(r.imported+' resource'+(r.imported===1?'':'s')+' imported into "'+WS+'"'+(r.truncated?' (truncated at max '+r.scanned+')':''),'ok');refreshFolders()}catch(e){el.textContent='';toast('Import failed: '+e.message,'err')}} -async function indexRepository(){if(!WS){toast('Select a workspace first','err');return}const repo=(document.getElementById('code-repo').value||'').trim(),root=(document.getElementById('code-root').value||'').trim(),el=document.getElementById('code-import-status');if(!repo||!root){toast('Enter a repository name and path','err');return}el.textContent='Incrementally indexing repository…';try{const r=await api('/code/index',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({workspace:WS,repo:repo,root_path:root})});el.textContent=`${r.files_indexed} changed, ${r.files_unchanged} unchanged · ${r.symbols} symbols · ${r.edges} edges · ${r.code_memory_links||0} memory links`;toast('Repository graph updated','ok')}catch(e){el.textContent='';toast(e.message,'err')}} -async function importPostgresSchema(){if(!WS){toast('Select a workspace first','err');return}const dsn=(document.getElementById('postgres-dsn').value||'').trim(),repo=(document.getElementById('postgres-repo').value||'').trim(),el=document.getElementById('code-import-status');if(!dsn){toast('Enter a PostgreSQL DSN','err');return}el.textContent='Reading PostgreSQL catalog…';try{const r=await api('/resources/postgres',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({workspace:WS,repo:repo||null,dsn:dsn})});document.getElementById('postgres-dsn').value='';el.textContent=`Imported ${r.schema.tables||0} tables, ${r.entities} entities, and ${r.relations} relations`;toast('Database schema imported','ok')}catch(e){el.textContent='';toast(e.message,'err')}} -async function wsRename(name){const nn=await textAction('Rename workspace','Choose a new name for "'+name+'".','Workspace name',name,{submit:'Rename'});if(nn===null)return;const v=nn.trim();if(!v||v===name)return;try{await api('/workspaces/rename',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({workspace:name,new_name:v})});if(WS===name)setWS(v);toast('Renamed','ok');refreshFolders()}catch(e){toast(e.message,'err')}} -async function wsDescribe(name){const cur=((WORKSPACES.find(w=>w.name===name)||{}).description)||'';const d=await textAction('Describe workspace','Update the optional description for "'+name+'".','Description',cur,{submit:'Save',optional:true,multiline:true});if(d===null)return;try{await api('/workspaces/describe',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({workspace:name,description:d})});toast('Saved','ok');refreshFolders()}catch(e){toast(e.message,'err')}} -async function wsMerge(name){const others=WORKSPACES.map(w=>w.name).filter(n=>n!==name);if(!others.length){toast('No other workspace to merge into','err');return}const result=await actionDialog({title:'Merge workspace',message:'All memories in "'+name+'" will move to the selected workspace and "'+name+'" will be removed. This cannot be undone.',submit:'Merge workspace',danger:true,fields:[{name:'target',label:'Destination workspace',required:true,options:others.map(value=>({value,label:value}))}]});if(result===null)return;const v=result.target;try{const r=await api('/workspaces/merge',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({source:name,target:v})});toast('Merged '+(r.memories_moved||0)+' memories into '+v,'ok');if(WS===name)setWS(v);refreshFolders()}catch(e){toast('Merge failed: '+e.message,'err')}} -async function wsCopy(name){try{const r=await api('/workspaces/copy',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({workspace:name})});toast('Copied to "'+r.workspace+'" ('+(r.memories_copied||0)+' memories)','ok');refreshFolders()}catch(e){toast('Copy failed: '+e.message,'err')}} -async function wsDelete(name,n){const active=name===WS?' This is the active workspace; another workspace will become active after deletion.':'';if(!await confirmAction('Delete workspace','Delete "'+name+'" and all '+n+' memories in it from this Engraphis store?'+active+' Export anything you need first. This cannot be undone.','Delete workspace',true))return;try{const r=await api('/workspaces/delete',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({workspace:name})});toast('Deleted ('+(r.memories_removed||0)+' memories)','ok');if(WS===name){WS=null;await loadWorkspaceList();if(WORKSPACES[0])setWS(WORKSPACES[0].name)}refreshFolders()}catch(e){toast(e.message,'err')}} - -/* memory detail + governance — a full-page editor (view-mem-editor), not a popup: - the raw content is always a live, editable textarea (Obsidian-style), with a - rendered preview alongside and the version history/audit trail below. */ -function setEditorActionsEnabled(enabled){['ed-save-btn','ed-pin-btn','ed-forget-btn'].forEach(id=>{const btn=document.getElementById(id);if(btn)btn.disabled=!enabled})} -async function openMem(id){window.CURMEM=null;setEditorActionsEnabled(false);navTo('mem-editor');const ta=document.getElementById('ed-content');document.getElementById('ed-title').value='';ta.value='';document.getElementById('ed-meta').innerHTML='';document.getElementById('ed-preview').innerHTML='';document.getElementById('ed-history').innerHTML='
';document.getElementById('topbar-title').textContent='Loading…';try{const d=await api('/memory/'+encodeURIComponent(id)+'?workspace='+encodeURIComponent(WS||''));const m=d.memory;if(!m){document.getElementById('topbar-title').textContent='Memory unavailable';document.getElementById('ed-history').innerHTML='
Memory not found.
';return false}window.CURMEM=m;document.getElementById('ed-title').value=m.title||'';document.getElementById('ed-type').value=m.memory_type||'semantic';ta.value=m.content||'';edPreviewUpdate();edRenderMeta();document.getElementById('topbar-title').textContent=m.title||m.id;document.getElementById('ed-history').innerHTML=renderChainDiff(d.chain)+renderAuditMini(d.audit);setEditorActionsEnabled(true);return true}catch(e){document.getElementById('topbar-title').textContent='Memory unavailable';document.getElementById('ed-history').innerHTML='
Could not load this memory: '+esc(e.message)+'
';return false}} -function closeMem(){navTo('memories')} -function edPreviewUpdate(){document.getElementById('ed-preview').innerHTML=renderMd(document.getElementById('ed-content').value)} -function edRenderMeta(){const m=window.CURMEM;if(!m)return;const btn=document.getElementById('ed-pin-btn');if(btn)btn.textContent=m.pinned?'Unpin':'Pin';document.getElementById('ed-meta').innerHTML=`${esc(m.memory_type)} ${esc(m.scope||'')} ${m.pinned?'pinned':''} ${esc((m.provenance&&m.provenance.source)||'')}${m.provenance&&m.provenance.trusted===false?' · untrusted':''} · id ${esc(m.id)}`} -async function edTogglePin(){const m=window.CURMEM;if(!m)return;try{await api('/pin',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:m.id,workspace:WS,pinned:!m.pinned})});m.pinned=!m.pinned;edRenderMeta();toast(m.pinned?'Pinned':'Unpinned','ok')}catch(e){toast(e.message,'err')}} -async function edSave(){const m=window.CURMEM;if(!m)return;const nt=document.getElementById('ed-title').value;const ntype=document.getElementById('ed-type').value;const nc=document.getElementById('ed-content').value;try{let meta=false,body=false,id=m.id;if(nt!==(m.title||'')||ntype!==(m.memory_type||'semantic')){await api('/memory/update',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:id,workspace:WS,title:nt,memory_type:ntype})});meta=true}if(nc!==(m.content||'')){const r=await api('/correct',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:id,workspace:WS,content:nc,reason:'dashboard edit'})});body=true;id=r.id}if(!meta&&!body){toast('No changes','ok');return}toast('Saved','ok');await openMem(id)}catch(e){toast(e.message,'err')}} -async function edForget(){const m=window.CURMEM;if(!m)return;if(!await confirmAction('Retire memory','Close the current validity of "'+(m.title||m.id)+'" in workspace "'+(WS||'')+'"? It will stop appearing as current truth but remain in bi-temporal history.','Retire memory',true))return;try{await api('/retire',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:m.id,workspace:WS,reason:'dashboard retirement'})});toast('Memory retired and retained in history','ok');closeMem()}catch(e){toast(e.message,'err')}} -let EDITOR_BASELINE='',EDITOR_FORCE_CLOSE=false; -function editorSnapshot(){return JSON.stringify({title:document.getElementById('ed-title').value,type:document.getElementById('ed-type').value,content:document.getElementById('ed-content').value})} -function editorIsDirty(){return !!window.CURMEM&&!!EDITOR_BASELINE&&editorSnapshot()!==EDITOR_BASELINE} -function editorRefreshDirty(){const state=document.getElementById('ed-save-state');if(!state)return;const dirty=editorIsDirty();state.textContent=dirty?'Unsaved changes':'Saved';state.classList.toggle('dirty',dirty)} -function editorCommitBaseline(){EDITOR_BASELINE=window.CURMEM?editorSnapshot():'';editorRefreshDirty()} -const openMemWithEditorState=openMem; -openMem=async function(id){const loaded=await openMemWithEditorState(id);if(loaded)editorCommitBaseline();else{EDITOR_BASELINE='';editorRefreshDirty()}return loaded} -const selectViewWithDirtyGuard=selectView; -selectView=async function(v){if(!EDITOR_FORCE_CLOSE&&v!=='mem-editor'&&document.getElementById('view-mem-editor').classList.contains('active')&&editorIsDirty()){const title=document.getElementById('ed-title').value||'this memory';if(!await confirmAction('Discard unsaved changes','Leave the Memory editor? Unsaved changes to "'+title+'" will be lost.','Discard changes',true))return false}selectViewWithDirtyGuard(v);return true} -const edForgetWithEditorState=edForget; -edForget=async function(){EDITOR_FORCE_CLOSE=true;try{return await edForgetWithEditorState()}finally{EDITOR_FORCE_CLOSE=false}} -;['ed-title','ed-type','ed-content'].forEach(id=>{const el=document.getElementById(id);el.addEventListener('input',editorRefreshDirty);el.addEventListener('change',editorRefreshDirty)}); -window.addEventListener('beforeunload',e=>{if(!editorIsDirty())return;e.preventDefault();e.returnValue=''}); -document.addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='s'&&document.getElementById('view-mem-editor').classList.contains('active')){e.preventDefault();edSave()}}); -/* license */ -async function loadLicense(){const el=document.getElementById('lic-body');try{const d=await api('/license');LIC=d;updateLicBadge();updateFeatureLocks();renderLicense(d)}catch(e){if(el)el.innerHTML='
'+esc(e.message)+'
'}} -/* Copy for the control plane's own entitlement status, when it named one. This is what - separates "your card was declined" from "you cancelled" inside a single lapsed state. */ -const LIC_STATUS_NOTE={past_due:'the last payment did not go through',canceled:'the subscription was cancelled',expired:'the billing period ended',revoked:'access was revoked'}; -const LIC_SOURCE_LABEL={environment:'operator override',session:'cloud handshake',cloud:'cloud entitlements read',connected:'not yet confirmed',local:'not connected',override:'operator override'}; -/* The whole point of the panel: when hosted features are locked, say why, and offer the - one action that fixes it. Never a plan badge over unexplained locks. */ -function licStateBanner(state,plan,ends,status){ - if(state==='trial_expired')return `
Your free trial has ended${ends?' on '+esc(ends):''}Hosted features are locked. Everything you have written is still in your local database and still fully usable — only the cloud capabilities stopped. The free trial runs once per account and cannot be started again, so restoring them means subscribing.
`; - if(state==='lapsed'){const note=LIC_STATUS_NOTE[status];return `
Your ${esc(plan||'hosted')} subscription is no longer active${note?esc(note.charAt(0).toUpperCase()+note.slice(1))+', so hosted':'Hosted'} features are locked until billing is up to date. Your local memories are unaffected. Open the account portal to restore access.
`} - if(state==='inactive')return `
No hosted plan on this installationThe local memory engine is free and complete on its own. Cloud Sync, Analytics, Automation, and Team administration run in Engraphis Cloud.
`; - return ''} -function licActionsHtml(state){if(state!=='active'&&state!=='lapsed'&&state!=='trial'&&state!=='inactive'&&state!=='trial_expired')return '';const pro=hostedCta('pro','license');return `
${ctaLinkHtml(pro,'btn btn-primary btn-sm','license')}
`} -function renderLicense(d){ - const el=document.getElementById('lic-body');if(!el)return; - const state=licAccessState(),raw=String(d.plan||'local').toLowerCase(); - const plan=raw==='pro'||raw==='team'?raw.toUpperCase():''; - const hosted=state!=='inactive',live=licAccessLive(),ends=licTrialEnds(); - const label=state==='trial'?(plan||'PRO')+' TRIAL':state==='trial_expired'?(plan||'PRO')+' TRIAL ENDED':state==='lapsed'?plan+' INACTIVE':state==='active'?plan:'LOCAL CORE'; - const known=d.known_features||{}; - const feats=hosted?Object.keys(known).map(f=>`${(d.features||[]).includes(f)?'✓':'○'} ${esc(known[f])}`).join(''):''; - let h=`
${hosted?'Hosted plan':'Local runtime'}${esc(label)}
`; - if(d.error)h+=`
Hosted authorization unavailable — ${esc(d.error)}
`; - h+=licStateBanner(state,plan,ends,d.entitlement_status); - if(hosted&&d.email&&d.email!=='trial')h+=`
Hosted account${esc(d.email)}
`; - if(state==='trial'&&ends)h+=`
Trial ends${esc(ends)}
`; - else if(hosted&&d.expires)h+=`
Authorization expires${esc(fmtDay(d.expires))}
`; - if(feats)h+=`
${feats}
`; - /* Support diagnostics: which rule produced this answer and when the cloud last confirmed - it. Emitted by /api/license since the plan resolver landed, and never shown until now — - so "the dashboard says PRO" and "the cloud says PRO" could not be told apart. */ - if(d.plan_source)h+=`
Plan source${esc(LIC_SOURCE_LABEL[d.plan_source]||d.plan_source)}${d.plan_checked_at?' · confirmed '+esc(fmtRel(d.plan_checked_at)):''}
`; - if(state==='active')h+=`
Thank you for supporting Engraphis. Your subscription helps fund hosted infrastructure and ongoing development.
`; - h+=licActionsHtml(state); - el.innerHTML=h; -} -async function exportWorkspace(){try{const d=await api('/export?workspace='+encodeURIComponent(WS||''));const blob=new Blob([JSON.stringify(d,null,2)],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='engraphis-export-'+Date.now()+'.json';a.click();URL.revokeObjectURL(a.href);toast('Exported','ok')}catch(e){toast(e.message,'err')}} - -/* Hosted Team is a service CTA; local identity and seat administration are not shipped. */ -async function loadTeam(){const el=document.getElementById('team-body'),teamCta=hostedCta('team','team_tab');try{const st=await api('/auth/state');if(teamCta.href==='#'&&st&&st.cloud_url)teamCta.href=safeUrl(st.cloud_url)}catch(e){}el.innerHTML=`
Engraphis Team Cloud HOSTED
Organizations, invitations, roles, named seats, scoped device credentials, and team audit run on the private hosted service. This local dashboard is intentionally single-user.
${esc(teamTeaserNote())} Private-service account grace is capped at 24 hours, never extends Team access, and never restricts the free local core.
${ctaLinkHtml(teamCta,'btn btn-primary btn-sm','team_tab')}
`} -/* health + settings */ -function connectionContext(){const host=(location.hostname||'').toLowerCase();return host==='localhost'||host==='127.0.0.1'||host==='::1'||host.endsWith('.localhost')?'Local engine':'Remote customer node'} -async function checkHealth(){const label=connectionContext();try{await api('/health');const d=document.getElementById('health-dot'),t=document.getElementById('health-text');if(d){d.classList.add('health-ok');d.classList.remove('health-error')}if(t)t.textContent=label+' connected'}catch(e){const d=document.getElementById('health-dot'),t=document.getElementById('health-text');if(d){d.classList.add('health-error');d.classList.remove('health-ok')}if(t)t.textContent=label+' unavailable'}try{const auth=await api('/auth/state');const m=document.getElementById('deployment-mode-indicator');if(m){const isLocal=auth.deployment_mode==='local';m.textContent=isLocal?'LOCAL':'HOSTED';m.title=isLocal?(auth.enabled?'Local API token required':'Local mode: no hosted cloud configured'):'Hosted mode: connected to Engraphis Cloud';m.className='deployment-mode '+(isLocal?'mode-local':'mode-hosted');m.hidden=false}}catch(e){}} -function loadSettings(){loadLicense();loadSyncStatus();loadHostedAgentAccess();loadLlmStatus();const s=document.getElementById('cfg-store');if(s)s.textContent=location.host;api('/info').then(function(d){var v=document.getElementById('cfg-version');if(v&&d&&d.version)v.textContent=d.version}).catch(function(){})} - -async function loadLlmStatus(){const el=document.getElementById('llm-body');if(!el)return;try{const st=await api('/llm/status');const ok=st.configured;const badge=ok?'configured':'not configured';const keyLine=st.key_set?'API key set ✓':'No API key set';let modelSel='';let provSel='';el.innerHTML=`
Provider · Model${badge}
${provSel}${modelSel}
${keyLine} · extractor: ${esc(st.extractor)}
Add this to your .env and restart Engraphis:
LLM extraction${st.extractor_enabled?'ON':'OFF'}
While ON, ingested memory content is sent to your LLM provider for schema-validated extraction. OFF disables extraction transfers only; retention supervision is configured separately.
`}catch(e){el.innerHTML='
'+esc(e.message)+'
'}} -function onLlmProvChange(){const p=document.getElementById('llm-prov').value;const sel=document.getElementById('llm-model');const defs={openai:'gpt-4o-mini',anthropic:'claude-sonnet-5-5',google:'gemini-1.5-flash',openrouter:'openai/gpt-4o-mini'};if(sel&&defs[p]){sel.value=defs[p]}updateLlmSnippet()} -function updateLlmSnippet(){const p=(document.getElementById('llm-prov')||{}).value||'openai';const m=(document.getElementById('llm-model')||{}).value||'';const ta=document.getElementById('llm-snippet');if(!ta)return;ta.value='ENGRAPHIS_LLM_PROVIDER='+p+'\nENGRAPHIS_LLM_MODEL='+m+'\nENGRAPHIS_LLM_API_KEY=\nENGRAPHIS_EXTRACTOR=llm_structured\n'} -function copyLlmSnippet(){const ta=document.getElementById('llm-snippet');if(!ta)return;ta.select();try{navigator.clipboard.writeText(ta.value);toast('Copied .env snippet','ok')}catch(e){toast('Copy failed — select and Ctrl+C','err')}} -async function setLlmExtractor(on){try{const d=await api('/llm/extractor',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({enabled:!!on})});const ok=!!d.extractor_enabled;toast(ok?'LLM extraction turned on — new memories will be sent to your provider':'LLM extraction turned off — extractor transfers are disabled'+(d.persisted===false?' (could not save for restart)':''),ok?'ok':'muted');loadLlmStatus()}catch(e){toast(e.message,'err')}} -async function testLlm(){const r=document.getElementById('llm-test-result');if(r){r.textContent='Testing…';setTone(r,'muted')}try{const d=await api('/llm/test',{method:'POST'});if(r){if(d.ok){const transient=d.auto_enabled&&d.persisted===false;r.textContent=(transient?'⚠ ':'✓ ')+'Connected — '+esc(d.provider)+'/'+esc(d.model)+(transient?' Extraction is active for this process, but the setting could not be saved for restart. Set ENGRAPHIS_EXTRACTOR=llm_structured and ENGRAPHIS_LLM_AUTO_EXTRACT=1 in the deployment environment.':'');setTone(r,transient?'red':'green')}else{r.textContent='✗ '+(d.error||'failed');setTone(r,'red')}}}catch(e){if(r){r.textContent='✗ '+esc(e.message);setTone(r,'red')}}} - -async function loadHostedAgentAccess(){const el=document.getElementById('tokens-body');if(!el)return;const teamCta=hostedCta('team','agent_access');try{const st=await api('/auth/state');if(teamCta.href==='#'&&st&&st.cloud_url)teamCta.href=safeUrl(st.cloud_url)}catch(e){}el.innerHTML=`
Per-member agent accounts, roles, named seats, and rotating device credentials are managed in Team Cloud, not by this local dashboard.
${ctaLinkHtml(teamCta,'btn btn-primary btn-sm','agent_access')}Agent Connect guide
`} -const setLlmExtractorBase=setLlmExtractor; -setLlmExtractor=async function(on){if(on&&!await confirmAction('Turn on LLM extraction',EXTERNAL_LLM_PRIVACY_COPY,'Turn on'))return;return setLlmExtractorBase(on)}; -/* Route by cause, exactly like loadAnalytics/loadAutomation. Rendering the purchase panel for - every failure told a paying customer to buy the plan they already own whenever the network - blipped or the cloud answered 5xx. Only 402 is an entitlement answer; 401/403 require reconnecting. */ -async function loadSyncStatus(){try{const d=await api('/sync/status');renderSync(d)}catch(e){const el=document.getElementById('sync-body');if(!el)return;if(managedConsentRequired(e))el.innerHTML=managedConsentHtml('Cloud Sync');else if(hostedFeatureUnavailable(e))el.innerHTML=unlockHtml('Cloud Sync','pro');else el.innerHTML='
'+esc(e.message)+'
'}} -function syncTotalAuthorizationDenial(last){if(!last||!(Number(last.attempted)>0)||Number(last.succeeded)!==0)return false;const errors=Array.isArray(last.errors)?last.errors:[];return errors.length===Number(last.attempted)&&errors.every(error=>[401,402,403].includes(Number(error&&error.status)))} -function syncRecoveryHtml(){return unlockHtml('Cloud Sync','pro')+`
`} -function syncReconnectHtml(){return `
Cloud Sync is not authorized for this installation. Reconnect in Engraphis Cloud or contact your administrator.
`} -function syncDenialHtml(last){const errors=Array.isArray(last&&last.errors)?last.errors:[];return errors.some(error=>[401,403].includes(Number(error&&error.status)))?syncReconnectHtml():syncRecoveryHtml()} -function renderSync(d){const el=document.getElementById('sync-body');if(!el)return;d=d||{};const last=d.last;if(!d.available){el.innerHTML=unlockHtml('Cloud Sync','pro');return}if (syncTotalAuthorizationDenial(last)) { el.innerHTML = syncDenialHtml(last); return; }let status='Cloud session connected; no sync recorded on this installation.';if(last){const when=new Date((last.at||0)*1000).toLocaleString();status='Last synced '+when+' — pushed '+(last.exported||0)+', +'+(last.added||0)+' received'+((last.errors&&last.errors.length)?' · '+last.errors.length+' issue(s)':'')+'.'}el.innerHTML=`
Hosted relayCONNECTED
Relay storage and authorization run in Engraphis Cloud. This package contains only the customer client; it does not run a local relay or background scheduler.
${esc(status)}
`} -async function syncNow(){const b=document.getElementById('sync-btn')||document.getElementById('sync-retry-btn');const original=b&&b.textContent;const s=document.getElementById('sync-status');if(b){b.disabled=true;b.textContent='Syncing…'}if(s)s.textContent='Contacting the cloud…';try{const d=await api('/sync/run',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'});const su=d.summary||{};toast('Synced — pushed '+(su.exported||0)+', '+(su.added||0)+' new from other devices','ok');await loadSyncStatus()}catch(e){const status=Number(e&&e.status);if(status===402){const el=document.getElementById('sync-body');if(el)el.innerHTML=syncRecoveryHtml();toast('Cloud Sync requires an active Pro or Team entitlement — open Engraphis Cloud to upgrade or renew.','err')}else if(status===401||status===403){const el=document.getElementById('sync-body');if(el)el.innerHTML=syncReconnectHtml();toast('Cloud Sync authorization is no longer active — reconnect in Engraphis Cloud.','err')}else{toast('Sync failed: '+e.message,'err');if(s)s.textContent='Sync failed — try again.'}}finally{if(b){b.disabled=false;b.textContent=original||'Sync now'}}} - -const syncNowBase=syncNow; -syncNow=async function(){if(!await confirmCloudTransfer('Sync shared workspaces','Cloud Sync sends eligible changes from your shared workspaces to Engraphis Cloud and receives authorized changes from your other installations; secret and session-scoped rows stay local.','Sync now',CLOUD_SYNC_PRIVACY_COPY))return;return syncNowBase()} - -/* ─── knowledge graph (force-graph + d3-force: compact defaults and selectable layouts) ─── */ -let GRAPH=null, FG=null, GRAPH_ENGINE=null, GRESIZE=false, GRESIZEFRAME=0, GADJ={}, GCOMM_ADJ={}, GCOMPONENTS={}, GCOMPONENT_LAYOUT=null, GHILITE=null, GHOVERSET=null, GLABELRANK={}, GLABELBOXES=[], GDATA_CACHE=null, GACTIVE_DATA=null, GREDRAWFRAME=0, GPERF={large:false,dense:false}, GRAPH_LOAD_REQUEST=0, GRAPH_LOAD_CONTROLLER=null; -const GRAPH_PRESETS={ - original:{label:'Original force',repel:120,link:30,gravity:14,font:13,size:3,linkw:1,labelDensity:40,curve:0,particles:0}, - compact:{label:'Compact clusters',repel:42,link:20,gravity:26,font:12,size:3,linkw:.7,labelDensity:30,curve:.08,particles:0}, - communities:{label:'Community islands',repel:48,link:16,gravity:48,font:12,size:3,linkw:.72,labelDensity:24,curve:.12,particles:0}, - radial:{label:'Radial orbit',repel:68,link:26,gravity:12,font:13,size:3,linkw:.75,labelDensity:55,curve:.22,particles:0}, - constellation:{label:'Constellation flow',repel:34,link:16,gravity:38,font:12,size:3,linkw:.65,labelDensity:35,curve:.32,particles:2}, - custom:{label:'Custom tuning',curve:.1,particles:0} -}; -window.GSET=window.GSET||{mode:'communities',font:12,size:3,repel:48,link:16,gravity:48,labels:false,linkw:.72,labelDensity:24,flow:true,frozen:false}; -// Freeze is session-only. Never let a pre-existing dashboard state make a newly opened graph -// look broken: every Classic graph begins with physics live until its visible switch is clicked. -window.GSET.frozen=false; -/* Keep legacy Classic geometry in the same compact world-space range as Ledger. The old - `size * sqrt(1 + degree)` rule let a highly connected entity become a giant disc, then - zoom-to-fit magnified that disc again. Degree still adds a restrained emphasis, but it is - normalized and bounded so material/theme painters cannot change node geometry. */ -function graphNodeRadius(node,base,metric){ - const size=Number.isFinite(+base)&&+base>0?+base:3; - const normalized=Math.max(0,Math.min(1,Number(metric)||0)); - const radius=size*.45*(.6+Math.min(1.6,normalized*1.9)); - return Math.max(.25,Math.min(size*1.1,radius)); -} -const ETYPE_TOKEN={person_or_concept:'--entity-concept',mention:'--entity-mention',hashtag:'--entity-hashtag',email:'--entity-email',organization:'--entity-organization',location:'--entity-location'}; -const GRAPH_PALETTES={ - theme:null, - aurora:{person_or_concept:'#8b7cf6',mention:'#2dd4bf',hashtag:'#fbbf24',email:'#60a5fa',organization:'#f472b6',location:'#a3e635'}, - ocean:{person_or_concept:'#38bdf8',mention:'#2dd4bf',hashtag:'#facc15',email:'#818cf8',organization:'#22d3ee',location:'#34d399'}, - ember:{person_or_concept:'#f97316',mention:'#fb7185',hashtag:'#facc15',email:'#a78bfa',organization:'#ef4444',location:'#84cc16'}, - contrast:{person_or_concept:'#0072b2',mention:'#009e73',hashtag:'#e69f00',email:'#56b4e9',organization:'#cc79a7',location:'#d55e00'} -}; -const GRAPH_COLOR_KEY='engraphis-graph-colors-v1'; -let GCOLOR_OVERRIDES={}, GCOLOR_PALETTE='theme'; -function cssvar(name,fallback){const value=getComputedStyle(document.body).getPropertyValue(name).trim();return value||fallback} -function graphValidColor(color){return /^#[0-9a-f]{6}$/i.test(color||'')} -function graphTypeLabel(type){return String(type||'entity').replace(/_/g,' ')} -function graphLoadColorPreferences(){ - try{ - const saved=JSON.parse(localStorage.getItem(GRAPH_COLOR_KEY)||'{}'),colors=saved&&saved.colors; - if(colors&&typeof colors==='object')Object.entries(colors).forEach(([type,color])=>{if(graphValidColor(color))GCOLOR_OVERRIDES[type]=color.toLowerCase()}); - if(saved&&saved.palette&&Object.prototype.hasOwnProperty.call(GRAPH_PALETTES,saved.palette))GCOLOR_PALETTE=saved.palette; - else if(Object.keys(GCOLOR_OVERRIDES).length)GCOLOR_PALETTE='custom'; - }catch(e){GCOLOR_OVERRIDES={};GCOLOR_PALETTE='theme'} -} -function graphSaveColorPreferences(){try{localStorage.setItem(GRAPH_COLOR_KEY,JSON.stringify({palette:GCOLOR_PALETTE,colors:GCOLOR_OVERRIDES}))}catch(e){}} -function graphTypeColor(type){if(GCOLOR_OVERRIDES[type])return GCOLOR_OVERRIDES[type];if(typeof GSTYLE!=='undefined'&&GSTYLE&&GSTYLE!=='classic'&&STYLE_PAL[GSTYLE]&&STYLE_PAL[GSTYLE][type])return STYLE_PAL[GSTYLE][type];return cssvar(ETYPE_TOKEN[type]||'--entity-concept',cssvar('--color-accent','#8c83e8'))} -/* The engine renders to a canvas, so it cannot read `--entity-*` itself the way the legend and - the controls do. Resolve the active theme's values here and hand them over; without this the - opt-in canvas keeps dark-theme node colours after a switch to Light/Solarized/Sepia. */ -function graphThemeTypeColors(){const colors={},fallback=cssvar('--color-accent','#8c83e8');Object.keys(ETYPE_TOKEN).forEach(type=>{colors[type]=cssvar(ETYPE_TOKEN[type],fallback)});colors.accent=fallback;colors.surface=cssvar('--color-panel','#15181e');colors.canvas=cssvar('--color-canvas','#0e1014');colors.relation_label=cssvar('--color-text-dim','#7e8795');colors.label=cssvar('--color-text','#e7e9ee');return colors} -/* Co-occurrence is implicit graph structure, not useful canvas text. */ -function graphShowRelationLabel(label){return !!label&&String(label).toLowerCase()!=='co_occurs'} -function graphContrastColor(color){if(!graphValidColor(color))return cssvar('--color-canvas','#0e1014');const n=parseInt(color.slice(1),16),lum=.2126*(n>>16)+.7152*((n>>8)&255)+.0722*(n&255);return lum>150?'#111827':'#f8fafc'} -const ETYPE_COLOR=new Proxy({},{get:(_,type)=>graphTypeColor(type)}); -graphLoadColorPreferences(); -function graphUpdateColorSwatches(){} -function graphRefreshNodeColors(){ - const nodes=FG&&FG.graphData?FG.graphData().nodes||[]:[]; - nodes.forEach(node=>{node.color=graphNodeColor(node);node.stroke=graphContrastColor(node.color)}); - graphUpdateColorSwatches(); - graphRedraw(); -} -function renderGraphColorControls(){ - const box=document.getElementById('graph-color-controls'),picker=document.getElementById('graph-palette'); - if(picker)picker.value=GCOLOR_PALETTE; - if(!box)return; - const types=GRAPH&&GRAPH.types&&GRAPH.types.length?GRAPH.types.map(item=>item.etype):Object.keys(ETYPE_TOKEN); - box.innerHTML=types.map(type=>{const label=graphTypeLabel(type),color=graphTypeColor(type);return ``}).join(''); -} -function graphSetTypeColor(type,color,persist){ - if(!type||!graphValidColor(color))return; - GCOLOR_OVERRIDES[type]=color.toLowerCase();GCOLOR_PALETTE='custom'; - graphClearMaterialCache(); - const picker=document.getElementById('graph-palette');if(picker)picker.value='custom'; - if(GRAPH_ENGINE){GRAPH_ENGINE.setTypeColor(type,color);if(persist)graphSaveColorPreferences();return} - graphRefreshNodeColors(); - if(persist)graphSaveColorPreferences(); -} -function graphApplyPalette(name){ - if(!Object.prototype.hasOwnProperty.call(GRAPH_PALETTES,name))name='theme'; - GCOLOR_PALETTE=name;GCOLOR_OVERRIDES=GRAPH_PALETTES[name]?{...GRAPH_PALETTES[name]}:{}; - graphSaveColorPreferences();graphRecolor(); -} -function graphResetColors(){graphApplyPalette('theme');toast('Node colors reset to the active theme','ok')} -function graphInjectCss(){} -function prefersReducedMotion(){return matchMedia('(prefers-reduced-motion: reduce)').matches} -function graphSetLayoutStatus(text,busy){ - for(const id of ['graph-layout-status','galaxy-layout-status']){const status=document.getElementById(id);if(status){status.textContent=text;status.classList.toggle('busy',!!busy)}} - for(const id of ['graph-net','galaxy-net']){const net=document.getElementById(id);if(net)net.setAttribute('aria-busy',String(!!busy))} -} -function graphSetSimulationStatus(text,busy){ - graphSetLayoutStatus(text,busy) -} -function graphUpdateHud(data){ - const mode=document.getElementById('graph-hud-mode'),count=document.getElementById('graph-hud-count'),badge=document.getElementById('graph-performance-badge'); - const preset=GRAPH_PRESETS[window.GSET.mode]||GRAPH_PRESETS.compact; - if(mode)mode.textContent=preset.label||'Custom graph'; - if(count&&data)count.textContent=data.nodes.length.toLocaleString()+' entities · '+data.links.length.toLocaleString()+' relations'; - if(badge)badge.textContent=GPERF.large?'Large graph mode':'Adaptive rendering'; -} -/* ── canonical graph renderer ──────────────────────────────────────────────────────────── - Classic now uses the same renderer as Ledger for its normal graph view. The legacy canvas - remains the rollback path: one engine failure latches GRAPH_ENGINE_FAILED and the graph - degrades to the legacy renderer instead of taking the view down. */ -let GRAPH_ENGINE_FAILED=false; -function graphEngineEnabled(){ - if(GRAPH_ENGINE_FAILED)return false; - try{return new URLSearchParams(window.location.search).get('graph-engine')==='next'||/(^|\/)classic\/?$/.test(window.location.pathname)}catch(e){return false} -} -function graphEngineFallback(error){ - GRAPH_ENGINE_FAILED=true; - try{if(GRAPH_ENGINE)GRAPH_ENGINE.destroy()}catch(e){} - GRAPH_ENGINE=null; - /* The classic renderer skips seeding when GACTIVE_DATA still points at the current data, - so a failure *after* a successful engine render would hand it an empty canvas. Clearing - the marker makes the very next graphRender() a full classic build. */ - GACTIVE_DATA=null;GCOMPONENT_LAYOUT=null;GHILITE=null;GHOVERSET=null; - if(window.console&&console.warn)console.warn('graph-engine=next failed; falling back to the classic renderer',error); -} -/* Explicit user-initiated Reload (boot()) must not be haunted by a prior failed attempt: - drop the quality-engine failure latch and any cached failed promises so the next render - retries with a fresh URL. Mirrors the per-loader cleanup that runs on each rejection. */ -function graphResetEngineFailure(){GRAPH_ENGINE_FAILED=false;FORCE_GRAPH_LOADING=null;FORCE_GRAPH_RETRY=0;GRAPH_ENGINE_LOADING=null;GRAPH_ENGINE_RETRY=0;try{if(GRAPH_ENGINE)GRAPH_ENGINE.destroy()}catch(e){}GRAPH_ENGINE=null;GACTIVE_DATA=null;} -function graphEngineEmptyMessage(){ - const total=(GRAPH&&GRAPH.nodes&&GRAPH.nodes.length)||0; - return total?('No connected entities — tick "Show unlinked" to see all '+total+'.'):'No entities in this workspace yet.'; -} -function graphRenderEngine(data,fit,reheat){ - const element=document.getElementById('graph-net'),empty=document.getElementById('graph-empty'); - if(!element||typeof EngraphisGraph==='undefined')return false; - try{ - if(!data.nodes.length){ - if(GRAPH_ENGINE)GRAPH_ENGINE.setData({nodes:[],links:[]}); - showAs(empty,true,'flex'); - if(empty)empty.textContent=graphEngineEmptyMessage(); - GACTIVE_DATA=null;graphSetLayoutStatus('No entities',false);return true; - } - showAs(empty,false);GPERF={large:data.nodes.length>600||data.links.length>2400,dense:data.links.length>1500}; - const created=!GRAPH_ENGINE; - if(created){ - GRAPH_ENGINE=EngraphisGraph.create(element,{ - renderMode:'overview', - reducedMotion:prefersReducedMotion, - onNodeClick:node=>{syncGraphExplorerSelection(node.id);graphNodeClick(node.label||node.name||node.id)}, - onBackgroundClick:()=>graphSetHighlight(null), - onStats:stats=>{const count=document.getElementById('graph-hud-count');if(count)count.textContent=stats.nodes.toLocaleString()+' entities · '+stats.links.toLocaleString()+' relations'} - }); - } - /* Re-seeding identical data would re-copy every node and throw its x/y away, restarting - the layout on each slider or preset change. The classic path guards the same way. */ - const dataChanged=created||GACTIVE_DATA!==data; - const layers={};document.querySelectorAll('#graph-layer-filters input').forEach(input=>{layers[input.value]=input.checked}); - /* "Show unlinked nodes" is applied twice: graphData() decides what is handed over, and the - engine re-filters by degree on its own state. Leaving the engine on its defaults - (showUnlinked:false, minDegree:1) drops every degree-zero entity graphData() just supplied, - so the checkbox appeared to do nothing under ?graph-engine=next. */ - const isolated=document.getElementById('graph-show-iso'),showUnlinked=!!(isolated&&isolated.checked); - GRAPH_ENGINE.apply(engine=>{ - engine.setSettings({...window.GSET}); - if(typeof engine.setRenderMode==='function')engine.setRenderMode('overview'); - engine.setStyle(typeof GSTYLE!=='undefined'?GSTYLE:'cyber'); - engine.setColorBy(typeof GCOLORBY!=='undefined'?GCOLORBY:'community'); - engine.setThemeColors(graphThemeTypeColors()); - engine.setPalette(typeof GCOLOR_PALETTE!=='undefined'?GCOLOR_PALETTE:'theme'); - engine.setTypeColors(GCOLOR_OVERRIDES||{}); - engine.setLayers(layers); - engine.setScope({showUnlinked,minDegree:showUnlinked?0:1}); - if(dataChanged)engine.setData(data); - },fit,reheat); - /* Mirror the engine's clustering back onto the dashboard's own node objects, or the - cluster legend (which reads GACTIVE_DATA) reports one community for the whole store. */ - const communityMap=GRAPH_ENGINE.communityMap(); - data.nodes.forEach(node=>{node.community=communityMap[node.id]||0}); - GACTIVE_DATA=data;graphSyncReadouts();graphUpdateEditedBadge();graphUpdateHud(data);graphRenderLegend(GRAPH); - if(dataChanged)graphSetHighlight(null); - if(window.GSET.frozen)GRAPH_ENGINE.freeze(true); - /* The renderer can be born after the user has already left the view: /graph and both lazy - scripts resolve asynchronously, and the pause on nav-away ran while GRAPH_ENGINE was still - null. Re-apply the parked state here so a renderer created against a hidden pane never - starts a rAF that nothing will stop. */ - if(GRAPH_ENGINE_PARKED)GRAPH_ENGINE.pause(); - graphSetSimulationStatus(window.GSET.frozen?'Layout frozen':'Adaptive layout',false); - return true; - }catch(error){ - graphEngineFallback(error); - return false; - } -} -/* Nav away from the graph view: park the engine's animation frame. Without this the opt-in - renderer keeps repainting a hidden canvas for the rest of the session. - The intent is *recorded* as well as applied, because pausing an engine that does not exist - yet is a no-op: leaving Graph before /graph (or either lazy script) resolves would otherwise - let the pending callback create and start a renderer against a hidden pane with no later - pause to stop it. graphRenderEngine() re-applies GRAPH_ENGINE_PARKED for that case. */ -let GRAPH_ENGINE_PARKED=false; -function graphEnginePause(){GRAPH_ENGINE_PARKED=true;try{if(GRAPH_ENGINE)GRAPH_ENGINE.pause()}catch(e){}} -function graphEngineResume(){GRAPH_ENGINE_PARKED=false;try{if(GRAPH_ENGINE)GRAPH_ENGINE.resume()}catch(e){}} -function graphInvalidateData(){ - if(GRAPH_ENGINE){try{GRAPH_ENGINE.destroy()}catch(e){}GRAPH_ENGINE=null} - GDATA_CACHE=null;GACTIVE_DATA=null;GCOMPONENT_LAYOUT=null;GHILITE=null;GHOVERSET=null -} -async function loadLegacyGraph(){ - const request=++GRAPH_LOAD_REQUEST; - const previousController=GRAPH_LOAD_CONTROLLER,controller=new AbortController();GRAPH_LOAD_CONTROLLER=controller; - if(previousController&&!previousController.signal.aborted)previousController.abort(); - /* Transactional reload: keep the existing graph visible until the replacement payload - succeeds. Only invalidate caches (not the rendered graph) so a network failure leaves - the user looking at the previous data rather than an error screen. */ - const previousGraph=GRAPH,previousEngine=GRAPH_ENGINE,previousActive=GACTIVE_DATA; - GDATA_CACHE=null; - const empty=document.getElementById('graph-empty'),net=document.getElementById('graph-net'),nodesBox=document.getElementById('graph-entity-list'),edgesBox=document.getElementById('graph-relation-list'); - showAs(empty,true,'flex');empty.textContent='Loading graph…';graphSetLayoutStatus('Loading data',true); - if(net)net.setAttribute('aria-busy','true'); - if(!GRESIZE){ - GRESIZE=true; - window.addEventListener('resize',()=>{ - if((!FG&&!GRAPH_ENGINE)||GRESIZEFRAME)return; - GRESIZEFRAME=requestAnimationFrame(()=>{GRESIZEFRAME=0;const element=document.getElementById('graph-net');if(GRAPH_ENGINE)GRAPH_ENGINE.resize();else if(FG&&element)FG.width(element.clientWidth).height(element.clientHeight)}); - }); - } - const layerInputs=Array.from(document.querySelectorAll('#graph-layer-filters input')),selectedLayers=layerInputs.filter(input=>input.checked).map(input=>input.value),layerFilter=selectedLayers.length===layerInputs.length?'':'&layers='+encodeURIComponent(selectedLayers.join(',')),includeCode=document.getElementById('graph-include-code').checked,repo=(document.getElementById('graph-repo-filter').value||'').trim(),showUnlinked=!!document.getElementById('graph-show-iso').checked; - try{ - const query=encodeURIComponent(WS||'')+(repo?'&repo='+encodeURIComponent(repo):'')+layerFilter; - const nextGraph=await api('/graph?workspace='+query+'&include_code='+(includeCode?'true':'false')+'&limit=1000&node_limit=1000&edge_limit=2000'+(showUnlinked?'':'&connected_only=true'),{signal:controller.signal}); - if(request!==GRAPH_LOAD_REQUEST)return; - /* Commit: the new payload arrived successfully. Now tear down the old renderer and - install the replacement graph. */ - if(previousEngine){try{previousEngine.destroy()}catch(e){}} - GRAPH_ENGINE=null;GACTIVE_DATA=null;GCOMPONENT_LAYOUT=null;GHILITE=null;GHOVERSET=null; - GRAPH=nextGraph; - renderGraphSide();renderGraphExplorer();graphRender(); - }catch(error){ - if(request!==GRAPH_LOAD_REQUEST||error.name==='AbortError')return; - /* Rollback: restore the previous graph so the user is not left staring at an error. - If there was no previous graph (first load), show the error message. */ - if(previousGraph){ - GRAPH=previousGraph;GRAPH_ENGINE=previousEngine;GACTIVE_DATA=previousActive; - showAs(empty,false);graphSetLayoutStatus('Reload failed — showing previous data',false); - toast('Reload data failed: '+error.message,'err'); - }else{ - showAs(empty,true,'flex');empty.textContent='Graph failed: '+error.message;graphSetLayoutStatus('Load failed',false); - } - }finally{ - if(request!==GRAPH_LOAD_REQUEST)return; - if(GRAPH_LOAD_CONTROLLER===controller)GRAPH_LOAD_CONTROLLER=null; - if(net)net.setAttribute('aria-busy','false'); - if(!GRAPH){ - if(FG)FG.graphData({nodes:[],links:[]}); - const message=empty.textContent||'Graph data unavailable.'; - if(nodesBox)nodesBox.innerHTML='
'+esc(message)+'
'; - if(edgesBox)edgesBox.innerHTML='
Relations are unavailable until graph data loads.
'; - } - } -} -function graphData(){ - const _si=document.getElementById('graph-show-iso');const hideIso=!(_si&&_si.checked); - if(GDATA_CACHE&&GDATA_CACHE.graph===GRAPH&&GDATA_CACHE.hideIso===hideIso)return GDATA_CACHE.data; - let sourceNodes=GRAPH.nodes;if(hideIso)sourceNodes=sourceNodes.filter(node=>node.degree>0); - const names=new Set(sourceNodes.map(node=>node.id)); - const nodes=sourceNodes.map(node=>({id:node.id,label:node.label||node.id,displayLabel:(node.label||node.id).length>30?(node.label||node.id).slice(0,29)+'…':(node.label||node.id),etype:node.etype,degree:node.degree||0,val:1+(node.degree||0)})); - const maxDegree=Math.max(1,...nodes.map(node=>node.degree||0)); - nodes.sort((a,b)=>b.degree-a.degree).forEach((node,index)=>{node.rank=index;node.hub=index<24;node.radius=graphNodeRadius(node,window.GSET.size,(node.degree||0)/maxDegree);node.color=graphTypeColor(node.etype);node.stroke=graphContrastColor(node.color)}); - const links=GRAPH.edges.filter(edge=>names.has(edge.from)&&names.has(edge.to)).map(edge=>({source:edge.from,target:edge.to,label:edge.label,layer:edge.layer||'semantic'})); - const data={nodes,links};GDATA_CACHE={graph:GRAPH,hideIso,data};return data; -} -function buildAdj(links){ - GADJ={};GCOMM_ADJ={}; - links.forEach(link=>{ - const source=(link.source&&link.source.id)||link.source,target=(link.target&&link.target.id)||link.target; - (GADJ[source]=GADJ[source]||new Set()).add(target);(GADJ[target]=GADJ[target]||new Set()).add(source); - GCOMM_ADJ[source]=GCOMM_ADJ[source]||new Set();GCOMM_ADJ[target]=GCOMM_ADJ[target]||new Set(); - // Influence links often connect otherwise distinct bodies of work. Keep them - // visible, but do not let a few such bridges collapse all communities into one. - if(link.label!=='influences'){ - (GCOMM_ADJ[source]=GCOMM_ADJ[source]||new Set()).add(target);(GCOMM_ADJ[target]=GCOMM_ADJ[target]||new Set()).add(source); - } - }); -} -function graphIndexComponents(nodes){ - const seen=new Set(),components=[]; - nodes.forEach(node=>{ - if(seen.has(node.id))return; - const ids=[],stack=[node.id];seen.add(node.id); - while(stack.length){const id=stack.pop();ids.push(id);(GADJ[id]||[]).forEach(next=>{if(!seen.has(next)){seen.add(next);stack.push(next)}})} - components.push(ids); - }); - components.sort((a,b)=>b.length-a.length); - GCOMPONENTS={}; - const cols=Math.max(1,Math.ceil(Math.sqrt(components.length))),gap=Math.max(84,window.GSET.link*4); - components.forEach((ids,index)=>{ - const row=Math.floor(index/cols),col=index%cols,used=Math.min(cols,components.length-row*cols); - const x=(col-(used-1)/2)*gap,y=(row-(Math.ceil(components.length/cols)-1)/2)*gap; - ids.forEach(id=>{GCOMPONENTS[id]={index,size:ids.length,x,y}}); - }); -} -function graphIndexCommunities(nodes){ - const groups={}; - nodes.forEach(node=>{const key=Number.isFinite(node.community)?node.community:0;(groups[key]=groups[key]||[]).push(node);}); - const communities=Object.entries(groups).sort((a,b)=>b[1].length-a[1].length); - const cols=Math.max(1,Math.ceil(Math.sqrt(communities.length))),gap=Math.max(150,window.GSET.link*9); - GCOMPONENTS={}; - communities.forEach(([key,members],index)=>{ - const row=Math.floor(index/cols),col=index%cols,used=Math.min(cols,communities.length-row*cols); - const x=(col-(used-1)/2)*gap,y=(row-(Math.ceil(communities.length/cols)-1)/2)*gap; - members.forEach(node=>{GCOMPONENTS[node.id]={index,size:members.length,x,y,community:Number(key)};}); - }); -} -function graphRefreshComponentCenters(nodes,force=false){ - const layout=window.GSET.mode+'|'+window.GSET.link; - if(!force&&GCOMPONENT_LAYOUT===layout)return; - if(window.GSET.mode==='communities')graphIndexCommunities(nodes);else graphIndexComponents(nodes);GCOMPONENT_LAYOUT=layout; -} -function graphAlpha(color,alpha){ - const hex=/^#([0-9a-f]{6})$/i.exec(color||''); - if(hex){const value=parseInt(hex[1],16);return `rgba(${value>>16},${(value>>8)&255},${value&255},${alpha})`} - const rgb=(color||'').match(/\d+(?:\.\d+)?/g); - return rgb&&rgb.length>=3?`rgba(${rgb[0]},${rgb[1]},${rgb[2]},${alpha})`:color; -} -function graphReadThemeColors(){ - const layers=(typeof GSTYLE!=='undefined'&&GSTYLE&&GSTYLE!=='classic'&&STYLE_LAYERS[GSTYLE])?Object.assign({},STYLE_LAYERS[GSTYLE]):{temporal:cssvar('--color-info','#6f9fd8'),entity:cssvar('--color-teal','#5aafb3'),causal:cssvar('--color-warning','#d7a84b'),semantic:cssvar('--color-accent','#8c83e8')}; - const baseAlpha=GPERF.dense?.18:.32,links={}; - Object.entries(layers).forEach(([layer,color])=>{links[layer]={base:graphAlpha(color,baseAlpha),active:graphAlpha(color,.82),dim:graphAlpha(color,.035)}}); - return {label:cssvar('--color-text','#e7e9ee'),dim:cssvar('--color-text-dim','#7e8795'),accent:cssvar('--color-accent','#8c83e8'),panel:cssvar('--color-panel','#15181e'),canvas:cssvar('--color-canvas','#0e1014'),layers,links}; -} -/* ─── graph aesthetic styles (Galaxy · Solar system · Cyberpunk), additive ─── */ -var STYLE_PAL={ - galaxy:{person_or_concept:'#b789ff',mention:'#7bb4ff',hashtag:'#ffcf6b',email:'#8aa2ff',organization:'#66e0d0',location:'#ff7ea8'}, - solar:{person_or_concept:'#ffb454',mention:'#3fd2c7',hashtag:'#ffd68a',email:'#8ea8ff',organization:'#5b9bff',location:'#ff8f6b'}, - cyber:{person_or_concept:'#ff3ea5',mention:'#b6ff3c',hashtag:'#ffe14d',email:'#8b7bff',organization:'#22e0ff',location:'#ff5c7a'} -}; -var STYLE_LAYERS={ - galaxy:{temporal:'#7bb4ff',entity:'#66e0d0',causal:'#ffcf6b',semantic:'#b789ff'}, - solar:{temporal:'#5b9bff',entity:'#3fd2c7',causal:'#ffb454',semantic:'#ffd68a'}, - cyber:{temporal:'#22e0ff',entity:'#b6ff3c',causal:'#ffe14d',semantic:'#ff3ea5'} -}; -var STYLE_BG={ - classic:'', - galaxy:'radial-gradient(58% 50% at 24% 22%,rgba(126,64,208,.30),transparent 66%),radial-gradient(52% 58% at 82% 78%,rgba(220,72,164,.20),transparent 68%),radial-gradient(46% 52% at 62% 42%,rgba(58,120,224,.16),transparent 70%),#06040f', - solar:'radial-gradient(40% 48% at 50% 50%,rgba(255,184,92,.16),transparent 60%),radial-gradient(90% 90% at 50% 50%,rgba(18,32,64,.55),transparent 82%),#05070d', - cyber:'linear-gradient(rgba(34,224,255,.055) 1px,transparent 1px) 0 0/30px 30px,linear-gradient(90deg,rgba(34,224,255,.055) 1px,transparent 1px) 0 0/30px 30px,radial-gradient(72% 60% at 50% 0%,rgba(255,62,165,.12),transparent 72%),#050810' -}; -var GSTYLE='cyber';try{var _gs=localStorage.getItem('engraphis-graph-style');if(_gs)GSTYLE=_gs;}catch(e){} -function graphMakeStars(){var a=[],c=['#dfe6ff','#dfe6ff','#c9b6ff','#a7c6ff','#ffd9ef'];for(var i=0;i<110;i++){a.push({x:(Math.random()-.5)*1200,y:(Math.random()-.5)*1200,r:Math.random()*1.1+.25,a:Math.random()*.7+.25,tw:Math.random()*1.6+.4,ph:Math.random()*6.28,c:c[i%c.length]});}return a;} -var GSTARS=graphMakeStars(); -function graphRgb(c){if(!c)return[140,131,232];if(c[0]==='#'){var h=c.length===4?c[1]+c[1]+c[2]+c[2]+c[3]+c[3]:c.slice(1,7),n=parseInt(h,16);if(Number.isFinite(n))return[n>>16&255,n>>8&255,n&255]}var m=c.match(/\d+/g);return m&&m.length>=3?[+m[0],+m[1],+m[2]]:[140,131,232]} -function graphMix(a,b,amount){var A=graphRgb(a),B=graphRgb(b),t=Math.max(0,Math.min(1,amount));return'rgb('+Math.round(A[0]+(B[0]-A[0])*t)+','+Math.round(A[1]+(B[1]-A[1])*t)+','+Math.round(A[2]+(B[2]-A[2])*t)+')'} -const GRAPH_MATERIAL_CACHE_LIMIT=192,GRAPH_MATERIAL_CACHE=new Map(); -const GRAPH_MATERIAL_RADIUS={signature:5,bezel:12,full:40}; -let GRAPH_MATERIAL_DPR=null; -function graphMaterialTier(screenRadius,forceLow){if(forceLow||!Number.isFinite(+screenRadius)||+screenRadius<6)return'signature';return+screenRadius<12?'bezel':'full'} -function graphMaterialProfile(style,col){ - var theme=window.GCOL||graphReadThemeColors(),surface=theme.panel||theme.surface||theme.canvas||'#0e1014'; - style=['cyber','galaxy','solar','classic'].includes(style)?style:'classic'; - var substrate=graphMix(surface,'#02050a',style==='classic'?.68:.78),identity='rgb('+graphRgb(col).join(',')+')',base={styleName:style,substrate:substrate,identity:identity,identityKey:graphRgb(col).join(','),substrateKey:graphRgb(substrate).join(',')}; - if(style==='cyber'){var cyber={cyan:'#21dff3',blue:'#367cff',violet:'#8d61ff',magenta:'#ec4fc4',teal:'#4ce4cf'};return Object.assign(base,{family:'iridescent-pvd',fixedPalette:cyber,film:cyber,outer:graphMix(substrate,'#01040a',.82),bezel:graphMix(substrate,'#101626',.46),face:graphMix(substrate,'#182237',.48),edge:'#677386',sheen:'#8d61ff'})} - if(style==='galaxy'){var galaxy={navy:'#111a3b',blue:'#3979e8',violet:'#8d68df',highlight:'#aab9ee'};return Object.assign(base,{family:'anodized-alloy',fixedPalette:galaxy,outer:graphMix(substrate,'#02040d',.76),bezel:graphMix(substrate,'#151a34',.54),face:graphMix(substrate,galaxy.navy,.68),edge:'#7587bb',sheen:galaxy.blue})} - if(style==='solar'){var solar={ember:'#713018',copper:'#b85c2f',amber:'#f18a32',gold:'#ffc46b',shadow:'#2b1008'};return Object.assign(base,{family:'brushed-copper',fixedPalette:solar,outer:graphMix(substrate,'#0a0402',.72),bezel:graphMix(substrate,'#351609',.62),face:graphMix(substrate,solar.copper,.48),edge:solar.amber,sheen:solar.gold})} - var classic={charcoal:'#242d36',steel:'#778593',highlight:'#c0c9cf',coolEdge:'#8aa7bd'};return Object.assign(base,{family:'satin-gunmetal',fixedPalette:classic,outer:graphMix(substrate,'#05080b',.68),bezel:graphMix(substrate,'#20272e',.52),face:graphMix(substrate,classic.charcoal,.72),edge:classic.coolEdge,sheen:classic.highlight}) -} -function graphMaterialFill(ctx,x,y,r,fill){ctx.beginPath();ctx.arc(x,y,Math.max(.1,r),0,6.2832);ctx.fillStyle=fill;ctx.fill()} -function graphMaterialStroke(ctx,x,y,r,stroke,width){ctx.beginPath();ctx.arc(x,y,Math.max(.1,r),0,6.2832);ctx.lineWidth=width;ctx.strokeStyle=stroke;ctx.stroke()} -function graphMaterialGradient(ctx,kind,args,stops){var maker=ctx[kind];if(typeof maker!=='function')return stops[Math.floor(stops.length/2)][1];var result=maker.apply(ctx,args);stops.forEach(stop=>result.addColorStop(stop[0],stop[1]));return result} -function graphMaterialIdentity(ctx,x,y,r,profile,strength){graphMaterialStroke(ctx,x,y,r*.955,graphAlpha(profile.identity,strength),Math.max(.32,r*.045))} -function graphMaterialHalo(ctx,x,y,r,tier,color,opacity,shiftX,shiftY){ - if(tier==='signature')return; - var reach=tier==='full'?1.12:1.14,halo=graphMaterialGradient(ctx,'createRadialGradient',[x+r*(shiftX||0),y+r*(shiftY||0),r*.48,x,y,r*reach],[[0,graphAlpha(color,opacity)],[.68,graphAlpha(color,opacity*.42)],[1,graphAlpha(color,0)]]); - graphMaterialFill(ctx,x,y,r*reach,halo); -} -function graphDirectionalBrush(ctx,x,y,r,angle,dark,light,strength){ - if(typeof ctx.moveTo!=='function'||typeof ctx.lineTo!=='function')return; - var alongX=Math.cos(angle),alongY=Math.sin(angle),normalX=-alongY,normalY=alongX,bound=r*.76; - for(var i=-13;i<=13;i++){var offset=i*r*.052,span=Math.sqrt(Math.max(0,bound*bound-offset*offset)),cx=x+normalX*offset,cy=y+normalY*offset;ctx.lineWidth=Math.max(.18,r*(.007+Math.abs(i%3)*.002));ctx.strokeStyle=graphAlpha(i%4===0?dark:light,strength*(.48+Math.abs(i%5)*.13));ctx.beginPath();ctx.moveTo(cx-alongX*span,cy-alongY*span);ctx.lineTo(cx+alongX*span,cy+alongY*span);ctx.stroke()} -} -function graphPaintCyberMaterial(ctx,x,y,r,p,tier){ - var f=p.fixedPalette;graphMaterialHalo(ctx,x,y,r,tier,f.cyan,.20,-.15,.12);graphMaterialHalo(ctx,x,y,r,tier,f.magenta,.17,.16,-.14);graphMaterialFill(ctx,x,y,r,p.outer);graphMaterialFill(ctx,x,y,r*.94,p.bezel); - if(tier==='signature'){graphMaterialFill(ctx,x,y,r*.79,graphMix(f.magenta,f.cyan,.58));graphMaterialStroke(ctx,x,y,r*.82,graphAlpha(f.violet,.84),Math.max(.35,r*.09));graphMaterialIdentity(ctx,x,y,r,p,.88);return} - var rimKind=typeof ctx.createConicGradient==='function'?'createConicGradient':'createLinearGradient',rimArgs=rimKind==='createConicGradient'?[-2.2,x,y]:[x-r*.8,y-r*.8,x+r*.8,y+r*.8]; - var rim=graphMaterialGradient(ctx,rimKind,rimArgs,[[0,f.cyan],[.20,f.blue],[.40,f.violet],[.61,f.magenta],[.80,f.teal],[1,f.cyan]]);graphMaterialFill(ctx,x,y,r*.89,rim); - var film=graphMaterialGradient(ctx,'createLinearGradient',[x-r*.16,y-r*.80,x+r*.22,y+r*.80],[[0,graphMix(p.face,f.magenta,.82)],[.22,graphMix(p.face,f.violet,.78)],[.48,graphMix(p.face,f.blue,.58)],[.73,graphMix(p.face,f.cyan,.82)],[1,graphMix(p.face,f.teal,.68)]]);graphMaterialFill(ctx,x,y,r*.81,film); - var band=graphMaterialGradient(ctx,'createLinearGradient',[x-r*.78,y+r*.48,x+r*.72,y-r*.56],[[0,graphAlpha(f.cyan,0)],[.31,graphAlpha(f.cyan,.16)],[.48,graphAlpha('#eef8ff',.28)],[.58,graphAlpha(f.magenta,.18)],[1,graphAlpha(f.magenta,0)]]);graphMaterialFill(ctx,x,y,r*.80,band); - var shade=graphMaterialGradient(ctx,'createRadialGradient',[x-r*.27,y-r*.34,r*.04,x,y,r*.82],[[0,graphAlpha('#f3f7ff',.38)],[.23,graphAlpha('#aebcff',.08)],[.66,graphAlpha('#02040a',.03)],[1,graphAlpha('#010207',.42)]]);graphMaterialFill(ctx,x,y,r*.80,shade); - if(tier==='full'){for(var i=0;i<13;i++){ctx.lineWidth=Math.max(.25,r*(.009+(i%3)*.003));ctx.strokeStyle=graphAlpha(i%3===0?f.cyan:(i%3===1?f.violet:f.magenta),.075+(i%4)*.018);ctx.beginPath();ctx.arc(x,y,r*(.16+i*.048),-2.88,.72);ctx.stroke()}} - ctx.lineWidth=Math.max(.36,r*.030);ctx.strokeStyle=graphAlpha('#f5fbff',.48);ctx.beginPath();ctx.arc(x,y,r*.73,-2.66,-1.14);ctx.stroke(); - graphMaterialIdentity(ctx,x,y,r,p,.78); -} -function graphPaintGalaxyMaterial(ctx,x,y,r,p,tier){ - var f=p.fixedPalette;graphMaterialHalo(ctx,x,y,r,tier,graphMix(f.blue,f.violet,.48),.11,-.10,-.10);graphMaterialFill(ctx,x,y,r,p.outer);graphMaterialFill(ctx,x,y,r*.93,p.bezel); - if(tier==='signature'){graphMaterialFill(ctx,x,y,r*.80,p.face);graphMaterialStroke(ctx,x,y,r*.84,graphAlpha(f.violet,.82),Math.max(.35,r*.08));graphMaterialIdentity(ctx,x,y,r,p,.82);return} - var face=graphMaterialGradient(ctx,'createLinearGradient',[x-r*.72,y-r*.72,x+r*.72,y+r*.72],[[0,graphMix(p.face,f.highlight,.34)],[.26,graphMix(p.face,f.blue,.40)],[.52,graphMix(p.face,f.violet,.28)],[.76,p.face],[1,graphMix(p.face,f.navy,.72)]]);graphMaterialFill(ctx,x,y,r*.83,face); - var sheen=graphMaterialGradient(ctx,'createLinearGradient',[x-r*.76,y+r*.64,x+r*.68,y-r*.70],[[0,graphAlpha(f.navy,0)],[.34,graphAlpha(f.blue,.07)],[.47,graphAlpha(f.violet,.34)],[.56,graphAlpha(f.highlight,.24)],[.68,graphAlpha(f.blue,.08)],[1,graphAlpha(f.navy,0)]]);graphMaterialFill(ctx,x,y,r*.82,sheen); - if(tier==='full'){graphDirectionalBrush(ctx,x,y,r,-.54,f.navy,f.highlight,.13);for(var i=0;i<14;i++){ctx.lineWidth=Math.max(.20,r*(.008+(i%2)*.003));ctx.strokeStyle=graphAlpha(i%2?f.blue:f.violet,.055+(i%4)*.018);ctx.beginPath();ctx.arc(x,y,r*(.14+i*.047),-2.94,.46);ctx.stroke()}} - ctx.lineWidth=Math.max(.34,r*.026);ctx.strokeStyle=graphAlpha(f.highlight,.38);ctx.beginPath();ctx.arc(x,y,r*.75,-2.70,-1.18);ctx.stroke(); - graphMaterialStroke(ctx,x,y,r*.88,graphAlpha(f.violet,.72),Math.max(.38,r*.046));graphMaterialIdentity(ctx,x,y,r,p,.76); -} -function graphPaintSolarMaterial(ctx,x,y,r,p,tier){ - var f=p.fixedPalette;graphMaterialHalo(ctx,x,y,r,tier,f.amber,.14,-.08,-.12);graphMaterialFill(ctx,x,y,r,p.outer);graphMaterialFill(ctx,x,y,r*.95,p.bezel); - if(tier==='signature'){graphMaterialFill(ctx,x,y,r*.78,f.copper);graphMaterialStroke(ctx,x,y,r*.84,f.amber,Math.max(.42,r*.10));graphMaterialIdentity(ctx,x,y,r,p,.70);return} - var copper=graphMaterialGradient(ctx,'createRadialGradient',[x-r*.20,y-r*.24,r*.025,x,y,r*.86],[[0,f.gold],[.15,f.amber],[.38,'#c66a38'],[.68,f.copper],[.86,f.ember],[1,f.shadow]]);graphMaterialFill(ctx,x,y,r*.82,copper); - var copperSheen=graphMaterialGradient(ctx,'createLinearGradient',[x-r*.74,y+r*.52,x+r*.70,y-r*.60],[[0,graphAlpha(f.shadow,0)],[.38,graphAlpha(f.amber,.08)],[.50,graphAlpha(f.gold,.34)],[.62,graphAlpha(f.ember,.10)],[1,graphAlpha(f.shadow,0)]]);graphMaterialFill(ctx,x,y,r*.80,copperSheen); - graphMaterialStroke(ctx,x,y,r*.90,f.gold,Math.max(.42,r*.055));graphMaterialStroke(ctx,x,y,r*.85,graphAlpha(f.ember,.94),Math.max(.34,r*.036)); - if(tier==='full'){for(var i=0;i<25;i++){var rr=r*(.12+i*.027);ctx.lineWidth=Math.max(.19,r*(.008+(i%3)*.0025));ctx.strokeStyle=graphAlpha(i%4===0?f.gold:f.shadow,.085+(i%5)*.018);ctx.beginPath();ctx.arc(x,y,rr,-3.02+(i%3)*.07,2.94-(i%4)*.05);ctx.stroke()}} - ctx.lineWidth=Math.max(.38,r*.030);ctx.strokeStyle=graphAlpha('#fff0c0',.48);ctx.beginPath();ctx.arc(x,y,r*.73,-2.70,-1.14);ctx.stroke(); - graphMaterialIdentity(ctx,x,y,r,p,.66); -} -function graphPaintClassicMaterial(ctx,x,y,r,p,tier){ - var f=p.fixedPalette;graphMaterialFill(ctx,x,y,r,p.outer);graphMaterialFill(ctx,x,y,r*.94,p.bezel); - if(tier==='signature'){graphMaterialFill(ctx,x,y,r*.79,p.face);graphMaterialStroke(ctx,x,y,r*.84,graphAlpha(f.coolEdge,.76),Math.max(.35,r*.08));graphMaterialIdentity(ctx,x,y,r,p,.68);return} - var steel=graphMaterialGradient(ctx,'createLinearGradient',[x-r*.72,y-r*.72,x+r*.72,y+r*.72],[[0,graphMix(p.face,f.highlight,.48)],[.24,graphMix(p.face,f.steel,.38)],[.50,p.face],[.76,graphMix(p.face,'#111820',.34)],[1,graphMix(p.face,'#05080b',.66)]]);graphMaterialFill(ctx,x,y,r*.83,steel); - var satin=graphMaterialGradient(ctx,'createRadialGradient',[x-r*.26,y-r*.31,r*.04,x,y,r*.86],[[0,graphAlpha(f.highlight,.26)],[.38,graphAlpha(f.steel,.03)],[.74,graphAlpha('#070a0d',.08)],[1,graphAlpha('#020304',.42)]]);graphMaterialFill(ctx,x,y,r*.82,satin); - if(tier==='full')graphDirectionalBrush(ctx,x,y,r,.04,'#020507',f.highlight,.16); - ctx.lineWidth=Math.max(.34,r*.026);ctx.strokeStyle=graphAlpha('#edf5fb',.34);ctx.beginPath();ctx.arc(x,y,r*.74,-2.70,-1.16);ctx.stroke(); - graphMaterialStroke(ctx,x,y,r*.88,graphAlpha(f.coolEdge,.62),Math.max(.34,r*.040));graphMaterialIdentity(ctx,x,y,r,p,.62); -} -function graphPaintMaterialDirect(ctx,x,y,r,p,tier){if(p.family==='iridescent-pvd')graphPaintCyberMaterial(ctx,x,y,r,p,tier);else if(p.family==='anodized-alloy')graphPaintGalaxyMaterial(ctx,x,y,r,p,tier);else if(p.family==='brushed-copper')graphPaintSolarMaterial(ctx,x,y,r,p,tier);else graphPaintClassicMaterial(ctx,x,y,r,p,tier)} -function graphClearMaterialCache(){GRAPH_MATERIAL_CACHE.clear();GRAPH_MATERIAL_DPR=null} -function graphMaterialDpr(){var d=window.devicePixelRatio||1;return Math.max(1,Math.min(3,Math.round(d*2)/2))} -function graphMaterialSprite(p,tier){ - var dpr=graphMaterialDpr();if(GRAPH_MATERIAL_DPR!==null&&GRAPH_MATERIAL_DPR!==dpr)graphClearMaterialCache();GRAPH_MATERIAL_DPR=dpr; - var key=[p.styleName,p.substrateKey,p.identityKey,tier,dpr].join('|');if(GRAPH_MATERIAL_CACHE.has(key)){var hit=GRAPH_MATERIAL_CACHE.get(key);GRAPH_MATERIAL_CACHE.delete(key);GRAPH_MATERIAL_CACHE.set(key,hit);return hit} - var radius=GRAPH_MATERIAL_RADIUS[tier]||24,padding=tier==='full'?3:1.5,half=radius+padding,pixels=Math.max(2,Math.ceil(half*2*dpr)),canvas=typeof OffscreenCanvas!=='undefined'?new OffscreenCanvas(pixels,pixels):document.createElement('canvas');canvas.width=pixels;canvas.height=pixels; - var spriteCtx=canvas.getContext('2d');if(!spriteCtx)return null;if(typeof spriteCtx.scale==='function'){spriteCtx.scale(dpr,dpr);graphPaintMaterialDirect(spriteCtx,half,half,radius,p,tier)}else graphPaintMaterialDirect(spriteCtx,half*dpr,half*dpr,radius*dpr,p,tier); - var value={canvas:canvas,half:half,radius:radius};GRAPH_MATERIAL_CACHE.set(key,value);if(GRAPH_MATERIAL_CACHE.size>GRAPH_MATERIAL_CACHE_LIMIT)GRAPH_MATERIAL_CACHE.delete(GRAPH_MATERIAL_CACHE.keys().next().value);return value; -} -function graphPaintMaterialSurface(ctx,x,y,r,scale,profile,large,paintDirect){ - var screenRadius=r*Math.max(.01,scale),tier=graphMaterialTier(screenRadius,large); - /* The full material sprite is intentionally bounded to 40 CSS pixels. On a focused or - high-rank node, enlarging that raster sprite is what produces the blocky/pixelated blob - users see in Classic. Paint only that exceptional node directly at its final size; keep - ordinary nodes on the cache so the large graph remains responsive. */ - if(paintDirect&&tier==='full'&&screenRadius>GRAPH_MATERIAL_RADIUS.full){graphPaintMaterialDirect(ctx,x,y,r,profile,tier);return tier} - var sprite=graphMaterialSprite(profile,tier); - if(sprite&&typeof ctx.drawImage==='function'){var half=r*sprite.half/sprite.radius;ctx.drawImage(sprite.canvas,x-half,y-half,half*2,half*2)}else graphPaintMaterialDirect(ctx,x,y,r,profile,tier); - return tier; -} -function graphStyleBackground(ctx,scale){ - if(GSTYLE==='galaxy'){ - if(GPERF.large)return;var t=performance.now()/1000,S=GSTARS;ctx.save();ctx.globalCompositeOperation='lighter'; - for(var i=0;i1,neighbor=focus&&GHOVERSET.has(node.id),dim=focus&&!neighbor; - var r=node.radius,col=node.color,profile,directMaterial=node.id===GHILITE||node.rank===0; - ctx.globalAlpha=dim?.12:1; - if(GSTYLE==='galaxy'){ - profile=graphMaterialProfile('galaxy',col);graphPaintMaterialSurface(ctx,node.x,node.y,r,scale,profile,GPERF.large,directMaterial); - }else if(GSTYLE==='solar'){ - var sun=node.rank===0; - profile=graphMaterialProfile('solar',sun?graphMix(col,'#d38b43',.46):col);graphPaintMaterialSurface(ctx,node.x,node.y,r,scale,profile,GPERF.large,directMaterial); - }else if(GSTYLE==='cyber'){ - profile=graphMaterialProfile('cyber',col);graphPaintMaterialSurface(ctx,node.x,node.y,r,scale,profile,GPERF.large,directMaterial); - }else{ - profile=graphMaterialProfile('classic',col);graphPaintMaterialSurface(ctx,node.x,node.y,r,scale,profile,GPERF.large,directMaterial); - } - if(node.id===GHILITE){ - graphMaterialFill(ctx,node.x,node.y,r*.76,graphAlpha('#ffffff',.065)); - ctx.lineWidth=1.15/scale;ctx.strokeStyle=graphAlpha(profile.sheen,.98);ctx.beginPath();ctx.arc(node.x,node.y,r+1.35/scale,0,6.2832);ctx.stroke(); - ctx.lineWidth=.55/scale;ctx.strokeStyle=graphAlpha(profile.identity,.92);ctx.beginPath();ctx.arc(node.x,node.y,r+2.45/scale,0,6.2832);ctx.stroke(); - } - ctx.globalAlpha=1; -} -function graphApplyStyleChrome(){ - var net=document.querySelector('.graph-network');if(net){for(const name of ['classic','galaxy','solar','cyber'])net.classList.toggle('graph-style-'+name,GSTYLE===name)} - var sel=document.getElementById('graph-style');if(sel&&sel.value!==GSTYLE)sel.value=GSTYLE; -} -function graphStyleLinkColor(link){ - var layer=link&&link.layer; - if(GSTYLE==='galaxy')return layer==='causal'?'#c58bff':'#91a8ff'; - if(GSTYLE==='solar')return layer==='causal'?'#ffc06d':'#ef913e'; - if(GSTYLE==='cyber')return layer==='causal'?'#ec71d2':'#6edce6'; - if(GSTYLE==='classic')return layer==='causal'?'#b9c8da':'#86c7d1'; - return (window.GCOL.links[layer]||window.GCOL.links.semantic).base; -} -function graphSetStyle(name){ - if(['classic','galaxy','solar','cyber'].indexOf(name)<0)name='cyber'; - GSTYLE=name;try{localStorage.setItem('engraphis-graph-style',name)}catch(e){} - graphClearMaterialCache();graphApplyStyleChrome(); - if(GRAPH_ENGINE){GRAPH_ENGINE.setStyle(name);return} - if(GRAPH&&FG){graphRefreshNodeColors();graphRenderLegend();graphRender(false,false);} -} -/* ─── colorful graphs even when every node is one entity type: color by community or connections ─── */ -var GRAPH_HEAT=['#3f7bff','#6a5cff','#a24bff','#e0479f','#ff6b6b','#ffc23d']; -var COMMUNITY_PALS={ - classic:['#8c83e8','#5aafb3','#d7a84b','#6f9fd8','#58b882','#df7478','#b07de0','#4fb0a0','#e0894a','#7c9be0','#e06a9a','#9ac25a'], - galaxy:['#b789ff','#7bb4ff','#66e0d0','#ffcf6b','#ff7ea8','#8aa2ff','#c98bff','#5ad0e0','#ffa0d0','#9d7bff','#6ad0b0','#ffb060'], - solar:['#ffb454','#5b9bff','#3fd2c7','#ffd68a','#ff8f6b','#8ea8ff','#ffc24a','#6ac0d0','#ff9f7a','#7ab0ff','#e0b050','#5fd0b0'], - cyber:['#22e0ff','#ff3ea5','#b6ff3c','#ffe14d','#8b7bff','#ff5c7a','#3affd0','#ff7be0','#7affea','#c0ff4a','#5c9bff','#ff9b3c'] -}; -var GCOLORBY='community';try{var _cb=localStorage.getItem('engraphis-graph-colorby');if(_cb)GCOLORBY=_cb;}catch(e){} -var GMAXDEG=1; -function graphCommunityPalette(){return COMMUNITY_PALS[(typeof GSTYLE!=='undefined'&&COMMUNITY_PALS[GSTYLE])?GSTYLE:'classic'];} -function graphComputeCommunities(nodes){ - var byId={},seen=new Set(),groups=[];nodes.forEach(function(node){byId[node.id]=node;}); - nodes.forEach(function(node){ - if(seen.has(node.id))return; - var group=[],stack=[node.id];seen.add(node.id); - while(stack.length){ - var id=stack.pop();group.push(id);var neighbours=GCOMM_ADJ[id]||new Set(); - neighbours.forEach(function(next){if(byId[next]&&!seen.has(next)){seen.add(next);stack.push(next);}}); - } - groups.push(group); - }); - groups.sort(function(a,b){return b.length-a.length;}); - groups.forEach(function(group,index){group.forEach(function(id){byId[id].community=index;});}); -} -function graphHeatColor(node){var total=(GACTIVE_DATA&&GACTIVE_DATA.nodes.length)||1;var t=(node.rank||0)/Math.max(1,total-1);var idx=Math.min(GRAPH_HEAT.length-1,Math.floor(t*GRAPH_HEAT.length));return GRAPH_HEAT[idx];} -function graphNodeColor(node){ - if(GCOLORBY==='community'){var pal=graphCommunityPalette();return pal[(node.community||0)%pal.length];} - if(GCOLORBY==='connections'){return graphHeatColor(node);} - return graphTypeColor(node.etype); -} -function graphRenderLegend(graph){ - graph=graph||GRAPH;var legend=document.getElementById('graph-legend'),legendCount=document.getElementById('graph-legend-count');if(!legend||!graph)return; - if(GCOLORBY==='community'){ - var nodes=(GACTIVE_DATA&&GACTIVE_DATA.nodes)||[],sizes={};nodes.forEach(function(n){var c=n.community||0;sizes[c]=(sizes[c]||0)+1;}); - var order=Object.keys(sizes).map(Number).sort(function(a,b){return sizes[b]-sizes[a];}),pal=graphCommunityPalette(); - legend.innerHTML=order.slice(0,10).map(function(c,i){return '
Cluster '+(i+1)+''+sizes[c].toLocaleString()+'
';}).join('')||'
None
'; - if(legendCount)legendCount.textContent=order.length?order.length+(order.length===1?' cluster':' clusters'):''; - return; - } - if(GCOLORBY==='connections'){ - legend.innerHTML='
'+GRAPH_HEAT.map(function(col,i){return '';}).join('')+'
Most connectedLeast
'; - if(legendCount)legendCount.textContent='by connections'; - return; - } - var types=graph.types||[]; - legend.innerHTML=types.map(function(item){return '
'+esc(graphTypeLabel(item.etype))+''+item.count.toLocaleString()+'
';}).join('')||'
None
'; - if(legendCount)legendCount.textContent=types.length?types.length+(types.length===1?' type':' types'):''; -} -function graphSetColorBy(mode){ - if(['type','community','connections'].indexOf(mode)<0)mode='community'; - GCOLORBY=mode;try{localStorage.setItem('engraphis-graph-colorby',mode)}catch(e){} - graphClearMaterialCache(); - var sel=document.getElementById('graph-colorby');if(sel&&sel.value!==mode)sel.value=mode; - if(GRAPH_ENGINE){GRAPH_ENGINE.setColorBy(mode);graphRenderLegend();return} - if(GRAPH&&FG&&GACTIVE_DATA){graphComputeCommunities(GACTIVE_DATA.nodes);GMAXDEG=GACTIVE_DATA.nodes.reduce(function(m,n){return Math.max(m,n.degree||0);},1);graphRefreshNodeColors();graphRenderLegend();} -} -function graphApplyForces(){ - if(!FG)return; - const settings=window.GSET,mode=settings.mode||'compact'; - FG.d3Force('charge').strength(-(mode==='communities'?Math.max(10,settings.repel*.68):settings.repel)); - FG.d3Force('link').distance(settings.link); - if(typeof d3==='undefined')return; - FG.d3Force('radial',null); - const layoutNodes=GACTIVE_DATA&&GACTIVE_DATA.nodes||[]; - /* Each named mode owns a different target geometry. Slider values still control local - spacing, but switching buttons must visibly change the arrangement even for one component. */ - if(mode==='communities'){ - const keys=[],seen=new Set();layoutNodes.forEach(node=>{const key=Number.isFinite(node.community)?node.community:0;if(!seen.has(key)){seen.add(key);keys.push(key)}});keys.sort((a,b)=>a-b); - const cols=Math.max(1,Math.ceil(Math.sqrt(keys.length))),rows=Math.max(1,Math.ceil(keys.length/cols)),gap=Math.max(180,(Number(settings.link)||16)*10),targets=new Map(); - keys.forEach((key,index)=>{const col=index%cols,row=Math.floor(index/cols);targets.set(key,{x:(col-(cols-1)/2)*gap,y:(row-(rows-1)/2)*gap*.72})}); - const centering=Math.max(.04,(Number(settings.gravity)||0)/100),target=node=>targets.get(Number.isFinite(node.community)?node.community:0)||{x:0,y:0};FG.d3Force('x',d3.forceX(node=>target(node).x).strength(centering));FG.d3Force('y',d3.forceY(node=>target(node).y).strength(centering)); - }else if(mode==='radial'&&d3.forceRadial){ - const outer=Math.max(180,Math.min(360,Math.sqrt(Math.max(1,layoutNodes.length))*18+(Number(settings.link)||16)*4)),maxDegree=Math.max(1,layoutNodes.reduce((max,node)=>Math.max(max,node.degree||0),1)); - FG.d3Force('x',d3.forceX(0).strength(Math.max(.05,(Number(settings.gravity)||0)/500)));FG.d3Force('y',d3.forceY(0).strength(Math.max(.05,(Number(settings.gravity)||0)/500))); - FG.d3Force('radial',d3.forceRadial(node=>{const hubness=Math.max(0,Math.min(1,(node.degree||0)/maxDegree));return 34+(outer-34)*(1-hubness)}).strength(.72)); - }else if(mode==='constellation'){ - const positions=new Map(),total=Math.max(1,layoutNodes.length-1),reach=Math.max(160,Math.min(330,80+Math.sqrt(Math.max(1,layoutNodes.length))*10)); - layoutNodes.forEach((node,index)=>{const rank=Number.isFinite(node.rank)?node.rank:index,fraction=Math.max(0,Math.min(1,rank/total)),angle=index*2.399963229728653,radius=48+fraction*reach;positions.set(node.id,{x:Math.cos(angle)*radius*1.18,y:Math.sin(angle)*radius*.76})}); - const target=node=>positions.get(node.id)||{x:0,y:0};FG.d3Force('x',d3.forceX(node=>target(node).x).strength(.18));FG.d3Force('y',d3.forceY(node=>target(node).y).strength(.18)); - }else{ - const centering=mode==='compact'?Math.max(.24,(Number(settings.gravity)||0)/100):Math.max(.06,(Number(settings.gravity)||0)/100); - FG.d3Force('x',d3.forceX(0).strength(centering));FG.d3Force('y',d3.forceY(0).strength(centering)); - } - FG.d3Force('collide',d3.forceCollide(node=>node.radius+1.5).iterations(GPERF.large?1:2)); -} -function graphSetHighlight(id){ - GHILITE=id||null; - GHOVERSET=id?new Set([id,...(GADJ[id]||[])]):null; - /* graphRedraw() is a no-op without the classic FG instance, so the opt-in engine needs - telling directly — otherwise hovering the entity list highlights nothing on canvas. */ - if(GRAPH_ENGINE){try{GRAPH_ENGINE.setHighlight(GHILITE)}catch(e){}return} - graphRedraw(); -} -function graphRefreshNodeMetrics(){ - const nodes=FG&&FG.graphData?FG.graphData().nodes||[]:[]; - const maxDegree=Math.max(1,...nodes.map(node=>node.degree||0)); - nodes.forEach(node=>{node.radius=graphNodeRadius(node,window.GSET.size,(node.degree||0)/maxDegree)}); -} -function graphRedraw(){ - if(!FG||GREDRAWFRAME)return; - GREDRAWFRAME=requestAnimationFrame(()=>{GREDRAWFRAME=0;if(FG)FG.nodeCanvasObject(FG.nodeCanvasObject())}); -} -/* ── on-demand graph assets ─────────────────────────────────────────────────────────────── - Neither script is in index.html. force-graph.min.js applies inline styles at runtime, and - under the production CSP (`style-src 'self'`) every one of those is blocked and reported; - loading it on a page that never opens the graph turns a plain dashboard view into a wall of - console errors. Both loaders are memoized, so a re-entrant graphRender() reuses the in-flight - fetch rather than appending a second - - - - - - - - + + + + + + + + Engraphis Ledger + + + + + + +
+ + +
+

+ + + +
+
+
+
+

Home ·

+

Home for this workspace

+

Everything below comes from this workspace’s memory records and audit trail.

+
+ +
+
—Live memories
+
—All versions, including history
+
—Workspaces
+
—Sessions
+
+ +
+
+

Needs review

Pending sources, quarantined records and conflicts reported by the engine.

Choose a workspace

+ +
+

+
+

Loading review state…

+
+
+ +
+
+

Recent activity

Privacy-safe operations from the audit log.

+ +
+
+ + + +
WhenActorActionScopeReceipt
Loading activity…
+
+
+
+ + +
+
+ +
+
+
+

Connections · local coding agents

+

Carry a project fact into your next session

+

Choose a workspace to begin.

+

This checklist stays on this browser, separately for each workspace and project. Local memory needs no account.

+
+

+
+

Use this workspace in your agent

+

Choose a workspace and project above, save its default, then copy the instructions into your agent’s project instructions. You only need to do this once per project.

+

Default workspace for this project

+

Save a default on this Engraphis server for agents that supply this project’s repository name and omit a workspace. An explicit workspace or a supplied session takes priority. Memory type does not choose a workspace.

+

Select a project to configure its default workspace.

+
+ + +
+

Copy agent instructions

+

Project instructions follow your saved default, so you can change it here later. With no project selected, the instructions use this workspace directly.

+
+ +

+
+
    +
  1. +

    Connect your agent

    +

    On the machine running your agent, run engraphis-init. Use its setup command for the same local database as this dashboard; it includes any configured database and encryption paths.

    + +

    For an installation already using the shared private configuration:

    +
    + +

    +

    Other clients may use different configuration formats. For HTTP or hosted connections, follow the connection guide.

    + +
  2. +
  3. +

    Save a fact and recall it after a restart

    +

    Save one useful project decision. Review its source and approve it for prompt context. Restart your agent, then ask it about the decision in the workspace and project shown above.

    +
    + +
  4. +
  5. +

    Correct the fact and inspect its history

    +

    Open the cited record in Library, revise it, and inspect the supersession chain. Confirm that your agent recalls the current version.

    + +
  6. +
+
+
+ +
+
+
+

Ask · grounded retrieval

+

Answer from what the store can support

+

Every claim links to a memory. If the evidence is weak, Engraphis says so.

+

Choose a workspace

+
+ +
+ + +
+ + + +
+
+ +
+ Experimental Jev retrieval planning +

Jev can prioritize one route already produced locally. The original query and retrieval filters stay fixed. A 40-task synthetic check found no retrieval-metric change; this option has no demonstrated user-workload benefit.

+ + + +
+ +

+

+
+ +
+ +
+

Ask a question to begin.

+
+ +
+ Inspect retrieval +

Retrieved candidates from this answer

+
+

Candidates from the grounded answer will appear here.

+
+
+
+
+ +
+
+
+
+

Library · active memory

+

Browse, add and govern memories

+

Live records stay editable without erasing their temporal history.

+

Choose a workspace

+
+
+ + + + +
+
+ +
+ + + 0 memories + + + +
+ +
+ + Select memories on this page to move them to another workspace. + +
+ +
+
+

Loading memories…

+
+ +
+
+

Selected memory

+

Choose a memory

+

Select a memory from the library to inspect its content, scope, provenance and history.

+
+ + +
+
+
+
+ +
+
+
+
+
+

Explore · evidence graph

+

Explore project connections

+
+
+ +
Open Graph & Relationships to load the graph.
+
+ 0 entities · 0 relations + Galaxy gravity +
+

The graph is a visual summary. Open the Analyse tab to inspect entities and relations with keyboard controls.

+
+ + +
+
+ +
+
+
+

Activity · provenance and history

+

Activity and memory history

+

Inspect support, supersessions and privacy-safe receipts without flattening history.

+
+ +
+ + + + +
+ +
+
+ + +
+

Search for a claim to inspect its live support and what it replaced.

+
+ +
+
+ + +
+

Search a topic to travel through its valid-time history.

+
+ +
+
+

Recorded operations

Actor, action, scope and verification state.

+
+ + +
+
+

Loading context savings…

+

Loading audit records…

+
+ +
+
+ + +
+

Search a topic to compare closed and current records.

+
+
+
+ +
+
+
+

Settings · workspace and services

+

Settings and local operations

+

Workspace, consolidation, hosted services and interface preferences in one place.

+
+ +
+
+

Runtime savings

+

Estimated context saved

+

Loading receipt-backed estimate…

+
+
+ — + tokens avoided +
+
+ — + +
+
+ +
+ + + + + + + + +
+ +
+
+

Workspaces

Each workspace is an independent visibility boundary.

+ +
+ +

Loading workspaces…

+
+
+
+

Pro · end-to-end encrypted

+

Sync eligible shared workspaces

+

Push this device’s changes and pull peer changes for every eligible shared workspace. Secret, session-scoped, and personal-workspace memories stay local.

+

Open this tab to check the Cloud Sync connection.

+
+
+ +
+
+
+

Sleep-time maintenance

+

Review before memory evolves

+

A dry run finds recurring episodes and decayed transients. Nothing is committed until you explicitly apply it.

+
+
+ + + +
+
+

No preview has been run.

+
+ +
+
+

Pro · hosted compute

+

Portfolio analytics without moving secret memory

+

Aggregate health, growth and reinforcement trends are computed through the connected Engraphis Cloud account.

+

Open this tab to check availability.

+ Subscribe to Pro +
+
+ +
+
+

Pro · managed maintenance

+

Schedule consolidation with an explicit upload boundary

+

Hosted automation receives a bounded workspace snapshot. Secret and session-scoped memory stays local.

+

Open this tab to check availability.

+ Subscribe to Pro +
+
+ +
+
+

Team · hosted control plane

+

Shared workspaces, member roles and named seats

+

The local dashboard stays single-user. Team authorization and remote-agent access live in the hosted service.

+

Checking local connection state…

+ Compare Team +
+
+ +
+
+

Plans & billing

Free is local forever. Pay only for hosted services.

+ +
+
+
+ + + + + + + + + + + + + + + +
CapabilityFreeProTeam
Local memory engineIncludedIncludedIncluded
Grounded recallIncludedIncludedIncluded
Bi-temporal provenanceIncludedIncludedIncluded
Relations graphIncludedIncludedIncluded
Manual consolidationIncludedIncludedIncluded
Cloud sync—IncludedIncluded
Managed automation—IncludedIncluded
Portfolio analytics—IncludedIncluded
Shared workspaces——Included
Members and roles——Included
Remote agent access——Included
+
+
+ +
+
+
+

Settings · local preferences

+

Make the workspace yours

+

Choose how Engraphis looks and connects while keeping memory, recall, and storage local by default.

+
+
Local-first runtime
+
+
+

Managed processing

+

Encrypted Cloud Sync has its own controls. Managed processing lets hosted services read an eligible snapshot to generate insights and run automation. Secret and session-scoped records stay local.

+
+

Select a workspace.

+ + + + +
+
+
+
+

Interface

+

Dashboard

+

Both interfaces read the same store. The choice changes presentation only.

+ +
+
+

Appearance

+

Theme

+

The preference stays on this device and is shared with Classic.

+ +
+
+

Pro · hosted account

+

Engraphis Cloud

+

Manage your subscription, connected devices, and hosted account settings in Engraphis Cloud.

+ +
+
+
+

Optional synthesis

+

Connect an LLM

+

Use a provider only when you want schema-validated extraction. Recall and storage remain local by default.

+
+

Checking local configuration…

+
+
+

Engine

+

Local runtime

+
API
127.0.0.1:8700
Engine
v2 · bi-temporal
Storage
local SQLite
+ Open Classic tools +
+
+
+
+
+
+
+ + +
+
+
+

Remote server access

+

Connect to a remote Engraphis server

+
+
+

This remote server requires an administrator access token. The dashboard on your own computer opens automatically.

+
+ Where do I find my access token? +

Ask the person who set up this Engraphis server. If you set it up yourself, use the ENGRAPHIS_API_TOKEN value in your Engraphis configuration.

+
+

This browser stays signed in for up to 12 hours. The token is cleared from this form after you submit it.

+ + +
+ + +
+
+
+ + +
+
+

Organize memory

Move selected memories

+
+

Preview the destination and preserved history before moving. Moving changes who can access these memories to the destination workspace’s access rules.

+

+ + +

Choose a destination, then preview the move.

+ +
+ + + +
+
+
+ + +
+
+
+

Graph connections

+

Connected nodes

+
+ + +
+

+
+
+

Memories

+
+
+
+
+ + +
+
+

Local document import

Import local documents

+ +
+

Choose individual files or a folder. Engraphis previews supported document formats before it writes anything; uploaded bytes are processed locally and are not kept as dashboard upload copies.

+
+ + + + + + + + + + + +
+ +
Choose files or a folder to preview its import.
+ +

No preview yet.

+
+ + + +
+
+
+ + + + + + + + + + diff --git a/engraphis/dashboard_assets/ledger.js b/engraphis/dashboard_assets/ledger.js index a659cd47f..a6e9bfb00 100644 --- a/engraphis/dashboard_assets/ledger.js +++ b/engraphis/dashboard_assets/ledger.js @@ -1,6075 +1,6137 @@ -(() => { - 'use strict'; - - const apiRoot = `${location.origin}/api`; - const state = { - workspace: '', - project: '', - workspaces: [], - stats: {}, - memories: [], - libraryTotal: 0, - libraryNextCursor: null, - libraryCursors: [null], - libraryPage: 0, - libraryLoading: false, - librarySelecting: false, - moveMemoryIds: new Set(), - memoryMove: null, - selectedMemory: '', - editorMemory: null, - editorSession: null, - memoryHistory: null, - editorReturnFocus: null, - view: 'today', - provenanceTab: 'belief', - savingsPreset: 'all', - manageTab: 'workspaces', - refreshEpoch: 0, - graphWorkspace: '', - graphData: null, - graphDataMode: 'overview', - graphGalaxyQuality: false, - graphDataPreset: 'galaxy', - graphDataIncludeCode: false, - graphDataShowUnlinked: false, - graphDataAsOf: null, - graphDataRepo: '', - graphMeta: null, - graphMode: 'overview', - graphShowUnlinked: true, - graphEngine: null, - graphLoadPromise: null, - graphLoadWorkspace: '', - graphLoadMode: '', - graphLoadIncludeCode: false, - graphLoadShowUnlinked: false, - graphLoadAsOf: null, - graphLoadRepo: '', - graphLoadKey: '', - graphLoadRequest: 0, - graphRetryPending: false, - graphLoadController: null, - graphConnectionsRequest: 0, - graphConnectionsController: null, - graphMetrics: {}, - graphFrozen: false, - graphOrbitPaused: false, - graphSpacetimeOverlay: null, - graphIncludeCode: false, - graphSavedView: '', - consolidationReview: null, - reviewCsrf: '', - hostedLoaded: new Set(), - analyticsStarting: new Set(), - analyticsJobs: new Map(), - analyticsResults: new Map(), - analyticsStartErrors: new Map(), - scopedRequests: Object.create(null), - scopedControllers: Object.create(null), - syncStatus: null, - license: null, - releaseVersion: '', - }; - - const byId = id => document.getElementById(id); - const memoryTitle = window.EngraphisWorkflow.title; - const all = selector => [...document.querySelectorAll(selector)]; - const text = value => value == null ? '' : String(value); - const number = value => Number.isFinite(Number(value)) ? Number(value) : 0; - const NOTICE_DURATION_MS = 3000; - let noticeTimer = null; - let graphRepoLoadTimer = null; - let librarySearchTimer = null; - const CLOUD_SYNC_PRIVACY_NOTICE = 'Cloud Sync encrypts eligible shared-workspace changes end-to-end before they leave this device. Engraphis Cloud cannot read their contents; secret and session-scoped memories stay local.'; - const EXTERNAL_LLM_PRIVACY_NOTICE = 'Memory text is sent to your configured LLM provider for processing under that provider’s terms. The provider must read that text to return extracted facts.'; - const truncate = (value, length = 260) => { - const source = text(value).trim(); - return source.length > length ? `${source.slice(0, length - 1)}…` : source; - }; - const empty = (message, className = 'empty-state') => { - const node = document.createElement('p'); - node.className = className; - node.textContent = message; - return node; - }; - const node = (tag, className = '', content = '') => { - const element = document.createElement(tag); - if (className) element.className = className; - if (content !== '') element.textContent = text(content); - return element; - }; - const button = (label, className, action) => { - const control = node('button', className, label); - control.type = 'button'; - control.addEventListener('click', action); - return control; - }; - const option = (value, label, selected = false) => { - const item = node('option', '', label); - item.value = value; - item.selected = selected; - return item; - }; - const query = (name = state.workspace) => `workspace=${encodeURIComponent(name || '')}`; - const memoryQuery = (workspace = state.workspace, project = state.project) => query(workspace) - + (project ? '&repo=' + encodeURIComponent(project) : ''); - const beginScopedRequest = kind => { - if (state.scopedControllers[kind]) state.scopedControllers[kind].abort(); - const controller = new AbortController(); - state.scopedControllers[kind] = controller; - const generation = number(state.scopedRequests[kind]) + 1; - state.scopedRequests[kind] = generation; - return { - kind, - generation, - workspace: state.workspace, - project: state.project, - epoch: state.refreshEpoch, - signal: controller.signal, - }; - }; - const isCurrentScopedRequest = request => Boolean(request - && request.workspace === state.workspace - && request.project === state.project - && request.epoch === state.refreshEpoch - && state.scopedRequests[request.kind] === request.generation); - const invalidateScopedRequests = () => { - Object.values(state.scopedControllers).forEach(controller => controller.abort()); - state.scopedControllers = Object.create(null); - Object.keys(state.scopedRequests).forEach(kind => { - state.scopedRequests[kind] = number(state.scopedRequests[kind]) + 1; - }); - }; - const GRAPH_INITIAL_NODE_LIMIT = 1500; - const GRAPH_INITIAL_EDGE_LIMIT = 3000; - const GRAPH_ALL_NODE_LIMIT = 20_000; - const GRAPH_ALL_EDGE_LIMIT = 200_000; - const GRAPH_LOAD_TIMEOUT_MS = 60_000; - // Cold complete scenes can legitimately spend ~25s in the bounded server projection - // before the renderer receives its payload. Keep enough headroom for the response body, - // graph assets, and renderer readiness instead of turning a healthy cold request into a - // generic client timeout. - const GRAPH_FULL_LOAD_TIMEOUT_MS = 90_000; - const GRAPH_CONNECTION_MEMORIES_TIMEOUT_MS = 8_000; - const GRAPH_PREFERENCES_KEY = 'engraphis-ledger-graph-preferences-v1'; - const GRAPH_PHYSICS_VERSION = 6; - const GRAPH_CUSTOM_VIEW_KEY = 'engraphis-ledger-graph-custom-view-v1'; - const GRAPH_LAYERS = ['temporal', 'entity', 'causal', 'semantic', 'code']; - const GRAPH_DEFAULT_LAYERS = { temporal: true, entity: true, causal: true, semantic: true, code: false }; - const GRAPH_TUNING = [ - { id: 'graph-repel', key: 'repel', fallback: 100 }, - { id: 'graph-link', key: 'link', fallback: 8 }, - { id: 'graph-gravity', key: 'gravity', fallback: 72 }, - { id: 'graph-node-size', key: 'size', fallback: 3 }, - { id: 'graph-text-size', key: 'font', fallback: 12 }, - { id: 'graph-line-width', key: 'linkw', fallback: 0.72, precision: 2 }, - { id: 'graph-label-density', key: 'labelDensity', fallback: 24 }, - ]; - const GRAPH_SPACETIME_TUNING = [ - { id: 'graph-gravitational-constant', key: 'gravitationalConstant', fallback: 60 }, - { id: 'graph-black-hole-mass', key: 'blackHoleMass', fallback: 96 }, - { id: 'graph-local-gravitational-constant', key: 'localGravitationalConstant', fallback: 60 }, - { id: 'graph-space-damping', key: 'damping', fallback: 1, precision: 1 }, - { id: 'graph-spring-stiffness', key: 'springStiffness', fallback: 32 }, - ]; - const GRAPH_PRESET_TUNING = { - original: { repel: 120, link: 30, gravity: 14, font: 13, size: 3, linkw: 1, labelDensity: 40 }, - compact: { repel: 42, link: 20, gravity: 26, font: 12, size: 3, linkw: 0.7, labelDensity: 30 }, - communities: { repel: 48, link: 16, gravity: 48, font: 12, size: 3, linkw: 0.72, labelDensity: 24 }, - galaxy: { repel: 100, link: 8, gravity: 72, font: 12, size: 3, linkw: 0.72, labelDensity: 24 }, - radial: { repel: 68, link: 26, gravity: 12, font: 13, size: 3, linkw: 0.75, labelDensity: 55 }, - constellation: { repel: 34, link: 16, gravity: 38, font: 12, size: 3, linkw: 0.65, labelDensity: 35 }, - }; - const GRAPH_SAVED_VIEWS = { - operations: { - preset: 'compact', style: 'cyber', color: 'connections', palette: 'contrast', - layers: { temporal: false, entity: true, causal: true, semantic: false, code: false }, - minDegree: 2, depth: 1, showUnlinked: false, includeCode: false, - }, - schema: { - preset: 'communities', style: 'cyber', color: 'community', palette: 'theme', - layers: { ...GRAPH_DEFAULT_LAYERS }, minDegree: 1, depth: 2, showUnlinked: true, includeCode: false, - }, - people: { - preset: 'radial', style: 'galaxy', color: 'community', palette: 'aurora', - layers: { temporal: false, entity: true, causal: false, semantic: true, code: false }, - minDegree: 1, depth: 2, showUnlinked: false, includeCode: false, - }, - code: { - preset: 'constellation', style: 'cyber', color: 'type', palette: 'ocean', - layers: { temporal: false, entity: true, causal: false, semantic: true, code: true }, - minDegree: 1, depth: 2, showUnlinked: false, includeCode: true, - }, - }; - const GRAPH_PRESET_LABELS = { - original: 'Spacious', - compact: 'Compact', - communities: 'Islands', - radial: 'Radial', - constellation: 'Constellation', - galaxy: 'Galaxy gravity', - every: 'Every node', - }; - const GRAPH_STYLE_NOTES = { - cyber: 'Iridescent PVD over graphite — cyan, violet, and magenta across each node.', - galaxy: 'Deep anodized alloy with a cool blue-violet directional sheen.', - solar: 'Brushed copper faces with amber bezels and warm radial grain.', - classic: 'Neutral satin gunmetal with a restrained cool steel edge.', - }; - const GRAPH_LOD_STYLE_NOTES = { - cyber: 'High-contrast cyan, violet and magenta points tuned for dense LOD views.', - galaxy: 'Cool blue-violet points separate clusters clearly across wide zoom ranges.', - solar: 'Warm copper and amber points keep dense relation fields legible.', - classic: 'Restrained steel points prioritize structure and long-session readability.', - }; - const GRAPH_CUSTOM_PALETTE = { - person_or_concept: '#8d82e3', - mention: '#5ba1a6', - hashtag: '#c9a15b', - email: '#8eb3e6', - organization: '#d48173', - location: '#7ebf8e', - memory: '#5ba1a6', - repo: '#c9a15b', - file: '#8eb3e6', - }; - const relative = value => { - const raw = typeof value === 'number' && value < 1e12 ? value * 1000 : value; - const time = typeof raw === 'number' ? raw : Date.parse(raw); - if (!Number.isFinite(time)) return 'stored locally'; - const seconds = Math.max(0, Math.round((Date.now() - time) / 1000)); - if (seconds < 60) return 'just now'; - if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`; - if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`; - if (seconds < 604800) return `${Math.floor(seconds / 86400)}d ago`; - return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(time); - }; - const errorMessage = (payload, status) => { - const detail = payload && (payload.detail || payload.error); - if (typeof detail === 'string') return detail; - if (detail && typeof detail.error === 'string') return detail.error; - return `Request failed (${status})`; - }; - - async function api(path, options = {}) { - const { timeoutMs = 30_000, signal, ...requestOptions } = options; - const controller = new AbortController(); - const abort = () => controller.abort(); - if (signal) { - if (signal.aborted) abort(); - else signal.addEventListener('abort', abort, { once: true }); - } - let timedOut = false; - const timer = window.setTimeout(() => { timedOut = true; controller.abort(); }, timeoutMs); - const init = { ...requestOptions, signal: controller.signal, headers: { ...(options.headers || {}) } }; - init.headers['X-Engraphis-Browser-Session'] = '1'; - if (init.body && !(init.body instanceof FormData) && typeof init.body !== 'string') { - init.headers['Content-Type'] = 'application/json'; - init.body = JSON.stringify(init.body); - } - try { - const response = await fetch(`${apiRoot}${path}`, init); - const payload = await response.json().catch(error => { - if (controller.signal.aborted) throw error; - return null; - }); - if (!response.ok) { - const error = new Error(errorMessage(payload, response.status)); - error.status = response.status; - error.code = payload && ((payload.detail && payload.detail.code) || payload.code); - throw error; - } - return payload; - } catch (error) { - if (timedOut) throw new Error('The request timed out. Please try again.'); - throw error; - } finally { - window.clearTimeout(timer); - if (signal) signal.removeEventListener('abort', abort); - } - } - - function promptBrowserToken(message = '') { - const dialog = byId('browser-auth-dialog'); - const form = byId('browser-auth-form'); - const input = byId('browser-auth-token'); - const error = byId('browser-auth-error'); - const cancel = byId('browser-auth-cancel'); - if (!dialog || !form || !input || !error || !cancel) return Promise.resolve(''); - - error.textContent = message; - error.hidden = !message; - input.value = ''; - const returnFocus = document.activeElement; - - return new Promise(resolve => { - let settled = false; - const cleanup = () => { - form.removeEventListener('submit', submit); - cancel.removeEventListener('click', dismiss); - dialog.removeEventListener('cancel', dismiss); - dialog.removeEventListener('close', closed); - }; - const finish = value => { - if (settled) return; - settled = true; - cleanup(); - input.value = ''; - if (dialog.open) dialog.close(); - if (returnFocus && typeof returnFocus.focus === 'function') returnFocus.focus(); - resolve(value); - }; - const submit = event => { - event.preventDefault(); - const value = input.value.trim(); - if (!value) { - error.textContent = 'Enter the deployment token.'; - error.hidden = false; - input.focus(); - return; - } - finish(value); - }; - const dismiss = event => { - if (event) event.preventDefault(); - finish(''); - }; - const closed = () => finish(''); - - form.addEventListener('submit', submit); - cancel.addEventListener('click', dismiss); - dialog.addEventListener('cancel', dismiss); - dialog.addEventListener('close', closed); - if (!dialog.open) dialog.showModal(); - input.focus(); - }); - } - - async function authenticateBrowser() { - let token = ''; - let failure = ''; - try { - const fragment = new URLSearchParams(location.hash.slice(1)); - token = fragment.get('token') || ''; - if (token) history.replaceState(null, '', `${location.pathname}${location.search}`); - } catch (_) {} - while (true) { - if (!token) token = await promptBrowserToken(failure); - if (!token) return false; - let submitted = token; - token = ''; - try { - const session = await api('/auth/session', { - method: 'POST', - body: { token: submitted }, - }); - state.reviewCsrf = text(session && session.review_csrf_token); - submitted = ''; - return true; - } catch (error) { - submitted = ''; - failure = error.message; - showNotice(`Authentication failed: ${failure}`); - } - } - } - - async function reviewCsrfToken() { - if (state.reviewCsrf) return state.reviewCsrf; - const response = await fetch(`${location.origin}/dashboard/review/csrf`, { - headers: { 'X-Engraphis-Browser-Session': '1' }, - }); - const payload = await response.json().catch(() => null); - if (!response.ok || !payload || !payload.review_csrf_token) { - const error = new Error(errorMessage(payload, response.status)); - error.status = response.status; - throw error; - } - state.reviewCsrf = text(payload.review_csrf_token); - return state.reviewCsrf; - } - - async function approveForPrompt(memory) { - if (!memory || !memory.id) return; - const provenance = memory.provenance || {}; - const reviewState = provenance.review_state || 'pending'; - const reason = window.prompt( - `Why is this ${reviewState} record safe to include in model context?`, - ); - if (reason === null) return; - if (!reason.trim()) { - showNotice('A non-empty review reason is required.'); - return; - } - if (!window.confirm( - 'Approve this record for model context? This creates a fresh, audited approved memory; the reviewed source remains preserved.', - )) return; - try { - const csrf = await reviewCsrfToken(); - const response = await fetch(`${location.origin}/dashboard/review/approve`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Engraphis-Browser-Session': '1', - 'X-Engraphis-Review-CSRF': csrf, - }, - body: JSON.stringify({ memory_id: memory.id, reason: reason.trim() }), - }); - const payload = await response.json().catch(() => null); - if (!response.ok) { - const error = new Error(errorMessage(payload, response.status)); - error.status = response.status; - throw error; - } - showNotice('Approved successor created. The reviewed source remains in the audit trail.'); - await selectWorkspace(state.workspace); - if (payload.id) await selectMemory(payload.id); - } catch (error) { - showNotice(`Could not approve this memory: ${error.message}`); - } - } - - let graphAssetsPromise = null; - let graphAssetsController = null; - let graphAllAssetsPromise = null; - let graphAllAssetsController = null; - let graphAssetsRetry = 0; - const graphAssetSource = source => graphAssetsRetry ? `${source}&retry=${graphAssetsRetry}` : source; - function loadScript(src, globalName, signal) { - if (window[globalName]) return Promise.resolve(); - return new Promise((resolve, reject) => { - const script = document.createElement('script'); - let settled = false; - const cleanup = () => { - if (signal) signal.removeEventListener('abort', abort); - }; - const finish = (callback, value) => { - if (settled) return; - settled = true; - cleanup(); - callback(value); - }; - const abort = () => { - script.remove(); - const error = new Error(`loading ${globalName} was aborted`); - error.name = 'AbortError'; - finish(reject, error); - }; - script.src = src; - script.dataset.engraphisGraphAsset = 'true'; - script.onload = () => window[globalName] - ? finish(resolve) - : finish(reject, new Error(`${globalName} did not register`)); - script.onerror = () => finish(reject, new Error(`could not load ${src}`)); - if (signal) { - if (signal.aborted) { - abort(); - return; - } - signal.addEventListener('abort', abort, { once: true }); - } - document.head.append(script); - }); - } - - function ensureGraphAllAsset() { - if (window.EngraphisEveryGraph) return Promise.resolve(); - if (!graphAllAssetsPromise) { - const controller = new AbortController(); - const attempt = loadScript( - graphAssetSource('/v2-assets/engraphis-graph-every.js?v=20260927-unmerged-readiness-3'), - 'EngraphisEveryGraph', controller.signal, - ); - graphAllAssetsPromise = attempt; - graphAllAssetsController = controller; - attempt.catch(() => { - if (graphAllAssetsPromise === attempt) releaseGraphAllAssetsAttempt(attempt); - }); - } - return graphAllAssetsPromise; - } - - function ensureGraphAssets(loadAll = false) { - /* The complete profile is an independent worker/WebGL renderer. Galaxy is the exception: - its solar-system view needs the authoritative hierarchical orbit integrator, so a full - Galaxy request uses the quality engine with the complete payload instead of the static - all-node worker. The factory decision is data-sensitive, not toolbar-sensitive: the - Every-node chip changes the preset to "every" before the scene arrives, and a fast click - can race the overview load. Keep both candidates available so an authored star/planet - scene cannot select an engine whose asset is still in flight. */ - if (loadAll) { - return Promise.all([ensureGraphAllAsset(), ensureGraphAssets(false)]); - } - const coreReady = window.ForceGraph && window.EngraphisGraph && window.EngraphisSpacetime; - if (!coreReady && !graphAssetsPromise) { - const controller = new AbortController(); - const attempt = loadScript( - graphAssetSource('/v2-assets/vendor/d3.min.js?v=20260727-final'), - 'd3', controller.signal, - ).then(() => loadScript( - graphAssetSource('/v2-assets/vendor/force-graph.min.js?v=20260727-final'), - 'ForceGraph', controller.signal, - )).then(() => loadScript( - graphAssetSource('/v2-assets/engraphis-graph.js?v=20260927-unmerged-readiness-3'), - 'EngraphisGraph', controller.signal, - )).then(() => loadScript( - graphAssetSource('/v2-assets/engraphis-spacetime.js?v=20260927-unmerged-readiness-3'), - 'EngraphisSpacetime', controller.signal, - )); - graphAssetsPromise = attempt; - graphAssetsController = controller; - attempt.catch(() => { - /* A fetched script can load successfully while failing to execute (for example, a - stale cached parse error). Retire that URL immediately so the next explicit Reload - advances the retry query instead of replaying the same broken response forever. */ - if (graphAssetsPromise === attempt) releaseGraphAssetsAttempt(attempt); - }); - } - const core = coreReady ? Promise.resolve() : graphAssetsPromise; - return core; - } - - function releaseGraphAssetsAttempt(attempt) { - // A browser can leave a script fetch pending indefinitely. Do not let that stale promise - // become a permanent single-flight lock: remove its fetches and give the next explicit - // reload a unique URL so it cannot join the browser's already-stalled request. - if (!attempt || graphAssetsPromise !== attempt) return; - graphAssetsPromise = null; - const controller = graphAssetsController; - graphAssetsController = null; - graphAssetsRetry = Math.min(graphAssetsRetry + 1, 10); - if (controller) controller.abort(); - all('script[data-engraphis-graph-asset="true"]').forEach(script => script.remove()); - } - - function releaseGraphAllAssetsAttempt(attempt) { - if (!attempt || graphAllAssetsPromise !== attempt) return; - graphAllAssetsPromise = null; - const controller = graphAllAssetsController; - graphAllAssetsController = null; - graphAssetsRetry = Math.min(graphAssetsRetry + 1, 10); - if (controller) controller.abort(); - } - - function showNotice(message) { - const text = String(message || ''); - if (noticeTimer !== null) { - clearTimeout(noticeTimer); - noticeTimer = null; - } - const textEl = byId('notice-text'); - if (textEl) textEl.textContent = text; - const banner = byId('notice-banner'); - if (!banner) return; - banner.textContent = text; - banner.hidden = !text; - if (!text) { - banner.removeAttribute('data-tone'); - return; - } - banner.dataset.tone = /\b(could not|unavailable|failed|broken|error)\b/i.test(text) ? 'error' : 'info'; - noticeTimer = setTimeout(() => { - noticeTimer = null; - if (banner.textContent !== text) return; - banner.textContent = ''; - banner.hidden = true; - if (textEl) textEl.textContent = ''; - }, NOTICE_DURATION_MS); - } - - function updateReleaseUrl(value) { - const fallback = 'https://github.com/Coding-Dev-Tools/engraphis/releases'; - try { - const url = new URL(value || fallback, location.href); - return ['http:', 'https:'].includes(url.protocol) ? url.href : fallback; - } catch (_) { - return fallback; - } - } - - // A compromised or misconfigured license server could otherwise push a crafted - // upgrade_url (e.g. `javascript:...`) that executes script when the plan link is - // clicked. Only http(s) survives; anything else — including a relative/empty value — - // returns '' so the caller falls back to an inert '#' href. - function safeUrl(value) { - if (!value || typeof value !== 'string') return ''; - try { - const url = new URL(value, location.href); - return ['http:', 'https:'].includes(url.protocol) ? url.href : ''; - } catch (_) { - return ''; - } - } - - function licenseAccessState(license = state.license) { - const value = license && license.access_state; - return ['active', 'trial', 'trial_expired', 'lapsed'].includes(value) ? value : 'inactive'; - } - - function licensePlanKey(license = state.license) { - const value = String((license && license.plan) || 'local').toLowerCase(); - return value === 'pro' || value === 'team' ? value : ''; - } - - function licenseTrialAvailable(license = state.license) { - return Boolean(license && license.trial && license.trial.available - && licenseAccessState(license) === 'inactive' && license.plan_source === 'local'); - } - - function licenseHasHostedAccess(license = state.license) { - const access = licenseAccessState(license); - return access === 'active' || access === 'trial'; - } - - function withCtaAttribution(raw, content, medium = 'product') { - const safe = safeUrl(raw); - if (!safe) return ''; - try { - const url = new URL(safe, location.href); - url.searchParams.set('utm_source', 'engraphis'); - url.searchParams.set('utm_medium', medium); - url.searchParams.set('utm_campaign', 'pro_conversion'); - url.searchParams.set('utm_content', content || 'plans'); - return url.href; - } catch (_) { - return safe; - } - } - - function hostedPlanUrl(plan, trial, interval = 'monthly', content = plan) { - const cadence = interval === 'annual' ? 'annual' : 'monthly'; - const license = state.license || {}; - const raw = license[`${plan}_${cadence}_upgrade_url`] - || license[`${plan}_upgrade_url`] || license.upgrade_url; - const safe = safeUrl(raw); - if (!safe) return ''; - try { - const url = new URL(safe, location.href); - url.searchParams.set('plan', plan); - url.searchParams.set('interval', cadence); - if (trial) url.searchParams.set('trial', plan); - if (!url.hash) url.hash = 'billing'; - return withCtaAttribution(url.href, content); - } catch (_) { - return safe; - } - } - - function hostedAccountUrl(content = 'account') { - const license = state.license || {}; - return withCtaAttribution(license.account_url || license.upgrade_url, content); - } - - function licenseTrialDays(plan = 'pro') { - const trial = (state.license && state.license.trial) || {}; - const days = (trial.days_by_plan || {})[plan] ?? (plan === 'pro' ? trial.trial_days : null); - return Number.isSafeInteger(days) && days > 0 ? days : null; - } - - function hostedCta(plan = 'pro', content = 'plans', interval = 'monthly') { - const stateName = licenseAccessState(); - const currentPlan = licensePlanKey(); - const name = plan === 'team' ? 'Team' : 'Pro'; - if (stateName === 'lapsed') { - return { label: 'Update billing', href: hostedAccountUrl(content), kind: 'account' }; - } - if (licenseHasHostedAccess() && (currentPlan === plan - || (currentPlan === 'team' && plan === 'pro'))) { - return { - label: currentPlan === 'team' && plan === 'team' ? 'Open Team Cloud' : 'Open Engraphis Cloud', - href: hostedAccountUrl(content), - kind: 'account', - }; - } - const trial = licenseTrialAvailable() && stateName === 'inactive'; - const days = licenseTrialDays(plan); - return { - label: trial ? `Start ${days ? `${days}-day ` : ''}${name} trial` : `Subscribe to ${name}`, - href: hostedPlanUrl(plan, trial, interval, content), - kind: trial ? 'trial' : 'subscribe', - }; - } - - function updatePlanBadge() { - const badge = byId('plan-badge'); - if (!badge || !state.license) return; - const access = licenseAccessState(); - const plan = licensePlanKey(); - const trial = licenseTrialAvailable(); - const label = access === 'active' ? plan.toUpperCase() - : access === 'trial' ? 'TRIAL' - : access === 'lapsed' ? 'BILLING' - : trial ? 'TRY PRO' : 'GET PRO'; - badge.hidden = access === 'inactive' && trial; - const aria = licenseHasHostedAccess() ? 'Open Engraphis Cloud account' - : access === 'lapsed' ? 'Update billing in Plans and billing' - : trial ? `${hostedCta('pro', 'header').label} in Plans and billing` - : 'Subscribe to Pro in Plans and billing'; - badge.textContent = label; - badge.setAttribute('aria-label', aria); - badge.title = aria; - const cta = hostedCta(plan || 'pro', 'header'); - const opensAccount = cta.kind === 'account' && Boolean(cta.href); - badge.href = opensAccount ? cta.href : '#'; - badge.target = opensAccount ? '_blank' : ''; - badge.rel = opensAccount ? 'noopener' : ''; - badge.dataset.opensAccount = String(opensAccount); - } - - function renderSidebarCta() { - const copy = byId('sidebar-pro-copy'); - const detail = byId('sidebar-pro-detail'); - const link = byId('sidebar-pro-cta'); - if (!copy || !detail || !link || !state.license) return; - const renderFeatureCtas = () => { - [ - ['analytics-pro-cta', 'analytics', 'pro'], - ['automation-pro-cta', 'automation', 'pro'], - ['team-cloud-cta', 'team', 'team'], - ].forEach(([id, content, plan]) => { - const featureLink = byId(id); - if (!featureLink) return; - const featureCta = hostedCta(plan, content); - featureLink.textContent = featureCta.label; - featureLink.href = featureCta.href || '#'; - featureLink.setAttribute('aria-disabled', featureCta.href ? 'false' : 'true'); - }); - }; - if (licenseHasHostedAccess()) { - const cta = hostedCta(licensePlanKey() || 'pro', 'sidebar'); - copy.textContent = 'Thank you for supporting Engraphis.'; - detail.textContent = 'Your subscription funds hosted infrastructure and ongoing development.'; - link.hidden = false; - link.textContent = cta.label; - link.href = cta.href || '#'; - link.setAttribute('aria-disabled', cta.href ? 'false' : 'true'); - renderFeatureCtas(); - return; - } - const cta = hostedCta('pro', 'sidebar'); - copy.textContent = 'Support continued Engraphis development with Pro.'; - detail.textContent = 'Cloud Sync, Analytics, and managed memory maintenance.'; - link.hidden = false; - link.textContent = cta.label; - link.href = cta.href || '#'; - link.setAttribute('aria-disabled', cta.href ? 'false' : 'true'); - link.dataset.proCta = 'sidebar'; - renderFeatureCtas(); - } - - function renderCloudAccountSettings() { - const target = byId('cloud-account-settings'); - if (!target) return; - target.replaceChildren(); - const plan = licensePlanKey() || 'pro'; - const cta = hostedCta(plan, 'settings'); - const live = licenseHasHostedAccess(); - const detail = live - ? 'Your hosted account is connected. Manage membership in Cloud, or edit this workspace’s hosted maintenance policy locally.' - : licenseAccessState() === 'lapsed' - ? 'Your hosted subscription needs attention. Update billing in Engraphis Cloud to restore hosted features.' - : 'Open Engraphis Cloud to start a trial, subscribe, or manage a connected hosted account.'; - const action = node('a', 'primary-button', cta.label); - action.href = cta.href || '#'; - if (cta.href) { - action.target = '_blank'; - action.rel = 'noopener'; - } else { - action.addEventListener('click', event => { - event.preventDefault(); - showNotice('Connect this installation to Engraphis Cloud to open hosted account settings.'); - }); - } - const actions = node('div', 'automation-policy-actions'); - actions.append(action); - if (live) actions.append(button('Configure hosted policy', 'secondary-button', () => switchManageTab('automation'))); - target.append(node('p', 'automation-policy-note', detail), actions); - } - - function renderUpdateBanner(update) { - const target = byId('update-banner'); - if (!target) return; - target.replaceChildren(); - if (!update || !update.enabled || !update.update_available || !update.latest) { - target.hidden = true; - return; - } - let dismissed = ''; - try { - dismissed = localStorage.getItem('engraphis-update-dismissed') || ''; - } catch (_) {} - if (dismissed === update.latest) { - target.hidden = true; - return; - } - const copy = node('div', 'update-copy'); - copy.append( - node('strong', '', 'Update available'), - document.createTextNode(` — Engraphis ${text(update.latest)} is out (you have ${text(update.current || '?')}). Upgrade with `), - node('code', '', 'pip install -U engraphis'), - document.createTextNode('.'), - ); - const actions = node('div', 'update-actions'); - const release = node('a', 'text-button', 'View release →'); - release.href = updateReleaseUrl(update.url); - release.target = '_blank'; - release.rel = 'noopener'; - const dismiss = button('Dismiss', 'update-dismiss', () => { - try { - localStorage.setItem('engraphis-update-dismissed', text(update.latest)); - } catch (_) {} - target.hidden = true; - target.replaceChildren(); - }); - actions.append(release, dismiss); - target.append(copy, actions); - target.hidden = false; - } - - function setConnection(message, healthy = true) { - const status = byId('connection-status'); - if (status) status.textContent = message; - byId('home-engine-status').textContent = 'Dashboard: ' + message + '. Checked at ' + new Date().toLocaleTimeString() + '.'; - const dot = document.querySelector('.status-dot'); - if (dot) dot.classList.toggle('unhealthy', !healthy); - } - - function setDeploymentMode(mode) { - const el = byId('deployment-mode-badge'); - if (!el) return; - const isLocal = mode === 'local'; - el.textContent = isLocal ? 'LOCAL' : 'HOSTED'; - el.title = isLocal - ? 'Local mode: no hosted cloud configured. Data stays on this machine.' - : 'Hosted mode: connected to Engraphis Cloud.'; - el.classList.toggle('mode-local', isLocal); - el.classList.toggle('mode-hosted', !isLocal); - el.hidden = false; - } - - function memoryType(memory) { - return memory.memory_type || memory.mtype || 'semantic'; - } - - function memoryTime(memory) { - return memory.ingested_at || memory.valid_from || memory.last_access; - } - - const memoryOwnershipLabels = new WeakMap(); - - function refreshMemoryOwnershipLabels() { - all('[data-memory-ownership]').forEach(label => { - const owner = memoryOwnershipLabels.get(label); - if (owner) label.textContent = workflow.ownership(owner.memory, owner.workspace); - }); - } - - function memoryMeta(memory) { - const meta = node('div', 'memory-meta'); - const ownership = node('span', 'memory-ownership', workflow.ownership(memory, state.workspace)); - ownership.dataset.memoryOwnership = ''; - // Bind the authorized read context, never the selected project preference. - memoryOwnershipLabels.set(ownership, { memory, workspace: state.workspace }); - meta.append( - node('span', 'type-chip', memoryType(memory)), - ownership, - node('span', '', relative(memoryTime(memory))), - ); - if (memory.pinned) meta.append(node('span', '', 'pinned')); - return meta; - } - - function renderMetricValues(stats) { - const values = [ - stats.memories, - stats.total_rows, - stats.workspaces || state.workspaces.length, - stats.sessions, - ]; - all('#metrics strong').forEach((element, index) => { - element.textContent = values[index] == null ? '—' : number(values[index]).toLocaleString(); - }); - } - - function renderTypeBars(stats) { - const target = byId('type-bars'); - target.replaceChildren(); - const types = stats.by_type || {}; - const entries = Object.entries(types).sort((a, b) => number(b[1]) - number(a[1])); - if (!entries.length) { - target.append(empty('No typed memories yet.')); - return; - } - const max = Math.max(1, ...entries.map(([, value]) => number(value))); - entries.forEach(([name, value]) => { - const row = node('div', 'type-bar'); - row.append(node('span', '', name)); - const bar = document.createElement('progress'); - bar.max = max; - bar.value = number(value); - bar.setAttribute('aria-label', `${name}: ${number(value)}`); - row.append(bar, node('strong', '', number(value).toLocaleString())); - target.append(row); - }); - } - - function savingsQuery(preset = 'all') { - if (preset === 'current' && state.releaseVersion) { - return `?release_version=${encodeURIComponent(state.releaseVersion)}`; - } - if (preset === '7d') return `?from_ts=${encodeURIComponent(Date.now() / 1000 - 604800)}`; - return ''; - } - - function savingsScopeLabel(payload) { - if (payload && payload.scope && payload.scope.workspace === 'all') { - return ` across ${number(payload.workspace_count).toLocaleString()} visible workspaces`; - } - return ''; - } - - function formatSavingsTokens(value) { - return Math.max(0, Math.round(number(value))).toLocaleString(); - } - - function savingsRatio(value) { - return Math.max(0, Math.min(1, number(value))); - } - - function savingsCounts(payload) { - const estimate = payload && payload.estimated ? payload.estimated : {}; - return { - estimate, - eligible: number(estimate.eligible_receipt_count), - excluded: number(estimate.excluded_receipt_count) - + number(estimate.unclassified_receipt_count) - + number(estimate.invalid_estimate_count), - }; - } - - function renderSavingsOverview(payload) { - const { estimate, eligible, excluded } = savingsCounts(payload); - const scopeLabel = savingsScopeLabel(payload); - const persistentValue = byId('context-savings-persistent-value'); - const persistentMeta = byId('context-savings-persistent-meta'); - const persistentRate = byId('context-savings-persistent-rate'); - const setPersistent = (value, meta, rate = '—') => { - if (persistentValue) persistentValue.textContent = value; - if (persistentMeta) persistentMeta.textContent = meta; - if (persistentRate) persistentRate.textContent = rate; - }; - if (!eligible) { - setPersistent('—', excluded ? `${excluded} excluded or unclassified deliveries so far.` : 'Tracking starts with the first eligible delivery.'); - return; - } - const ratio = savingsRatio(estimate.savings_ratio); - setPersistent( - formatSavingsTokens(estimate.saved_tokens), - `Across ${eligible.toLocaleString()} eligible context deliveries${scopeLabel} · ${estimate.confidence || 'unknown'} confidence`, - `${(ratio * 100).toFixed(1)}% estimated reduction`, - ); - } - - function renderSavingsDetail(payload) { - const target = byId('savings-detail'); - if (!target) return; - const { estimate, eligible, excluded } = savingsCounts(payload); - const scopeLabel = savingsScopeLabel(payload); - target.replaceChildren(); - const header = node('div', 'savings-detail-header'); - header.append( - node('strong', 'savings-number', `${formatSavingsTokens(estimate.saved_tokens)} tokens`), - node('span', '', eligible - ? `${eligible} eligible deliveries${scopeLabel} · ${(number(estimate.savings_ratio) * 100).toFixed(1)}% estimated reduction` - : 'No eligible estimates in this range.'), - ); - const presets = node('div', 'savings-presets'); - [ - ['since', 'Since tracking started'], - ['current', 'Current release'], - ['7d', 'Last 7 days'], - ['all', 'All time'], - ].forEach(([value, label]) => { - const control = button(label, '', () => { - state.savingsPreset = value; - loadAudit(); - }); - control.classList.toggle('active', state.savingsPreset === value); - control.setAttribute('aria-pressed', String(state.savingsPreset === value)); - presets.append(control); - }); - header.append(presets); - target.append(header); - if (eligible) { - target.append(node('p', 'field-note', `Baseline ${formatSavingsTokens(estimate.baseline_tokens)} → emitted ${formatSavingsTokens(estimate.emitted_tokens)} · confidence: ${text(estimate.confidence || 'unknown')}`)); - target.append(node('p', 'field-note', 'Packed context is packing savings; adaptive history is estimated avoided prompt context.')); - const basisTitle = node('h3', '', 'Savings basis'); - const basisRows = node('div', 'savings-breakdown'); - (estimate.by_basis || []).forEach(row => { - const item = node('div', 'savings-breakdown-row'); - item.append( - node('span', '', `${text(row.basis || 'unclassified').replaceAll('_', ' ')} · ${text(row.confidence || 'unknown')}`), - node('span', '', `${formatSavingsTokens(row.baseline_tokens)} → ${formatSavingsTokens(row.emitted_tokens)} · ${formatSavingsTokens(row.saved_tokens)} saved`), - ); - basisRows.append(item); - }); - target.append(basisTitle, basisRows); - if ((estimate.by_token_counter || []).length) { - target.append(node('h3', '', 'Token counters')); - const counterRows = node('div', 'savings-breakdown'); - (estimate.by_token_counter || []).forEach(row => { - const item = node('div', 'savings-breakdown-row'); - item.append( - node('span', '', text(row.token_counter || 'unknown')), - node('span', '', `${formatSavingsTokens(row.saved_tokens)} saved · ${row.receipt_count || 0} eligible deliver${number(row.receipt_count) === 1 ? 'y' : 'ies'}`), - ); - counterRows.append(item); - }); - target.append(counterRows); - } - } - target.append(node('p', 'savings-note', `${excluded} excluded or unclassified deliver${excluded === 1 ? 'y' : 'ies'}. Measures estimated prompt-context reduction; it does not measure provider billing.`)); - } - - function renderReviewInbox(result) { - const target = byId('decision-list'); - target.replaceChildren(); - const items = result.items; - const partial = result.has_more || result.truncated; - byId('review-status').textContent = `${items.length} review ${items.length === 1 ? 'item' : 'items'} shown.` - + (partial ? ' This is a partial list; more records may need review.' : ''); - if (!items.length) { - target.append(empty(partial ? 'No review items in this sample. More records may remain.' - : 'No records currently need review in this context.')); - return; - } - items.forEach((item, index) => { - const card = node(item.id ? 'button' : 'article', 'decision-card memory-link-card'); - if (item.id) { - card.type = 'button'; - card.dataset.memoryId = item.id; - card.addEventListener('click', () => openMemory(item)); - } - const states = []; - if (item.quarantined) states.push('Quarantined'); - if (item.conflict_with) states.push('Conflicting evidence'); - if (item.review_state === 'pending') states.push('Source review pending'); - const header = node('div', 'decision-card-header'); - header.append( - node('span', 'tag', states.join(' · ') || 'Review required'), - node('h3', '', 'Inspect review item ' + (index + 1)), - ); - card.append(header, node('p', '', item.excerpt || 'Open the record to review its source and history.')); - target.append(card); - }); - } - - async function loadReviewInbox() { - if (!state.workspace) return; - const request = beginScopedRequest('reviews'); - const target = byId('decision-list'); - target.replaceChildren(empty('Loading review state…')); - target.setAttribute('aria-busy', 'true'); - byId('review-status').textContent = ''; - byId('review-refresh').disabled = true; - try { - const result = await api('/review-inbox?' + memoryQuery(request.workspace, request.project) + '&limit=6', { signal: request.signal }); - if (!isCurrentScopedRequest(request)) return; - if (!result || !Array.isArray(result.items)) throw new Error('No review state was returned.'); - renderReviewInbox(result); - } catch (error) { - if (!isCurrentScopedRequest(request)) return; - target.replaceChildren(empty('Review state is unavailable: ' + error.message)); - byId('review-status').textContent = 'Review status is unknown. Refresh reviews to try again.'; - } finally { - if (isCurrentScopedRequest(request)) { - target.setAttribute('aria-busy', 'false'); - byId('review-refresh').disabled = false; - } - } - } - - function auditItems(payload) { - if (Array.isArray(payload)) return payload; - return payload.audit || payload.entries || payload.records || payload.events || []; - } - - function receiptItems(payload) { - if (Array.isArray(payload)) return payload; - return payload.receipts || payload.entries || payload.records || []; - } - - function provenanceTimestampMs(item) { - // Audit rows use seconds (`ts`), while receipts use milliseconds (`ts_ms`). - // Normalize before merging so both the newest-first order and 120-row cap are - // chronological across the two independently paginated feeds. - const raw = item && (item.ts_ms ?? item.ts ?? item.timestamp ?? item.created_at); - const numeric = Number(raw); - if (Number.isFinite(numeric)) return numeric < 1e12 ? numeric * 1000 : numeric; - const parsed = Date.parse(raw); - return Number.isFinite(parsed) ? parsed : 0; - } - - function auditField(item, ...names) { - for (const name of names) { - if (item && item[name] != null && item[name] !== '') return item[name]; - } - return ''; - } - - function renderActivity(items) { - const target = byId('activity-body'); - target.replaceChildren(); - if (!items.length) { - const row = node('tr'); - const cell = node('td', '', 'No audit entries yet.'); - cell.colSpan = 5; - row.append(cell); - target.append(row); - return; - } - items.slice(0, 8).forEach(item => { - const row = node('tr'); - const timestamp = auditField(item, 'ts', 'timestamp', 'created_at', 'valid_from'); - const values = [ - relative(timestamp), - auditField(item, 'actor', 'source') || 'local operator', - auditField(item, 'action', 'operation', 'event') || 'recorded', - auditField(item, 'scope', 'workspace', 'target') || state.workspace, - truncate(auditField(item, 'hash', 'id', 'receipt_id'), 14) || '—', - ]; - values.forEach(value => row.append(node('td', '', value))); - target.append(row); - }); - } - - function renderProactive(memories, unavailableMessage = '') { - const target = byId('proactive-list'); - target.replaceChildren(); - if (!memories.length) { - target.append(empty(unavailableMessage || 'No proactive context is available.')); - return; - } - memories.slice(0, 5).forEach(memory => { - const row = node('button', 'compact-row'); - row.type = 'button'; - if (memory.id) row.dataset.memoryId = memory.id; - row.append( - node('strong', '', memoryTitle(memory)), - node('span', '', truncate(memory.summary || memory.content, 140)), - ); - row.addEventListener('click', () => openMemory(memory)); - target.append(row); - }); - } - - async function loadStats(workspace, epoch) { - try { - const stats = await api(`/stats?${query(workspace)}`); - if (epoch !== state.refreshEpoch) return; - state.stats = stats; - renderMetricValues(stats); - renderTypeBars(stats); - renderFirstMemoryJourney(); - } catch (error) { - if (epoch === state.refreshEpoch) byId('type-bars').replaceChildren(empty('Workspace composition is unavailable.')); - throw error; - } - } - - async function loadSavings(epoch) { - try { - const payload = await api(`/context-savings${savingsQuery()}`); - if (epoch !== state.refreshEpoch) return; - renderSavingsOverview(payload); - } catch (error) { - if (epoch !== state.refreshEpoch) return; - const persistentValue = byId('context-savings-persistent-value'); - const persistentMeta = byId('context-savings-persistent-meta'); - const persistentRate = byId('context-savings-persistent-rate'); - if (persistentValue) persistentValue.textContent = 'Unavailable'; - if (persistentMeta) persistentMeta.textContent = 'Receipt-backed estimate could not be loaded.'; - if (persistentRate) persistentRate.textContent = '—'; - } - } - - async function loadMemories(workspace, epoch, page = 0) { - resetMoveSelection(); - const request = beginScopedRequest('library'); - const params = new URLSearchParams({ workspace, limit: '100' }); - if (request.project) params.set('repo', request.project); - const search = byId('library-filter').value.trim(); - const type = byId('library-type').value; - if (search) params.set('q', search); - if (type) params.set('mtype', type); - const cursor = state.libraryCursors[page]; - if (cursor) params.set('cursor', cursor); - state.libraryLoading = true; - renderLibraryPaging(); - try { - const payload = await api(`/memories?${params}`, { signal: request.signal }); - if (epoch !== state.refreshEpoch || !isCurrentScopedRequest(request)) return; - state.memories = payload.memories || []; - state.libraryTotal = number(payload.total_count == null ? state.memories.length : payload.total_count); - state.libraryNextCursor = payload.next_cursor || null; - state.libraryPage = page; - if (!page) state.libraryCursors = [null]; - if (state.libraryNextCursor) state.libraryCursors[page + 1] = state.libraryNextCursor; - renderLibrary(); - renderFirstMemoryJourney(); - } catch (error) { - if (!isCurrentScopedRequest(request)) return; - if (cursor && error.status === 409 && error.code === 'cursor_stale') { - state.libraryCursors = [null]; - showNotice('Memory changed. Showing the first page with your filters preserved.'); - return await loadMemories(workspace, epoch); - } - byId('library-list').replaceChildren(empty(`Library is unavailable: ${error.message}`)); - throw error; - } finally { - if (isCurrentScopedRequest(request)) { - state.libraryLoading = false; - renderLibraryPaging(); - } - } - } - - function renderLibraryPaging() { - byId('library-previous').disabled = state.libraryLoading || state.libraryPage === 0; - byId('library-next').disabled = state.libraryLoading || !state.libraryNextCursor; - byId('library-refresh').disabled = state.libraryLoading || !state.workspace; - byId('library-list').setAttribute('aria-busy', String(state.libraryLoading)); - byId('library-list').querySelectorAll('.memory-card').forEach(card => { - card.disabled = state.librarySelecting && state.libraryLoading; - }); - renderMoveSelection(); - } - - function refreshLibrary(page = 0) { - window.clearTimeout(librarySearchTimer); - if (!state.workspace) return; - loadMemories(state.workspace, state.refreshEpoch, page).catch(error => showNotice(error.message)); - } - - function renderFirstMemoryJourney() { - const journey = byId('first-memory-journey'); - if (!journey) return; - journey.hidden = Boolean(state.workspace) && (state.stats.memories == null - || number(state.stats.memories) > 0 || state.libraryTotal > 0); - byId('first-memory-add').textContent = state.workspace ? 'Add your first memory' : 'Create your first workspace'; - } - - async function loadToday(workspace, epoch) { - const [proactiveResult, auditResult] = await Promise.allSettled([ - api(`/proactive?${query(workspace)}&k=8`), - api(`/audit?${query(workspace)}&limit=12`), - ]); - if (epoch !== state.refreshEpoch) return; - const proactive = proactiveResult.status === 'fulfilled' - ? (proactiveResult.value.memories || proactiveResult.value.results || []) - : []; - renderProactive(proactive, proactiveResult.status === 'rejected' - ? 'Strongest memories are unavailable. Try refreshing this workspace.' : ''); - renderActivity(auditResult.status === 'fulfilled' ? auditItems(auditResult.value) : []); - if (auditResult.status === 'rejected') { - const cell = byId('activity-body').querySelector('td'); - if (cell) cell.textContent = 'Activity is unavailable. Try refreshing this workspace.'; - } - } - - function renderWorkspaceNames() { - all('[data-workspace-name]').forEach(element => { - element.textContent = state.workspace || 'this workspace'; - }); - } - - function workspaceName(item) { - return typeof item === 'string' ? item : item.name; - } - function resetScopedPanels() { - const messages = { - 'answer-panel': 'Ask a question to receive a grounded answer with citations.', - 'retrieval-list': 'Retrieved memories will appear here.', - 'why-result': 'Trace a claim to inspect live and superseded support.', - 'timeline-result': 'Search a topic to inspect its temporal history.', - 'supersession-list': 'Search a topic to compare closed and current records.', - 'audit-list': 'Open Audit to load this workspace’s records and receipts.', - 'savings-detail': 'Open Audit to load this workspace’s receipt-backed estimate.', - 'analytics-result': 'Open this tab to check availability.', - 'automation-result': 'Open this tab to check availability.', - 'team-result': 'Open this tab to check connection state.', - }; - Object.entries(messages).forEach(([id, message]) => { - const target = byId(id); - if (target) target.replaceChildren(empty(message)); - }); - } - - const graphLifecycle = window.EngraphisGraphLifecycle.create({ - isVisible: () => state.view === 'relations', - onStatus: ({ visible, capability }) => { - const status = byId('graph-lifecycle-status'); - status.dataset.lifecycle = capability === 'drawing' ? (visible ? 'active' : 'paused') : capability; - status.textContent = capability === 'unloaded' ? 'The graph loads when you open Explore.' - : capability === 'physics' ? 'This renderer can pause physics only when you leave.' - : capability === 'unsupported' ? 'Automatic pause is unavailable for this renderer.' - : visible ? 'Select a node to inspect its evidence. Drawing pauses when you leave.' - : 'Graph drawing paused. Camera and selection are kept.'; - }, - onError: () => showNotice('A graph resource could not be paused or released. Reload this page if it stays active.'), - }); - const narrowSidebar = window.matchMedia('(max-width: 860px)'); - const syncSidebarOptions = () => { byId('sidebar-options').open = !narrowSidebar.matches; }; - if (narrowSidebar.addEventListener) narrowSidebar.addEventListener('change', syncSidebarOptions); - else narrowSidebar.addListener(syncSidebarOptions); - syncSidebarOptions(); - - const processingControls = window.EngraphisProcessingControls.create(api); - const askRequests = window.EngraphisAskRequests.create({ renderAnswer, renderPreview }); - const workflow = window.EngraphisWorkflow.create({ - api, - onProjectChange: () => { void selectWorkspace(state.workspace); }, - onNavigate: view => switchView(view), - onNewMemory: () => { switchView('library'); openEditor(); }, - }); - - async function loadProjects(workspace, epoch) { - const request = beginScopedRequest('projects'); - try { - const result = await api('/repos?' + query(workspace), { signal: request.signal }); - if (epoch === state.refreshEpoch && isCurrentScopedRequest(request)) { - workflow.setProjects(result && result.repos); - refreshMemoryOwnershipLabels(); - } - } catch (_) { - if (isCurrentScopedRequest(request)) { - workflow.projectsUnavailable(); - refreshMemoryOwnershipLabels(); - } - } - } - - async function selectWorkspace(name) { - if (!name) return; - resetMoveSelection(true); - invalidateConsolidationReview(); - const epoch = ++state.refreshEpoch; - invalidateScopedRequests(); - askRequests.reset(); - window.clearTimeout(librarySearchTimer); - state.libraryCursors = [null]; - state.libraryPage = 0; - state.libraryNextCursor = null; - state.libraryTotal = 0; - state.memories = []; - renderLibrary(); - renderLibraryPaging(); - closeGraphConnections(); - state.workspace = name; - state.stats = {}; - renderMetricValues({}); - byId('type-bars').replaceChildren(empty('Loading workspace composition…')); - renderFirstMemoryJourney(); - workflow.selectWorkspace(name); - state.project = workflow.project(); - void processingControls.selectWorkspace(name); - state.graphWorkspace = ''; - state.graphData = null; - state.graphDataPreset = 'galaxy'; - state.graphDataIncludeCode = false; - state.graphDataShowUnlinked = false; - state.graphDataRepo = ''; - state.selectedMemory = ''; - // Detail/editor handlers close over a memory record. Clear both before the - // workspace fetches begin so a stale form cannot write that record into the - // newly selected workspace. - state.editorMemory = null; - resetEditorSession(); - destroyMemoryHistory(); - byId('memory-editor').hidden = true; - const memoryDetail = byId('memory-detail'); - memoryDetail.replaceChildren(); - memoryDetail.hidden = true; - resetScopedPanels(); - state.syncStatus = null; - graphLifecycle.clear(); - state.graphSpacetimeOverlay = null; - state.graphEngine = null; - byId('workspace-select').value = name; - renderWorkspaceNames(); - try { - localStorage.setItem('engraphis-workspace', name); - } catch (_) {} - showNotice(''); - try { - const results = await Promise.allSettled([ - loadStats(name, epoch), - loadProjects(name, epoch), - loadMemories(name, epoch), - loadReviewInbox(), - loadToday(name, epoch), - ]); - if (epoch !== state.refreshEpoch) return; - const failed = results.find(result => result.status === 'rejected'); - if (failed) showNotice(`Some workspace panels could not refresh: ${failed.reason.message}`); - renderWorkspaceList(); - if (state.view === 'relations') await loadGraph(); - if (state.view === 'provenance' && state.provenanceTab === 'audit') await loadAudit(); - if (state.view === 'manage') { - await loadSavings(epoch); - await loadManageTab(state.manageTab); - } - } catch (error) { - if (epoch === state.refreshEpoch) showNotice(`Could not refresh ${name}: ${error.message}`); - } - } - - function memoryCard(memory) { - const card = node('button', 'memory-card'); - card.type = 'button'; - card.setAttribute('role', 'option'); - card.dataset.memoryId = memory.id; - const selected = state.librarySelecting ? state.moveMemoryIds.has(memory.id) : state.selectedMemory === memory.id; - card.setAttribute('aria-selected', String(selected)); - if (selected) card.classList.add('selected'); - if (state.librarySelecting) { - const marker = node('span', 'memory-selection-marker', selected ? 'Selected for move' : 'Select for move'); - marker.setAttribute('aria-hidden', 'true'); - card.append(marker); - } - card.append( - node('h2', '', memoryTitle(memory)), - node('p', '', truncate(memory.content || memory.summary, 240)), - memoryMeta(memory), - ); - card.addEventListener('click', () => { - if (!state.librarySelecting) { openMemory(memory); return; } - if (state.libraryLoading) return; - if (state.moveMemoryIds.has(memory.id)) state.moveMemoryIds.delete(memory.id); - else state.moveMemoryIds.add(memory.id); - closeMemoryMove(); - renderLibrary(); - byId('library-list').querySelectorAll('[data-memory-id]').forEach(item => { - if (item.dataset.memoryId === memory.id) { item.tabIndex = 0; item.focus(); } - else item.tabIndex = -1; - }); - }); - return card; - } - - function filteredMemories() { - return state.memories; - } - - function renderLibrary() { - const target = byId('library-list'); - target.setAttribute('aria-multiselectable', String(state.librarySelecting)); - renderMoveSelection(); - if (!target.dataset.keyboardBound) { - target.dataset.keyboardBound = 'true'; - target.addEventListener('keydown', event => { - const cards = [...target.querySelectorAll('[role="option"]')]; - const current = event.target.closest('[role="option"]'); - if (!current || !cards.length) return; - let index = cards.indexOf(current); - if (event.key === 'Home') index = 0; - else if (event.key === 'End') index = cards.length - 1; - else if (event.key === 'ArrowDown' || event.key === 'ArrowRight') index = Math.min(cards.length - 1, index + 1); - else if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') index = Math.max(0, index - 1); - else return; - event.preventDefault(); - cards.forEach((card, cardIndex) => { card.tabIndex = cardIndex === index ? 0 : -1; }); - cards[index].focus(); - }); - } - target.replaceChildren(); - const memories = filteredMemories(); - const total = state.libraryTotal; - const start = state.libraryPage * 100 + (memories.length ? 1 : 0); - byId('library-count').textContent = total > memories.length - ? `${start.toLocaleString()}–${(start + memories.length - 1).toLocaleString()} of ${total.toLocaleString()} memories` - : `${total.toLocaleString()} ${total === 1 ? 'memory' : 'memories'}`; - if (!memories.length) { - target.append(empty(byId('library-filter').value.trim() || byId('library-type').value - ? 'No memories match these filters.' : 'No active memories in this workspace. Add a fact or import local documents to begin.')); - return; - } - memories.forEach(memory => target.append(memoryCard(memory))); - const cards = [...target.querySelectorAll('[role="option"]')]; - const selectedIndex = cards.findIndex(card => card.getAttribute('aria-selected') === 'true'); - cards.forEach((card, index) => { card.tabIndex = index === (selectedIndex >= 0 ? selectedIndex : 0) ? 0 : -1; }); - } - - function renderMoveSelection() { - const toggle = byId('library-selection-toggle'); - toggle.disabled = state.libraryLoading || !state.workspace || !state.memories.length; - toggle.setAttribute('aria-pressed', String(state.librarySelecting)); - toggle.textContent = state.librarySelecting ? 'Cancel selection' : 'Select memories'; - byId('library-selection-status').textContent = state.librarySelecting - ? `${state.moveMemoryIds.size} selected on this page. Changing results clears the selection.` - : 'Select memories on this page to move them to another workspace.'; - byId('library-move').disabled = state.libraryLoading || !state.moveMemoryIds.size; - } - - function resetMoveSelection(finish = false) { - closeMemoryMove(); - state.moveMemoryIds.clear(); - if (finish) state.librarySelecting = false; - renderMoveSelection(); - } - - function closeMemoryMove() { - const dialog = byId('memory-move-dialog'); - const move = state.memoryMove; - state.memoryMove = null; - beginScopedRequest('memory-move'); - if (dialog.open) dialog.close(); - if (move && move.returnFocus && move.returnFocus.isConnected) move.returnFocus.focus(); - } - - function invalidateMemoryMovePreview() { - const move = state.memoryMove; - if (!move || move.applying) return; - beginScopedRequest('memory-move'); - move.preview = null; - move.loading = false; - byId('memory-move-error').hidden = true; - byId('memory-move-preview').replaceChildren(empty('Choose a destination, then preview the move.')); - renderMemoryMoveControls(); - } - - function renderMemoryMoveControls() { - const move = state.memoryMove; - if (!move) return; - const busy = move.loading || move.applying; - const target = byId('memory-move-target'); - target.disabled = move.applying; - byId('memory-move-cancel').disabled = move.applying; - byId('memory-move-preview-button').disabled = busy || !target.value; - byId('memory-move-apply').disabled = busy || !move.preview || move.preview.can_move !== true; - byId('memory-move-form').setAttribute('aria-busy', String(busy)); - } - - function openMemoryMove() { - if (!state.moveMemoryIds.size || state.libraryLoading) return; - closeMemoryMove(); - state.memoryMove = { - workspace: state.workspace, project: state.project, - ids: [...state.moveMemoryIds].sort(), preview: null, loading: false, applying: false, - returnFocus: document.activeElement, - }; - const target = byId('memory-move-target'); - target.replaceChildren(option('', 'Choose a workspace')); - state.workspaces.filter(item => workspaceName(item) && workspaceName(item) !== state.workspace) - .sort((a, b) => workspaceName(a).localeCompare(workspaceName(b))).forEach(item => { - const name = workspaceName(item); - const access = item.visibility === 'personal' ? 'Personal' - : item.visibility === 'shared' ? 'Shared' : 'Access shown in preview'; - target.append(option(name, `${name} · ${access}`)); - }); - byId('memory-move-source').textContent = `From ${JSON.stringify(state.workspace)} · ${state.moveMemoryIds.size} selected`; - invalidateMemoryMovePreview(); - if (target.options.length === 1) { - byId('memory-move-preview').replaceChildren(empty('Create another accessible workspace in Settings before moving memories.')); - } - byId('memory-move-dialog').showModal(); - target.focus(); - } - - function memoryMoveBody(move) { - return { workspace: move.workspace, target_workspace: byId('memory-move-target').value, memory_ids: move.ids }; - } - - function renderMemoryMovePreview(preview, move) { - const target = byId('memory-move-preview'); - const rows = [ - ['From workspace', preview.source], ['To workspace', preview.target], - ['Selected memories', move.ids.length], ['Total records to move', preview.count], - ['Related memories and history', preview.related_count], - ]; - if (preview.sessions != null) rows.push(['Closed sessions included', preview.sessions]); - if (preview.graph_edges != null) rows.push(['Graph relationships included', preview.graph_edges]); - if (Array.isArray(preview.repos) && preview.repos.length) rows.push(['Projects preserved', preview.repos.join(', ')]); - target.replaceChildren(definitionList(rows.map(([label, value]) => [label, text(value)]))); - const visibilityLabel = value => value === 'personal' ? 'Personal' : value === 'shared' ? 'Shared' : 'Unknown'; - target.append(node('p', 'project-help', `Access: ${visibilityLabel(preview.source_visibility)} → ${visibilityLabel(preview.target_visibility)}.`)); - if (preview.target_visibility === 'shared') { - target.append(node('p', 'project-help', 'The destination is shared. Other users with workspace access can read the moved workspace and project memories. Session ownership restrictions still apply.')); - } else if (preview.target_visibility === 'personal') { - target.append(node('p', 'project-help', 'The destination is personal. Its owner controls access to the moved memories.')); - } - target.append(node('p', 'project-help', 'Related memories and their history, closed sessions and their events move together. Original identifiers and preserved history stay attached to the memories.')); - if (Array.isArray(preview.memories) && preview.memories.length) { - const details = node('details', 'memory-move-records'); - details.append(node('summary', '', `Review all ${preview.memories.length} records`)); - const list = node('ul'); - preview.memories.forEach(memory => { - const item = node('li'); - item.append(node('strong', '', memory.title || memory.id), - node('span', '', ` · ${memory.id}${memory.related ? ' · related record' : ' · selected'}`)); - list.append(item); - }); - details.append(list); - target.append(details); - } - const blockers = Array.isArray(preview.blockers) ? preview.blockers : []; - if (blockers.length) { - target.append(node('p', 'form-error', 'These records cannot be moved yet:')); - const list = node('ul', 'memory-move-blockers'); - blockers.forEach(blocker => list.append(node('li', '', blocker.message || blocker.code || 'Move is blocked.'))); - target.append(list); - } else if (preview.can_move === true) { - target.append(node('p', 'project-help', 'Review this destination and complete record list, then choose Move memories.')); - } - } - - async function previewMemoryMove() { - const move = state.memoryMove; - if (!move || move.loading || move.applying || !byId('memory-move-target').value) return; - const request = beginScopedRequest('memory-move'); - const body = memoryMoveBody(move); - move.preview = null; - move.loading = true; - byId('memory-move-error').hidden = true; - byId('memory-move-preview').replaceChildren(empty('Checking the complete move and its related history…')); - renderMemoryMoveControls(); - try { - const preview = await api('/memories/move-preview', { method: 'POST', body, signal: request.signal }); - if (state.memoryMove !== move || !isCurrentScopedRequest(request) || body.target_workspace !== byId('memory-move-target').value) return; - const sameIds = Array.isArray(preview.requested_ids) - && JSON.stringify([...preview.requested_ids].sort()) === JSON.stringify(move.ids); - if (preview.source !== body.workspace || preview.target !== body.target_workspace || !sameIds - || (preview.can_move === true && (typeof preview.preview_token !== 'string' || !preview.preview_token))) { - throw new Error('The preview does not match this selection. Request a new preview.'); - } - if (Array.isArray(preview.blockers) && preview.blockers.length) preview.can_move = false; - move.preview = preview; - renderMemoryMovePreview(preview, move); - } catch (error) { - if (state.memoryMove !== move || !isCurrentScopedRequest(request)) return; - byId('memory-move-preview').replaceChildren(empty('No move has been submitted.')); - byId('memory-move-error').textContent = `Could not preview the move: ${error.message}`; - byId('memory-move-error').hidden = false; - } finally { - if (state.memoryMove === move && isCurrentScopedRequest(request)) { - move.loading = false; - renderMemoryMoveControls(); - } - } - } - - async function applyMemoryMove(event) { - event.preventDefault(); - const move = state.memoryMove; - if (!move || move.loading || move.applying || !move.preview || move.preview.can_move !== true) return; - const request = beginScopedRequest('memory-move'); - const body = { ...memoryMoveBody(move), preview_token: move.preview.preview_token, confirmed: true }; - if (body.target_workspace !== move.preview.target || body.workspace !== move.preview.source) { - invalidateMemoryMovePreview(); - return; - } - move.applying = true; - byId('memory-move-error').hidden = true; - renderMemoryMoveControls(); - try { - const result = await api('/memories/move', { method: 'POST', body, signal: request.signal }); - if (state.memoryMove !== move || !isCurrentScopedRequest(request)) return; - closeMemoryMove(); - await selectWorkspace(move.workspace); - if (state.workspace === move.workspace) showNotice(`Moved ${number(result.count)} records to ${JSON.stringify(result.workspace)}. History is preserved.`); - } catch (error) { - if (state.memoryMove !== move || !isCurrentScopedRequest(request)) return; - move.preview = null; - byId('memory-move-error').textContent = error.status === 409 - ? 'Memory or workspace state changed. Preview the move again before continuing.' - : `The move could not be confirmed: ${error.message} Preview again to check the current records before retrying.`; - byId('memory-move-error').hidden = false; - } finally { - if (state.memoryMove === move && isCurrentScopedRequest(request)) { - move.applying = false; - renderMemoryMoveControls(); - } - } - } - - function definitionList(entries) { - const list = node('dl', 'definition-list'); - entries.forEach(([term, value]) => { - const row = node('div'); - row.append(node('dt', '', term), node('dd', '', value || '—')); - list.append(row); - }); - return list; - } - - async function selectMemory(id) { - const workspace = state.workspace; - const project = state.project; - const request = beginScopedRequest('memory-detail'); - state.selectedMemory = id; - resetEditorSession(); - destroyMemoryHistory(); - renderLibrary(); - const target = byId('memory-detail'); - target.hidden = false; - byId('memory-editor').hidden = true; - target.replaceChildren(empty('Loading memory…')); - try { - const payload = await api(`/memory/${encodeURIComponent(id)}?${query(workspace)}`, { signal: request.signal }); - const memory = payload.memory || state.memories.find(item => item.id === id); - if (!memory || state.selectedMemory !== id || !isCurrentScopedRequest(request)) return; - state.editorMemory = memory; - target.replaceChildren(); - const title = node('h2', '', memoryTitle(memory)); - title.id = 'memory-detail-title'; - target.append( - node('p', 'eyebrow', `${memoryType(memory)} · ${memory.scope || 'workspace'}`), - title, - node('p', '', memory.content || memory.summary || 'No content.'), - memoryMeta(memory), - definitionList([ - ['Memory id', memory.id], - ['Workspace id', memory.workspace_id], - ['Project id', memory.repo_id], - ['Session id', memory.session_id], - ['Importance', memory.importance == null ? '—' : number(memory.importance).toFixed(2)], - ['Valid from', relative(memory.valid_from)], - ['Valid to', memory.valid_to ? relative(memory.valid_to) : 'current'], - ['Source', memory.provenance && (memory.provenance.source || memory.provenance.kind)], - ['Review', memory.provenance && (memory.provenance.review_state || 'pending')], - ]), - ); - const actions = node('div', 'detail-actions'); - const provenance = memory.provenance || {}; - if (provenance.review_state !== 'approved' || provenance.trusted !== true) { - actions.append(button('Approve for prompt…', 'primary-button', () => approveForPrompt(memory))); - } - actions.append( - button('Review with Jev', 'secondary-button', () => openJevReview(memory)), - button(memory.can_revise === false ? 'Review saved versions' : 'Edit', 'secondary-button', () => openEditor(memory)), - button(memory.pinned ? 'Unpin' : 'Pin', 'secondary-button', () => togglePin(memory)), - button('Search topic timeline', 'secondary-button', () => openMemoryTimeline(memory)), - button('Retire', 'danger-button', () => retireMemory(memory)), - button('Secure erase leak', 'danger-button', () => secureEraseMemory(memory)), - ); - target.append(actions); - state.memoryHistory = window.EngraphisMemoryHistory.create({ - api, id, workspace, repo: project, - isCurrent: () => isCurrentScopedRequest(request) && state.selectedMemory === id, - onOpen: openMemory, - }); - target.append(state.memoryHistory.element); - void state.memoryHistory.load(); - } catch (error) { - if (state.selectedMemory === id && isCurrentScopedRequest(request)) { - target.replaceChildren(empty(`Could not inspect memory: ${error.message}`)); - } - } - } - - function openJevReview(memory) { - const target = byId('memory-detail'); - const prior = byId('jev-review-panel'); - if (prior) { prior.remove(); return; } - const panel = node('section', 'jev-review-panel'); - panel.id = 'jev-review-panel'; - panel.setAttribute('aria-labelledby', 'jev-review-title'); - panel.append( - node('h3', '', 'Review selected evidence with Jev'), - node('p', 'project-help', 'This advisory checks whether the selected memory supports your claim and, if you choose a second memory, whether the two conflict. It never changes stored memories.'), - ); - panel.querySelector('h3').id = 'jev-review-title'; - const form = node('form'); - const claimLabel = node('label'); - claimLabel.append(node('span', '', 'Question for the selected evidence')); - const claim = node('textarea'); - claim.rows = 3; - claim.maxLength = 1200; - claim.required = true; - claim.value = memory.title || ''; - claim.placeholder = 'What should this memory support?'; - claimLabel.append(claim); - const peerLabel = node('label'); - peerLabel.append(node('span', '', 'Compare with another memory (optional)')); - const peer = node('select'); - peer.append(option('', 'No contradiction comparison')); - state.memories.filter(item => item && item.id && item.id !== memory.id).forEach(item => { - peer.append(option(item.id, memoryTitle(item))); - }); - peerLabel.append(peer); - const remoteLabel = node('label', 'check-row'); - const remote = node('input'); - remote.type = 'checkbox'; - remoteLabel.append(remote, document.createTextNode(' Allow this action to send the question and selected excerpts to Jev')); - const classificationLabel = node('label'); - classificationLabel.append(node('span', '', 'Data classification for this action')); - const classification = node('select'); - classification.append(option('internal', 'Internal', true), option('public', 'Public')); - classificationLabel.append(classification); - const status = node('p', 'project-help'); - status.setAttribute('role', 'status'); - status.setAttribute('aria-live', 'polite'); - const result = node('div', 'jev-review-result'); - const submit = node('button', 'primary-button', 'Run read-only review'); - submit.type = 'submit'; - form.append(claimLabel, peerLabel, remoteLabel, classificationLabel, submit); - form.addEventListener('submit', event => { event.preventDefault(); void runJevReview(); }); - panel.append(form, status, result); - target.append(panel); - - async function runJevReview() { - const claimText = claim.value.trim(); - if (!claimText) { - status.textContent = 'Enter a question before reviewing.'; - claim.focus(); - return; - } - if (!state.workspace || state.selectedMemory !== memory.id) return; - const request = beginScopedRequest('jev-review'); - const memoryIds = [memory.id, ...(peer.value ? [peer.value] : [])]; - submit.disabled = true; - status.textContent = 'Running a read-only advisory review…'; - result.replaceChildren(); - try { - const response = await api('/jev/review', { - method: 'POST', signal: request.signal, - body: { - workspace: request.workspace, - ...(request.project ? { repo: request.project } : {}), - memory_ids: memoryIds, - claim: claimText, - allow_remote: remote.checked, - data_classification: classification.value, - }, - }); - if (!isCurrentScopedRequest(request) || state.selectedMemory !== memory.id) return; - renderJevReviewResult(result, response); - status.textContent = response.remote_blocked_reason - ? `Remote consent was granted, but sending was blocked (${response.remote_blocked_reason.replaceAll('_', ' ')}). See the fallback result.` - : response.remote_consent_granted - ? 'Remote consent was granted for this action only. This does not confirm a provider request; check each result for fallback or uncertainty.' - : 'Remote consent was not granted for this action. Check each result for fallback or uncertainty.'; - } catch (error) { - if (isCurrentScopedRequest(request) && state.selectedMemory === memory.id) { - status.textContent = `Review unavailable: ${error.message}`; - } - } finally { - submit.disabled = false; - } - } - } - - function renderJevReviewResult(target, response) { - const fallbackLabels = { - allowance_exhausted: 'The included Jev allowance is currently used up. Check account usage for the next available window.', - provider_protection_limit: 'Jev is temporarily paused by a service-protection limit. Try again later.', - remote_timeout: 'Jev did not respond before the request deadline. You can retry later.', - session_changed: 'The Cloud session changed during the request. Sign in again before retrying.', - managed_operation_unsupported: 'This Jev operation is unavailable in the managed service.', - remote_unavailable: 'The Jev service is unavailable. You can retry later.', - malformed_response: 'Jev returned a response that could not be validated. Review the sources manually.', - }; - const labelStatus = item => item.status === 'decision' ? 'Advisory decision' - : item.status === 'uncertain' ? 'Uncertain; no conclusion' - : item.status === 'not_requested' ? 'Not checked' - : 'Fallback; review manually'; - const describe = (name, item) => { - const card = node('article'); - card.append(node('strong', '', `${name} · ${labelStatus(item)}`)); - if (item.status === 'decision') { - const value = item.value === true ? 'Evidence appears sufficient to answer the question.' - : item.value === false ? 'Evidence appears insufficient to answer the question.' - : item.value === 'contradicts_and_supersedes' ? 'The memories may contradict.' - : item.value === 'reinforces' ? 'The memories appear consistent.' - : item.value === 'orthogonal' ? 'No direct relationship was found.' - : 'Jev returned an advisory result.'; - card.append(node('p', '', value)); - } else if (item.reason) card.append(node('p', '', item.reason.replaceAll('_', ' '))); - if (item.fallback_reason) card.append(node('p', '', fallbackLabels[item.fallback_reason] - || 'Jev could not complete this check. Review the sources manually.')); - if (Number.isFinite(item.probability)) card.append(node('p', '', `Support probability: ${item.probability.toFixed(2)} · advisory only`)); - if (Number.isFinite(item.confidence)) card.append(node('p', '', `Confidence: ${item.confidence.toFixed(2)} · advisory only`)); - target.append(card); - }; - target.replaceChildren(); - describe('Evidence support', response.support || {}); - describe('Contradiction check', response.contradiction || {}); - target.append(node('p', 'project-help', 'Jev cannot authorize a memory change, grounded answer, or command. Review the sources yourself.')); - } - - function openMemory(memory) { - if (!memory || !memory.id) { - showNotice('This result no longer identifies a memory to inspect.'); - return; - } - switchView('library'); - selectMemory(memory.id); - } - - function simpleMemoryCard(memory, className = 'memory-card') { - const interactive = Boolean(memory && memory.id); - const card = node(interactive ? 'button' : 'article', `${className}${interactive ? ' memory-link-card' : ''}`); - if (interactive) { - card.type = 'button'; - card.dataset.memoryId = memory.id; - card.addEventListener('click', () => openMemory(memory)); - } - card.append( - node('h3', '', memoryTitle(memory)), - node('p', '', truncate(memory.content || memory.summary, 500)), - memoryMeta(memory), - ); - return card; - } - - function openEditor(memory = null) { - resetEditorSession(); - state.editorSession = { - revision: window.EngraphisMemoryRevision.create(memory), busy: false, - conflicted: Boolean(memory && memory.can_revise === false), - }; - state.editorMemory = memory; - state.editorReturnFocus = document.activeElement instanceof HTMLElement - ? document.activeElement : byId('new-memory-button'); - byId('memory-detail').hidden = true; - const editor = byId('memory-editor'); - editor.hidden = false; - byId('editor-title').textContent = memory ? 'Revise memory' : 'New memory'; - byId('editor-memory-title').value = memory ? (memory.title || '') : ''; - byId('editor-memory-type').value = memory ? memoryType(memory) : 'semantic'; - const scope = byId('editor-memory-scope'); - scope.querySelector('[value="repo"]').disabled = !state.project; - scope.querySelector('[value="repo"]').textContent = state.project - ? 'Project: ' + state.project : 'Select a project first'; - scope.value = state.project ? 'repo' : 'workspace'; - scope.disabled = Boolean(memory); - byId('editor-scope-control').hidden = Boolean(memory); - byId('editor-scope-note').textContent = memory - ? 'This revision preserves the existing ' + (memory.scope || 'workspace') + ' scope.' - : 'Workspace: ' + state.workspace + '. Choose where this new memory belongs.'; - byId('editor-memory-content').value = memory ? (memory.content || memory.summary || '') : ''; - byId('editor-memory-content').removeAttribute('aria-invalid'); - byId('editor-error').hidden = true; - byId('editor-error').textContent = ''; - const importanceControl = byId('editor-memory-importance'); - const storedImportance = memory && memory.importance != null ? memory.importance : 0.5; - importanceControl.value = String(graphSliderInputValue( - 'editor-memory-importance', storedImportance, 0.5, - )); - const respVal = graphSliderResponseValue( - 'editor-memory-importance', importanceControl.value, 0.5, - ); - importanceControl.setAttribute('aria-valuetext', `${respVal.toFixed(2)} importance`); - const importanceOutput = byId('editor-memory-importance-output'); - if (importanceOutput) importanceOutput.textContent = respVal.toFixed(2); - if (state.editorSession.conflicted) { - byId('editor-error').textContent = 'This version cannot be revised. Refresh saved versions and choose an editing base; your draft will be retained.'; - byId('editor-error').hidden = false; - byId('editor-refresh').hidden = false; - setEditorBusy(false); - } - byId('editor-memory-title').focus(); - } - - function destroyMemoryHistory() { - if (state.memoryHistory) state.memoryHistory.destroy(); - state.memoryHistory = null; - } - - function resetEditorSession() { - const session = state.editorSession; - if (session && session.history) session.history.destroy(); - state.editorSession = null; - byId('editor-refresh').hidden = true; - byId('editor-history').replaceChildren(); - byId('editor-history').hidden = true; - setEditorBusy(false); - } - - function setEditorBusy(busy) { - const editor = byId('memory-editor'); - editor.setAttribute('aria-busy', String(busy)); - editor.querySelectorAll('input, select, textarea, button').forEach(control => { control.disabled = busy; }); - byId('editor-memory-scope').disabled = busy || Boolean(state.editorMemory); - editor.querySelector('button[type="submit"]').disabled = busy || Boolean(state.editorSession && state.editorSession.conflicted); - } - - function refreshEditorVersions() { - const session = state.editorSession; - const original = state.editorMemory; - const workspace = state.workspace; - const project = state.project; - if (!session || !original || session.busy) return; - if (session.history) session.history.destroy(); - const target = byId('editor-history'); - target.hidden = false; - session.history = window.EngraphisMemoryHistory.create({ - api, id: original.id, workspace, repo: project, original, - isCurrent: () => state.editorSession === session && state.workspace === workspace && state.project === project, - onUseBase: record => { - state.editorMemory = record; - session.revision = window.EngraphisMemoryRevision.create(record); - session.conflicted = false; - session.history.destroy(); - target.replaceChildren(); - target.hidden = true; - byId('editor-refresh').hidden = true; - byId('editor-error').textContent = 'Editing base refreshed. Your draft is unchanged; review it before saving.'; - byId('editor-error').hidden = false; - setEditorBusy(false); - byId('editor-memory-content').focus(); - }, - }); - target.replaceChildren(session.history.element); - void session.history.load(); - } - - function closeEditor() { - const returnFocus = state.editorReturnFocus; - resetEditorSession(); - byId('memory-editor').hidden = true; - byId('memory-detail').hidden = false; - state.editorMemory = null; - state.editorReturnFocus = null; - if (returnFocus && document.contains(returnFocus) && !returnFocus.hidden - && !returnFocus.disabled) returnFocus.focus(); - else byId('new-memory-button').focus(); - } - - async function saveMemory(event) { - event.preventDefault(); - const session = state.editorSession; - if (!session || session.busy || session.conflicted) return; - const current = state.editorMemory; - let savedId = current && current.id; - const workspace = state.workspace; - const epoch = state.refreshEpoch; - const title = byId('editor-memory-title').value.trim(); - const memoryTypeValue = byId('editor-memory-type').value; - const content = byId('editor-memory-content').value.trim(); - const importance = graphSliderResponseValue( - 'editor-memory-importance', number(byId('editor-memory-importance').value), 0.5, - ); - const creationScope = byId('editor-memory-scope').value; - const creationProject = creationScope === 'repo' ? state.project : ''; - const currentImportance = current && current.importance != null - ? number(current.importance) : 0.5; - const contentField = byId('editor-memory-content'); - const editorError = byId('editor-error'); - contentField.removeAttribute('aria-invalid'); - editorError.hidden = true; - editorError.textContent = ''; - if (!current && creationScope === 'repo' && !creationProject) { - editorError.textContent = 'Select a project before saving a project memory.'; - editorError.hidden = false; - return; - } - if (!content) { - contentField.setAttribute('aria-invalid', 'true'); - editorError.textContent = 'Enter memory content before saving.'; - editorError.hidden = false; - showNotice('Enter memory content before saving.'); - contentField.focus(); - return; - } - try { - session.busy = true; - setEditorBusy(true); - if (current) { - if (content !== (current.content || current.summary || '') - || title !== (current.title || '') || memoryTypeValue !== memoryType(current) - || importance !== currentImportance) { - const body = session.revision.body({ workspace, content, title, mtype: memoryTypeValue, importance, reason: 'revised in Ledger' }); - const revised = await api('/memory/revise', { method: 'POST', body }); - if (!revised || !revised.id || !revised.version || !revised.receipt - || revised.receipt.status !== 'committed' || revised.receipt.operation !== 'revise' - || revised.receipt.operation_id !== body.operation_id) { - throw new Error('The save could not be confirmed. Retry the unchanged draft to check this operation.'); - } - savedId = revised.id; - if (state.editorSession === session) { - state.editorMemory = { ...current, id: revised.id, version: revised.version, content, title, memory_type: memoryTypeValue, importance }; - } - } - if (workspace === state.workspace && epoch === state.refreshEpoch) { - showNotice('Memory revision recorded with temporal history preserved.'); - } - } else { - const saved = await api('/remember', { - method: 'POST', - body: { - workspace, - content, - title, - mtype: memoryTypeValue, - scope: creationScope, - ...(creationProject ? { repo: creationProject } : {}), - importance, - source: 'human:ledger', - trusted: true, - }, - }); - savedId = saved && saved.id; - if (workspace === state.workspace && epoch === state.refreshEpoch) { - showNotice('Memory saved locally. Review its source before approving it for model context.'); - } - } - if (workspace !== state.workspace || epoch !== state.refreshEpoch || state.editorSession !== session) return; - closeEditor(); - await selectWorkspace(workspace); - if (savedId && workspace === state.workspace && state.refreshEpoch === epoch + 1) await selectMemory(savedId); - } catch (error) { - if (workspace === state.workspace && epoch === state.refreshEpoch && state.editorSession === session) { - session.conflicted = Boolean(current && (error.status === 409 || !current.version)); - editorError.textContent = session.conflicted - ? 'This saved record changed. Your draft is retained. Refresh saved versions and choose an editing base before saving.' - : `Could not save memory: ${error.message} Your draft is retained.`; - editorError.hidden = false; - byId('editor-refresh').hidden = !session.conflicted; - showNotice(editorError.textContent); - } - } finally { - session.busy = false; - if (state.editorSession === session) setEditorBusy(false); - } - } - - async function togglePin(memory) { - try { - await api('/pin', { - method: 'POST', - body: { id: memory.id, workspace: state.workspace, pinned: !memory.pinned }, - }); - showNotice(memory.pinned ? 'Memory unpinned.' : 'Memory pinned against decay.'); - await selectWorkspace(state.workspace); - selectMemory(memory.id); - } catch (error) { - showNotice(`Could not change pin: ${error.message}`); - } - } - - async function retireMemory(memory) { - if (!window.confirm(`Retire “${memory.title || memory.id}”? The record stays in temporal history but leaves live recall.`)) return; - try { - await api('/retire', { - method: 'POST', - body: { id: memory.id, workspace: state.workspace, reason: 'retired in Ledger' }, - }); - state.selectedMemory = ''; - byId('memory-detail').replaceChildren(empty('Memory moved out of live recall. Its history is retained.')); - showNotice('Memory retired without hard deletion.'); - await selectWorkspace(state.workspace); - } catch (error) { - showNotice(`Could not retire memory: ${error.message}`); - } - } - - async function secureEraseMemory(memory) { - const name = memory.title || memory.id; - if (!window.confirm(`Securely erase “${name}”? This destroys temporal history and local index copies. Rotate the leaked credential; copied exports, snapshots, remote peers, and an already-compromised agent cannot be erased here.`)) return; - try { - const result = await api('/secure-erase', { - method: 'POST', body: { id: memory.id, workspace: state.workspace }, - }); - state.selectedMemory = ''; - byId('memory-detail').replaceChildren(empty('Memory securely erased from this local store. Review the reported backup limitations and rotate the credential.')); - showNotice(result.vector_index_cleanup === 'deleted' - ? 'Memory securely erased from local persistence.' - : 'Memory removed locally; configured vector index needs separate remediation.'); - await selectWorkspace(state.workspace); - } catch (error) { - showNotice(`Could not securely erase memory: ${error.message}`); - } - } - - function openMemoryTimeline(memory) { - switchView('provenance'); - switchProvenanceTab('timeline'); - byId('timeline-input').value = memory.title || truncate(memory.content, 80); - byId('timeline-form').requestSubmit(); - } - - async function importFiles(files) { - if (!files.length) return; - const form = new FormData(); - form.append('workspace', state.workspace); - form.append('memory_type', 'semantic'); - form.append('derive_facts', 'false'); - [...files].forEach(file => form.append('files', file)); - try { - showNotice(`Importing ${files.length} ${files.length === 1 ? 'file' : 'files'} locally…`); - const result = await api('/workspaces/import-files', { method: 'POST', body: form }); - showNotice(`Import complete${result.count != null ? ` · ${result.count} memories` : ''}.`); - await selectWorkspace(state.workspace); - } catch (error) { - showNotice(`Import failed: ${error.message}`); - } finally { - byId('import-files').value = ''; - } - } - - const obsidianImport = { - preview: null, job: null, poll: null, selection: null, sources: [], - jobWorkspace: '', running: false, reviewGeneration: 0, - }; - let documentExtensions = null; - - async function obsidianApi(path, options = {}) { - const attempt = async () => { - const csrf = await reviewCsrfToken(); - return api(path, { - ...options, - headers: { ...(options.headers || {}), 'X-Engraphis-Review-CSRF': csrf }, - }); - }; - try { - return await attempt(); - } catch (error) { - if (error && error.status === 403) { - // A dashboard restart or re-login invalidates the cached process-local - // nonce. Refresh it once so the wizard recovers without a page reload. - state.reviewCsrf = ''; - return attempt(); - } - throw error; - } - } - - function obsidianSelection() { - const files = [ - ...byId('obsidian-import-files').files, - ...byId('obsidian-import-folder').files, - ]; - const sourceMode = byId('obsidian-source-mode').value; - const markdown = files.filter(file => /\.md$/i.test(file.name)); - const documents = files.filter(file => { - const suffix = (file.name.split('.').pop() || '').toLowerCase(); - // The format endpoint is an owner-only convenience hint. The server still - // enforces its registry for every byte if the hint is temporarily unavailable. - return !documentExtensions || documentExtensions.has(suffix); - }); - const uploadFiles = sourceMode === 'obsidian' ? markdown : documents; - const attachments = sourceMode === 'obsidian' - ? files.filter(file => !/\.md$/i.test(file.name)).map(file => ({ - path: file.webkitRelativePath || file.name, size: file.size, - })) : []; - const unsupported = sourceMode === 'obsidian' - ? 0 : files.length - uploadFiles.length; - const fields = { - workspace: byId('obsidian-workspace').value.trim(), - repo: byId('obsidian-repo').value.trim(), - session_id: byId('obsidian-session').value.trim(), - scope: byId('obsidian-scope').value.trim(), - memory_type: byId('obsidian-memory-type').value, - source_id: byId('obsidian-vault-id').value, - source_label: byId('obsidian-vault-label').value.trim(), - on_conflict: byId('obsidian-conflict').value, - source_mode: sourceMode, - }; - return { uploadFiles, attachments, unsupported, sourceMode, fields }; - } - - function obsidianFormData(selection, { confirmed = false, reviewToken = '' } = {}) { - const form = new FormData(); - Object.entries(selection.fields).forEach(([name, value]) => form.append(name, value)); - form.append('confirmed', confirmed ? 'true' : 'false'); - if (reviewToken) form.append('review_token', reviewToken); - form.append('attachment_manifest', JSON.stringify(selection.attachments)); - selection.uploadFiles.forEach(file => ( - form.append('files', file, file.webkitRelativePath || file.name) - )); - return form; - } - - function invalidateDocumentImportPreview(message = 'Selection changed. Preview again before importing.') { - obsidianImport.reviewGeneration += 1; - obsidianImport.preview = null; - obsidianImport.selection = null; - byId('obsidian-confirmed').checked = false; - byId('obsidian-run').disabled = true; - if (obsidianImport.running) return; - obsidianImport.job = null; - obsidianImport.jobWorkspace = ''; - byId('obsidian-cancel').hidden = true; - delete byId('obsidian-cancel').dataset.jobId; - renderObsidianReport(null); - if (message) byId('obsidian-import-progress').textContent = message; - } - - function updateDocumentImportMode() { - const obsidian = byId('obsidian-source-mode').value === 'obsidian'; - byId('obsidian-files-label').textContent = obsidian ? 'Individual Markdown notes' : 'Individual documents'; - byId('obsidian-folder-label').textContent = obsidian ? 'Obsidian vault folder' : 'Document folder'; - byId('obsidian-import-description').textContent = obsidian - ? 'Choose an Obsidian vault folder. Engraphis previews Markdown note bytes and attachment metadata before it writes anything; attachment bytes are never uploaded.' - : 'Choose individual files or a folder. Engraphis previews supported document formats before it writes anything; uploaded bytes are processed locally and are not kept as dashboard upload copies.'; - byId('obsidian-run').textContent = obsidian ? 'Import vault notes' : 'Import documents'; - byId('obsidian-import-files').value = ''; - byId('obsidian-import-folder').value = ''; - invalidateDocumentImportPreview('Choose files or a folder to preview its import.'); - } - - function updateSourceLabelRequirement() { - const label = byId('obsidian-vault-label'); - const isNewSource = !byId('obsidian-vault-id').value; - label.required = isNewSource; - label.setAttribute('aria-required', isNewSource ? 'true' : 'false'); - label.placeholder = isNewSource ? 'Required for a new source' : 'Saved source label'; - } - - function prefillNewSourceLabelFromFolder() { - if (byId('obsidian-vault-id').value || byId('obsidian-vault-label').value.trim()) return; - const firstFolderFile = [...byId('obsidian-import-folder').files] - .find(file => file.webkitRelativePath && file.webkitRelativePath.includes('/')); - if (!firstFolderFile) return; - const folderName = firstFolderFile.webkitRelativePath.split('/')[0].trim(); - if (folderName) byId('obsidian-vault-label').value = folderName; - } - - function requireNewSourceLabel() { - if (byId('obsidian-vault-id').value || byId('obsidian-vault-label').value.trim()) return true; - byId('obsidian-import-progress').textContent = 'Enter a Source label before creating a new source.'; - byId('obsidian-vault-label').focus(); - return false; - } - - function obsidianRows(result) { - const rows = result && (result.files || result.details || result.entries || []); - return Array.isArray(rows) ? rows : []; - } - - function renderObsidianReport(result) { - const target = byId('obsidian-import-report'); - const wanted = byId('obsidian-report-filter').value; - target.replaceChildren(); - const rows = obsidianRows(result).filter(row => { - const status = String(row.status || row.action || row.result || '').toLowerCase(); - if (wanted === 'all') return true; - if (wanted === 'reject') return /reject|error|warn|conflict/.test(status) || Boolean(row.warning || row.error); - return status.includes(wanted); - }); - if (!rows.length) { - target.append(empty(wanted === 'all' ? 'No per-file details were returned.' : 'No files match this filter.')); - return; - } - const list = node('ul'); - rows.forEach(row => { - const status = String(row.status || row.action || row.result || 'reported').toLowerCase(); - const action = row.action && String(row.action).toLowerCase() !== status - ? ` · action: ${row.action}` : ''; - const format = row.format || row.format_name ? ` · format: ${row.format || row.format_name}` : ''; - const warning = row.warning || row.error || row.reason - || (Number(row.warning_count) ? `${row.warning_count} warning(s)` : ''); - const item = node('li', '', `${status.toUpperCase()} · ${row.path || row.file || row.relative_path || 'unnamed document'}${format}${action}${warning ? ` · ${warning}` : ''}`); - item.dataset.status = /reject|error/.test(status) || row.error || row.reason ? 'reject' : status; - list.append(item); - }); - target.append(list); - } - - function obsidianSummary(result, prefix = 'Preview') { - const counts = result && (result.counts || result); - const keys = ['documents', 'markdown', 'formats', 'imported', 'updated', 'renamed', 'skipped', 'rejected', 'conflict', 'missing', 'error']; - const summary = keys.filter(key => Number.isFinite(Number(counts && counts[key]))) - .map(key => `${key.replace('_', ' ')}: ${counts[key]}`); - const unsupported = obsidianImport.selection && obsidianImport.selection.unsupported; - const warning = unsupported ? ` · warning: ${unsupported} unsupported files were not uploaded` : ''; - byId('obsidian-import-progress').textContent = summary.length ? `${prefix} · ${summary.join(' · ')}${warning}` : `${prefix} ready.${warning}`; - } - - async function loadObsidianVaults() { - const select = byId('obsidian-vault-id'); - try { - const result = await obsidianApi(`/workspaces/import-documents/sources?${query(state.workspace)}`); - const vaults = result.sources || result.vaults || result || []; - obsidianImport.sources = Array.isArray(vaults) ? vaults : []; - select.replaceChildren(option('', 'New source')); - obsidianImport.sources.forEach(vault => select.append(option(vault.id, vault.label || vault.name || vault.id))); - } catch (_) { - // A first-run vault list is optional; preview/import still present a useful error. - select.replaceChildren(option('', 'New source')); - obsidianImport.sources = []; - } - } - - async function loadDocumentFormats() { - try { - const result = await obsidianApi('/workspaces/import-documents/formats'); - const extensions = Array.isArray(result.extensions) ? result.extensions : []; - documentExtensions = new Set(extensions.map(extension => String(extension).replace(/^\./, '').toLowerCase())); - } catch (_) { - // Server-side validation remains authoritative; do not invent a stale client registry. - documentExtensions = null; - } - } - - function applySelectedDocumentSource() { - const source = obsidianImport.sources.find(item => item.id === byId('obsidian-vault-id').value); - if (!source) { - byId('obsidian-vault-label').value = ''; - updateSourceLabelRequirement(); - invalidateDocumentImportPreview(); - return; - } - byId('obsidian-vault-label').value = source.label || source.name || ''; - if (source.repo != null) byId('obsidian-repo').value = source.repo; - if (source.session_id != null) byId('obsidian-session').value = source.session_id; - if (source.scope) byId('obsidian-scope').value = source.scope; - if (source.memory_type) byId('obsidian-memory-type').value = source.memory_type; - byId('obsidian-source-mode').value = source.adapter === 'obsidian' || source.kind === 'obsidian' - ? 'obsidian' : 'documents'; - updateSourceLabelRequirement(); - updateDocumentImportMode(); - } - - async function previewObsidianImport() { - if (obsidianImport.running) return; - if (!requireNewSourceLabel()) return; - const selection = obsidianSelection(); - if (!selection.uploadFiles.length) { - byId('obsidian-import-progress').textContent = selection.sourceMode === 'obsidian' - ? 'Choose a folder containing Markdown notes.' - : 'Choose supported documents to import.'; - return; - } - invalidateDocumentImportPreview(''); - const generation = obsidianImport.reviewGeneration; - const type = selection.sourceMode === 'obsidian' ? 'Markdown notes' : 'supported documents'; - const ignored = selection.unsupported ? ` · ${selection.unsupported} unsupported files will not be uploaded` : ''; - byId('obsidian-import-progress').textContent = `Previewing ${selection.uploadFiles.length} ${type}${selection.attachments.length ? ` and ${selection.attachments.length} attachment manifests` : ''}${ignored}…`; - byId('obsidian-preview').disabled = true; - try { - const preview = await obsidianApi('/workspaces/import-documents/preview', { - method: 'POST', body: obsidianFormData(selection), - }); - if (generation !== obsidianImport.reviewGeneration) return; - if (!preview || typeof preview.review_token !== 'string' || !preview.review_token) { - throw new Error('The server did not bind this preview. Preview again.'); - } - selection.reviewToken = preview.review_token; - obsidianImport.selection = selection; - obsidianImport.preview = preview; - byId('obsidian-confirmed').checked = false; - renderObsidianReport(obsidianImport.preview); - obsidianSummary(obsidianImport.preview); - byId('obsidian-run').disabled = false; - } catch (error) { - if (generation !== obsidianImport.reviewGeneration) return; - obsidianImport.selection = null; - obsidianImport.preview = null; - byId('obsidian-import-progress').textContent = `Preview failed: ${error.message}`; - byId('obsidian-run').disabled = true; - } finally { - byId('obsidian-preview').disabled = false; - } - } - - async function pollObsidianImport(jobId, workspace) { - try { - const result = await obsidianApi(`/workspaces/import-documents/jobs/${encodeURIComponent(jobId)}?${query(workspace)}`); - obsidianImport.job = result; - renderObsidianReport(result); - obsidianSummary(result, 'Import'); - if (!['complete', 'completed', 'partial', 'failed', 'cancelled'].includes(String(result.state || result.status || '').toLowerCase())) { - obsidianImport.poll = window.setTimeout(() => pollObsidianImport(jobId, workspace), 750); - return; - } - obsidianImport.running = false; - obsidianImport.poll = null; - obsidianImport.selection = null; - obsidianImport.preview = null; - byId('obsidian-confirmed').checked = false; - byId('obsidian-cancel').hidden = true; - byId('obsidian-run').disabled = true; - byId('obsidian-preview').disabled = false; - showNotice('Document import finished.'); - await selectWorkspace(state.workspace); - } catch (error) { - byId('obsidian-import-progress').textContent = `Could not read import progress: ${error.message}`; - byId('obsidian-run').disabled = true; - } - } - - async function runObsidianImport(event) { - event.preventDefault(); - if (!requireNewSourceLabel()) return; - if (!byId('obsidian-confirmed').checked) { - byId('obsidian-import-progress').textContent = 'Confirm the selected scope before importing.'; - byId('obsidian-confirmed').focus(); - return; - } - const selection = obsidianImport.selection; - if (!selection || !selection.reviewToken) { - byId('obsidian-import-progress').textContent = 'Preview this exact selection before importing.'; - byId('obsidian-run').disabled = true; - return; - } - const workspace = selection.fields.workspace; - const runBody = obsidianFormData(selection, { - confirmed: true, reviewToken: selection.reviewToken, - }); - // The server token is one-time. Clear the client copy before the request so - // a double submit or ambiguous network failure cannot reuse it. - selection.reviewToken = ''; - byId('obsidian-run').disabled = true; - byId('obsidian-preview').disabled = true; - byId('obsidian-import-progress').textContent = 'Starting local document import…'; - obsidianImport.running = true; - obsidianImport.jobWorkspace = workspace; - try { - const result = await obsidianApi('/workspaces/import-documents/run', { - method: 'POST', - body: runBody, - }); - obsidianImport.job = result; - renderObsidianReport(result); - obsidianSummary(result, 'Import'); - const jobId = result.job_id || result.id; - if (jobId) { - byId('obsidian-cancel').hidden = false; - byId('obsidian-cancel').dataset.jobId = jobId; - byId('obsidian-cancel').dataset.workspace = workspace; - await pollObsidianImport(jobId, workspace); - } - else { - obsidianImport.running = false; - obsidianImport.selection = null; - obsidianImport.preview = null; - byId('obsidian-confirmed').checked = false; - byId('obsidian-run').disabled = true; - byId('obsidian-preview').disabled = false; - showNotice('Document import finished.'); - await selectWorkspace(state.workspace); - } - } catch (error) { - obsidianImport.running = false; - obsidianImport.selection = null; - obsidianImport.preview = null; - byId('obsidian-confirmed').checked = false; - byId('obsidian-import-progress').textContent = `Import failed: ${error.message} Preview again before retrying.`; - byId('obsidian-run').disabled = true; - byId('obsidian-preview').disabled = false; - } - } - - async function cancelObsidianImport() { - const button = byId('obsidian-cancel'); - const jobId = button.dataset.jobId; - const workspace = button.dataset.workspace || obsidianImport.jobWorkspace; - if (!jobId || !workspace) return; - button.disabled = true; - const form = new FormData(); - form.append('workspace', workspace); - try { - await obsidianApi(`/workspaces/import-documents/jobs/${encodeURIComponent(jobId)}/cancel`, { method: 'POST', body: form }); - byId('obsidian-import-progress').textContent = 'Cancellation requested; finishing the current document safely…'; - } catch (error) { - byId('obsidian-import-progress').textContent = `Could not cancel import: ${error.message}`; - } finally { - button.disabled = false; - } - } - - async function openObsidianImport() { - const dialog = byId('obsidian-import-dialog'); - byId('obsidian-confirmed').checked = false; - if (!obsidianImport.running) { - if (obsidianImport.poll) window.clearTimeout(obsidianImport.poll); - obsidianImport.preview = null; - obsidianImport.job = null; - obsidianImport.poll = null; - obsidianImport.selection = null; - obsidianImport.jobWorkspace = ''; - delete byId('obsidian-cancel').dataset.jobId; - delete byId('obsidian-cancel').dataset.workspace; - } - byId('obsidian-workspace').value = state.workspace; - byId('obsidian-repo').value = state.project; - byId('obsidian-scope').value = state.project ? 'repo' : 'workspace'; - byId('obsidian-session').value = ''; - byId('obsidian-vault-label').value = ''; - if (!obsidianImport.running) { - byId('obsidian-import-progress').textContent = 'Choose individual files or a folder to preview its import.'; - } - byId('obsidian-run').disabled = true; - byId('obsidian-preview').disabled = obsidianImport.running; - byId('obsidian-cancel').hidden = !obsidianImport.running; - if (!obsidianImport.running) renderObsidianReport(null); - await Promise.all([loadObsidianVaults(), loadDocumentFormats()]); - byId('obsidian-vault-id').value = ''; - updateSourceLabelRequirement(); - updateDocumentImportMode(); - dialog.showModal(); - byId('obsidian-import-files').focus(); - } - - function renderAnswer(result) { - const target = byId('answer-panel'); - target.replaceChildren(); - const meta = node('div', 'answer-meta'); - const grounded = Boolean(result.grounded); - meta.append( - node('span', `support-pill ${grounded ? 'grounded' : 'abstained'}`, grounded ? 'Grounded' : 'Abstained'), - node('span', 'support-pill', Number.isFinite(result.support) ? `Support ${result.support.toFixed(2)}` : 'Support unavailable'), - node('span', 'support-pill', `${(result.citations || []).length} ${(result.citations || []).length === 1 ? 'citation' : 'citations'}`), - ); - target.append(meta); - const advisory = result.planning_advisory; - if (advisory && typeof advisory === 'object') { - const reasonLabels = { - route_selected: 'Jev prioritized a locally generated alternate route. A 40-task synthetic check found no retrieval-metric change; user-workload benefit is unproven.', - jev_uncertain: 'Jev was uncertain, so the deterministic route order was used.', - remote_consent_required: 'Remote consent was not granted, so deterministic route order was used.', - planning_disabled: 'Query planning is disabled, so the original query route was used.', - backend_unavailable: 'Jev is unavailable, so deterministic route order was used.', - allowance_exhausted: 'The included Jev allowance is currently used up, so deterministic route order was used. Check account usage for the next available window.', - provider_protection_limit: 'Jev is temporarily paused by a service-protection limit, so deterministic route order was used. Try again later.', - remote_timeout: 'Jev did not respond before the request deadline, so deterministic route order was used. You can retry later.', - session_changed: 'The Cloud session changed during the request, so deterministic route order was used. Sign in again before retrying.', - managed_operation_unsupported: 'This Jev operation is unavailable in the managed service, so deterministic route order was used.', - planner_timeout: 'Route planning reached its deadline, so deterministic route order was used.', - remote_unavailable: 'The Jev request failed, so deterministic route order was used.', - malformed_response: 'Jev returned an invalid response, so deterministic route order was used.', - sensitive_content: 'The route text was not sent because it matched a sensitive-data filter; deterministic route order was used.', - }; - const message = reasonLabels[advisory.reason] - || (advisory.status === 'decision' - ? 'Jev prioritized a locally generated route. A 40-task synthetic check found no retrieval-metric change; user-workload benefit is unproven.' - : 'The Jev advisory did not select a route; deterministic route order was used.'); - target.append(node('p', 'project-help jev-recall-status', message)); - } - const coverage = ['unknown', 'partial', 'complete'].includes(result.answer_coverage) ? result.answer_coverage : 'unknown'; - const coverageNote = { - unknown: 'This answer has not been checked against every part of your question.', - partial: 'The answering service reports that some requested information is missing.', - complete: 'The answering service reports that every part of the question is covered.', - }; - const coveragePanel = node('div', 'answer-coverage'); - coveragePanel.dataset.coverage = coverage; - coveragePanel.append(node('strong', '', 'Question coverage: ' + coverage), - node('p', '', coverageNote[coverage] + ' Support and citations describe the returned evidence.')); - target.append(coveragePanel); - if (!grounded) { - target.append( - node('h2', '', 'Insufficient evidence'), - node('p', 'answer-copy', result.reason || 'The active workspace does not support a grounded answer.'), - ); - return; - } - target.append(node('h2', '', 'Answer'), node('p', 'answer-copy', result.answer || 'The cited memories support this answer.')); - const citations = node('div', 'citation-list'); - (result.citations || []).forEach(citation => { - const card = node(citation.id ? 'button' : 'article', 'citation-card memory-link-card'); - if (citation.id) { - card.type = 'button'; - card.dataset.memoryId = citation.id; - card.addEventListener('click', () => openMemory(citation)); - } - card.append( - node('h3', '', `[${citation.n || citation.number || '•'}] ${memoryTitle(citation)}`), - node('p', '', citation.content || citation.summary || ''), - node('div', 'memory-meta', `support ${number(citation.support || citation.score).toFixed(2)} · ${citation.id || ''}`), - ); - citations.append(card); - }); - target.append(citations); - } - - async function askMemory(event) { - event.preventDefault(); - const input = byId('ask-input'); - const question = input.value; - if (!question.trim()) { - showNotice('Enter a question before requesting a grounded answer.'); - input.focus(); - return; - } - if (!state.workspace) { - showNotice('Choose a workspace before requesting a grounded answer.'); - return; - } - const request = beginScopedRequest('ask'); - const workspace = request.workspace; - showNotice(''); - const k = number(byId('ask-k').value) || 5; - const jevAssisted = byId('ask-jev-assisted').checked; - const allowRemote = jevAssisted && byId('ask-jev-remote').checked; - const body = { - query: question, - workspace, - ...(request.project ? { repo: request.project } : {}), - k: Math.max(8, k), - max_citations: k, - planning: jevAssisted ? 'auto' : 'off', - jev_assisted: jevAssisted, - allow_remote: allowRemote, - include_retrieval_preview: true, - ...(jevAssisted ? { data_classification: byId('ask-jev-classification').value } : {}), - }; - await askRequests.start({ - question, - scopeLabel: workspace + (request.project ? ' / ' + request.project : ' / all projects'), - isCurrent: () => isCurrentScopedRequest(request), - answer: signal => api('/answer', { - method: 'POST', signal, body, - }), - }); - } - - function renderPreview(retrieval) { - const target = byId('retrieval-list'); - target.replaceChildren(); - const memories = retrieval.memories || []; - if (!memories.length) target.append(empty('No raw candidates were returned.')); - else memories.forEach(memory => target.append(simpleMemoryCard(memory))); - } - - function graphCommunityIndex(value) { - const numeric = Number(value); - if (Number.isFinite(numeric)) return numeric; - const source = text(value); - let hash = 0; - for (let index = 0; index < source.length; index += 1) hash = ((hash * 31) + source.charCodeAt(index)) | 0; - return Math.abs(hash); - } - - function optionalGraphNumber(value) { - return value == null || value === '' ? undefined : number(value); - } - - function graphNodes(payload) { - const source = payload.nodes || payload.entities || []; - return source.map(item => ({ - ...item, - id: item.id, - name: item.label || item.name || item.id, - label: item.label || item.name || item.id, - etype: item.etype || item.type || 'person_or_concept', - nodeKind: item.node_kind || item.kind || '', - degree: number(item.degree != null ? item.degree : item.weighted_degree), - community: item.community_id != null ? graphCommunityIndex(item.community_id) - : (item.community != null ? graphCommunityIndex(item.community) : undefined), - community_id: item.community_id == null ? item.community : item.community_id, - gravity_mass: optionalGraphNumber(item.gravity_mass), - evidence_mass: optionalGraphNumber(item.evidence_mass ?? item.gravity_mass ?? item.mass_score), - visual_radius: optionalGraphNumber(item.visual_radius), - anchor_role: item.anchor_role || '', - x: Number.isFinite(Number(item.x)) ? Number(item.x) : undefined, - y: Number.isFinite(Number(item.y)) ? Number(item.y) : undefined, - repo_names: Array.isArray(item.repo_names) ? item.repo_names.filter(name => typeof name === 'string') : [], - // The legacy engine reads `repo`; scene-aware engines use `repo_names`. Keeping both - // makes filtering work during an asset-cache transition without mutating scene data. - repo: item.repo || (Array.isArray(item.repo_names) ? item.repo_names.join(' ') : ''), - topic: item.topic || '', - valid_from: item.valid_from, - valid_to: item.valid_to, - ghost: item.ghost === true, - member_count: optionalGraphNumber(item.member_count), - visible_by_default: item.visible_by_default !== false, - })); - } - - function graphEndpoint(value) { - if (value && typeof value === 'object') return value.id ?? value; - return value; - } - - function graphLinks(payload) { - const source = payload.edges || payload.links || []; - return source.map((item, index) => ({ - ...item, - id: item.id || `edge-${index}`, - source: item.from ?? graphEndpoint(item.source), - target: item.to ?? graphEndpoint(item.target), - label: item.label || item.relation || 'related', - layer: item.layer || 'semantic', - valid_from: item.valid_from, - valid_to: item.valid_to, - rest_length: optionalGraphNumber(item.rest_length), - spring_strength: optionalGraphNumber(item.spring_strength), - physics_strength: optionalGraphNumber(item.physics_strength), - strength: optionalGraphNumber(item.strength), - ghost: item.ghost === true, - bridge: item.bridge === true, - visible_by_default: item.visible_by_default !== false, - })).filter(item => item.source !== undefined && item.source !== null - && item.target !== undefined && item.target !== null); - } - - function revealGraphNode(id, label = 'Selected entity') { - const engine = state.graphEngine; - if (!engine) return; - let attempts = 0; - const reveal = () => { - if (state.graphEngine !== engine) return; - if (engine.reveal(id)) return; - attempts += 1; - if (attempts < 8) { - window.requestAnimationFrame(reveal); - return; - } - showNotice(`${label} is outside the current graph scope.`); - }; - reveal(); - } - - function focusGraphNode(id, label = 'Selected entity') { - const engine = state.graphEngine; - if (!engine) return; - let attempted = false; - const focus = () => { - if (state.graphEngine !== engine || attempted) return; - attempted = true; - if (typeof engine.focus === 'function' && engine.focus(id)) return; - if (typeof engine.reveal === 'function' && engine.reveal(id)) return; - showNotice(`${label} is outside the current graph scope.`); - }; - focus(); - } - - function cancelGraphConnectionMemoryLoad() { - state.graphConnectionsRequest += 1; - if (state.graphConnectionsController) state.graphConnectionsController.abort(); - state.graphConnectionsController = null; - } - - function closeGraphConnections() { - cancelGraphConnectionMemoryLoad(); - const dialog = byId('graph-connections-dialog'); - if (dialog.open) dialog.close(); - } - - function graphMemoryCard(evidence) { - return { - id: evidence.memory_id || evidence.id, - title: evidence.title || evidence.label || evidence.memory_id || evidence.id, - content: evidence.excerpt || evidence.content || evidence.summary || '', - mtype: evidence.memory_type || evidence.mtype, - valid_from: evidence.valid_from, - valid_to: evidence.valid_to, - ingested_at: evidence.ingested_at, - provenance: evidence.provenance, - }; - } - - function graphMemoryEvidenceCard(memory) { - const card = node('article', 'graph-memory-evidence'); - card.append( - node('h4', '', memoryTitle(memory)), - node('p', '', truncate(memory.content || memory.summary, 500)), - memoryMeta(memory), - ); - if (memory.id) { - card.append(button('Open in Library', 'secondary-button', () => { - closeGraphConnections(); - openMemory(memory); - })); - } - return card; - } - - function renderGraphConnectionMemories(memories, message) { - const target = byId('graph-connection-memory-list'); - target.replaceChildren(); - if (!memories.length) { - const placeholder = empty(message); - placeholder.setAttribute('role', 'listitem'); - target.append(placeholder); - return; - } - memories.forEach(memory => { - const card = graphMemoryEvidenceCard(memory); - card.setAttribute('role', 'listitem'); - target.append(card); - }); - } - - function isGraphMemoryNode(item) { - const kind = String(item.nodeKind || '').toLowerCase(); - const type = String(item.etype || '').toLowerCase(); - return kind === 'memory' || type === 'memory' || type.startsWith('memory_'); - } - - function graphConnectionEntries(item) { - const graph = state.graphEngine && state.graphEngine.exportData - ? state.graphEngine.exportData() : state.graphData; - if (!graph) return []; - const nodes = new Map(graph.nodes.map(candidate => [candidate.id, candidate])); - const connections = new Map(); - graph.links.forEach(link => { - const source = link.source; - const target = link.target; - if (source !== item.id && target !== item.id) return; - const otherId = source === item.id ? target : source; - const other = nodes.get(otherId); - if (!other || other.id === item.id) return; - const entry = connections.get(other.id) || { - item: other, relations: new Set(), includeHistory: false, - }; - if (link.label) entry.relations.add(link.label); - entry.includeHistory = entry.includeHistory || link.ghost === true; - connections.set(other.id, entry); - }); - return [...connections.values()].sort((left, right) => { - const degree = number(right.item.degree) - number(left.item.degree); - return degree || left.item.name.localeCompare(right.item.name); - }); - } - - async function showGraphConnectionMemories(item, includeHistory = false) { - if (!item || item.id === undefined || item.id === null || !state.workspace) return; - cancelGraphConnectionMemoryLoad(); - const request = ++state.graphConnectionsRequest; - const workspace = state.workspace; - const repo = (byId('graph-repo-filter').value || '').trim(); - const title = item.name || item.label || item.id; - const historicalMemberId = includeHistory && item.ghost && Array.isArray(item.member_ids) - ? item.member_ids.find(value => typeof value === 'string' && value) || '' - : ''; - const historyQuery = includeHistory - ? `&include_history=true${historicalMemberId ? `&member_id=${encodeURIComponent(historicalMemberId)}` : ''}` - : ''; - byId('graph-connection-memory-title').textContent = `Memories for ${title}`; - renderGraphConnectionMemories([], 'Loading memory evidence…'); - if (isGraphMemoryNode(item)) { - const known = state.memories.find(memory => memory.id === item.id); - if (request !== state.graphConnectionsRequest || workspace !== state.workspace) return; - renderGraphConnectionMemories( - [known || graphMemoryCard(item)], 'No memory details are available for this node.', - ); - return; - } - const controller = new AbortController(); - state.graphConnectionsController = controller; - const timeout = window.setTimeout(() => controller.abort(), GRAPH_CONNECTION_MEMORIES_TIMEOUT_MS); - try { - const detail = await api( - `/graph/entities/${encodeURIComponent(item.id)}/memories?${query(workspace)}${repo ? `&repo=${encodeURIComponent(repo)}` : ''}${graphAsOfQuery()}${historyQuery}`, - { signal: controller.signal }, - ); - if (request !== state.graphConnectionsRequest || workspace !== state.workspace) return; - const evidence = detail.evidence || []; - const total = number(detail.totals && detail.totals.evidence) || evidence.length; - byId('graph-connection-memory-title').textContent = `${total} ${total === 1 ? 'memory' : 'memories'} for ${title}`; - renderGraphConnectionMemories( - evidence.map(graphMemoryCard), - 'No active memories support this connected node.', - ); - } catch (error) { - if (request !== state.graphConnectionsRequest || workspace !== state.workspace) return; - byId('graph-connection-memory-title').textContent = `Memories for ${title}`; - renderGraphConnectionMemories([], error && error.name === 'AbortError' - ? 'Memory evidence loading timed out. Choose this node again to retry.' - : `Could not load memory evidence: ${error.message}`); - } finally { - window.clearTimeout(timeout); - if (state.graphConnectionsController === controller) state.graphConnectionsController = null; - } - } - - function graphConnectionRow(entry) { - const item = entry.item; - const row = node('article', 'graph-connection-row'); - row.setAttribute('role', 'listitem'); - const details = node('div'); - const relations = [...entry.relations]; - const relationLabel = relations.length ? ` · ${relations.join(', ')}` : ''; - details.append( - node('h3', '', item.name), - node('p', '', `${number(item.degree)} connections · ${item.etype}${relationLabel}`), - ); - const actions = node('div', 'graph-connection-actions'); - actions.append( - button('Focus graph', 'secondary-button', () => { - closeGraphConnections(); - revealGraphNode(item.id, item.name); - }), - button('Memories', 'secondary-button', () => ( - showGraphConnectionMemories(item, entry.includeHistory) - )), - ); - row.append(details, actions); - return row; - } - - function openGraphConnections(item) { - if (!item || item.id === undefined || item.id === null) return; - cancelGraphConnectionMemoryLoad(); - state.graphConnectionsFocusId = String(item.id); - state.graphConnectionsFocusLabel = item.name || item.label || item.id; - const focusButton = byId('graph-connections-focus'); - if (focusButton) focusButton.hidden = !state.graphEngine; - const dialog = byId('graph-connections-dialog'); - const entries = graphConnectionEntries(item); - const title = item.name || item.label || item.id; - byId('graph-connections-title').textContent = `Connected to ${title}`; - byId('graph-connections-meta').textContent = `${entries.length} direct ${entries.length === 1 ? 'connection' : 'connections'} visible in this graph view`; - const target = byId('graph-connections-list'); - target.replaceChildren(); - if (!entries.length) target.append(empty('No connected nodes are visible in this graph view.')); - else entries.forEach(entry => target.append(graphConnectionRow(entry))); - byId('graph-connection-memory-title').textContent = 'Memories'; - renderGraphConnectionMemories([], 'Choose a connected node to inspect its memory evidence.'); - if (!dialog.open) dialog.showModal(); - } - - function updateGraphFacts(data) { - const stats = byId('graph-stats'); - stats.replaceChildren(); - const degrees = data.nodes.map(item => number(item.degree)).sort((a, b) => a - b); - const values = [ - ['Entities', data.nodes.length], - ['Relations', data.links.length], - ['Unlinked', data.nodes.filter(item => !number(item.degree)).length], - ['Median links', degrees.length ? degrees[Math.floor(degrees.length / 2)] : 0], - ]; - values.forEach(([label, value]) => { - const item = node('div', 'stat-item'); - item.append(node('span', '', label), node('strong', '', number(value).toLocaleString())); - stats.append(item); - }); - const top = byId('graph-top'); - top.replaceChildren(); - [...data.nodes].sort((a, b) => number(b.degree) - number(a.degree)).slice(0, 7).forEach(item => { - const control = node('button', 'compact-row'); - control.type = 'button'; - control.append(node('strong', '', item.name), node('span', '', `${number(item.degree)} connections · ${item.etype}`)); - control.addEventListener('click', () => openGraphConnections(item)); - top.append(control); - }); - } - - function updateGraphModeControls() { - const full = state.graphMode === 'full'; - const repoFilter = byId('graph-repo-filter'); - const repoLabel = document.querySelector('label[for="graph-repo-filter"]'); - if (repoFilter) { - repoFilter.placeholder = full - ? 'Filter by exact repository name…' - : 'Filter to a repository or topic…'; - repoFilter.title = full - ? 'All Nodes accepts an exact repository name from this workspace.' - : ''; - } - if (repoLabel) repoLabel.textContent = full - ? 'Filter by exact repository name' - : 'Filter to a repository or topic'; - ['graph-min-degree', 'graph-tune-min-degree', 'graph-collapse', 'graph-depth', - 'graph-show-unlinked', 'graph-flow', 'graph-flow-speed'].forEach(id => { - const control = byId(id); - if (control) { control.disabled = false; control.title = ''; } - }); - all('[data-graph-layer="code"]').forEach(control => { - control.disabled = false; - control.title = full - ? 'Choose an exact repository first, then add its code overlay within the All Nodes capacity.' - : ''; - }); - /* Focus-depth and auto-collapse are not implemented in the Every-node engine yet. - Disable them honestly instead of leaving controls that silently do nothing. */ - if (full && byId('graph-preset').value === 'every') { - ['graph-collapse', 'graph-depth'].forEach(id => { - const control = byId(id); - if (!control) return; - control.disabled = true; - control.title = 'Not yet available in the Every-node view.'; - }); - } - const lodNote = byId('graph-lod-note'); - if (lodNote) lodNote.hidden = !full; - byId('graph-reheat').textContent = full ? 'Reflow layout' : 'Reheat layout'; - const freezeRow = byId('graph-freeze-row'); - if (freezeRow) freezeRow.hidden = full; - const orbitPause = byId('graph-orbit-pause-row'); - const galaxy = graphIsGalaxy(); - const orbitCapable = galaxy && (!full || state.graphGalaxyQuality); - if (orbitPause) orbitPause.hidden = !orbitCapable; - const style = byId('graph-style').value; - const styleNotes = full ? GRAPH_LOD_STYLE_NOTES : GRAPH_STYLE_NOTES; - byId('graph-style-note').textContent = styleNotes[style] || styleNotes.classic; - updateGraphGalaxyControls(); - const preset = GRAPH_PRESET_LABELS[byId('graph-preset').value] || 'Galaxy gravity'; - byId('graph-mode').textContent = `${full ? 'All nodes · LOD' : 'High quality'} · ${preset}`; - } - - function graphIsGalaxy() { - return byId('graph-preset').value === 'galaxy'; - } - - function graphOverlayEnabled(galaxyQuality = state.graphGalaxyQuality) { - // Every can use the authored Galaxy renderer even though its toolbar preset is 'every'. - return graphIsGalaxy() || (galaxyQuality && byId('graph-preset').value === 'every'); - } - - function graphSizeBy() { - return graphIsGalaxy() && state.graphMode !== 'full' - ? 'evidence_mass' : byId('graph-size').value; - } - - function updateGraphGalaxyControls() { - const galaxy = graphIsGalaxy(); - const full = state.graphMode === 'full'; - const orbitCapable = galaxy && (!full || state.graphGalaxyQuality); - const size = byId('graph-size'); - if (galaxy && !full) { - if (['degree', 'betweenness'].includes(size.value)) size.dataset.legacyValue = size.value; - size.value = 'evidence_mass'; - size.disabled = true; - size.title = 'Galaxy gravity sizes stars by evidence mass.'; - } else { - size.disabled = false; - size.title = ''; - if (size.value === 'evidence_mass') size.value = size.dataset.legacyValue || 'degree'; - } - const labels = full - ? ['Repel force', 'Link distance', 'Centre gravity'] - : galaxy - ? ['Orbital speed', 'Link distance · tight ↔ loose', 'Galactic gravity · loose ↔ tight'] - : ['Repel force', 'Link distance', 'Centre gravity']; - ['graph-repel-label', 'graph-link-label', 'graph-gravity-label'].forEach((id, index) => { - const label = byId(id); - if (label) label.textContent = labels[index]; - }); - /* Keep the shared spacetime controls available for every renderer. Galaxy-specific rows - are gated below, while the normalized force controls remain useful to compact and - community layouts as well. */ - byId('graph-spacetime-tuning').hidden = false; - const forceLabels = full - ? ['Core attraction', 'Core mass', 'Cluster cohesion', 'Settling resistance', 'Link spring'] - : ['Galactic gravity', 'Black hole mass', 'Local solar gravity', 'Space friction', 'Spring stiffness']; - ['graph-gravitational-constant-label', 'graph-black-hole-mass-label', - 'graph-local-gravitational-constant-label', 'graph-space-damping-label', -'graph-spring-stiffness-label'].forEach((id, index) => { - const label = byId(id); - if (label) label.textContent = forceLabels[index]; - }); - const springLabel = byId('graph-spring-stiffness-label'); - const springCapable = galaxy || (full && !state.graphGalaxyQuality); - if (springLabel && springLabel.parentElement) { - springLabel.parentElement.hidden = !springCapable; - } - const galaxyRenderer = galaxy && (!full || state.graphGalaxyQuality); - const everyRenderer = full && !state.graphGalaxyQuality; - byId('graph-spacetime-summary').textContent = galaxyRenderer - ? 'Spacetime · black-hole orbit controls' - : everyRenderer ? 'All-node force refinement' : 'Responsive force controls'; - byId('graph-spacetime-note').textContent = galaxyRenderer - ? 'Drag and release a node to slingshot it into a new orbit.' - : everyRenderer - ? 'These values refine the settled worker layout.' - : 'These values tune the responsive force layout.'; - byId('graph-orbits-pause-label').textContent = 'Pause orbits'; - byId('graph-orbits-pause-detail').textContent = 'physics'; - byId('graph-orbits-pause').setAttribute('aria-label', 'Pause orbital physics'); - const orbitPauseRow = byId('graph-orbit-pause-row'); - if (orbitPauseRow) orbitPauseRow.hidden = !orbitCapable; - } - - function setChoicePressed(selector, dataKey, selected) { - all(selector).forEach(control => { - const active = control.dataset[dataKey] === selected; - control.classList.toggle('active', active); - control.setAttribute('aria-pressed', String(active)); - }); - } - - function syncGraphChoices() { - const preset = byId('graph-preset').value; - const style = byId('graph-style').value; - const color = byId('graph-color').value; - const palette = byId('graph-palette').value; - setChoicePressed('[data-graph-preset-choice]', 'graphPresetChoice', preset); - setChoicePressed('[data-graph-style-choice]', 'graphStyleChoice', style); - setChoicePressed('[data-graph-color-choice]', 'graphColorChoice', color); - setChoicePressed('[data-graph-palette-choice]', 'graphPaletteChoice', palette); - const styleNotes = state.graphMode === 'full' ? GRAPH_LOD_STYLE_NOTES : GRAPH_STYLE_NOTES; - byId('graph-style-note').textContent = styleNotes[style] || styleNotes.classic; - updateGraphGalaxyControls(); - syncGraphSavedViews(); - } - - function setGraphSwitch(id, on) { - const control = byId(id); - control.classList.toggle('on', on); - control.setAttribute('aria-checked', String(on)); - } - - function graphValueInRange(id, value, fallback) { - const control = byId(id); - const raw = Number(value); - const safe = Number.isFinite(raw) ? raw : fallback; - if (!control) return safe; - const min = Number(control.min); - const max = Number(control.max); - return Math.min(Number.isFinite(max) ? max : safe, Math.max(Number.isFinite(min) ? min : safe, safe)); - } - /* Controls keep their human-readable ranges and defaults. The engine value is computed by - a clamped identity 1:1 mapping: the slider's raw position flows straight to the engine, - bounded by the HTML min/max. The earlier 2x response saturated against the HTML bounds - for slider values near the loose and tight ends, producing visible plateaus where the - user dragged the slider but the engine value didn't change. Identity 1:1 with - [min, max] clamping gives every integer tick in the slider's full HTML range a - strictly distinct engine value. */ - function graphSliderResponseBaseline(item) { - if (!item) return 0; - if (item.id === 'graph-flow-speed') return 45; - const preset = byId('graph-preset'); - const tuning = preset ? graphPresetTuning(preset.value) : null; - const candidate = tuning && tuning[item.key]; - return Number.isFinite(Number(candidate)) ? Number(candidate) : item.fallback; - } - function graphSliderResponseValue(id, value, baseline) { - const control = byId(id); - if (!control) return Number.isFinite(Number(value)) ? Number(value) : baseline; - /* Flow speed is a linear control: the 2x response centered at 45 would - clamp every visible value in the lower quarter of the slider to - 0, which the compat engine then treats as "stop" and the user - sees an inert slider end. Use the raw value for flow-speed and - keep the 2x response for the geometry controls (gravity, repel, - link, etc.) where the centered calibration is intentional. */ - if (id === 'graph-flow-speed') { - const rawFlow = graphValueInRange(id, value, baseline); - return Number.isFinite(Number(rawFlow)) ? Number(rawFlow) : baseline; - } - const raw = graphValueInRange(id, value, baseline); - const min = Number(control.min); - const max = Number(control.max); - return Math.min(Number.isFinite(max) ? max : raw, - Math.max(Number.isFinite(min) ? min : raw, raw)); - } - function graphSliderInputValue(id, value, baseline) { - const control = byId(id); - if (!control) return Number.isFinite(Number(value)) ? Number(value) : baseline; - const min = Number(control.min); - const max = Number(control.max); - const safe = graphValueInRange(id, value, baseline); - /* Identity inverse of the clamped identity response. */ - return Math.min(Number.isFinite(max) ? max : safe, - Math.max(Number.isFinite(min) ? min : safe, safe)); - } - - function graphScopeValue(id, value, fallback) { - const control = byId(id); - const raw = Number(value); - const safe = Number.isFinite(raw) ? raw : fallback; - if (!control) return Math.round(safe); - const min = Number(control.min); - const max = Number(control.max); - return Math.round(Math.min(Number.isFinite(max) ? max : safe, - Math.max(Number.isFinite(min) ? min : safe, safe))); - } - - - function graphTuningEngineSettings() { - return GRAPH_TUNING.reduce((settings, item) => { - const raw = number(byId(item.id).value); - settings[item.key] = graphSliderResponseValue( - item.id, raw, graphSliderResponseBaseline(item), - ); - return settings; - }, { - flowSpeed: graphSliderResponseValue( - 'graph-flow-speed', number(byId('graph-flow-speed').value), 45, - ), - }); - } - - function graphSpacetimeEngineSettings() { - const controls = GRAPH_SPACETIME_TUNING.reduce((settings, item) => { - const raw = number(byId(item.id).value); - settings[item.key] = graphSliderResponseValue(item.id, raw, item.fallback); - return settings; - }, {}); - return { - gravitationalConstant: controls.gravitationalConstant / 30, - blackHoleMass: graphBlackHoleMassMultiplier(controls.blackHoleMass), - localGravitationalConstant: controls.localGravitationalConstant / 30, - damping: controls.damping, - springStiffness: controls.springStiffness / 32, - orbitPaused: state.graphOrbitPaused, - }; - } - - function graphScopeEngine() { - return { - minDegree: graphScopeValue('graph-min-degree', byId('graph-min-degree').value, 1), - showUnlinked: state.graphShowUnlinked, - depth: graphScopeValue('graph-depth', byId('graph-depth').value, 2), - }; - } - - - let graphPreferencesSaveScheduled = false; - let graphPreferencesSaveCancel = null; - function scheduleGraphPreferencesSave() { - if (graphPreferencesSaveScheduled) return; - graphPreferencesSaveScheduled = true; - const flush = () => { - if (!graphPreferencesSaveScheduled) return; - graphPreferencesSaveScheduled = false; - graphPreferencesSaveCancel = null; - saveGraphPreferences(); - }; - if (typeof requestAnimationFrame === 'function') { - const frame = requestAnimationFrame(flush); - graphPreferencesSaveCancel = () => cancelAnimationFrame(frame); - } else { - const timer = setTimeout(flush, 0); - graphPreferencesSaveCancel = () => clearTimeout(timer); - } - } - - function flushGraphPreferencesSave() { - if (!graphPreferencesSaveScheduled) return; - graphPreferencesSaveScheduled = false; - const cancel = graphPreferencesSaveCancel; - graphPreferencesSaveCancel = null; - if (cancel) cancel(); - saveGraphPreferences(); - } - - function graphPresetTuning(preset) { - const available = window.EngraphisGraph && window.EngraphisGraph.PRESETS; - const source = (available && available[preset]) || GRAPH_PRESET_TUNING[preset] || GRAPH_PRESET_TUNING.communities; - return GRAPH_TUNING.reduce((settings, item) => { - settings[item.key] = source && Number.isFinite(Number(source[item.key])) - ? Number(source[item.key]) : item.fallback; - return settings; - }, {}); - } - - function setGraphTuningControl(item, value) { - const control = byId(item.id); - const next = graphValueInRange(item.id, value, item.fallback); - control.value = String(next); - const rendered = item.precision ? next.toFixed(item.precision) : String(Math.round(next)); - const output = byId(`${item.id}-output`); - output.value = rendered; - output.textContent = rendered; - return next; - } - - function graphTuningSettings() { - return GRAPH_TUNING.reduce((settings, item) => { - settings[item.key] = number(byId(item.id).value); - return settings; - }, { flowSpeed: number(byId('graph-flow-speed').value) }); - } - - function setGraphSpacetimeControl(item, value) { - const control = byId(item.id); - const next = graphValueInRange(item.id, value, item.fallback); - control.value = String(next); - const rendered = item.precision ? next.toFixed(item.precision) : String(Math.round(next)); - const output = byId(`${item.id}-output`); - output.value = rendered; - output.textContent = rendered; - return next; - } - - - const GRAPH_BLACK_HOLE_MASS_BASELINE = 96; - function graphBlackHoleMassMultiplier(controlValue) { - const value = number(controlValue); - /* Below the 96 baseline the multiplier scales linearly. Above it, every +10 slider units - adds exactly +0.10 to the compact central-mass multiplier: 96→1.0, 106→1.1, 116→1.2. - Local stellar wells remain owned exclusively by Local solar gravity. */ - return value <= GRAPH_BLACK_HOLE_MASS_BASELINE - ? Math.max(0, value / GRAPH_BLACK_HOLE_MASS_BASELINE) - : 1 + (value - GRAPH_BLACK_HOLE_MASS_BASELINE) / 100; - } - - - - function syncGraphSpacetimeTuning(settings) { - GRAPH_SPACETIME_TUNING.forEach(item => setGraphSpacetimeControl(item, - settings && settings[item.key])); - setGraphSwitch('graph-orbits-pause', settings && settings.orbitPaused === true); - } - - function syncGraphTuning(settings) { - GRAPH_TUNING.forEach(item => setGraphTuningControl(item, settings && settings[item.key])); - const flowSpeed = graphValueInRange('graph-flow-speed', settings && settings.flowSpeed, 45); - byId('graph-flow-speed').value = String(flowSpeed); - byId('graph-flow-speed-output').value = String(Math.round(flowSpeed)); - byId('graph-flow-speed-output').textContent = String(Math.round(flowSpeed)); - } - - function graphScope() { - return graphScopeEngine(); - } - - function applyGraphScope() { - if (state.graphEngine) state.graphEngine.setScope(graphScope()); - } - - function setGraphMinDegree(value, apply = true) { - const next = graphScopeValue('graph-min-degree', value, 1); - byId('graph-min-degree').value = String(next); - byId('graph-min-degree-output').value = String(Math.round(next)); - byId('graph-min-degree-output').textContent = String(Math.round(next)); - byId('graph-tune-min-degree').value = String(next); - byId('graph-tune-min-degree-output').value = String(Math.round(next)); - byId('graph-tune-min-degree-output').textContent = String(Math.round(next)); - if (apply) applyGraphScope(); - } - - function setGraphDepth(value, apply = true) { - const next = graphScopeValue('graph-depth', value, 2); - byId('graph-depth').value = String(next); - byId('graph-depth-output').value = String(Math.round(next)); - byId('graph-depth-output').textContent = String(Math.round(next)); - if (apply) applyGraphScope(); - } - - function setGraphShowUnlinked(on, apply = true) { - const next = on === true; - state.graphShowUnlinked = next; - const control = byId('graph-show-unlinked'); - control.textContent = 'Unlinked nodes'; - control.setAttribute('aria-pressed', String(next)); - control.title = next - ? 'Hide entities that have no relations in this graph view' - : 'Show entities that have no relations in this graph view'; - if (apply) applyGraphScope(); - } - - function graphLayerState() { - return all('[data-graph-layer]').reduce((layers, control) => { - layers[control.dataset.graphLayer] = control.getAttribute('aria-pressed') === 'true'; - return layers; - }, {}); - } - - function setGraphLayers(layers) { - const source = layers && typeof layers === 'object' ? layers : GRAPH_DEFAULT_LAYERS; - all('[data-graph-layer]').forEach(control => { - const active = source[control.dataset.graphLayer] !== false; - control.classList.toggle('active', active); - control.setAttribute('aria-pressed', String(active)); - }); - } - - function updateGraphLayerCounts(data, supplied) { - const counts = GRAPH_LAYERS.reduce((result, layer) => { result[layer] = 0; return result; }, {}); - if (Array.isArray(supplied)) supplied.forEach(item => { - if (item && GRAPH_LAYERS.includes(item.layer)) counts[item.layer] = number(item.count); - }); - else (data.links || []).forEach(link => { - if (GRAPH_LAYERS.includes(link.layer)) counts[link.layer] += 1; - }); - GRAPH_LAYERS.forEach(layer => { byId(`graph-layer-${layer}-count`).textContent = counts[layer].toLocaleString(); }); - } - - function syncGraphSavedViews() { - all('[data-graph-saved-view]').forEach(control => { - const active = control.dataset.graphSavedView === state.graphSavedView; - control.classList.toggle('active', active); - control.setAttribute('aria-pressed', String(active)); - }); - } - - function clearGraphSavedView() { - if (!state.graphSavedView) return; - state.graphSavedView = ''; - syncGraphSavedViews(); - } - - function graphPreference(name, fallback, allowed) { - try { - const saved = JSON.parse(localStorage.getItem(GRAPH_PREFERENCES_KEY) || '{}'); - const value = saved && typeof saved === 'object' ? saved[name] : undefined; - return allowed && !allowed.includes(value) ? fallback : value === undefined ? fallback : value; - } catch (_) { - return fallback; - } - } - - function graphPreferenceSnapshot() { - const layers = graphLayerState(); - return { - physicsVersion: GRAPH_PHYSICS_VERSION, - preset: byId('graph-preset').value, - style: byId('graph-style').value, - color: byId('graph-color').value, - palette: byId('graph-palette').value, - flow: byId('graph-flow').getAttribute('aria-checked') === 'true', - labels: byId('graph-labels').getAttribute('aria-checked') === 'true', - tuning: graphTuningSettings(), - /* Pause is a session action, like Freeze. Persist the numeric spacetime tuning without - silently reopening a future dashboard with every orbit stopped. */ - spacetimeTuning: GRAPH_SPACETIME_TUNING.reduce((settings, item) => { - settings[item.key] = number(byId(item.id).value); - return settings; - }, {}), - minDegree: number(byId('graph-min-degree').value), - depth: number(byId('graph-depth').value), - showUnlinked: state.graphShowUnlinked, - layers, - includeCode: state.graphIncludeCode, - savedView: state.graphSavedView, - bridges: byId('graph-bridges').checked, - collapse: byId('graph-collapse').checked, - asOf: byId('graph-as-of').value, - ghosts: byId('graph-ghosts').checked, - size: byId('graph-size').value, - repoFilter: byId('graph-repo-filter').value.slice(0, 200), - }; - } - - function saveGraphPreferences() { - try { - localStorage.setItem(GRAPH_PREFERENCES_KEY, JSON.stringify(graphPreferenceSnapshot())); - } catch (_) {} - } - - function restoreGraphPreferences() { - let hasSavedPreferences = false; - try { hasSavedPreferences = localStorage.getItem(GRAPH_PREFERENCES_KEY) !== null; } catch (_) {} - const preset = graphPreference('preset', byId('graph-preset').value, - ['original', 'compact', 'communities', 'radial', 'constellation', 'galaxy']); - const style = graphPreference('style', byId('graph-style').value, - ['classic', 'galaxy', 'solar', 'cyber']); - const color = graphPreference('color', byId('graph-color').value, - ['community', 'connections', 'type']); - const palette = graphPreference('palette', byId('graph-palette').value, - ['theme', 'aurora', 'ocean', 'ember', 'contrast', 'custom']); - byId('graph-preset').value = preset; - byId('graph-style').value = style; - byId('graph-color').value = color; - byId('graph-palette').value = palette; - - const savedTuning = graphPreference('tuning', {}); - const savedPhysicsVersion = Number(graphPreference('physicsVersion', 0)); - const sourcePhysicsVersion = Number.isFinite(savedPhysicsVersion) ? savedPhysicsVersion : 0; - const needsPhysicsMigration = version => hasSavedPreferences - && sourcePhysicsVersion < version; - const legacyPhysics = needsPhysicsMigration(GRAPH_PHYSICS_VERSION); - const needsPhysicsV3Migration = needsPhysicsMigration(3); - const needsPhysicsV4Migration = needsPhysicsMigration(4); - const effectiveTuning = savedTuning && typeof savedTuning === 'object' - ? { ...savedTuning } : {}; - const savedSpacetimeTuning = graphPreference('spacetimeTuning', {}); - /* A failed physics-control experiment could persist every attractive force at its maximum, - friction at zero, and the Galaxy spacing control at 400. That exact vector is not a - useful custom preset: it collapses the visible graph and can reduce hundreds of loaded - entities to a small central knot. Physics v3 resets only this known-bad snapshot. */ - const staleMaxedPhysics = needsPhysicsV3Migration && Number(effectiveTuning.gravity) === 400 - && Number(savedSpacetimeTuning && savedSpacetimeTuning.gravitationalConstant) === 200 - && Number(savedSpacetimeTuning && savedSpacetimeTuning.blackHoleMass) === 500 - && Number(savedSpacetimeTuning && savedSpacetimeTuning.localGravitationalConstant) === 200 - && Number(savedSpacetimeTuning && savedSpacetimeTuning.damping) === 0 - && Number(savedSpacetimeTuning && savedSpacetimeTuning.springStiffness) === 100; - if (staleMaxedPhysics) { - delete effectiveTuning.repel; - delete effectiveTuning.link; - delete effectiveTuning.gravity; - } - /* Older preferences persisted 48 and then 60 as Galaxy's default orbital speed. Physics v4 - defines the control as a percentage with 100 as neutral, so migrate only those exact - retired defaults. Every other custom speed and every unrelated preference remains intact. */ - if (needsPhysicsV4Migration && preset === 'galaxy' - && [48, 60].includes(Number(effectiveTuning.repel))) { - effectiveTuning.repel = 100; - } - /* Physics v5 made 120 the Galaxy gravity default. Migrate only the exact retired default; - a saved 96 in an already-versioned v5 snapshot remains an intentional user choice. */ - if (needsPhysicsMigration(5) && preset === 'galaxy' && Number(effectiveTuning.gravity) === 96) { - effectiveTuning.gravity = 120; - } - /* Physics v6 recalibrates Galaxy gravity to 72 and the spacetime panel to 60/96/60. Move - only the exact retired default vector, so a value the person actually chose survives. */ - if (needsPhysicsMigration(6)) { - if (preset === 'galaxy' && Number(effectiveTuning.gravity) === 120) { - effectiveTuning.gravity = 72; - } - const retiredSpacetimeDefaults = { - gravitationalConstant: 100, blackHoleMass: 160, localGravitationalConstant: 100, - }; - const savedSpacetime = savedSpacetimeTuning && typeof savedSpacetimeTuning === 'object' - ? savedSpacetimeTuning : {}; - const retiredKeys = Object.keys(retiredSpacetimeDefaults); - const hasRetiredVector = retiredKeys.every(key => - Number(savedSpacetime[key]) === retiredSpacetimeDefaults[key]); - if (hasRetiredVector) { - retiredKeys.forEach(key => { - savedSpacetime[key] = GRAPH_SPACETIME_TUNING.find(item => item.key === key).fallback; - }); - } - } - syncGraphTuning({ - ...graphPresetTuning(preset), - ...effectiveTuning, - }); - /* Pause orbits is deliberately session-only. Old snapshots may contain orbitPaused=true; - ignore it so a fresh dashboard always starts with live galactic motion. */ - state.graphOrbitPaused = false; - syncGraphSpacetimeTuning({ - ...(!staleMaxedPhysics && savedSpacetimeTuning - && typeof savedSpacetimeTuning === 'object' - ? savedSpacetimeTuning : {}), - orbitPaused: false, - }); - - const savedMin = Number(graphPreference('minDegree', number(byId('graph-min-degree').value))); - const minDegree = Number.isFinite(savedMin) ? Math.max(0, Math.min(12, Math.round(savedMin))) : 1; - setGraphMinDegree(minDegree); - setGraphDepth(graphPreference('depth', 2)); - const savedRepo = graphPreference('repoFilter', ''); - byId('graph-repo-filter').value = typeof savedRepo === 'string' ? savedRepo.slice(0, 200) : ''; - const savedAsOf = graphPreference('asOf', ''); - byId('graph-as-of').value = typeof savedAsOf === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(savedAsOf) - ? savedAsOf : ''; - setGraphShowUnlinked(staleMaxedPhysics - || graphPreference('showUnlinked', state.graphShowUnlinked) === true); - byId('graph-bridges').checked = graphPreference('bridges', byId('graph-bridges').checked) === true; - byId('graph-collapse').checked = graphPreference('collapse', byId('graph-collapse').checked) === true; - byId('graph-ghosts').checked = graphPreference('ghosts', byId('graph-ghosts').checked) !== false; - byId('graph-size').value = graphPreference('size', byId('graph-size').value, - ['degree', 'betweenness', 'evidence_mass']); - // Freeze is deliberately session-only. A previously frozen arrangement must not make a - // freshly opened graph look broken; physics starts live until the person clicks Freeze. - state.graphFrozen = false; - setGraphSwitch('graph-freeze', state.graphFrozen); - setGraphSwitch('graph-flow', graphPreference('flow', true) !== false); - setGraphSwitch('graph-labels', graphPreference('labels', false) === true); - const savedLayers = graphPreference('layers', GRAPH_DEFAULT_LAYERS); - setGraphLayers(GRAPH_LAYERS.reduce((layers, layer) => { - layers[layer] = !savedLayers || typeof savedLayers !== 'object' || savedLayers[layer] !== false; - return layers; - }, {})); - state.graphIncludeCode = graphPreference('includeCode', false) === true; - state.graphSavedView = graphPreference('savedView', '', ['', ...Object.keys(GRAPH_SAVED_VIEWS)]); - syncGraphSavedViews(); - if (legacyPhysics) saveGraphPreferences(); - } - - function savedGraphView(id) { - if (id === 'custom') { - try { - const custom = JSON.parse(localStorage.getItem(GRAPH_CUSTOM_VIEW_KEY) || 'null'); - return custom && typeof custom === 'object' ? custom : null; - } catch (_) { - return null; - } - } - return GRAPH_SAVED_VIEWS[id] || null; - } - - function applyGraphView(id) { - const view = savedGraphView(id); - if (!view) { - showNotice(id === 'custom' ? 'No locally saved graph view yet.' : 'That saved graph view is unavailable.'); - return; - } - const preset = Object.prototype.hasOwnProperty.call(GRAPH_PRESET_LABELS, view.preset) - ? view.preset : byId('graph-preset').value; - const style = ['classic', 'galaxy', 'solar', 'cyber'].includes(view.style) ? view.style : byId('graph-style').value; - const color = ['community', 'connections', 'type'].includes(view.color) ? view.color : byId('graph-color').value; - const palette = ['theme', 'aurora', 'ocean', 'ember', 'contrast', 'custom'].includes(view.palette) - ? view.palette : byId('graph-palette').value; - const previousIncludeCode = state.graphIncludeCode; - const previousShowUnlinked = state.graphShowUnlinked; - const previousAsOf = byId('graph-as-of').value; - const previousRepo = (byId('graph-repo-filter').value || '').trim(); - const asOf = typeof view.asOf === 'string' ? view.asOf : previousAsOf; - const repoFilter = typeof view.repoFilter === 'string' - ? view.repoFilter.slice(0, 200) : byId('graph-repo-filter').value; - const nextRepo = repoFilter.trim(); - state.graphIncludeCode = typeof view.includeCode === 'boolean' - ? view.includeCode : state.graphIncludeCode; - byId('graph-preset').value = preset; - byId('graph-style').value = style; - byId('graph-color').value = color; - byId('graph-palette').value = palette; - byId('graph-as-of').value = asOf; - byId('graph-repo-filter').value = repoFilter; - if (typeof view.ghosts === 'boolean') byId('graph-ghosts').checked = view.ghosts; - if (['degree', 'betweenness', 'evidence_mass'].includes(view.size)) byId('graph-size').value = view.size; - if (typeof view.bridges === 'boolean') byId('graph-bridges').checked = view.bridges; - if (typeof view.collapse === 'boolean') byId('graph-collapse').checked = view.collapse; - if (typeof view.flow === 'boolean') setGraphSwitch('graph-flow', view.flow); - if (typeof view.labels === 'boolean') setGraphSwitch('graph-labels', view.labels); - setGraphSwitch('graph-freeze', state.graphFrozen); - syncGraphTuning({ - ...graphPresetTuning(preset), - ...(view.tuning && typeof view.tuning === 'object' ? view.tuning : {}), - }); - syncGraphSpacetimeTuning( - view.spacetimeTuning && typeof view.spacetimeTuning === 'object' - ? view.spacetimeTuning : {} - ); - setGraphMinDegree(view.minDegree == null ? 1 : view.minDegree, false); - setGraphDepth(view.depth == null ? 2 : view.depth, false); - setGraphShowUnlinked(view.showUnlinked === true, false); - setGraphLayers(view.layers); - state.graphSavedView = id === 'custom' ? '' : id; - syncGraphChoices(); - if (state.graphEngine) { - state.graphEngine.apply(graph => { - graph.setPreset(preset); - graph.setStyle(style); - graph.setColorBy(color); - applyGraphPalette(palette); - graph.setSettings({ - ...graphTuningEngineSettings(), - ...graphSpacetimeEngineSettings(), - flow: byId('graph-flow').getAttribute('aria-checked') === 'true', - labels: byId('graph-labels').getAttribute('aria-checked') === 'true', - frozen: state.graphFrozen, - }); - graph.setScope(graphScope()); - graph.setLayers(graphLayerState()); - graph.setRepoFilter(repoFilter); - graph.setAsOf(graphAsOfTimestamp()); - graph.setSizeBy(graphSizeBy()); - graph.setBridges(byId('graph-bridges').checked); - graph.setCollapse(byId('graph-collapse').checked ? 'auto' : false); - graph.setGhosts(byId('graph-ghosts').checked); - }, false, !state.graphFrozen); - state.graphEngine.freeze(state.graphFrozen); - } - graphLifecycle.sync(); - saveGraphPreferences(); - if (previousIncludeCode !== state.graphIncludeCode - || previousShowUnlinked !== state.graphShowUnlinked || previousAsOf !== asOf - || previousRepo !== nextRepo) { - loadGraph({ force: true }); - } - const label = all('[data-graph-saved-view]').find(control => control.dataset.graphSavedView === id); - showNotice(`${id === 'custom' ? 'Saved' : (label ? label.textContent : 'Saved')} graph view applied.`); - } - - function saveCurrentGraphView() { - try { - localStorage.setItem(GRAPH_CUSTOM_VIEW_KEY, JSON.stringify(graphPreferenceSnapshot())); - byId('graph-saved-view-status').textContent = 'Current graph view saved locally.'; - showNotice('Current graph view saved locally.'); - } catch (_) { - showNotice('Could not save this graph view in local storage.'); - } - } - - function resetGraphTuning() { - const preset = byId('graph-preset').value; - const previousIncludeCode = state.graphIncludeCode; - const previousShowUnlinked = state.graphShowUnlinked; - state.graphIncludeCode = false; - syncGraphTuning({ ...graphPresetTuning(preset), flowSpeed: 45 }); - state.graphOrbitPaused = false; - syncGraphSpacetimeTuning({}); - setGraphMinDegree(1, false); - setGraphDepth(2, false); - setGraphShowUnlinked(true, false); - setGraphLayers(GRAPH_DEFAULT_LAYERS); - clearGraphSavedView(); - if (state.graphEngine) { - state.graphEngine.apply(graph => { - graph.setPreset(preset); - graph.setSettings({ - ...graphTuningEngineSettings(), - ...graphSpacetimeEngineSettings(), - frozen: state.graphFrozen, - }); - graph.setScope(graphScope()); - graph.setLayers(graphLayerState()); - }, false, !state.graphFrozen); - state.graphEngine.freeze(state.graphFrozen); - } - saveGraphPreferences(); - if (previousIncludeCode || previousShowUnlinked) loadGraph({ force: true }); - showNotice('Graph tuning reset to the selected layout defaults.'); - } - - function applyGraphPalette(name) { - const graph = state.graphEngine; - if (!graph) return; - graph.setPalette(name); - if (name === 'custom') graph.setTypeColors(GRAPH_CUSTOM_PALETTE); - } - - function graphThemeColors() { - const css = getComputedStyle(document.body); - return { - accent: css.getPropertyValue('--c-acc').trim() || '#a39bf1', - surface: css.getPropertyValue('--c-surface').trim() || '#16191f', - canvas: css.getPropertyValue('--c-bg').trim() || '#0e1014', - label: css.getPropertyValue('--c-fg').trim() || '#e7e9ee', - relation_label: css.getPropertyValue('--c-dim').trim() || '#929baa', - }; - } - - function setGraphTab(tab) { - all('[data-graph-tab]').forEach(control => { - const active = control.dataset.graphTab === tab; - control.classList.toggle('active', active); - control.setAttribute('aria-selected', String(active)); - control.tabIndex = active ? 0 : -1; - }); - all('[data-graph-tab-panel]').forEach(panel => { - panel.hidden = panel.dataset.graphTabPanel !== tab; - }); - } - - function downloadGraphFile(blob, name) { - const href = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = href; - link.download = name; - document.body.append(link); - link.click(); - link.remove(); - window.setTimeout(() => URL.revokeObjectURL(href), 0); - } - - function setGraphExportMenuOpen(open, restoreTriggerFocus = false) { - const trigger = byId('graph-export'); - const menu = byId('graph-export-menu'); - if (open) { - menu.hidden = false; - trigger.setAttribute('aria-expanded', 'true'); - byId('graph-export-png').focus(); - return; - } - trigger.setAttribute('aria-expanded', 'false'); - if (restoreTriggerFocus || menu.contains(document.activeElement)) trigger.focus(); - menu.hidden = true; - } - - function exportGraphJson() { - const graph = state.graphEngine && state.graphEngine.exportData - ? state.graphEngine.exportData() - : state.graphData || { nodes: [], links: [] }; - const payload = { - workspace: state.workspace, - exported_at: new Date().toISOString(), - nodes: graph.nodes, - links: graph.links, - }; - // Pretty-print normal exports for readability. An All Nodes payload stays compact - // to avoid the indentation expansion and extra main-thread work at the release limit. - const indentation = state.graphMode === 'full' ? undefined : 2; - downloadGraphFile(new Blob([JSON.stringify(payload, null, indentation)], { type: 'application/json' }), 'engraphis-graph.json'); - showNotice('Graph data exported as JSON.'); - } - - function exportGraphPng() { - const canvas = state.graphEngine && typeof state.graphEngine.exportImageCanvas === 'function' - ? state.graphEngine.exportImageCanvas() - : byId('graph-canvas').querySelector('canvas'); - if (!canvas || !canvas.toBlob) { - showNotice('The graph image is not ready yet. Export JSON data instead.'); - return; - } - canvas.toBlob(blob => { - if (!blob) { - showNotice('Could not capture the graph image. Export JSON data instead.'); - return; - } - downloadGraphFile(blob, 'engraphis-graph.png'); - showNotice('Graph image exported as PNG.'); - }, 'image/png'); - } - - function graphCountText(nodes, links, drawnLinks = null, visibleNodes = null) { - const available = number(state.graphMeta && state.graphMeta.nodes_available) || nodes; - const prefix = state.graphMode === 'full' ? 'All nodes · LOD' : 'High quality'; - const entityText = visibleNodes != null && number(visibleNodes) < number(nodes) - ? `${number(visibleNodes).toLocaleString()} visible of ${number(nodes).toLocaleString()} entities` - : available > nodes - ? `${number(nodes).toLocaleString()} of ${available.toLocaleString()} entities` - : `${number(nodes).toLocaleString()} entities`; - const totalRelations = state.graphMeta && (state.graphMeta.relations_available != null - ? state.graphMeta.relations_available : state.graphMeta.total_edges); - const hiddenRelations = drawnLinks == null - ? (totalRelations == null ? null : Math.max(0, number(totalRelations) - number(links))) - : Math.max(0, number(links) - number(drawnLinks)); - const hidden = state.graphMode === 'full' && hiddenRelations != null - ? ` · ${hiddenRelations.toLocaleString()} hidden relationships` - : ''; - return `${prefix} · ${entityText} · ${number(links).toLocaleString()} relations${hidden}`; - } - - function graphStatsChanged(stats) { - if (!stats) return; - const nodes = stats.nodes == null ? state.graphData.nodes.length : stats.nodes; - const links = stats.links == null ? state.graphData.links.length : stats.links; - byId('graph-count').textContent = graphCountText( - nodes, links, stats.drawnLinks, stats.visibleNodes, - ); - if (state.graphMode === 'full') { - const note = byId('graph-lod-note'); - const detail = note && note.querySelector('span'); - if (detail) detail.textContent = stats.layoutPending - ? 'Reflowing the complete graph in the background…' - : stats.collapsed - ? 'Clusters are condensed into representative nodes. Zoom in to expand them.' - : 'Layout, forces, scope, colour and relation flow update without reloading the complete graph.'; - } - } - - function graphMetricsChanged(metrics) { - state.graphMetrics = metrics || {}; - byId('graph-bridge-count').textContent = metrics && metrics.bridges != null - ? `${metrics.bridges} bridge ${metrics.bridges === 1 ? 'edge' : 'edges'}` - : ''; - } - - function graphAsOfTimestamp() { - const value = byId('graph-as-of').value; - if (!value) return null; - // A date picker represents the complete selected day, not midnight at its start. - const timestamp = Date.parse(`${value}T23:59:59.999Z`); - return Number.isFinite(timestamp) ? timestamp : null; - } - - function graphAsOfQuery() { - const timestamp = graphAsOfTimestamp(); - return timestamp === null ? '' : `&as_of=${encodeURIComponent(timestamp / 1000)}`; - } - - function graphLoadKey(workspace, mode, includeCode, showUnlinked, asOf, repo) { - return JSON.stringify([workspace, mode, includeCode, showUnlinked, asOf, repo || '']); - } - - function graphRepositoryNames() { - const names = new Set(); - const add = value => { - const name = text(value).trim(); - if (name) names.add(name); - }; - if (state.graphData && Array.isArray(state.graphData.repositories)) { - state.graphData.repositories.forEach(add); - } - const workspace = state.workspaces.find(item => workspaceName(item) === state.workspace); - if (workspace && Array.isArray(workspace.repos)) workspace.repos.forEach(add); - if (state.graphData && Array.isArray(state.graphData.nodes)) { - state.graphData.nodes.forEach(item => { - if (item && Array.isArray(item.repo_names)) item.repo_names.forEach(add); - }); - } - return names; - } - - function validatedGraphRepository(value) { - const candidate = text(value).trim().toLowerCase(); - if (!candidate) return ''; - for (const name of graphRepositoryNames()) { - if (name.toLowerCase() === candidate) return name; - } - return ''; - } - - function cancelGraphRepositoryReload() { - if (graphRepoLoadTimer === null) return; - window.clearTimeout(graphRepoLoadTimer); - graphRepoLoadTimer = null; - } - - function scheduleGraphRepositoryReload() { - cancelGraphRepositoryReload(); - graphRepoLoadTimer = window.setTimeout(() => { - graphRepoLoadTimer = null; - if (state.view === 'relations' - && (state.graphIncludeCode || state.graphMode === 'full')) { - loadGraph({ force: true }); - } - }, 250); - } - - function isCurrentGraphLoad(request) { - return Boolean(request - && request.id === state.graphLoadRequest - && request.key === state.graphLoadKey - && request.workspace === state.workspace - && request.mode === state.graphMode - && request.includeCode === state.graphIncludeCode - && request.showUnlinked === state.graphShowUnlinked - && request.asOf === graphAsOfTimestamp() - && request.repo === (byId('graph-repo-filter').value || '').trim()); - } - - function setGraphLoadControlsBusy(busy, disableRetry = true) { - const controls = disableRetry ? ['graph-retry'] : []; - controls.forEach(id => { - const control = byId(id); - if (control) control.disabled = busy; - }); - const every = document.querySelector('[data-graph-preset-choice="every"]'); - if (every) every.disabled = busy; - const retry = byId('graph-retry'); - if (retry && ((busy && disableRetry) || !busy)) { - retry.textContent = busy ? 'Reloading graph…' : 'Reload data'; - } - } - - function retryGraphLoad() { - // A Retry click starts a new request rather than inheriting a timed-out promise. Keep its - // pending state local to the button so rapid clicks cannot repeatedly cancel fresh work. - if (state.graphRetryPending) return; - state.graphRetryPending = true; - Promise.resolve(loadGraph({ force: true })).finally(() => { - state.graphRetryPending = false; - }); - } - - async function loadGraph({ force = false } = {}) { - if (!state.workspace) return; - const currentRepo = (byId('graph-repo-filter').value || '').trim(); - if (!force && state.graphWorkspace === state.workspace - && state.graphDataMode === state.graphMode - && state.graphDataIncludeCode === state.graphIncludeCode - && state.graphDataShowUnlinked === state.graphShowUnlinked - && state.graphDataAsOf === graphAsOfTimestamp() - && state.graphDataRepo === currentRepo && state.graphData) { - if (state.graphEngine) state.graphEngine.resize(); - graphLifecycle.sync(); - return; - } - const targetWorkspace = state.workspace; - const targetMode = state.graphMode; - const targetIncludeCode = state.graphIncludeCode; - const targetShowUnlinked = state.graphShowUnlinked; - const targetAsOf = graphAsOfTimestamp(); - const targetRepo = currentRepo; - const fullGraph = targetMode === 'full'; - const key = graphLoadKey( - targetWorkspace, targetMode, targetIncludeCode, targetShowUnlinked, targetAsOf, targetRepo, - ); - if (!force && state.graphLoadPromise && state.graphLoadKey === key) { - return state.graphLoadPromise; - } - const request = { - id: state.graphLoadRequest + 1, - key, - workspace: targetWorkspace, - mode: targetMode, - includeCode: targetIncludeCode, - showUnlinked: targetShowUnlinked, - asOf: targetAsOf, - repo: targetRepo, - }; - const controller = new AbortController(); - const previousController = state.graphLoadController; - // Publish the new identity before cancelling the old request. Its timeout/error handler - // then becomes a no-op even when the next request has identical filters (a true retry). - state.graphLoadRequest = request.id; - state.graphLoadKey = key; - state.graphLoadWorkspace = targetWorkspace; - state.graphLoadMode = targetMode; - state.graphLoadIncludeCode = targetIncludeCode; - state.graphLoadShowUnlinked = targetShowUnlinked; - state.graphLoadAsOf = targetAsOf; - state.graphLoadRepo = targetRepo; - state.graphLoadController = controller; - if (previousController && !previousController.signal.aborted) previousController.abort(); - // The initial load remains retryable; once a user explicitly starts a replacement, lock - // the retry control until that transaction settles so repeated clicks cannot churn it. - setGraphLoadControlsBusy(true, force); - const oldEngine = state.graphEngine; - /* Loading is a transaction: freeze the committed renderer before fetching its replacement. - A failed request restores it; a successful request destroys it immediately before commit. */ - if (state.graphEngine && typeof state.graphEngine.freeze === 'function') { - state.graphEngine.freeze(true); - } - if (state.graphSpacetimeOverlay) state.graphSpacetimeOverlay.setEnabled(false); - const canvasEl = byId('graph-canvas'); - if (canvasEl) canvasEl.setAttribute('aria-busy', 'true'); - byId('graph-empty').hidden = false; - byId('graph-empty').textContent = fullGraph - ? 'Loading all nodes with progressive level of detail…' - : 'Loading the responsive evidence graph…'; - const task = (async () => { - const assets = ensureGraphAssets(fullGraph); - const deadline = fullGraph ? GRAPH_FULL_LOAD_TIMEOUT_MS : GRAPH_LOAD_TIMEOUT_MS; - let rejectTimeout; - const timeoutPromise = new Promise((_, reject) => { - rejectTimeout = reject; - }); - // Transactional candidate tracking: host/engine/overlay live here until commit. - // destroyCandidate() is the single cleanup path for stale, error, and timeout outcomes. - const restoreCommittedRenderer = () => { - /* A newer request owns the committed renderer while it is replacing this one. Do not - thaw that renderer from a stale response; its own transaction will settle it. */ - if (state.graphEngine !== oldEngine - || (state.graphLoadController && state.graphLoadController !== controller)) return; - if (oldEngine && typeof oldEngine.freeze === 'function') { - oldEngine.freeze(state.graphFrozen); - } - graphLifecycle.sync(); - }; - let candidateHost = null; - let candidateEngine = null; - let candidateOverlay = null; - let candidateStats = null; - let candidateMetrics = null; - let candidatePhysicsSnapshot = null; - const destroyCandidate = () => { - if (!candidateEngine) { - candidateOverlay = null; - if (candidateHost && candidateHost.parentNode) { - candidateHost.remove(); - } - candidateHost = null; - return; - } - // Keep committed references live: renderer callbacks close over candidateEngine. - // Nulling it here would make every post-readiness stats/metrics callback look stale. - if (state.graphEngine === candidateEngine) return; - graphLifecycle.release(candidateEngine, candidateOverlay); - candidateOverlay = null; - candidateEngine = null; - if (candidateHost && candidateHost.parentNode) { - candidateHost.remove(); - } - candidateHost = null; - }; - const timeout = window.setTimeout(() => { - if (!window.ForceGraph || !window.EngraphisGraph || !window.EngraphisSpacetime) { - releaseGraphAssetsAttempt(graphAssetsPromise); - } - if (fullGraph && !window.EngraphisEveryGraph) { - releaseGraphAllAssetsAttempt(graphAllAssetsPromise); - } - if (!controller.signal.aborted) controller.abort(); - const error = new Error('graph loading timed out'); - error.name = 'AbortError'; - rejectTimeout(error); - }, deadline); - try { - const level = fullGraph ? 'complete' : 'overview'; - const presentation = fullGraph ? '&presentation=all' : '&presentation=quality'; - const limits = fullGraph ? '' - : `&node_limit=${GRAPH_INITIAL_NODE_LIMIT}&edge_limit=${GRAPH_INITIAL_EDGE_LIMIT}`; - const connectedOnly = !fullGraph && !targetShowUnlinked ? '&connected_only=true' : ''; - const includeCode = targetIncludeCode ? '&include_code=true' : ''; - const validatedRepo = targetIncludeCode || fullGraph - ? validatedGraphRepository(targetRepo) : ''; - const scopedRepo = validatedRepo - ? `&repo=${encodeURIComponent(validatedRepo)}` : ''; - const asOf = targetAsOf === null ? '' : `&as_of=${encodeURIComponent(targetAsOf / 1000)}`; - const history = targetAsOf === null ? '' : '&include_history=true'; - // Complete Ledger views are canonical entity projections. Memory nodes remain available - // to compatible callers, but must not change the existing entity evidence click path. - const memoryProjection = fullGraph ? '&include_memory_nodes=false' : ''; - const [payload] = await Promise.race([ - Promise.all([ - api(`/graph/scene?${query(targetWorkspace)}&level=${level}${presentation}${limits}${connectedOnly}${includeCode}${scopedRepo}${asOf}${history}${memoryProjection}`, { - signal: controller.signal, - timeoutMs: deadline, - }), - assets, - ]), - timeoutPromise, - ]); - if (!isCurrentGraphLoad(request)) { - restoreCommittedRenderer(); - return; - } - if (payload && payload.error) throw new Error(String(payload.error)); - const scene = payload.scene && typeof payload.scene === 'object' ? payload.scene : payload; - const data = { - nodes: graphNodes(scene), - links: graphLinks(scene), - repositories: Array.isArray(scene.repos) - ? scene.repos.filter(repo => typeof repo === 'string') : [], - suggestions: scene.suggestions || [], - communities: scene.communities || [], - community_bridges: scene.community_bridges || scene.bridges || [], - meta: scene.meta || payload.meta || {}, - metadata: scene.metadata || payload.metadata || {}, - layout_seed: scene.layout_seed ?? (scene.meta && scene.meta.layout_seed) ?? (payload.meta && payload.meta.layout_seed), - }; - const sceneMeta = scene.meta || payload.meta || {}; - const nextMeta = { - ...sceneMeta, - nodes_available: sceneMeta.nodes_available == null ? (sceneMeta.total_nodes == null - ? data.nodes.length : sceneMeta.total_nodes) : sceneMeta.nodes_available, - nodes_complete: sceneMeta.nodes_complete == null - ? (sceneMeta.truncated == null ? fullGraph : !sceneMeta.truncated) - : sceneMeta.nodes_complete, - }; - const responseIncludeCode = sceneMeta.include_code === false ? false : targetIncludeCode; - const codeOverlayDegraded = targetIncludeCode && !responseIncludeCode; - const oldHost = byId('graph-canvas'); - if (!oldHost) throw new Error('graph canvas container missing'); - // oldEngine was captured before the first await so the failure path - // can restore the exact committed renderer even when candidate setup never begins. - candidateHost = oldHost.cloneNode(false); - candidateHost.id = `graph-canvas-candidate-${request.id}`; - candidateHost.classList.add('graph-canvas-candidate'); - candidateHost.setAttribute('aria-hidden', 'true'); - oldHost.insertAdjacentElement('afterend', candidateHost); - /* Authored-Galaxy detection must key off scene markers, not the toolbar preset: - entering Every via its chip sets the preset to 'every', but a complete scene - with system anchors still needs the hierarchical orbit engine and overlay. - The live-limit guard keeps very large complete scenes on the lightweight - Every renderer instead of the full physics galaxy. */ - const galaxyWithinLiveLimit = data.nodes.length <= GRAPH_INITIAL_NODE_LIMIT - && data.links.length <= GRAPH_INITIAL_EDGE_LIMIT; - const galaxyQuality = fullGraph && galaxyWithinLiveLimit - && data.nodes.some(node => node.anchor_role === 'community' - && (node.system_anchor_id !== undefined - || Number.isFinite(Number(node.galactic_radius)))); - const graphFactory = galaxyQuality ? window.EngraphisGraph - : fullGraph ? window.EngraphisEveryGraph : window.EngraphisGraph; - if (!graphFactory || typeof graphFactory.create !== 'function') { - throw new Error(fullGraph - ? 'all-node graph engine asset is unavailable' - : 'graph engine asset is unavailable'); - } - candidateEngine = graphFactory.create(candidateHost, { - renderMode: fullGraph ? 'all' : 'overview', - onNodeClick: item => openGraphConnections(item), - onBackgroundClick: () => candidateEngine.clearFocus(), - onStats: stats => { - if (state.graphEngine === candidateEngine - && state.graphLoadRequest === request.id) graphStatsChanged(stats); - else if (state.graphLoadRequest === request.id) candidateStats = stats; - }, - onMetrics: metrics => { - if (state.graphEngine === candidateEngine - && state.graphLoadRequest === request.id) graphMetricsChanged(metrics); - else if (state.graphLoadRequest === request.id) candidateMetrics = metrics; - }, - onPhysicsFrame: snapshot => { - candidatePhysicsSnapshot = snapshot; - if (candidateOverlay) candidateOverlay.setSnapshot(snapshot); - }, - onError: error => { - if (state.graphEngine !== candidateEngine || !fullGraph - || state.graphLoadRequest !== request.id || state.graphMode !== 'full') return; - byId('graph-empty').hidden = false; - byId('graph-empty').textContent = error && error.code === 'GRAPH_CAPACITY' - ? `All nodes exceed renderer capacity. Enter an exact repository filter or reduce the workspace graph. (${error.message})` - : 'The all-node renderer stopped. Choose Reload data to start a fresh worker.'; - byId('graph-canvas').setAttribute('aria-busy', 'false'); - }, - onCollapseChange: collapsed => { - if (targetMode === 'overview') showNotice(collapsed ? 'Clusters collapsed for overview.' : ''); - else { - const note = byId('graph-lod-note'); - const detail = note && note.querySelector('span'); - if (detail) detail.textContent = collapsed - ? 'Clusters are condensed into representative nodes. Zoom in to expand them.' - : 'Layout, forces, scope, colour and relation flow update without reloading the complete graph.'; - } - }, - onSlingshotRelease: () => { - if (state.graphSpacetimeOverlay && state.graphEngine - && typeof state.graphEngine.getPhysicsSnapshot === 'function') { - state.graphSpacetimeOverlay.setSnapshot(state.graphEngine.getPhysicsSnapshot()); - } - }, - }); - candidateEngine.apply(graph => { - const preset = byId('graph-preset').value; - graph.setPreset(galaxyQuality && preset === 'every' ? 'galaxy' : preset); - graph.setStyle(byId('graph-style').value); - graph.setColorBy(byId('graph-color').value); - graph.setThemeColors(graphThemeColors()); - const paletteName = byId('graph-palette').value; - graph.setPalette(paletteName); - if (paletteName === 'custom') graph.setTypeColors(GRAPH_CUSTOM_PALETTE); - graph.setSettings({ - ...graphTuningEngineSettings(), - ...graphSpacetimeEngineSettings(), - flow: byId('graph-flow').getAttribute('aria-checked') === 'true', - labels: byId('graph-labels').getAttribute('aria-checked') === 'true', - frozen: fullGraph ? false : state.graphFrozen, - }); - graph.setScope(graphScope()); - graph.setLayers(graphLayerState()); - graph.setRepoFilter(byId('graph-repo-filter').value); - graph.setAsOf(graphAsOfTimestamp()); - graph.setSizeBy(graphSizeBy()); - graph.setBridges(byId('graph-bridges').checked); - graph.setCollapse(byId('graph-collapse').checked ? 'auto' : false); - graph.setGhosts(byId('graph-ghosts').checked); - }, false, false); - candidateEngine.setData(data); - candidateEngine.freeze(fullGraph ? false : state.graphFrozen); - if ((!fullGraph || galaxyQuality) && window.EngraphisSpacetime - && window.EngraphisSpacetime.create) { - candidateOverlay = window.EngraphisSpacetime.create( - candidateHost, candidateEngine - ); - candidateOverlay.setEnabled(galaxyQuality || graphIsGalaxy()); - if (candidatePhysicsSnapshot) candidateOverlay.setSnapshot(candidatePhysicsSnapshot); - } - // Pending renderers also follow visibility while finite worker preparation finishes. - graphLifecycle.track(candidateEngine, candidateOverlay, () => graphOverlayEnabled(galaxyQuality)); - if (typeof candidateEngine.whenReady === 'function') { - await Promise.race([candidateEngine.whenReady(), timeoutPromise]); - if (!isCurrentGraphLoad(request)) { - destroyCandidate(); - restoreCommittedRenderer(); - return; - } - } - oldHost.id = `graph-canvas-retired-${request.id}`; - candidateHost.id = 'graph-canvas'; - candidateHost.classList.remove('graph-canvas-candidate'); - candidateHost.removeAttribute('aria-hidden'); - oldHost.replaceWith(candidateHost); - candidateHost = null; - state.graphEngine = candidateEngine; - state.graphSpacetimeOverlay = candidateOverlay; - state.graphData = data; - state.graphWorkspace = targetWorkspace; - state.graphDataMode = targetMode; - state.graphGalaxyQuality = galaxyQuality; - state.graphDataPreset = byId('graph-preset').value; - state.graphDataIncludeCode = responseIncludeCode; - state.graphDataShowUnlinked = targetShowUnlinked; - state.graphDataAsOf = targetAsOf; - state.graphDataRepo = targetRepo; - state.graphMeta = nextMeta; - if (codeOverlayDegraded) { - state.graphIncludeCode = false; - const layers = { ...graphLayerState(), code: false }; - setGraphLayers(layers); - candidateEngine.setLayers(layers); - clearGraphSavedView(); - saveGraphPreferences(); - showNotice(sceneMeta.degraded_reason === 'code_overlay_requires_repository_filter' - ? 'Code overlay needs a repository filter; showing entity relationships only.' - : 'Code overlay is unavailable; showing entity relationships only.'); - } - graphLifecycle.replace(candidateEngine, candidateOverlay, - () => graphOverlayEnabled()); - byId('graph-empty').hidden = Boolean(data.nodes.length); - if (!data.nodes.length) byId('graph-empty').textContent = 'No entities exist in this workspace yet.'; - updateGraphModeControls(); - updateGraphFacts(data); - // Candidate stats emitted before commit are retained, then published only after the - // renderer transaction commits. Raw payload counts are wrong when a frozen renderer - // has already applied repository, layer, ghost, or collapse visibility filters. - graphStatsChanged(candidateStats || { nodes: data.nodes.length, links: data.links.length }); - if (candidateMetrics) graphMetricsChanged(candidateMetrics); - updateGraphLayerCounts(data, scene.layers || payload.layers); - } catch (error) { - if (!isCurrentGraphLoad(request)) { - destroyCandidate(); - restoreCommittedRenderer(); - return; - } - destroyCandidate(); - byId('graph-empty').hidden = false; - byId('graph-empty').textContent = error && error.name === 'AbortError' - ? `${fullGraph ? 'All-node graph' : 'High-quality graph'} loading timed out. Choose Reload data to try again.` - : fullGraph && (error.status === 413 || error.code === 'GRAPH_CAPACITY') - ? `All nodes exceed the server capacity. Enter an exact repository filter or reduce the workspace graph. (${error.message})` - : `Graph unavailable: ${error.message}. Choose Reload data to try again.`; - if (state.graphData) { - if (state.graphDataMode !== targetMode) state.graphMode = state.graphDataMode; - /* The toolbar preset changes before a replacement request begins. Restore the - committed preset together with the committed renderer so a failed Every-node - transition cannot leave aria-pressed and the active engine disagreeing. */ - byId('graph-preset').value = state.graphDataPreset || 'galaxy'; - updateGraphModeControls(); - syncGraphChoices(); - } - // Restore the committed renderer's freeze/overlay state. The old engine survived - // because we never mutated state.graphEngine on the failure path. - if (oldEngine && typeof oldEngine.freeze === 'function') { - oldEngine.freeze(state.graphFrozen); - } - graphLifecycle.sync(); - } finally { - // Safety net: if control left the try/catch without committing or cleaning up - // (e.g. an unexpected throw in finally itself), ensure no candidate leaks. - destroyCandidate(); - window.clearTimeout(timeout); - if (state.graphLoadRequest === request.id && state.graphLoadController === controller) { - const canvasEl = byId('graph-canvas'); - if (canvasEl) canvasEl.setAttribute('aria-busy', 'false'); - setGraphLoadControlsBusy(false); - } - if (state.graphLoadController === controller) state.graphLoadController = null; - } - })(); - state.graphLoadPromise = task; - try { - return await task; - } finally { - if (state.graphLoadPromise === task) { - state.graphLoadPromise = null; - state.graphLoadWorkspace = ''; - state.graphLoadMode = ''; - state.graphLoadIncludeCode = false; - state.graphLoadShowUnlinked = false; - state.graphLoadAsOf = null; - state.graphLoadRepo = ''; - state.graphLoadKey = ''; - } - } - } - - function searchGraph(value) { - const target = byId('graph-search-results'); - target.replaceChildren(); - const needle = value.trim().toLowerCase(); - if (!needle || !state.graphData) return; - state.graphData.nodes - .filter(item => item.name.toLowerCase().includes(needle)) - .slice(0, 8) - .forEach(item => { - target.append(button(`${item.name} · ${item.degree}`, 'search-result', () => { - revealGraphNode(item.id, item.name); - target.replaceChildren(); - openGraphConnections(item); - })); - }); - } - - function renderMemoryCollection(target, memories, message) { - target.replaceChildren(); - if (!memories.length) { - target.append(empty(message)); - return; - } - memories.forEach(memory => target.append(simpleMemoryCard(memory))); - } - - function switchProvenanceTab(tab) { - state.provenanceTab = tab; - all('[data-provenance-tab]').forEach(control => { - const active = control.dataset.provenanceTab === tab; - control.classList.toggle('active', active); - control.setAttribute('aria-selected', String(active)); - control.tabIndex = active ? 0 : -1; - }); - all('[data-provenance-panel]').forEach(panel => panel.classList.toggle('active', panel.dataset.provenancePanel === tab)); - if (tab === 'audit') loadAudit(); - } - - async function whySearch(event) { - event.preventDefault(); - const question = byId('why-input').value.trim(); - if (!question) { - showNotice('Enter a claim or topic before tracing belief.'); - byId('why-input').focus(); - return; - } - const request = beginScopedRequest('why'); - showNotice(''); - const target = byId('why-result'); - target.replaceChildren(empty('Tracing the live belief and supersession chain…')); - try { - const payload = await api(`/why?q=${encodeURIComponent(question)}&${query(request.workspace)}&k=8`); - if (!isCurrentScopedRequest(request)) return; - target.replaceChildren(); - const live = payload.answer || []; - const superseded = payload.supersedes || []; - target.append(node('h2', '', 'Live support')); - if (!live.length) target.append(empty('No live supporting memory was found.')); - else live.forEach(memory => target.append(simpleMemoryCard(memory))); - target.append(node('h2', '', 'Superseded history')); - if (!superseded.length) target.append(empty('No superseded versions were found.')); - else superseded.forEach(memory => target.append(simpleMemoryCard(memory, 'timeline-card'))); - } catch (error) { - if (!isCurrentScopedRequest(request)) return; - target.replaceChildren(empty(`Could not trace belief: ${error.message}`)); - } - } - - async function timelineSearch(event, supersessionsOnly = false) { - event.preventDefault(); - const input = byId(supersessionsOnly ? 'supersession-input' : 'timeline-input'); - const target = byId(supersessionsOnly ? 'supersession-list' : 'timeline-result'); - const question = input.value.trim(); - if (!question) { - showNotice(`Enter a topic before ${supersessionsOnly ? 'finding supersessions' : 'showing history'}.`); - input.focus(); - return; - } - const request = beginScopedRequest(supersessionsOnly ? 'supersessions' : 'timeline'); - showNotice(''); - target.replaceChildren(empty('Loading temporal history…')); - try { - const payload = await api(`/timeline?q=${encodeURIComponent(question)}&${query(request.workspace)}&limit=50`); - if (!isCurrentScopedRequest(request)) return; - let history = payload.history || []; - if (supersessionsOnly) history = history.filter(item => item.valid_to || item.expired_at); - renderMemoryCollection(target, history, supersessionsOnly ? 'No closed versions were found for this topic.' : 'No temporal history was found.'); - } catch (error) { - if (!isCurrentScopedRequest(request)) return; - target.replaceChildren(empty(`Could not load history: ${error.message}`)); - } - } - - function renderAuditCards(audit, receipts) { - const target = byId('audit-list'); - target.replaceChildren(); - const combined = [ - ...audit.map(item => ({ ...item, _kind: 'audit' })), - ...receipts.map(item => ({ ...item, _kind: 'receipt' })), - ].sort((a, b) => provenanceTimestampMs(b) - provenanceTimestampMs(a)); - if (!combined.length) { - target.append(empty('No audit records or receipts yet.')); - return; - } - combined.slice(0, 120).forEach(item => { - const card = node('article', 'audit-card'); - card.append( - node('span', '', relative(provenanceTimestampMs(item))), - node('strong', '', item.actor || item.source || 'local operator'), - node('span', 'tag', item.operation || item.action || item.event || item._kind), - node('span', '', item.scope || item.workspace || item.status || state.workspace), - node('code', '', truncate(item.hash || item.id || item.receipt_id, 24) || '—'), - ); - target.append(card); - }); - } - - async function loadAudit() { - const request = beginScopedRequest('audit'); - const target = byId('audit-list'); - target.replaceChildren(empty('Loading audit records and receipts…')); - byId('savings-detail').replaceChildren(empty('Loading receipt-backed estimate…')); - const [auditResult, receiptsResult, savingsResult] = await Promise.allSettled([ - api(`/audit?${query(request.workspace)}&limit=100`), - api(`/receipts?${query(request.workspace)}&limit=100`), - api(`/context-savings${savingsQuery(state.savingsPreset)}`), - ]); - if (!isCurrentScopedRequest(request)) return; - if (savingsResult.status === 'fulfilled') { - renderSavingsDetail(savingsResult.value); - } else { - byId('savings-detail').replaceChildren(empty(`Could not load context savings: ${savingsResult.reason.message}`)); - } - const audit = auditResult.status === 'fulfilled' ? auditItems(auditResult.value) : []; - const receipts = receiptsResult.status === 'fulfilled' ? receiptItems(receiptsResult.value) : []; - if (auditResult.status === 'rejected' && receiptsResult.status === 'rejected') { - target.replaceChildren(empty('Could not load audit records or receipts. Try again.')); - } else { - renderAuditCards(audit, receipts); - } - if (auditResult.status === 'rejected' || receiptsResult.status === 'rejected') { - showNotice('Some provenance data could not be loaded; available records remain visible.'); - } - } - - async function verifyReceipts() { - try { - const result = await api(`/receipts/verify?${query()}`); - const valid = result.valid != null ? result.valid : result.verified; - showNotice(valid === false ? 'Receipt verification found a broken chain.' : 'Receipt chain verified.'); - } catch (error) { - showNotice(`Could not verify receipts: ${error.message}`); - } - } - - async function exportReceipts() { - try { - const receipts = await api(`/receipts/export?${query()}`); - const blob = new Blob([JSON.stringify(receipts, null, 2)], { type: 'application/json' }); - const link = document.createElement('a'); - const url = URL.createObjectURL(blob); - link.href = url; - link.download = `engraphis-receipts-${state.workspace || 'workspace'}.json`; - document.body.append(link); - link.click(); - link.remove(); - URL.revokeObjectURL(url); - showNotice('Privacy-safe receipts exported.'); - } catch (error) { - showNotice(`Could not export receipts: ${error.message}`); - } - } - - function switchManageTab(tab) { - state.manageTab = tab; - all('[data-manage-tab]').forEach(control => { - const active = control.dataset.manageTab === tab; - control.classList.toggle('active', active); - control.setAttribute('aria-selected', String(active)); - control.tabIndex = active ? 0 : -1; - }); - all('[data-manage-panel]').forEach(panel => panel.classList.toggle('active', panel.dataset.managePanel === tab)); - loadManageTab(tab); - } - - async function loadManageTab(tab) { - if (tab === 'workspaces') renderWorkspaceList(); - if (tab === 'settings') await loadSettings(); - if (tab === 'plans') await loadPlans(); - if (tab === 'analytics') await loadHosted('analytics'); - if (tab === 'automation') await loadHosted('automation'); - if (tab === 'team') await loadHosted('team'); - if (tab === 'sync') await loadSync(); - } - - function renderWorkspaceList() { - const target = byId('workspace-list'); - target.replaceChildren(); - if (!state.workspaces.length) { - target.append(empty('Create the first workspace to begin.')); - return; - } - state.workspaces.forEach(item => { - const name = workspaceName(item); - const card = node('article', `workspace-card${name === state.workspace ? ' active' : ''}`); - const copy = node('div'); - copy.append( - node('h3', '', name), - ...(item.description ? [node('p', '', item.description)] : []), - node('p', '', `${number(item.memories).toLocaleString()} memories · ${item.visibility === 'personal' - ? 'Personal · excluded from Cloud Sync' - : item.visibility === 'shared' - ? 'Shared · eligible for encrypted Cloud Sync' - : 'Local workspace'}`), - ); - const actions = node('div', 'workspace-card-actions'); - if (name !== state.workspace) actions.append(button('Switch to', 'secondary-button', () => selectWorkspace(name))); - actions.append( - button('Rename', 'secondary-button', () => renameWorkspace(name)), - button('Copy', 'secondary-button', () => copyWorkspace(name)), - ); - if (item.visibility === 'personal' && item.can_change_access === true) { - actions.append(button('Share for Cloud Sync', 'secondary-button', () => changeWorkspaceVisibility(name, 'shared'))); - } else if (item.visibility === 'shared' && item.can_change_access === true) { - actions.append(button('Make personal', 'secondary-button', () => changeWorkspaceVisibility(name, 'personal'))); - } - if (name !== state.workspace) actions.append(button('Delete', 'danger-button', () => deleteWorkspace(name))); - card.append(copy, actions); - target.append(card); - }); - } - - async function createWorkspace(event) { - event.preventDefault(); - const name = byId('new-workspace-name').value.trim(); - const description = byId('new-workspace-description').value.trim(); - if (!name) { - showNotice('Enter a workspace name before creating it.'); - byId('new-workspace-name').focus(); - return; - } - showNotice(''); - try { - await api('/workspaces/create', { - method: 'POST', - body: { workspace: name, description, visibility: 'personal', confirmed: false }, - }); - showNotice(`Workspace ${name} created.`); - byId('create-workspace-form').reset(); - byId('create-workspace-form').hidden = true; - await refreshBootstrap(name); - } catch (error) { - showNotice(`Could not create workspace: ${error.message}`); - } - } - - async function changeWorkspaceVisibility(name, visibility) { - const sharing = visibility === 'shared'; - const question = sharing - ? `Share “${name}” with signed-in Team members and make it eligible for encrypted Cloud Sync? Other Team members with access can read this workspace. This changes its visibility from personal to shared.` - : `Make “${name}” personal? Other Team members will lose access and future Cloud Sync rounds will exclude it. This does not erase data already uploaded to the encrypted relay.`; - if (!window.confirm(question)) return; - try { - await api('/workspaces/visibility', { - method: 'POST', - body: { workspace: name, visibility, confirmed: true }, - }); - await refreshBootstrap(state.workspace); - showNotice(sharing - ? `Workspace ${name} is shared and eligible for Cloud Sync. Run Sync now to upload eligible changes.` - : `Workspace ${name} is personal and excluded from future Cloud Sync rounds.`); - } catch (error) { - showNotice(`Could not change access for workspace ${name}: ${error.message}`); - } - } - - async function renameWorkspace(name) { - const next = window.prompt(`Rename ${name} to:`, name); - if (!next || next === name) return; - try { - await api('/workspaces/rename', { method: 'POST', body: { workspace: name, new_name: next } }); - showNotice(`Workspace renamed to ${next}.`); - await refreshBootstrap(name === state.workspace ? next : state.workspace); - } catch (error) { - showNotice(`Could not rename workspace: ${error.message}`); - } - } - - async function copyWorkspace(name) { - try { - const result = await api('/workspaces/copy', { method: 'POST', body: { workspace: name } }); - showNotice(`Workspace copied${result.name ? ` to ${result.name}` : ''}.`); - await refreshBootstrap(state.workspace); - } catch (error) { - showNotice(`Could not copy workspace: ${error.message}`); - } - } - - async function deleteWorkspace(name) { - if (!window.confirm(`Delete workspace “${name}”? Its memories are retired through the governed workspace operation.`)) return; - try { - await api('/workspaces/delete', { method: 'POST', body: { workspace: name } }); - showNotice(`Workspace ${name} deleted.`); - await refreshBootstrap(state.workspace); - } catch (error) { - showNotice(`Could not delete workspace: ${error.message}`); - } - } - - function renderObject(target, payload, title = 'Result') { - target.replaceChildren(); - target.append(node('h3', '', title)); - const entries = Object.entries(payload || {}).filter(([, value]) => ['string', 'number', 'boolean'].includes(typeof value)).slice(0, 12); - if (entries.length) target.append(definitionList(entries.map(([key, value]) => [key.replaceAll('_', ' '), text(value)]))); - else target.append(node('p', '', 'The operation completed.')); - } - - function runFreshAnalytics(workspace) { - if (workspace !== state.workspace) return; - state.analyticsJobs.delete(workspace); - state.analyticsResults.delete(workspace); - state.analyticsStartErrors.delete(workspace); - void loadHosted('analytics'); - } - - function renderAnalyticsStartError(target, workspace, message) { - target.replaceChildren( - node('h3', '', 'Analytics request unconfirmed'), - node('p', 'automation-policy-note', - `Could not confirm the Analytics run: ${message} It may still be processing in Cloud. Starting a fresh analysis creates another run.`), - button('Run fresh analysis', 'secondary-button', () => runFreshAnalytics(workspace)), - ); - } - - function hasAnalyticsMetrics(result) { - const totals = result && result.totals; - const forecast = result && result.decay_forecast; - return result && result.kind === 'analytics' - && Number.isInteger(result.memory_count) && result.memory_count >= 0 - && totals && Number.isInteger(totals.live) && totals.live >= 0 - && Number.isInteger(totals.pinned) && totals.pinned >= 0 - && Number.isFinite(totals.avg_retention) - && forecast && Number.isInteger(forecast.at_risk_7d); - } - - function renderAnalyticsResult(target, result, workspace, stale = false) { - const totals = result.totals; - const live = totals.live; - target.replaceChildren(node('h3', '', live === 0 ? 'Analytics has no eligible memories' : 'Analytics result')); - if (live === 0) { - target.append(node('p', 'automation-policy-note', - 'This result has no eligible memories. Add a workspace memory, then run a fresh analysis.')); - } else { - const metrics = [ - ['Live memories', live.toLocaleString()], - ['Average retention', `${Math.round(totals.avg_retention * 100)}%`], - ['At risk within 7 days', result.decay_forecast.at_risk_7d.toLocaleString()], - ['Pinned memories', totals.pinned.toLocaleString()], - ]; - const grid = node('div', 'stat-grid'); - metrics.forEach(([label, value]) => { - const item = node('div', 'stat-item'); - item.append(node('span', '', label), node('strong', '', value)); - grid.append(item); - }); - target.append(grid); - } - if (stale) target.append(node('p', 'automation-policy-note', - 'A newer workspace snapshot exists. Run a fresh analysis to see current data.')); - target.append(button('Run fresh analysis', 'secondary-button', () => runFreshAnalytics(workspace))); - } - - function renderAnalyticsPending(target, workspace, jobId, status, error = '') { - target.replaceChildren( - node('h3', '', error ? 'Analytics result unavailable' : 'Analytics is processing'), - node('p', 'automation-policy-note', error || - `The Cloud job is ${status === 'running' ? 'running' : 'queued'}. Check this result again without starting another run.`), - button('Refresh result', 'secondary-button', () => { void refreshAnalyticsResult(workspace, jobId); }), - ); - if (error) target.append(button('Run fresh analysis', 'secondary-button', - () => runFreshAnalytics(workspace))); - } - - async function refreshAnalyticsResult(workspace, jobId) { - if (workspace !== state.workspace || state.analyticsJobs.get(workspace) !== jobId) return; - const request = beginScopedRequest('hosted-analytics'); - const target = byId('analytics-result'); - target.replaceChildren(empty('Checking the existing Analytics job…')); - try { - const status = await api(`/analytics/job-result?${query(workspace)}&job_id=${encodeURIComponent(jobId)}`, - { signal: request.signal, timeoutMs: 45_000 }); - if (!isCurrentScopedRequest(request)) return; - if (status.pending === true) { - renderAnalyticsPending(target, workspace, jobId, status.state); - } else if (status.failed === true) { - state.analyticsJobs.delete(workspace); - target.replaceChildren( - node('p', 'automation-policy-note', 'This Analytics run did not complete.'), - button('Run fresh analysis', 'secondary-button', () => runFreshAnalytics(workspace)), - ); - } else if (hasAnalyticsMetrics(status.result)) { - state.analyticsJobs.delete(workspace); - state.analyticsResults.set(workspace, { result: status.result, stale: status.state === 'stale' }); - renderAnalyticsResult(target, status.result, workspace, status.state === 'stale'); - } else { - throw new Error('The Analytics result is unavailable.'); - } - } catch (error) { - if (isCurrentScopedRequest(request)) { - renderAnalyticsPending(target, workspace, jobId, '', `Could not check this result: ${error.message}`); - } - } - } - - function consolidationOptions() { - return { - workspace: state.workspace, - infer: false, - structured: byId('consolidate-structured').checked, - }; - } - - function sameConsolidationOptions(left, right) { - return Boolean(left && right) - && left.workspace === right.workspace - && left.infer === right.infer - && left.structured === right.structured; - } - - function invalidateConsolidationReview() { - state.consolidationReview = null; - byId('consolidate-commit').disabled = true; - } - - async function previewConsolidation(event) { - event.preventDefault(); - const options = consolidationOptions(); - invalidateConsolidationReview(); - const target = byId('consolidate-result'); - target.replaceChildren(empty('Scanning local memory without writing changes…')); - try { - const result = await api('/consolidate', { - method: 'POST', - body: { - ...options, - dry_run: true, - }, - }); - // The preview is an approval only for the exact workspace and choices that - // produced it; never let a late response authorize a changed form. - if (!sameConsolidationOptions(options, consolidationOptions())) return; - state.consolidationReview = options; - byId('consolidate-commit').disabled = false; - renderObject(target, result, 'Dry preview complete · nothing written'); - } catch (error) { - invalidateConsolidationReview(); - target.replaceChildren(empty(`Preview failed: ${error.message}`)); - } - } - - async function commitConsolidation() { - const options = consolidationOptions(); - if (!sameConsolidationOptions(state.consolidationReview, options)) { - invalidateConsolidationReview(); - showNotice('Run a new dry preview after changing the workspace or consolidation options.'); - return; - } - if (!window.confirm(`Commit the reviewed consolidation result for ${state.workspace}? Original records remain in temporal history.`)) return; - const target = byId('consolidate-result'); - target.replaceChildren(empty('Committing the reviewed local consolidation…')); - try { - const result = await api('/consolidate', { - method: 'POST', - body: { - ...options, - dry_run: false, - }, - }); - invalidateConsolidationReview(); - renderObject(target, result, 'Consolidation committed'); - await selectWorkspace(state.workspace); - } catch (error) { - target.replaceChildren(empty(`Commit failed: ${error.message}`)); - } - } - - function automationCheckbox(id, label, checked) { - const field = node('label', 'check-row'); - const input = node('input'); - input.id = id; - input.type = 'checkbox'; - input.checked = Boolean(checked); - field.htmlFor = id; - field.append(input, document.createTextNode(label)); - return field; - } - - function automationNumber(id, label, value, min, max) { - const field = node('label', '', label); - const input = node('input'); - input.id = id; - input.type = 'number'; - input.min = String(min); - input.max = String(max); - input.value = String(value); - field.htmlFor = id; - field.append(input); - return field; - } - - function renderAutomationPolicy(policy, workspace = state.workspace) { - const target = byId('automation-result'); - if (!target) return; - target.replaceChildren(); - const form = node('form', 'automation-policy-form'); - form.dataset.workspace = workspace; - form.dataset.lastRun = String(policy.last_run || ''); - if (policy.bootstrap_required) { - form.append( - node('p', 'automation-policy-note', 'Hosted automation is not initialized for this workspace. Initializing it uploads one bounded workspace snapshot and saves the default Cloud policy. No upload occurs until you choose this action.'), - ); - const actions = node('div', 'automation-policy-actions'); - const bootstrap = node('button', 'primary-button', 'Initialize hosted automation'); - bootstrap.type = 'button'; - bootstrap.addEventListener('click', () => bootstrapAutomation(workspace, bootstrap)); - actions.append(bootstrap); - form.append(actions); - target.append(form); - return; - } - const enabled = Boolean(policy.enabled); - const dreamEnabled = policy.dream_enabled != null ? policy.dream_enabled : policy.dream; - const lastRun = policy.last_run ? ` Last managed run: ${relative(policy.last_run)}.` : ''; - form.append( - node('p', 'automation-policy-note', enabled - ? `This workspace has an active hosted maintenance policy.${lastRun}` - : 'Hosted maintenance is paused for this workspace.'), - automationCheckbox('automation-enabled', 'Enable hosted maintenance', enabled), - automationNumber('automation-cadence', 'Run every (hours)', Math.max(1, Number(policy.cadence_hours) || 24), 1, 8760), - automationCheckbox('automation-dream', 'Enable Auto Dreaming after accumulation and idle time', dreamEnabled), - automationNumber('automation-dream-min', 'Minimum new memories', Math.max(1, Number(policy.dream_min_new) || 25), 1, 100000), - automationNumber('automation-dream-idle', 'Idle minutes before Dreaming', Math.max(0, Number(policy.dream_idle_minutes) || 0), 0, 10080), - automationCheckbox('automation-infer', 'Allow hosted relationship inference proposals', policy.infer), - node('p', 'automation-policy-note', `Cloud Sync: ${CLOUD_SYNC_PRIVACY_NOTICE} Managed compute: saving an enabled policy submits a bounded snapshot of this workspace’s normal and sensitive memory content to Engraphis Cloud. Cloud work returns proposals and never silently changes the local database.`), - ); - const actions = node('div', 'automation-policy-actions'); - const save = node('button', 'primary-button', enabled ? 'Save & send policy to Cloud' : 'Save hosted policy'); - save.type = 'submit'; - actions.append(save); - form.append(actions); - form.addEventListener('submit', saveAutomationPolicy); - target.append(form); - } - - async function bootstrapAutomation(workspace, control) { - if (!workspace || workspace !== state.workspace) return; - if (!window.confirm( - `Initialize hosted automation for ${workspace}? Engraphis will upload one bounded snapshot of that workspace's normal and sensitive memory content and save the default Cloud policy.`, - )) return; - const request = beginScopedRequest('automation-bootstrap'); - control.disabled = true; - control.textContent = 'Initializing…'; - try { - const policy = await api(`/automation/bootstrap?${query(workspace)}`, { method: 'POST' }); - if (!isCurrentScopedRequest(request) || !control.isConnected) return; - state.hostedLoaded.add(`automation:${workspace}`); - renderAutomationPolicy(policy, workspace); - showNotice('Hosted automation initialized.'); - } catch (error) { - if (!isCurrentScopedRequest(request) || !control.isConnected) return; - control.disabled = false; - control.textContent = 'Initialize hosted automation'; - showNotice(`Could not initialize hosted automation: ${error.message}`); - } - } - - async function saveAutomationPolicy(event) { - event.preventDefault(); - const form = event.currentTarget; - const workspace = form.dataset.workspace || ''; - if (!workspace || workspace !== state.workspace) { - showNotice('This policy belongs to a different workspace. Reloading the active workspace policy.'); - state.hostedLoaded.delete(`automation:${state.workspace}`); - await loadHosted('automation'); - return; - } - const request = beginScopedRequest('automation-save'); - const policy = { - enabled: byId('automation-enabled').checked, - cadence_hours: Math.max(1, Number(byId('automation-cadence').value) || 1), - dream_enabled: byId('automation-dream').checked, - dream_min_new: Math.max(1, Number(byId('automation-dream-min').value) || 1), - dream_idle_minutes: Math.max(0, Number(byId('automation-dream-idle').value) || 0), - infer: byId('automation-infer').checked, - }; - if (policy.enabled && !window.confirm( - `Save this hosted policy for ${workspace}? Engraphis will submit a bounded snapshot of that workspace’s normal and sensitive memory content to Cloud for managed compute.\n\nCloud Sync: ${CLOUD_SYNC_PRIVACY_NOTICE}`, - )) return; - const save = form.querySelector('button[type="submit"]'); - if (save) { - save.disabled = true; - save.textContent = 'Saving…'; - } - try { - const saved = await api(`/automation?${query(workspace)}`, { method: 'POST', body: policy }); - if (!isCurrentScopedRequest(request) || !form.isConnected) return; - state.hostedLoaded.add(`automation:${workspace}`); - renderAutomationPolicy({ ...saved, last_run: form.dataset.lastRun }, workspace); - showNotice('Hosted maintenance policy saved to Engraphis Cloud.'); - } catch (error) { - if (!isCurrentScopedRequest(request) || !form.isConnected) return; - if (save) { - save.disabled = false; - save.textContent = policy.enabled ? 'Save & send policy to Cloud' : 'Save hosted policy'; - } - showNotice(`Could not save the hosted policy: ${error.message}`); - } - } - - async function loadHosted(kind) { - if (kind === 'analytics' && state.analyticsStarting.has(state.workspace)) { - byId('analytics-result').replaceChildren(empty('An Analytics request is already running. Reopen this tab if it does not update.')); - return; - } - const request = beginScopedRequest(`hosted-${kind}`); - const workspace = request.workspace; - const cacheKey = `${kind}:${workspace}`; - const target = byId(`${kind}-result`); - if (kind === 'analytics') { - const completed = state.analyticsResults.get(workspace); - if (completed) { - renderAnalyticsResult(target, completed.result, workspace, completed.stale); - return; - } - const pendingJob = state.analyticsJobs.get(workspace); - if (pendingJob) { - await refreshAnalyticsResult(workspace, pendingJob); - return; - } - const previousError = state.analyticsStartErrors.get(workspace); - if (previousError) { - renderAnalyticsStartError(target, workspace, previousError); - return; - } - } else if (state.hostedLoaded.has(cacheKey)) return; - target.replaceChildren(empty(`Checking ${kind} availability…`)); - try { - if (kind === 'team') { - const [auth, license] = await Promise.all([api('/auth/state'), api('/license')]); - if (!isCurrentScopedRequest(request)) return; - state.license = license; - updatePlanBadge(); - renderSidebarCta(); - setDeploymentMode(auth.deployment_mode || 'local'); - renderObject(target, { - deployment_mode: auth.deployment_mode || 'local', - local_mode: auth.mode || 'open', - hosted_team: Boolean(auth.hosted_team), - local_invitations: Boolean(auth.local_invitations), - cloud_access: Boolean(license.cloud_access_active), - plan: license.plan || 'local', - }, 'Connection state'); - } else { - if (kind === 'analytics') state.analyticsStarting.add(workspace); - const result = await api(`/${kind}?${query(workspace)}`, - kind === 'analytics' ? { timeoutMs: 90_000 } : {}); - // A submitted Analytics job can outlive a workspace selection change. Keep its - // identifier/result for that workspace even when the visible panel has moved on. - if (kind !== 'analytics' && !isCurrentScopedRequest(request)) return; - if (kind === 'analytics' && !state.workspaces.some(item => workspaceName(item) === workspace)) return; - const showAnalytics = kind === 'analytics' && state.workspace === workspace - && state.view === 'manage' && state.manageTab === 'analytics'; - if (kind === 'automation') renderAutomationPolicy(result, workspace); - else if (kind === 'analytics' && result.pending === true) { - const jobId = text(result.job_id); - if (!/^[A-Za-z0-9_-]{1,64}$/.test(jobId)) { - throw new Error('Engraphis Cloud did not return a usable Analytics job.'); - } - state.analyticsJobs.set(workspace, jobId); - state.analyticsStartErrors.delete(workspace); - if (showAnalytics) renderAnalyticsPending(target, workspace, jobId, result.state); - } else if (kind === 'analytics') { - if (!hasAnalyticsMetrics(result)) { - throw new Error('Engraphis Cloud did not return a completed Analytics result.'); - } - const stale = result.state === 'stale'; - state.analyticsResults.set(workspace, { result, stale }); - state.analyticsStartErrors.delete(workspace); - if (showAnalytics) renderAnalyticsResult(target, result, workspace, stale); - } else renderObject(target, result, `${kind[0].toUpperCase()}${kind.slice(1)} status`); - } - if (kind !== 'analytics' && isCurrentScopedRequest(request)) state.hostedLoaded.add(cacheKey); - } catch (error) { - if (kind === 'analytics') { - state.analyticsStartErrors.set(workspace, error.message); - if (state.workspace === workspace && state.view === 'manage' && state.manageTab === 'analytics') { - renderAnalyticsStartError(target, workspace, error.message); - } - } else if (isCurrentScopedRequest(request)) { - target.replaceChildren(empty(`${kind[0].toUpperCase()}${kind.slice(1)} is not active: ${error.message}`)); - } - } finally { - if (kind === 'analytics') state.analyticsStarting.delete(workspace); - } - } - function syncSummaryMessage(summary) { - if (!summary) return 'No sync has run in this dashboard process.'; - const attempted = number(summary.attempted); - const succeeded = number(summary.succeeded); - const errors = Array.isArray(summary.errors) ? summary.errors : []; - const complete = summary.complete === true - || (summary.complete !== false && errors.length === 0 && succeeded >= attempted); - if (complete && attempted === 0) { - return 'Last sync found no eligible shared workspaces · 0/0 completed. Create a workspace, or share a personal Team workspace you own, then try again.'; - } - const counts = `${succeeded}/${attempted} eligible workspaces completed`; - const changes = `${number(summary.added)} added · ${number(summary.updated)} updated · ${number(summary.exported)} exported`; - return `${complete ? 'Last sync complete' : 'Last sync incomplete'} · ${counts} · ${changes}${errors.length ? ` · ${errors.length} ${errors.length === 1 ? 'error' : 'errors'}` : ''}.`; - } - - function renderSyncStatus(status, message = '') { - state.syncStatus = status || {}; - const connected = state.syncStatus.available === true; - const ready = connected && state.syncStatus.ready === true; - const hasKey = state.syncStatus.has_key === true; - const target = byId('sync-result'); - if (!target) return; - target.replaceChildren(); - if (message) target.append(empty(message, 'form-error')); - target.append( - node('p', 'automation-policy-note', syncSummaryMessage(state.syncStatus.last)), - definitionList([ - ['Connection', connected ? 'Connected' : 'Not connected'], - ['Encryption key', hasKey ? 'Valid' : state.syncStatus.key_state === 'invalid' - ? 'Invalid' : 'Not configured'], - ['Sync readiness', ready ? 'Ready' : 'Needs setup'], - ['Mode', state.syncStatus.read_only ? 'Read only · pull without upload' : 'Push and pull'], - ['Credential', state.syncStatus.has_cloud_session - ? 'Managed Cloud session' - : (state.syncStatus.has_user_token ? 'Local sync token' : 'None')], - ]), - node('p', 'automation-policy-note', CLOUD_SYNC_PRIVACY_NOTICE), - ); - const actions = node('div', 'automation-policy-actions'); - const run = button('Sync now', 'primary-button', runCloudSync); - run.id = 'sync-now'; - run.disabled = !ready; - actions.append(run); - if (state.syncStatus.last && number(state.syncStatus.last.attempted) === 0) { - actions.append(button('Review workspaces', 'secondary-button', () => switchManageTab('workspaces'))); - } - if (!connected) { - const url = safeUrl(state.syncStatus.upgrade_url) || hostedAccountUrl('sync'); - if (url) { - const connect = node('a', 'secondary-button', 'Connect Engraphis Cloud'); - connect.href = url; - connect.target = '_blank'; - connect.rel = 'noopener'; - actions.append(connect); - } - } else if (!ready) { - const missingKey = !hasKey; - 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.' - : 'Cloud Sync encryption support is unavailable. Install engraphis[cloud-sync] and restart this dashboard.'; - target.append(node('p', 'automation-policy-note', detail)); - 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'; - guide.rel = 'noopener'; - actions.append(guide); - } - target.append(actions); - } - - async function loadSync() { - const request = beginScopedRequest('sync-status'); - const target = byId('sync-result'); - if (!target) return; - target.replaceChildren(empty('Checking Cloud Sync connection…')); - try { - const status = await api('/sync/status'); - if (!isCurrentScopedRequest(request)) return; - renderSyncStatus(status); - } catch (error) { - if (!isCurrentScopedRequest(request)) return; - target.replaceChildren(empty(`Could not load Cloud Sync status: ${error.message}`, 'form-error')); - } - } - - async function runCloudSync() { - const request = beginScopedRequest('sync-run'); - const buttonNode = byId('sync-now'); - if (buttonNode) { - buttonNode.disabled = true; - buttonNode.textContent = 'Syncing…'; - } - try { - const result = await api('/sync/run', { method: 'POST' }); - if (!isCurrentScopedRequest(request)) return; - const summary = result && result.summary ? result.summary : {}; - const responseOk = Boolean(result) && result.ok !== false; - const displayedSummary = responseOk ? summary : { ...summary, complete: false }; - renderSyncStatus({ ...(state.syncStatus || {}), last: displayedSummary }); - const errors = Array.isArray(summary.errors) ? summary.errors : []; - const complete = responseOk && (summary.complete === true - || (summary.complete !== false && errors.length === 0 - && number(summary.succeeded) >= number(summary.attempted))); - showNotice(complete && number(summary.attempted) === 0 - ? 'No eligible shared workspaces were synced.' - : complete - ? 'Cloud Sync completed for every eligible workspace.' - : 'Cloud Sync is incomplete. Review the status before retrying.'); - } catch (error) { - if (!isCurrentScopedRequest(request)) return; - renderSyncStatus(state.syncStatus || {}, `Cloud Sync failed: ${error.message}`); - showNotice(`Cloud Sync failed: ${error.message}`); - } - } - - function planPrices() { - const annual = byId('billing-select').value === 'annual'; - return annual - ? { free: '$0', pro: '$100 / owner / year', team: '$200 / seat / year' } - : { free: '$0', pro: '$10 / owner / month', team: '$20 / seat / month' }; - } - - function renderPlans() { - const target = byId('plan-cards'); - target.replaceChildren(); - const prices = planPrices(); - const plans = [ - { id: 'free', name: 'Free', price: prices.free, note: 'The complete local memory engine and every core operation.', action: 'Current local plan' }, - { id: 'pro', name: 'Pro', price: prices.pro, note: 'Cloud sync, managed automation and portfolio analytics.' }, - { id: 'team', name: 'Team', price: prices.team, note: 'Shared workspaces, member roles, seats and remote agents.' }, - ]; - plans.forEach(plan => { - const card = node('article', `plan-card${plan.id === 'pro' ? ' featured' : ''}`); - card.append( - node('p', 'eyebrow', plan.id === (state.license && state.license.plan) ? 'Current plan' : plan.id), - node('h2', '', plan.name), - node('div', 'price', plan.price), - node('p', '', plan.note), - ); - if (plan.id === 'pro') { - card.append( - node('p', 'plan-support', 'Support continued Engraphis development with Pro. Your subscription helps cover hosted infrastructure and ongoing development.'), - node('p', 'plan-benefits', 'Cloud Sync, Analytics, Auto Consolidation, and Auto Dreaming across your installations.'), - ); - } - if (plan.id === 'free') { - const status = node('span', 'secondary-button', plan.action); - card.append(status); - } else { - const interval = byId('billing-select').value === 'annual' ? 'annual' : 'monthly'; - const cta = hostedCta(plan.id, 'plans', interval); - const action = node('a', 'primary-button', cta.label); - const url = cta.href; - action.dataset.proCta = plan.id; - action.href = url || '#'; - if (url) { - action.target = '_blank'; - action.rel = 'noopener'; - } else { - action.addEventListener('click', event => { - event.preventDefault(); - showNotice('Connect this installation to Engraphis Cloud to open hosted plan options.'); - }); - } - card.append(action); - } - target.append(card); - }); - } - - async function loadPlans() { - const request = beginScopedRequest('plans'); - try { - const license = await api(`/license?${query(request.workspace)}`); - if (!isCurrentScopedRequest(request)) return; - state.license = license; - } catch (_) { - if (!isCurrentScopedRequest(request)) return; - state.license = { plan: 'free' }; - } - updatePlanBadge(); - renderSidebarCta(); - renderPlans(); - } - - function llmSnippet(provider, model, keySet) { - return [ - `ENGRAPHIS_LLM_PROVIDER=${provider}`, - `ENGRAPHIS_LLM_MODEL=${model}`, - 'ENGRAPHIS_LLM_API_KEY=', - keySet ? 'ENGRAPHIS_EXTRACTOR=llm_structured' : '# set ENGRAPHIS_EXTRACTOR=llm_structured to use it', - 'ENGRAPHIS_LLM_AUTO_EXTRACT=1', - ].join('\n'); - } - - function setLlmTestResult(message, tone = '') { - const target = byId('llm-test-result'); - if (!target) return; - target.textContent = message; - target.dataset.tone = tone; - } - - function updateLlmSnippet(status) { - const provider = byId('llm-provider').value; - const model = byId('llm-model').value; - byId('llm-env-snippet').value = llmSnippet(provider, model, Boolean(status.key_set)); - } - - function renderLlmSettings(status) { - const target = byId('llm-connection'); - target.replaceChildren(); - const defaults = status.default_models || {}; - const provider = status.provider || 'openai'; - const model = status.model || defaults[provider] || ''; - const providers = [...new Set([...Object.keys(defaults), provider])]; - const models = [...new Set([model, ...Object.values(defaults)].filter(Boolean))]; - const configured = Boolean(status.configured); - const extractionEnabled = Boolean(status.extractor_enabled); - const stateLabel = status.working ? 'verified' : (configured ? 'configured' : 'not configured'); - - const overview = node('div', 'llm-status-line'); - overview.append( - node('span', '', 'Provider · Model'), - node('span', `llm-status-badge ${configured ? 'ready' : 'muted'}`, stateLabel), - ); - - const pickerGrid = node('div', 'llm-picker-grid'); - const providerLabel = node('label', '', 'Provider'); - const providerSelect = node('select'); - providerSelect.id = 'llm-provider'; - providers.forEach(value => providerSelect.append(option(value, value, value === provider))); - providerLabel.htmlFor = providerSelect.id; - providerLabel.append(providerSelect); - const modelLabel = node('label', '', 'Model'); - const modelSelect = node('select'); - modelSelect.id = 'llm-model'; - models.forEach(value => modelSelect.append(option(value, value, value === model))); - modelLabel.htmlFor = modelSelect.id; - modelLabel.append(modelSelect); - pickerGrid.append(providerLabel, modelLabel); - - const keyState = node('p', 'llm-key-state', status.key_set ? 'API key set' : 'No API key set'); - keyState.append(node('span', '', ` · extractor: ${status.extractor || 'none'}`)); - const setupNote = node('p', 'llm-setup-note', 'Choose a provider and model for the copyable .env snippet. Update it locally, then restart Engraphis to apply the change.'); - const snippetLabel = node('label', 'llm-snippet-label', 'Local .env setup'); - const snippet = node('textarea', 'llm-env-snippet'); - snippet.id = 'llm-env-snippet'; - snippet.readOnly = true; - snippet.rows = 5; - snippet.value = llmSnippet(provider, model, Boolean(status.key_set)); - snippetLabel.htmlFor = snippet.id; - snippetLabel.append(snippet); - const copy = button('Copy', 'secondary-button', copyLlmSnippet); - copy.classList.add('llm-copy-button'); - const snippetWrap = node('div', 'llm-snippet-wrap'); - snippetWrap.append(snippetLabel, copy); - - const extraction = node('div', 'llm-status-line'); - extraction.append( - node('span', '', 'LLM extraction'), - node('span', `llm-status-badge ${extractionEnabled ? 'ready' : 'muted'}`, extractionEnabled ? 'ON' : 'OFF'), - ); - const extractionNote = node('p', 'llm-extraction-note', 'While ON, ingested memory content is sent to your configured provider for schema-validated extraction. OFF disables extraction transfers only; retention supervision is configured separately.'); - const retentionUsesLlm = text(status.retention_supervisor).toLowerCase() === 'llm'; - const retentionNote = node( - 'p', - 'llm-extraction-note', - retentionUsesLlm - ? 'Retention supervision is ON. New memories may send their title and a bounded excerpt to the configured provider.' - : 'Retention supervision is OFF.', - ); - const extractionActions = node('div', 'llm-actions'); - const turnOn = button('Turn on', 'primary-button', () => setLlmExtractor(true)); - turnOn.disabled = extractionEnabled || !configured; - const turnOff = button('Turn off', 'secondary-button', () => setLlmExtractor(false)); - turnOff.disabled = !extractionEnabled; - extractionActions.append(turnOn, turnOff); - - const testActions = node('div', 'llm-actions'); - testActions.append(button('Test connection', 'secondary-button', testLlm)); - const testResult = node('p', 'llm-test-result'); - testResult.id = 'llm-test-result'; - testResult.setAttribute('role', 'status'); - testResult.setAttribute('aria-live', 'polite'); - testActions.append(testResult); - - providerSelect.addEventListener('change', () => { - const defaultModel = defaults[providerSelect.value]; - if (defaultModel && models.includes(defaultModel)) modelSelect.value = defaultModel; - updateLlmSnippet(status); - }); - modelSelect.addEventListener('change', () => updateLlmSnippet(status)); - target.append(overview, pickerGrid, keyState, setupNote, snippetWrap, extraction, extractionNote, retentionNote, extractionActions, testActions); - } - - async function copyLlmSnippet() { - const snippet = byId('llm-env-snippet'); - try { - await navigator.clipboard.writeText(snippet.value); - showNotice('Copied the local .env setup snippet.'); - } catch (_) { - snippet.focus(); - snippet.select(); - if (document.execCommand('copy')) showNotice('Copied the local .env setup snippet.'); - else showNotice('Select the snippet and copy it manually.'); - } - } - - async function loadSettings() { - try { - state.license = await api('/license'); - updatePlanBadge(); - renderSidebarCta(); - } catch (_) {} - renderCloudAccountSettings(); - try { - renderLlmSettings(await api('/llm/status')); - } catch (error) { - byId('llm-connection').replaceChildren(empty(`Model status unavailable: ${error.message}`)); - } - } - - async function setLlmExtractor(enabled) { - if (enabled && !window.confirm(`Turn on LLM extraction? ${EXTERNAL_LLM_PRIVACY_NOTICE}`)) return; - setLlmTestResult(enabled ? 'Verifying the configured provider…' : 'Turning extraction off…'); - try { - const result = await api('/llm/extractor', { method: 'POST', body: { enabled } }); - await loadSettings(); - const state = result.extractor_enabled ? 'LLM extraction is on for new ingested memories.' : 'LLM extraction is off for new ingested memories.'; - setLlmTestResult(`${state}${result.persisted === false ? ' The restart setting could not be saved.' : ''}`, result.extractor_enabled ? 'ready' : 'muted'); - } catch (error) { - setLlmTestResult(`Could not change extraction: ${error.message}`, 'error'); - } - } - - async function testLlm() { - setLlmTestResult('Testing the configured model…'); - try { - const result = await api('/llm/test', { method: 'POST' }); - await loadSettings(); - if (result.ok) { - const suffix = result.auto_enabled ? ' Extraction is active for new ingested memories.' : ''; - setLlmTestResult(`Connected — ${result.provider}/${result.model}.${suffix}`, 'ready'); - } else { - setLlmTestResult(`Could not connect: ${result.error || 'Check the provider, model, API key, and network.'}`, 'error'); - } - } catch (error) { - setLlmTestResult(`Model connection failed: ${error.message}`, 'error'); - } - } - - function switchView(view, { pushHistory = true } = {}) { - const validViews = ['today', 'ask', 'library', 'connections', 'relations', 'provenance', 'manage']; - if (!validViews.includes(view)) view = 'today'; - if (pushHistory && state.view !== view) { - const url = new URL(location.href); - url.searchParams.set('view', view); - window.history.pushState({ view }, '', url); - } - state.view = view; - all('[data-view-panel]').forEach(panel => panel.classList.toggle('active', panel.dataset.viewPanel === view)); - all('[data-view]').forEach(control => { - const active = control.dataset.view === view; - control.classList.toggle('active', active); - if (active) control.setAttribute('aria-current', 'page'); - else control.removeAttribute('aria-current'); - }); - try { - localStorage.setItem('engraphis-ledger-view', view); - } catch (_) {} - graphLifecycle.sync(); - if (view === 'relations') loadGraph(); - if (view === 'provenance' && state.provenanceTab === 'audit') loadAudit(); - if (view === 'manage') { - loadSavings(state.refreshEpoch); - loadManageTab(state.manageTab); - } - window.scrollTo({ top: 0, behavior: 'instant' }); - const heading = byId(`${view}-title`); - if (heading) { - heading.setAttribute('tabindex', '-1'); - heading.focus({ preventScroll: true }); - } - } - - function applyTheme(theme) { - const valid = ['slate', 'midnight', 'paper', 'matrix']; - const selected = valid.includes(theme) ? theme : 'slate'; - document.body.dataset.theme = selected; - byId('theme-select').value = selected; - byId('sidebar-theme-select').value = selected; - try { - localStorage.setItem('engraphis-ledger-theme', selected); - localStorage.setItem('engraphis-theme', ({ slate: 'dark', paper: 'light', midnight: 'midnight', matrix: 'matrix' })[selected]); - } catch (_) {} - if (state.graphEngine) state.graphEngine.setThemeColors(graphThemeColors()); - } - - async function refreshBootstrap(preferred = '') { - const bootstrap = (await api('/bootstrap')) || {}; - renderUpdateBanner(bootstrap.update); - if (typeof bootstrap.version === 'string' && bootstrap.version.trim()) { - state.releaseVersion = bootstrap.version.trim(); - } - state.workspaces = bootstrap.workspaces || []; - state.license = bootstrap.license || state.license; - updatePlanBadge(); - renderSidebarCta(); - const select = byId('workspace-select'); - select.replaceChildren(); - state.workspaces.forEach(item => { - const name = workspaceName(item); - select.append(option(name, name)); - }); - if (!state.workspaces.length) { - select.append(option('', 'No workspace')); - select.disabled = true; - setConnection('Local engine connected · no workspace'); - state.workspace = ''; - state.project = ''; - workflow.selectWorkspace(''); - state.stats = {}; - state.libraryTotal = 0; - renderFirstMemoryJourney(); - renderWorkspaceNames(); - renderWorkspaceList(); - renderMetricValues({ memories: 0, total_rows: 0, workspaces: 0, sessions: 0 }); - byId('decision-list').replaceChildren(empty('Create a workspace in Manage to start reviewing memory.')); - byId('review-status').textContent = 'Create a workspace to load its review state.'; - byId('review-refresh').disabled = true; - const emptyActivity = node('tr'); - const emptyActivityCell = node('td', '', 'No workspace selected yet.'); - emptyActivityCell.colSpan = 5; - emptyActivity.append(emptyActivityCell); - byId('activity-body').replaceChildren(emptyActivity); - byId('proactive-list').replaceChildren(empty('Create a workspace to see proactive context.')); - byId('context-savings-persistent-value').textContent = '—'; - byId('context-savings-persistent-meta').textContent = 'Create a workspace to start tracking context savings.'; - byId('context-savings-persistent-rate').textContent = '—'; - return; - } - select.disabled = false; - let saved = preferred; - try { - saved = preferred || localStorage.getItem('engraphis-workspace') || ''; - } catch (_) {} - const names = state.workspaces.map(workspaceName); - const selected = names.includes(saved) - ? saved - : workspaceName([...state.workspaces].sort((a, b) => number(b.memories) - number(a.memories))[0]); - await selectWorkspace(selected); - setConnection('Local engine connected'); - } - - async function boot() { - byId('today-date').textContent = new Intl.DateTimeFormat(undefined, { dateStyle: 'long' }).format(new Date()); - let theme = 'slate'; - try { - theme = localStorage.getItem('engraphis-ledger-theme') || theme; - } catch (_) {} - applyTheme(theme); - try { - const entry = new URL(location.href); - // Classic links to this workspace's approval controls. Bootstrap still - // validates the name against the authorized workspace list. - await refreshBootstrap(entry.searchParams.get('workspace') || ''); - let view = 'today'; - try { - const saved = localStorage.getItem('engraphis-ledger-view'); - if (['today', 'ask', 'library', 'connections', 'relations', 'provenance', 'manage'].includes(saved)) view = saved; - } catch (_) {} - const urlView = new URL(location.href).searchParams.get('view'); - switchView(['today', 'ask', 'library', 'connections', 'relations', 'provenance', 'manage'].includes(urlView) ? urlView : view, { pushHistory: false }); - const requestedManageTab = entry.searchParams.get('tab'); - if (urlView === 'manage' && ['settings', 'sync', 'analytics'].includes(requestedManageTab)) { - switchManageTab(requestedManageTab); - } - } catch (error) { - if (error.status === 401 && await authenticateBrowser()) { - location.reload(); - return; - } - setConnection('Local engine unavailable', false); - showNotice(`Ledger could not connect: ${error.message}`); - } - } - - all('[data-view]').forEach(control => control.addEventListener('click', () => switchView(control.dataset.view))); - all('[data-go]').forEach(control => control.addEventListener('click', () => switchView(control.dataset.go))); - all('[data-manage]').forEach(control => control.addEventListener('click', () => { - switchView('manage'); - switchManageTab(control.dataset.manage); - })); - const planBadge = byId('plan-badge'); - if (planBadge) { - planBadge.addEventListener('click', event => { - if (event.currentTarget.dataset.opensAccount === 'true') return; - event.preventDefault(); - switchView('manage'); - switchManageTab('plans'); - }); - } - all('[data-provenance]').forEach(control => control.addEventListener('click', () => { - switchView('provenance'); - switchProvenanceTab(control.dataset.provenance); - })); - all('[data-provenance-tab]').forEach(control => control.addEventListener('click', () => switchProvenanceTab(control.dataset.provenanceTab))); - all('[data-manage-tab]').forEach(control => control.addEventListener('click', () => switchManageTab(control.dataset.manageTab))); - function wireTabKeyboard(selector, dataKey, activate) { - const controls = all(selector); - controls.forEach((control, index) => { - control.tabIndex = control.getAttribute('aria-selected') === 'true' ? 0 : (index ? -1 : 0); - control.addEventListener('keydown', event => { - const direction = event.key === 'ArrowRight' || event.key === 'ArrowDown' ? 1 - : event.key === 'ArrowLeft' || event.key === 'ArrowUp' ? -1 : 0; - let nextIndex = index; - if (event.key === 'Home') nextIndex = 0; - else if (event.key === 'End') nextIndex = controls.length - 1; - else if (direction) nextIndex = (index + direction + controls.length) % controls.length; - else return; - event.preventDefault(); - const next = controls[nextIndex]; - next.focus(); - activate(next.dataset[dataKey]); - }); - }); - } - wireTabKeyboard('[data-graph-tab]', 'graphTab', setGraphTab); - wireTabKeyboard('[data-provenance-tab]', 'provenanceTab', switchProvenanceTab); - wireTabKeyboard('[data-manage-tab]', 'manageTab', switchManageTab); - window.addEventListener('popstate', event => { - const view = event.state && event.state.view - ? event.state.view - : new URL(location.href).searchParams.get('view') || 'today'; - switchView(view, { pushHistory: false }); - }); - - byId('workspace-select').addEventListener('change', event => selectWorkspace(event.target.value)); - byId('ask-form').addEventListener('submit', askMemory); - const updateJevRecallControls = () => { - const enabled = byId('ask-jev-assisted').checked; - byId('ask-jev-remote').disabled = !enabled; - byId('ask-jev-classification').disabled = !enabled; - if (!enabled) byId('ask-jev-remote').checked = false; - }; - ['ask-jev-assisted', 'ask-jev-remote', 'ask-jev-classification'].forEach(id => { - byId(id).addEventListener('change', () => { - updateJevRecallControls(); - beginScopedRequest('ask'); - askRequests.reset(); - byId('answer-panel').replaceChildren(empty('Submit the question with the current Jev settings.')); - byId('retrieval-list').replaceChildren(empty('The next answer will include its matching retrieval preview.')); - byId('ask-status').textContent = 'Jev settings changed. Submit the question to apply them.'; - }); - }); - updateJevRecallControls(); - byId('review-refresh').addEventListener('click', () => { void loadReviewInbox(); }); - byId('library-filter').addEventListener('input', () => { - resetMoveSelection(); - window.clearTimeout(librarySearchTimer); - // Invalidate immediately: an earlier query must not paint while the new one debounces. - beginScopedRequest('library'); - state.libraryLoading = Boolean(state.workspace); - renderLibraryPaging(); - librarySearchTimer = window.setTimeout(() => refreshLibrary(), 250); - }); - byId('library-type').addEventListener('change', () => refreshLibrary()); - byId('library-previous').addEventListener('click', () => refreshLibrary(state.libraryPage - 1)); - byId('library-next').addEventListener('click', () => refreshLibrary(state.libraryPage + 1)); - byId('library-refresh').addEventListener('click', () => refreshLibrary()); - byId('library-selection-toggle').addEventListener('click', () => { - state.librarySelecting = !state.librarySelecting; - resetMoveSelection(); - renderLibrary(); - }); - byId('library-move').addEventListener('click', openMemoryMove); - byId('memory-move-target').addEventListener('change', invalidateMemoryMovePreview); - byId('memory-move-preview-button').addEventListener('click', () => { void previewMemoryMove(); }); - byId('memory-move-form').addEventListener('submit', applyMemoryMove); - byId('memory-move-cancel').addEventListener('click', closeMemoryMove); - byId('memory-move-dialog').addEventListener('cancel', event => { - event.preventDefault(); - if (!state.memoryMove || !state.memoryMove.applying) closeMemoryMove(); - }); - byId('first-memory-add').addEventListener('click', () => { - if (!state.workspace) { - switchView('manage'); - switchManageTab('workspaces'); - byId('create-workspace-form').hidden = false; - byId('new-workspace-name').focus(); - return; - } - switchView('library'); - openEditor(); - }); - byId('new-memory-button').addEventListener('click', () => openEditor()); - byId('editor-close').addEventListener('click', closeEditor); - byId('editor-cancel').addEventListener('click', closeEditor); - byId('editor-refresh').addEventListener('click', refreshEditorVersions); - byId('memory-editor').addEventListener('submit', saveMemory); - byId('editor-memory-importance').addEventListener('input', event => { - const effective = graphSliderResponseValue( - 'editor-memory-importance', event.target.value, 0.5, - ); - event.target.setAttribute('aria-valuetext', `${effective.toFixed(2)} importance`); - const importanceOutput = byId('editor-memory-importance-output'); - if (importanceOutput) importanceOutput.textContent = effective.toFixed(2); - }); - byId('import-button').addEventListener('click', () => byId('import-files').click()); - byId('import-files').addEventListener('change', event => importFiles(event.target.files)); - byId('obsidian-import-button').addEventListener('click', openObsidianImport); - byId('obsidian-import-close').addEventListener('click', () => byId('obsidian-import-dialog').close()); - byId('obsidian-preview').addEventListener('click', previewObsidianImport); - byId('obsidian-cancel').addEventListener('click', cancelObsidianImport); - byId('obsidian-import-form').addEventListener('submit', runObsidianImport); - byId('obsidian-source-mode').addEventListener('change', updateDocumentImportMode); - byId('obsidian-vault-id').addEventListener('change', applySelectedDocumentSource); - byId('obsidian-import-files').addEventListener('change', () => invalidateDocumentImportPreview()); - byId('obsidian-import-folder').addEventListener('change', () => { - prefillNewSourceLabelFromFolder(); - invalidateDocumentImportPreview(); - }); - [ - ['obsidian-workspace', 'input'], - ['obsidian-repo', 'input'], - ['obsidian-session', 'input'], - ['obsidian-scope', 'change'], - ['obsidian-memory-type', 'change'], - ['obsidian-vault-label', 'input'], - ['obsidian-conflict', 'change'], - ].forEach(([id, eventName]) => { - byId(id).addEventListener(eventName, () => invalidateDocumentImportPreview()); - }); - byId('obsidian-report-filter').addEventListener('change', () => renderObsidianReport(obsidianImport.job || obsidianImport.preview)); - - all('[data-graph-tab]').forEach(control => control.addEventListener('click', () => setGraphTab(control.dataset.graphTab))); - byId('graph-fit').addEventListener('click', () => state.graphEngine && state.graphEngine.fit()); - byId('graph-reheat').addEventListener('click', () => state.graphEngine && state.graphEngine.reheat()); - byId('graph-clear-focus').addEventListener('click', () => { - if (state.graphEngine) state.graphEngine.clearFocus(); - }); - byId('graph-freeze').addEventListener('click', () => { - state.graphFrozen = !state.graphFrozen; - setGraphSwitch('graph-freeze', state.graphFrozen); - if (state.graphEngine) state.graphEngine.freeze(state.graphFrozen); - saveGraphPreferences(); - }); - byId('graph-flow').addEventListener('click', event => { - const on = event.currentTarget.getAttribute('aria-checked') !== 'true'; - setGraphSwitch('graph-flow', on); - if (state.graphEngine) state.graphEngine.setSettings({ flow: on }); - clearGraphSavedView(); - saveGraphPreferences(); - }); - byId('graph-labels').addEventListener('click', event => { - const on = event.currentTarget.getAttribute('aria-checked') !== 'true'; - setGraphSwitch('graph-labels', on); - if (state.graphEngine) state.graphEngine.setSettings({ labels: on }); - clearGraphSavedView(); - saveGraphPreferences(); - }); - byId('graph-flow-speed').addEventListener('input', event => { - const speed = graphValueInRange('graph-flow-speed', event.target.value, 45); - const effectiveSpeed = graphSliderResponseValue('graph-flow-speed', speed, 45); - byId('graph-flow-speed').value = String(speed); - byId('graph-flow-speed-output').value = String(Math.round(speed)); - byId('graph-flow-speed-output').textContent = String(Math.round(speed)); - if (state.graphEngine) state.graphEngine.setSettings({ flowSpeed: effectiveSpeed }); - clearGraphSavedView(); - scheduleGraphPreferencesSave(); - }); - byId('graph-search').addEventListener('input', event => searchGraph(event.target.value)); - byId('graph-repo-filter').addEventListener('input', event => { - if (state.graphEngine) state.graphEngine.setRepoFilter(event.target.value); - clearGraphSavedView(); - scheduleGraphPreferencesSave(); - // Repository-scoped payloads need a server reload, but do not issue a 20k-node request - // for every keystroke. The current input is still reflected immediately by the renderer. - if (state.graphMode === 'full') { - const candidate = (event.target.value || '').trim(); - if (candidate && !validatedGraphRepository(candidate)) { - cancelGraphRepositoryReload(); - return; - } - } - if (state.graphIncludeCode || state.graphMode === 'full') scheduleGraphRepositoryReload(); - }); - all('[data-graph-preset-choice]').forEach(control => control.addEventListener('click', () => { - const preset = control.dataset.graphPresetChoice; - /* Every node is its own presentation: selecting the Every node chip loads the - complete LOD scene; the Show-all toggle remains the canonical exit that - restores overview filters. Other layout presets while in Every-node re-run - the seeded Every-node layout without leaving the presentation. */ - if (preset === 'every') { - if (state.graphMode !== 'full') { - byId('graph-preset').value = preset; - clearGraphSavedView(); - syncGraphChoices(); - saveGraphPreferences(); - /* Every node means every node: entering the map clears the unlinked/degree - filters that the overview uses, but remembers them so leaving restores the - person's overview exactly as they had configured it. */ - if (!state.everyPriorFilters) { - state.everyPriorFilters = { - minDegree: Number(byId('graph-min-degree').value) || 0, - unlinked: byId('graph-show-unlinked').getAttribute('aria-pressed') === 'true', - }; - } - setGraphMinDegree(0, false); - setGraphShowUnlinked(true, false); - cancelGraphRepositoryReload(); - state.graphMode = 'full'; - updateGraphModeControls(); - loadGraph({ force: true }); - } else { - // Clicking Every node while already in Every-node exits back to overview, - // mirroring the old Show-all toggle but now via the layout chip. - if (state.everyPriorFilters) { - const prior = state.everyPriorFilters; - state.everyPriorFilters = null; - setGraphMinDegree(prior.minDegree, false); - setGraphShowUnlinked(prior.unlinked, false); - } - byId('graph-preset').value = 'galaxy'; - cancelGraphRepositoryReload(); - state.graphMode = 'overview'; - clearGraphSavedView(); - syncGraphChoices(); - saveGraphPreferences(); - updateGraphModeControls(); - loadGraph({ force: true }); - } - return; - } - if (state.graphMode === 'full') { - byId('graph-preset').value = preset; - clearGraphSavedView(); - syncGraphChoices(); - saveGraphPreferences(); - if (state.graphEngine && state.graphEngine.setPreset) { - const result = state.graphEngine.setPreset(preset); - if (result && typeof result === 'object') syncGraphTuning(result); - } else { - syncGraphTuning(graphPresetTuning(preset)); - } - updateGraphModeControls(); - if (state.graphEngine) state.graphEngine.setSizeBy(graphSizeBy()); - graphLifecycle.sync(); - return; - } - const resumeLayout = state.graphFrozen; - byId('graph-preset').value = preset; - if (state.graphEngine && resumeLayout) { - // Freeze is the safe default for arranging nodes by hand. Selecting a named layout is an - // explicit request to run physics, so make that transition visible and leave the switch - // truthful; the person can freeze the settled arrangement again when they are happy. - state.graphFrozen = false; - setGraphSwitch('graph-freeze', false); - state.graphEngine.freeze(false); - } - let settings = graphPresetTuning(preset); - if (state.graphEngine) settings = state.graphEngine.setPreset(preset); - syncGraphTuning(settings); - updateGraphModeControls(); - if (state.graphEngine) state.graphEngine.setSizeBy(graphSizeBy()); - graphLifecycle.sync(); - clearGraphSavedView(); - syncGraphChoices(); - saveGraphPreferences(); - if (resumeLayout) showNotice('Layout applied. Simulation resumed — freeze it to lock node positions.'); - })); - all('[data-graph-style-choice]').forEach(control => control.addEventListener('click', () => { - byId('graph-style').value = control.dataset.graphStyleChoice; - if (state.graphEngine) state.graphEngine.setStyle(control.dataset.graphStyleChoice); - clearGraphSavedView(); - syncGraphChoices(); - saveGraphPreferences(); - })); - all('[data-graph-color-choice]').forEach(control => control.addEventListener('click', () => { - byId('graph-color').value = control.dataset.graphColorChoice; - if (state.graphEngine) state.graphEngine.setColorBy(control.dataset.graphColorChoice); - clearGraphSavedView(); - syncGraphChoices(); - saveGraphPreferences(); - })); - all('[data-graph-palette-choice]').forEach(control => control.addEventListener('click', () => { - const palette = control.dataset.graphPaletteChoice; - byId('graph-palette').value = palette; - applyGraphPalette(palette); - clearGraphSavedView(); - syncGraphChoices(); - saveGraphPreferences(); - showNotice(`${control.textContent.trim()} palette applied to the graph.`); - })); - byId('graph-min-degree').addEventListener('input', event => { - setGraphMinDegree(event.target.value); - clearGraphSavedView(); - scheduleGraphPreferencesSave(); - }); - byId('graph-show-unlinked').addEventListener('click', event => { - setGraphShowUnlinked(event.currentTarget.getAttribute('aria-pressed') !== 'true'); - clearGraphSavedView(); - saveGraphPreferences(); - if (state.graphMode !== 'full') loadGraph({ force: true }); - }); - byId('graph-tune-min-degree').addEventListener('input', event => { - setGraphMinDegree(event.target.value); - clearGraphSavedView(); - scheduleGraphPreferencesSave(); - }); - byId('graph-depth').addEventListener('input', event => { - setGraphDepth(event.target.value); - clearGraphSavedView(); - scheduleGraphPreferencesSave(); - }); - GRAPH_TUNING.forEach(item => byId(item.id).addEventListener('input', event => { - const value = setGraphTuningControl(item, event.target.value); - const effectiveValue = graphSliderResponseValue( - item.id, value, graphSliderResponseBaseline(item), - ); - if (state.graphEngine) state.graphEngine.setSettings({ [item.key]: effectiveValue }); - clearGraphSavedView(); - scheduleGraphPreferencesSave(); - })); - GRAPH_SPACETIME_TUNING.forEach(item => byId(item.id).addEventListener('input', event => { - setGraphSpacetimeControl(item, event.target.value); - /* Controls use human-scale values (G=100, mass=160, spring=32), while the engine API is - normalized around 1. Apply the same conversion used during graph creation on every live - input event; passing the raw slider value would immediately clamp G to 8 and mass to 16. */ - if (state.graphEngine) { - const settings = graphSpacetimeEngineSettings(); - state.graphEngine.setSettings({ [item.key]: settings[item.key] }); - } - clearGraphSavedView(); - scheduleGraphPreferencesSave(); - })); - byId('graph-orbits-pause').addEventListener('click', event => { - state.graphOrbitPaused = event.currentTarget.getAttribute('aria-checked') !== 'true'; - setGraphSwitch('graph-orbits-pause', state.graphOrbitPaused); - if (state.graphEngine) state.graphEngine.setSettings({ orbitPaused: state.graphOrbitPaused }); - clearGraphSavedView(); - saveGraphPreferences(); - }); - all('[data-graph-layer]').forEach(control => control.addEventListener('click', () => { - const layers = graphLayerState(); - const layer = control.dataset.graphLayer; - const next = !layers[layer]; - if (layer === 'code' && next && state.graphMode === 'full' - && !validatedGraphRepository(byId('graph-repo-filter').value)) { - showNotice('Choose an exact repository before adding its code overlay to All nodes.'); - byId('graph-repo-filter').focus(); - return; - } - layers[layer] = next; - const previousIncludeCode = state.graphIncludeCode; - state.graphIncludeCode = layers.code === true; - setGraphLayers(layers); - if (state.graphEngine) state.graphEngine.setLayers(layers); - clearGraphSavedView(); - saveGraphPreferences(); - if (previousIncludeCode !== state.graphIncludeCode) loadGraph({ force: true }); - })); - all('[data-graph-saved-view]').forEach(control => control.addEventListener('click', () => applyGraphView(control.dataset.graphSavedView))); - byId('graph-save-view').addEventListener('click', saveCurrentGraphView); - byId('graph-reset-tuning').addEventListener('click', resetGraphTuning); - byId('graph-retry').addEventListener('click', retryGraphLoad); - byId('graph-bridges').addEventListener('change', event => { - clearGraphSavedView(); - if (state.graphEngine) state.graphEngine.setBridges(event.target.checked); - saveGraphPreferences(); - }); - byId('graph-collapse').addEventListener('change', event => { - clearGraphSavedView(); - if (state.graphEngine) state.graphEngine.setCollapse(event.target.checked ? 'auto' : false); - saveGraphPreferences(); - }); - byId('graph-as-of').addEventListener('change', event => { - clearGraphSavedView(); - if (state.graphEngine) state.graphEngine.setAsOf(graphAsOfTimestamp()); - saveGraphPreferences(); - loadGraph({ force: true }); - }); - byId('graph-ghosts').addEventListener('change', event => { - clearGraphSavedView(); - if (state.graphEngine) state.graphEngine.setGhosts(event.target.checked); - saveGraphPreferences(); - }); - byId('graph-size').addEventListener('change', event => { - clearGraphSavedView(); - if (state.graphEngine) state.graphEngine.setSizeBy(graphSizeBy()); - saveGraphPreferences(); - }); - const graphExportWrap = byId('graph-export').closest('.graph-export-wrap'); - byId('graph-export').addEventListener('click', () => { - setGraphExportMenuOpen(byId('graph-export-menu').hidden); - }); - graphExportWrap.addEventListener('keydown', event => { - if (event.key !== 'Escape' || byId('graph-export-menu').hidden) return; - event.preventDefault(); - event.stopPropagation(); - setGraphExportMenuOpen(false, true); - }); - document.addEventListener('focusin', event => { - if (!byId('graph-export-menu').hidden && !graphExportWrap.contains(event.target)) { - setGraphExportMenuOpen(false); - } - }); - document.addEventListener('pointerdown', event => { - if (!byId('graph-export-menu').hidden && !graphExportWrap.contains(event.target)) { - setGraphExportMenuOpen(false); - } - }); - byId('graph-export-png').addEventListener('click', () => { - setGraphExportMenuOpen(false, true); - exportGraphPng(); - }); - byId('graph-export-json').addEventListener('click', () => { - setGraphExportMenuOpen(false, true); - exportGraphJson(); - }); - byId('graph-connections-close').addEventListener('click', closeGraphConnections); - byId('graph-connections-dialog').addEventListener('close', cancelGraphConnectionMemoryLoad); - byId('graph-connections-focus').addEventListener('click', () => { - const id = state.graphConnectionsFocusId; - if (!id) return; - closeGraphConnections(); - focusGraphNode(id, state.graphConnectionsFocusLabel || 'Selected entity'); - }); - byId('graph-connections-dialog').addEventListener('click', event => { - if (event.target === event.currentTarget) closeGraphConnections(); - }); - restoreGraphPreferences(); - syncGraphChoices(); - - byId('why-form').addEventListener('submit', whySearch); - byId('timeline-form').addEventListener('submit', event => timelineSearch(event, false)); - byId('supersession-form').addEventListener('submit', event => timelineSearch(event, true)); - byId('verify-receipts').addEventListener('click', verifyReceipts); - byId('export-receipts').addEventListener('click', exportReceipts); - - byId('create-workspace-toggle').addEventListener('click', () => { - byId('create-workspace-form').hidden = !byId('create-workspace-form').hidden; - if (!byId('create-workspace-form').hidden) byId('new-workspace-name').focus(); - }); - window.addEventListener('pagehide', flushGraphPreferencesSave); - byId('create-workspace-form').addEventListener('submit', createWorkspace); - byId('consolidate-form').addEventListener('submit', previewConsolidation); - byId('consolidate-commit').addEventListener('click', commitConsolidation); - ['consolidate-structured'].forEach(id => { - byId(id).addEventListener('change', invalidateConsolidationReview); - }); - byId('billing-select').addEventListener('change', renderPlans); - byId('dashboard-select').addEventListener('change', event => { - location.assign(event.target.value === 'classic' ? '/classic' : '/'); - }); - byId('theme-select').addEventListener('change', event => applyTheme(event.target.value)); - byId('sidebar-theme-select').addEventListener('change', event => applyTheme(event.target.value)); - boot(); -})(); +(() => { + 'use strict'; + + const apiRoot = `${location.origin}/api`; + const state = { + workspace: '', + project: '', + workspaces: [], + stats: {}, + memories: [], + libraryTotal: 0, + libraryNextCursor: null, + libraryCursors: [null], + libraryPage: 0, + libraryLoading: false, + librarySelecting: false, + moveMemoryIds: new Set(), + memoryMove: null, + selectedMemory: '', + editorMemory: null, + editorSession: null, + memoryHistory: null, + editorReturnFocus: null, + view: 'today', + provenanceTab: 'belief', + savingsPreset: 'all', + manageTab: 'workspaces', + refreshEpoch: 0, + graphWorkspace: '', + graphData: null, + graphDataMode: 'overview', + graphGalaxyQuality: false, + graphDataPreset: 'galaxy', + graphDataIncludeCode: false, + graphDataShowUnlinked: false, + graphDataAsOf: null, + graphDataRepo: '', + graphMeta: null, + graphMode: 'overview', + graphShowUnlinked: true, + graphEngine: null, + graphLoadPromise: null, + graphLoadWorkspace: '', + graphLoadMode: '', + graphLoadIncludeCode: false, + graphLoadShowUnlinked: false, + graphLoadAsOf: null, + graphLoadRepo: '', + graphLoadKey: '', + graphLoadRequest: 0, + graphRetryPending: false, + graphLoadController: null, + graphConnectionsRequest: 0, + graphConnectionsController: null, + graphMetrics: {}, + graphFrozen: false, + graphOrbitPaused: false, + graphSpacetimeOverlay: null, + graphIncludeCode: false, + graphSavedView: '', + consolidationReview: null, + reviewCsrf: '', + hostedLoaded: new Set(), + analyticsStarting: new Set(), + analyticsJobs: new Map(), + analyticsResults: new Map(), + analyticsStartErrors: new Map(), + scopedRequests: Object.create(null), + scopedControllers: Object.create(null), + syncStatus: null, + license: null, + releaseVersion: '', + }; + + const byId = id => document.getElementById(id); + const memoryTitle = window.EngraphisWorkflow.title; + const all = selector => [...document.querySelectorAll(selector)]; + const text = value => value == null ? '' : String(value); + const number = value => Number.isFinite(Number(value)) ? Number(value) : 0; + const NOTICE_DURATION_MS = 3000; + let noticeTimer = null; + let localSessionPromise = null; + let graphRepoLoadTimer = null; + let librarySearchTimer = null; + const CLOUD_SYNC_PRIVACY_NOTICE = 'Cloud Sync encrypts eligible shared-workspace changes end-to-end before they leave this device. Engraphis Cloud cannot read their contents; secret and session-scoped memories stay local.'; + const EXTERNAL_LLM_PRIVACY_NOTICE = 'Memory text is sent to your configured LLM provider for processing under that provider’s terms. The provider must read that text to return extracted facts.'; + const truncate = (value, length = 260) => { + const source = text(value).trim(); + return source.length > length ? `${source.slice(0, length - 1)}…` : source; + }; + const empty = (message, className = 'empty-state') => { + const node = document.createElement('p'); + node.className = className; + node.textContent = message; + return node; + }; + const node = (tag, className = '', content = '') => { + const element = document.createElement(tag); + if (className) element.className = className; + if (content !== '') element.textContent = text(content); + return element; + }; + const button = (label, className, action) => { + const control = node('button', className, label); + control.type = 'button'; + control.addEventListener('click', action); + return control; + }; + const option = (value, label, selected = false) => { + const item = node('option', '', label); + item.value = value; + item.selected = selected; + return item; + }; + const query = (name = state.workspace) => `workspace=${encodeURIComponent(name || '')}`; + const memoryQuery = (workspace = state.workspace, project = state.project) => query(workspace) + + (project ? '&repo=' + encodeURIComponent(project) : ''); + const beginScopedRequest = kind => { + if (state.scopedControllers[kind]) state.scopedControllers[kind].abort(); + const controller = new AbortController(); + state.scopedControllers[kind] = controller; + const generation = number(state.scopedRequests[kind]) + 1; + state.scopedRequests[kind] = generation; + return { + kind, + generation, + workspace: state.workspace, + project: state.project, + epoch: state.refreshEpoch, + signal: controller.signal, + }; + }; + const isCurrentScopedRequest = request => Boolean(request + && request.workspace === state.workspace + && request.project === state.project + && request.epoch === state.refreshEpoch + && state.scopedRequests[request.kind] === request.generation); + const invalidateScopedRequests = () => { + Object.values(state.scopedControllers).forEach(controller => controller.abort()); + state.scopedControllers = Object.create(null); + Object.keys(state.scopedRequests).forEach(kind => { + state.scopedRequests[kind] = number(state.scopedRequests[kind]) + 1; + }); + }; + const GRAPH_INITIAL_NODE_LIMIT = 1500; + const GRAPH_INITIAL_EDGE_LIMIT = 3000; + const GRAPH_ALL_NODE_LIMIT = 20_000; + const GRAPH_ALL_EDGE_LIMIT = 200_000; + const GRAPH_LOAD_TIMEOUT_MS = 60_000; + // Cold complete scenes can legitimately spend ~25s in the bounded server projection + // before the renderer receives its payload. Keep enough headroom for the response body, + // graph assets, and renderer readiness instead of turning a healthy cold request into a + // generic client timeout. + const GRAPH_FULL_LOAD_TIMEOUT_MS = 90_000; + const GRAPH_CONNECTION_MEMORIES_TIMEOUT_MS = 8_000; + const GRAPH_PREFERENCES_KEY = 'engraphis-ledger-graph-preferences-v1'; + const GRAPH_PHYSICS_VERSION = 6; + const GRAPH_CUSTOM_VIEW_KEY = 'engraphis-ledger-graph-custom-view-v1'; + const GRAPH_LAYERS = ['temporal', 'entity', 'causal', 'semantic', 'code']; + const GRAPH_DEFAULT_LAYERS = { temporal: true, entity: true, causal: true, semantic: true, code: false }; + const GRAPH_TUNING = [ + { id: 'graph-repel', key: 'repel', fallback: 100 }, + { id: 'graph-link', key: 'link', fallback: 8 }, + { id: 'graph-gravity', key: 'gravity', fallback: 72 }, + { id: 'graph-node-size', key: 'size', fallback: 3 }, + { id: 'graph-text-size', key: 'font', fallback: 12 }, + { id: 'graph-line-width', key: 'linkw', fallback: 0.72, precision: 2 }, + { id: 'graph-label-density', key: 'labelDensity', fallback: 24 }, + ]; + const GRAPH_SPACETIME_TUNING = [ + { id: 'graph-gravitational-constant', key: 'gravitationalConstant', fallback: 60 }, + { id: 'graph-black-hole-mass', key: 'blackHoleMass', fallback: 96 }, + { id: 'graph-local-gravitational-constant', key: 'localGravitationalConstant', fallback: 60 }, + { id: 'graph-space-damping', key: 'damping', fallback: 1, precision: 1 }, + { id: 'graph-spring-stiffness', key: 'springStiffness', fallback: 32 }, + ]; + const GRAPH_PRESET_TUNING = { + original: { repel: 120, link: 30, gravity: 14, font: 13, size: 3, linkw: 1, labelDensity: 40 }, + compact: { repel: 42, link: 20, gravity: 26, font: 12, size: 3, linkw: 0.7, labelDensity: 30 }, + communities: { repel: 48, link: 16, gravity: 48, font: 12, size: 3, linkw: 0.72, labelDensity: 24 }, + galaxy: { repel: 100, link: 8, gravity: 72, font: 12, size: 3, linkw: 0.72, labelDensity: 24 }, + radial: { repel: 68, link: 26, gravity: 12, font: 13, size: 3, linkw: 0.75, labelDensity: 55 }, + constellation: { repel: 34, link: 16, gravity: 38, font: 12, size: 3, linkw: 0.65, labelDensity: 35 }, + }; + const GRAPH_SAVED_VIEWS = { + operations: { + preset: 'compact', style: 'cyber', color: 'connections', palette: 'contrast', + layers: { temporal: false, entity: true, causal: true, semantic: false, code: false }, + minDegree: 2, depth: 1, showUnlinked: false, includeCode: false, + }, + schema: { + preset: 'communities', style: 'cyber', color: 'community', palette: 'theme', + layers: { ...GRAPH_DEFAULT_LAYERS }, minDegree: 1, depth: 2, showUnlinked: true, includeCode: false, + }, + people: { + preset: 'radial', style: 'galaxy', color: 'community', palette: 'aurora', + layers: { temporal: false, entity: true, causal: false, semantic: true, code: false }, + minDegree: 1, depth: 2, showUnlinked: false, includeCode: false, + }, + code: { + preset: 'constellation', style: 'cyber', color: 'type', palette: 'ocean', + layers: { temporal: false, entity: true, causal: false, semantic: true, code: true }, + minDegree: 1, depth: 2, showUnlinked: false, includeCode: true, + }, + }; + const GRAPH_PRESET_LABELS = { + original: 'Spacious', + compact: 'Compact', + communities: 'Islands', + radial: 'Radial', + constellation: 'Constellation', + galaxy: 'Galaxy gravity', + every: 'Every node', + }; + const GRAPH_STYLE_NOTES = { + cyber: 'Iridescent PVD over graphite — cyan, violet, and magenta across each node.', + galaxy: 'Deep anodized alloy with a cool blue-violet directional sheen.', + solar: 'Brushed copper faces with amber bezels and warm radial grain.', + classic: 'Neutral satin gunmetal with a restrained cool steel edge.', + }; + const GRAPH_LOD_STYLE_NOTES = { + cyber: 'High-contrast cyan, violet and magenta points tuned for dense LOD views.', + galaxy: 'Cool blue-violet points separate clusters clearly across wide zoom ranges.', + solar: 'Warm copper and amber points keep dense relation fields legible.', + classic: 'Restrained steel points prioritize structure and long-session readability.', + }; + const GRAPH_CUSTOM_PALETTE = { + person_or_concept: '#8d82e3', + mention: '#5ba1a6', + hashtag: '#c9a15b', + email: '#8eb3e6', + organization: '#d48173', + location: '#7ebf8e', + memory: '#5ba1a6', + repo: '#c9a15b', + file: '#8eb3e6', + }; + const relative = value => { + const raw = typeof value === 'number' && value < 1e12 ? value * 1000 : value; + const time = typeof raw === 'number' ? raw : Date.parse(raw); + if (!Number.isFinite(time)) return 'stored locally'; + const seconds = Math.max(0, Math.round((Date.now() - time) / 1000)); + if (seconds < 60) return 'just now'; + if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`; + if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`; + if (seconds < 604800) return `${Math.floor(seconds / 86400)}d ago`; + return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(time); + }; + const errorMessage = (payload, status) => { + const detail = payload && (payload.detail || payload.error); + if (typeof detail === 'string') return detail; + if (detail && typeof detail.error === 'string') return detail.error; + return `Request failed (${status})`; + }; + + function localDashboardUrl() { + return /^(localhost|127(?:\.\d{1,3}){3}|\[::1\])$/i.test(location.hostname); + } + + async function ensureLocalBrowserSession() { + if (!localSessionPromise) { + localSessionPromise = (async () => { + const controller = new AbortController(); + const timer = window.setTimeout(() => controller.abort(), 8000); + try { + const response = await fetch(`${apiRoot}/auth/local-session`, { + method: 'POST', cache: 'no-store', signal: controller.signal, + headers: { 'X-Engraphis-Browser-Session': '1' }, + }); + const session = await response.json(); + if (!response.ok || !session.authenticated) throw new Error('Local session unavailable'); + state.reviewCsrf = text(session.review_csrf_token); + return true; + } catch (_) { + const error = new Error('Local Engraphis could not connect. Reload this page to retry.'); + error.localBrowser = true; + throw error; + } finally { + window.clearTimeout(timer); + } + })().finally(() => { localSessionPromise = null; }); + } + return localSessionPromise; + } + + async function api(path, options = {}) { + const { timeoutMs = 30_000, signal, ...requestOptions } = options; + const controller = new AbortController(); + const abort = () => controller.abort(); + if (signal) { + if (signal.aborted) abort(); + else signal.addEventListener('abort', abort, { once: true }); + } + let timedOut = false; + const timer = window.setTimeout(() => { timedOut = true; controller.abort(); }, timeoutMs); + const init = { ...requestOptions, signal: controller.signal, headers: { ...(options.headers || {}) } }; + init.headers['X-Engraphis-Browser-Session'] = '1'; + if (init.body && !(init.body instanceof FormData) && typeof init.body !== 'string') { + init.headers['Content-Type'] = 'application/json'; + init.body = JSON.stringify(init.body); + } + try { + const request = async () => { + const response = await fetch(`${apiRoot}${path}`, init); + const payload = await response.json().catch(error => { + if (controller.signal.aborted) throw error; + return null; + }); + return { response, payload }; + }; + let { response, payload } = await request(); + // Only this middleware code guarantees the rejected request never ran. + // A hosted account's 401 must never trigger a local retry or hide its error. + if (response.status === 401 && payload?.code === 'local_browser_session_required' + && payload.local_browser === true) { + await ensureLocalBrowserSession(); + if (init.headers['X-Engraphis-Review-CSRF']) { + init.headers['X-Engraphis-Review-CSRF'] = state.reviewCsrf; + } + ({ response, payload } = await request()); + } + if (!response.ok) { + const error = new Error(errorMessage(payload, response.status)); + error.status = response.status; + error.code = payload && ((payload.detail && payload.detail.code) || payload.code); + error.localBrowser = payload && payload.local_browser; + throw error; + } + return payload; + } catch (error) { + if (timedOut) throw new Error('The request timed out. Please try again.'); + throw error; + } finally { + window.clearTimeout(timer); + if (signal) signal.removeEventListener('abort', abort); + } + } + + function promptBrowserToken(message = '') { + const dialog = byId('browser-auth-dialog'); + const form = byId('browser-auth-form'); + const input = byId('browser-auth-token'); + const error = byId('browser-auth-error'); + const cancel = byId('browser-auth-cancel'); + if (!dialog || !form || !input || !error || !cancel) return Promise.resolve(''); + + error.textContent = message; + error.hidden = !message; + input.value = ''; + const returnFocus = document.activeElement; + + return new Promise(resolve => { + let settled = false; + const cleanup = () => { + form.removeEventListener('submit', submit); + cancel.removeEventListener('click', dismiss); + dialog.removeEventListener('cancel', dismiss); + dialog.removeEventListener('close', closed); + }; + const finish = value => { + if (settled) return; + settled = true; + cleanup(); + input.value = ''; + if (dialog.open) dialog.close(); + if (returnFocus && typeof returnFocus.focus === 'function') returnFocus.focus(); + resolve(value); + }; + const submit = event => { + event.preventDefault(); + const value = input.value.trim(); + if (!value) { + error.textContent = 'Enter your access token.'; + error.hidden = false; + input.focus(); + return; + } + finish(value); + }; + const dismiss = event => { + if (event) event.preventDefault(); + finish(''); + }; + const closed = () => finish(''); + + form.addEventListener('submit', submit); + cancel.addEventListener('click', dismiss); + dialog.addEventListener('cancel', dismiss); + dialog.addEventListener('close', closed); + if (!dialog.open) dialog.showModal(); + input.focus(); + }); + } + + async function authenticateBrowser(error = null) { + if (error?.localBrowser === true || (error?.localBrowser !== false && localDashboardUrl())) { + showNotice('Local Engraphis could not connect. Reload this page to retry.'); + return false; + } + let token = ''; + let failure = ''; + try { + const fragment = new URLSearchParams(location.hash.slice(1)); + token = fragment.get('token') || ''; + if (token) history.replaceState(null, '', `${location.pathname}${location.search}`); + } catch (_) {} + while (true) { + if (!token) token = await promptBrowserToken(failure); + if (!token) return false; + let submitted = token; + token = ''; + try { + const session = await api('/auth/session', { + method: 'POST', + body: { token: submitted }, + }); + state.reviewCsrf = text(session && session.review_csrf_token); + submitted = ''; + return true; + } catch (error) { + submitted = ''; + failure = error.status === 401 + ? 'That access token was not accepted. Check it with the person who set up Engraphis and try again.' + : error.message; + showNotice(`Sign-in failed: ${failure}`); + } + } + } + + async function reviewCsrfToken() { + if (localDashboardUrl()) await ensureLocalBrowserSession(); + if (state.reviewCsrf) return state.reviewCsrf; + const response = await fetch(`${location.origin}/dashboard/review/csrf`, { + headers: { 'X-Engraphis-Browser-Session': '1' }, + }); + const payload = await response.json().catch(() => null); + if (!response.ok || !payload || !payload.review_csrf_token) { + const error = new Error(errorMessage(payload, response.status)); + error.status = response.status; + throw error; + } + state.reviewCsrf = text(payload.review_csrf_token); + return state.reviewCsrf; + } + + async function approveForPrompt(memory) { + if (!memory || !memory.id) return; + const provenance = memory.provenance || {}; + const reviewState = provenance.review_state || 'pending'; + const reason = window.prompt( + `Why is this ${reviewState} record safe to include in model context?`, + ); + if (reason === null) return; + if (!reason.trim()) { + showNotice('A non-empty review reason is required.'); + return; + } + if (!window.confirm( + 'Approve this record for model context? This creates a fresh, audited approved memory; the reviewed source remains preserved.', + )) return; + try { + const csrf = await reviewCsrfToken(); + const response = await fetch(`${location.origin}/dashboard/review/approve`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Engraphis-Browser-Session': '1', + 'X-Engraphis-Review-CSRF': csrf, + }, + body: JSON.stringify({ memory_id: memory.id, reason: reason.trim() }), + }); + const payload = await response.json().catch(() => null); + if (!response.ok) { + const error = new Error(errorMessage(payload, response.status)); + error.status = response.status; + throw error; + } + showNotice('Approved successor created. The reviewed source remains in the audit trail.'); + await selectWorkspace(state.workspace); + if (payload.id) await selectMemory(payload.id); + } catch (error) { + showNotice(`Could not approve this memory: ${error.message}`); + } + } + + let graphAssetsPromise = null; + let graphAssetsController = null; + let graphAllAssetsPromise = null; + let graphAllAssetsController = null; + let graphAssetsRetry = 0; + const graphAssetSource = source => graphAssetsRetry ? `${source}&retry=${graphAssetsRetry}` : source; + function loadScript(src, globalName, signal) { + if (window[globalName]) return Promise.resolve(); + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + let settled = false; + const cleanup = () => { + if (signal) signal.removeEventListener('abort', abort); + }; + const finish = (callback, value) => { + if (settled) return; + settled = true; + cleanup(); + callback(value); + }; + const abort = () => { + script.remove(); + const error = new Error(`loading ${globalName} was aborted`); + error.name = 'AbortError'; + finish(reject, error); + }; + script.src = src; + script.dataset.engraphisGraphAsset = 'true'; + script.onload = () => window[globalName] + ? finish(resolve) + : finish(reject, new Error(`${globalName} did not register`)); + script.onerror = () => finish(reject, new Error(`could not load ${src}`)); + if (signal) { + if (signal.aborted) { + abort(); + return; + } + signal.addEventListener('abort', abort, { once: true }); + } + document.head.append(script); + }); + } + + function ensureGraphAllAsset() { + if (window.EngraphisEveryGraph) return Promise.resolve(); + if (!graphAllAssetsPromise) { + const controller = new AbortController(); + const attempt = loadScript( + graphAssetSource('/v2-assets/engraphis-graph-every.js?v=20260927-unmerged-readiness-3'), + 'EngraphisEveryGraph', controller.signal, + ); + graphAllAssetsPromise = attempt; + graphAllAssetsController = controller; + attempt.catch(() => { + if (graphAllAssetsPromise === attempt) releaseGraphAllAssetsAttempt(attempt); + }); + } + return graphAllAssetsPromise; + } + + function ensureGraphAssets(loadAll = false) { + /* The complete profile is an independent worker/WebGL renderer. Galaxy is the exception: + its solar-system view needs the authoritative hierarchical orbit integrator, so a full + Galaxy request uses the quality engine with the complete payload instead of the static + all-node worker. The factory decision is data-sensitive, not toolbar-sensitive: the + Every-node chip changes the preset to "every" before the scene arrives, and a fast click + can race the overview load. Keep both candidates available so an authored star/planet + scene cannot select an engine whose asset is still in flight. */ + if (loadAll) { + return Promise.all([ensureGraphAllAsset(), ensureGraphAssets(false)]); + } + const coreReady = window.ForceGraph && window.EngraphisGraph && window.EngraphisSpacetime; + if (!coreReady && !graphAssetsPromise) { + const controller = new AbortController(); + const attempt = loadScript( + graphAssetSource('/v2-assets/vendor/d3.min.js?v=20260727-final'), + 'd3', controller.signal, + ).then(() => loadScript( + graphAssetSource('/v2-assets/vendor/force-graph.min.js?v=20260727-final'), + 'ForceGraph', controller.signal, + )).then(() => loadScript( + graphAssetSource('/v2-assets/engraphis-graph.js?v=20260927-unmerged-readiness-3'), + 'EngraphisGraph', controller.signal, + )).then(() => loadScript( + graphAssetSource('/v2-assets/engraphis-spacetime.js?v=20260927-unmerged-readiness-3'), + 'EngraphisSpacetime', controller.signal, + )); + graphAssetsPromise = attempt; + graphAssetsController = controller; + attempt.catch(() => { + /* A fetched script can load successfully while failing to execute (for example, a + stale cached parse error). Retire that URL immediately so the next explicit Reload + advances the retry query instead of replaying the same broken response forever. */ + if (graphAssetsPromise === attempt) releaseGraphAssetsAttempt(attempt); + }); + } + const core = coreReady ? Promise.resolve() : graphAssetsPromise; + return core; + } + + function releaseGraphAssetsAttempt(attempt) { + // A browser can leave a script fetch pending indefinitely. Do not let that stale promise + // become a permanent single-flight lock: remove its fetches and give the next explicit + // reload a unique URL so it cannot join the browser's already-stalled request. + if (!attempt || graphAssetsPromise !== attempt) return; + graphAssetsPromise = null; + const controller = graphAssetsController; + graphAssetsController = null; + graphAssetsRetry = Math.min(graphAssetsRetry + 1, 10); + if (controller) controller.abort(); + all('script[data-engraphis-graph-asset="true"]').forEach(script => script.remove()); + } + + function releaseGraphAllAssetsAttempt(attempt) { + if (!attempt || graphAllAssetsPromise !== attempt) return; + graphAllAssetsPromise = null; + const controller = graphAllAssetsController; + graphAllAssetsController = null; + graphAssetsRetry = Math.min(graphAssetsRetry + 1, 10); + if (controller) controller.abort(); + } + + function showNotice(message) { + const text = String(message || ''); + if (noticeTimer !== null) { + clearTimeout(noticeTimer); + noticeTimer = null; + } + const textEl = byId('notice-text'); + if (textEl) textEl.textContent = text; + const banner = byId('notice-banner'); + if (!banner) return; + banner.textContent = text; + banner.hidden = !text; + if (!text) { + banner.removeAttribute('data-tone'); + return; + } + banner.dataset.tone = /\b(could not|unavailable|failed|broken|error)\b/i.test(text) ? 'error' : 'info'; + noticeTimer = setTimeout(() => { + noticeTimer = null; + if (banner.textContent !== text) return; + banner.textContent = ''; + banner.hidden = true; + if (textEl) textEl.textContent = ''; + }, NOTICE_DURATION_MS); + } + + function updateReleaseUrl(value) { + const fallback = 'https://github.com/Coding-Dev-Tools/engraphis/releases'; + try { + const url = new URL(value || fallback, location.href); + return ['http:', 'https:'].includes(url.protocol) ? url.href : fallback; + } catch (_) { + return fallback; + } + } + + // A compromised or misconfigured license server could otherwise push a crafted + // upgrade_url (e.g. `javascript:...`) that executes script when the plan link is + // clicked. Only http(s) survives; anything else — including a relative/empty value — + // returns '' so the caller falls back to an inert '#' href. + function safeUrl(value) { + if (!value || typeof value !== 'string') return ''; + try { + const url = new URL(value, location.href); + return ['http:', 'https:'].includes(url.protocol) ? url.href : ''; + } catch (_) { + return ''; + } + } + + function licenseAccessState(license = state.license) { + const value = license && license.access_state; + return ['active', 'trial', 'trial_expired', 'lapsed'].includes(value) ? value : 'inactive'; + } + + function licensePlanKey(license = state.license) { + const value = String((license && license.plan) || 'local').toLowerCase(); + return value === 'pro' || value === 'team' ? value : ''; + } + + function licenseTrialAvailable(license = state.license) { + return Boolean(license && license.trial && license.trial.available + && licenseAccessState(license) === 'inactive' && license.plan_source === 'local'); + } + + function licenseHasHostedAccess(license = state.license) { + const access = licenseAccessState(license); + return access === 'active' || access === 'trial'; + } + + function withCtaAttribution(raw, content, medium = 'product') { + const safe = safeUrl(raw); + if (!safe) return ''; + try { + const url = new URL(safe, location.href); + url.searchParams.set('utm_source', 'engraphis'); + url.searchParams.set('utm_medium', medium); + url.searchParams.set('utm_campaign', 'pro_conversion'); + url.searchParams.set('utm_content', content || 'plans'); + return url.href; + } catch (_) { + return safe; + } + } + + function hostedPlanUrl(plan, trial, interval = 'monthly', content = plan) { + const cadence = interval === 'annual' ? 'annual' : 'monthly'; + const license = state.license || {}; + const raw = license[`${plan}_${cadence}_upgrade_url`] + || license[`${plan}_upgrade_url`] || license.upgrade_url; + const safe = safeUrl(raw); + if (!safe) return ''; + try { + const url = new URL(safe, location.href); + url.searchParams.set('plan', plan); + url.searchParams.set('interval', cadence); + if (trial) url.searchParams.set('trial', plan); + if (!url.hash) url.hash = 'billing'; + return withCtaAttribution(url.href, content); + } catch (_) { + return safe; + } + } + + function hostedAccountUrl(content = 'account') { + const license = state.license || {}; + return withCtaAttribution(license.account_url || license.upgrade_url, content); + } + + function licenseTrialDays(plan = 'pro') { + const trial = (state.license && state.license.trial) || {}; + const days = (trial.days_by_plan || {})[plan] ?? (plan === 'pro' ? trial.trial_days : null); + return Number.isSafeInteger(days) && days > 0 ? days : null; + } + + function hostedCta(plan = 'pro', content = 'plans', interval = 'monthly') { + const stateName = licenseAccessState(); + const currentPlan = licensePlanKey(); + const name = plan === 'team' ? 'Team' : 'Pro'; + if (stateName === 'lapsed') { + return { label: 'Update billing', href: hostedAccountUrl(content), kind: 'account' }; + } + if (licenseHasHostedAccess() && (currentPlan === plan + || (currentPlan === 'team' && plan === 'pro'))) { + return { + label: currentPlan === 'team' && plan === 'team' ? 'Open Team Cloud' : 'Open Engraphis Cloud', + href: hostedAccountUrl(content), + kind: 'account', + }; + } + const trial = licenseTrialAvailable() && stateName === 'inactive'; + const days = licenseTrialDays(plan); + return { + label: trial ? `Start ${days ? `${days}-day ` : ''}${name} trial` : `Subscribe to ${name}`, + href: hostedPlanUrl(plan, trial, interval, content), + kind: trial ? 'trial' : 'subscribe', + }; + } + + function updatePlanBadge() { + const badge = byId('plan-badge'); + if (!badge || !state.license) return; + const access = licenseAccessState(); + const plan = licensePlanKey(); + const trial = licenseTrialAvailable(); + const label = access === 'active' ? plan.toUpperCase() + : access === 'trial' ? 'TRIAL' + : access === 'lapsed' ? 'BILLING' + : trial ? 'TRY PRO' : 'GET PRO'; + badge.hidden = access === 'inactive' && trial; + const aria = licenseHasHostedAccess() ? 'Open Engraphis Cloud account' + : access === 'lapsed' ? 'Update billing in Plans and billing' + : trial ? `${hostedCta('pro', 'header').label} in Plans and billing` + : 'Subscribe to Pro in Plans and billing'; + badge.textContent = label; + badge.setAttribute('aria-label', aria); + badge.title = aria; + const cta = hostedCta(plan || 'pro', 'header'); + const opensAccount = cta.kind === 'account' && Boolean(cta.href); + badge.href = opensAccount ? cta.href : '#'; + badge.target = opensAccount ? '_blank' : ''; + badge.rel = opensAccount ? 'noopener' : ''; + badge.dataset.opensAccount = String(opensAccount); + } + + function renderSidebarCta() { + const copy = byId('sidebar-pro-copy'); + const detail = byId('sidebar-pro-detail'); + const link = byId('sidebar-pro-cta'); + if (!copy || !detail || !link || !state.license) return; + const renderFeatureCtas = () => { + [ + ['analytics-pro-cta', 'analytics', 'pro'], + ['automation-pro-cta', 'automation', 'pro'], + ['team-cloud-cta', 'team', 'team'], + ].forEach(([id, content, plan]) => { + const featureLink = byId(id); + if (!featureLink) return; + const featureCta = hostedCta(plan, content); + featureLink.textContent = featureCta.label; + featureLink.href = featureCta.href || '#'; + featureLink.setAttribute('aria-disabled', featureCta.href ? 'false' : 'true'); + }); + }; + if (licenseHasHostedAccess()) { + const cta = hostedCta(licensePlanKey() || 'pro', 'sidebar'); + copy.textContent = 'Thank you for supporting Engraphis.'; + detail.textContent = 'Your subscription funds hosted infrastructure and ongoing development.'; + link.hidden = false; + link.textContent = cta.label; + link.href = cta.href || '#'; + link.setAttribute('aria-disabled', cta.href ? 'false' : 'true'); + renderFeatureCtas(); + return; + } + const cta = hostedCta('pro', 'sidebar'); + copy.textContent = 'Support continued Engraphis development with Pro.'; + detail.textContent = 'Cloud Sync, Analytics, and managed memory maintenance.'; + link.hidden = false; + link.textContent = cta.label; + link.href = cta.href || '#'; + link.setAttribute('aria-disabled', cta.href ? 'false' : 'true'); + link.dataset.proCta = 'sidebar'; + renderFeatureCtas(); + } + + function renderCloudAccountSettings() { + const target = byId('cloud-account-settings'); + if (!target) return; + target.replaceChildren(); + const plan = licensePlanKey() || 'pro'; + const cta = hostedCta(plan, 'settings'); + const live = licenseHasHostedAccess(); + const detail = live + ? 'Your hosted account is connected. Manage membership in Cloud, or edit this workspace’s hosted maintenance policy locally.' + : licenseAccessState() === 'lapsed' + ? 'Your hosted subscription needs attention. Update billing in Engraphis Cloud to restore hosted features.' + : 'Open Engraphis Cloud to start a trial, subscribe, or manage a connected hosted account.'; + const action = node('a', 'primary-button', cta.label); + action.href = cta.href || '#'; + if (cta.href) { + action.target = '_blank'; + action.rel = 'noopener'; + } else { + action.addEventListener('click', event => { + event.preventDefault(); + showNotice('Connect this installation to Engraphis Cloud to open hosted account settings.'); + }); + } + const actions = node('div', 'automation-policy-actions'); + actions.append(action); + if (live) actions.append(button('Configure hosted policy', 'secondary-button', () => switchManageTab('automation'))); + target.append(node('p', 'automation-policy-note', detail), actions); + } + + function renderUpdateBanner(update) { + const target = byId('update-banner'); + if (!target) return; + target.replaceChildren(); + if (!update || !update.enabled || !update.update_available || !update.latest) { + target.hidden = true; + return; + } + let dismissed = ''; + try { + dismissed = localStorage.getItem('engraphis-update-dismissed') || ''; + } catch (_) {} + if (dismissed === update.latest) { + target.hidden = true; + return; + } + const copy = node('div', 'update-copy'); + copy.append( + node('strong', '', 'Update available'), + document.createTextNode(` — Engraphis ${text(update.latest)} is out (you have ${text(update.current || '?')}). Upgrade with `), + node('code', '', 'pip install -U engraphis'), + document.createTextNode('.'), + ); + const actions = node('div', 'update-actions'); + const release = node('a', 'text-button', 'View release →'); + release.href = updateReleaseUrl(update.url); + release.target = '_blank'; + release.rel = 'noopener'; + const dismiss = button('Dismiss', 'update-dismiss', () => { + try { + localStorage.setItem('engraphis-update-dismissed', text(update.latest)); + } catch (_) {} + target.hidden = true; + target.replaceChildren(); + }); + actions.append(release, dismiss); + target.append(copy, actions); + target.hidden = false; + } + + function setConnection(message, healthy = true) { + const status = byId('connection-status'); + if (status) status.textContent = message; + byId('home-engine-status').textContent = 'Dashboard: ' + message + '. Checked at ' + new Date().toLocaleTimeString() + '.'; + const dot = document.querySelector('.status-dot'); + if (dot) dot.classList.toggle('unhealthy', !healthy); + } + + function setDeploymentMode(mode) { + const el = byId('deployment-mode-badge'); + if (!el) return; + const isLocal = mode === 'local'; + el.textContent = isLocal ? 'LOCAL' : 'HOSTED'; + el.title = isLocal + ? 'Local mode: no hosted cloud configured. Data stays on this machine.' + : 'Hosted mode: connected to Engraphis Cloud.'; + el.classList.toggle('mode-local', isLocal); + el.classList.toggle('mode-hosted', !isLocal); + el.hidden = false; + } + + function memoryType(memory) { + return memory.memory_type || memory.mtype || 'semantic'; + } + + function memoryTime(memory) { + return memory.ingested_at || memory.valid_from || memory.last_access; + } + + const memoryOwnershipLabels = new WeakMap(); + + function refreshMemoryOwnershipLabels() { + all('[data-memory-ownership]').forEach(label => { + const owner = memoryOwnershipLabels.get(label); + if (owner) label.textContent = workflow.ownership(owner.memory, owner.workspace); + }); + } + + function memoryMeta(memory) { + const meta = node('div', 'memory-meta'); + const ownership = node('span', 'memory-ownership', workflow.ownership(memory, state.workspace)); + ownership.dataset.memoryOwnership = ''; + // Bind the authorized read context, never the selected project preference. + memoryOwnershipLabels.set(ownership, { memory, workspace: state.workspace }); + meta.append( + node('span', 'type-chip', memoryType(memory)), + ownership, + node('span', '', relative(memoryTime(memory))), + ); + if (memory.pinned) meta.append(node('span', '', 'pinned')); + return meta; + } + + function renderMetricValues(stats) { + const values = [ + stats.memories, + stats.total_rows, + stats.workspaces || state.workspaces.length, + stats.sessions, + ]; + all('#metrics strong').forEach((element, index) => { + element.textContent = values[index] == null ? '—' : number(values[index]).toLocaleString(); + }); + } + + function renderTypeBars(stats) { + const target = byId('type-bars'); + target.replaceChildren(); + const types = stats.by_type || {}; + const entries = Object.entries(types).sort((a, b) => number(b[1]) - number(a[1])); + if (!entries.length) { + target.append(empty('No typed memories yet.')); + return; + } + const max = Math.max(1, ...entries.map(([, value]) => number(value))); + entries.forEach(([name, value]) => { + const row = node('div', 'type-bar'); + row.append(node('span', '', name)); + const bar = document.createElement('progress'); + bar.max = max; + bar.value = number(value); + bar.setAttribute('aria-label', `${name}: ${number(value)}`); + row.append(bar, node('strong', '', number(value).toLocaleString())); + target.append(row); + }); + } + + function savingsQuery(preset = 'all') { + if (preset === 'current' && state.releaseVersion) { + return `?release_version=${encodeURIComponent(state.releaseVersion)}`; + } + if (preset === '7d') return `?from_ts=${encodeURIComponent(Date.now() / 1000 - 604800)}`; + return ''; + } + + function savingsScopeLabel(payload) { + if (payload && payload.scope && payload.scope.workspace === 'all') { + return ` across ${number(payload.workspace_count).toLocaleString()} visible workspaces`; + } + return ''; + } + + function formatSavingsTokens(value) { + return Math.max(0, Math.round(number(value))).toLocaleString(); + } + + function savingsRatio(value) { + return Math.max(0, Math.min(1, number(value))); + } + + function savingsCounts(payload) { + const estimate = payload && payload.estimated ? payload.estimated : {}; + return { + estimate, + eligible: number(estimate.eligible_receipt_count), + excluded: number(estimate.excluded_receipt_count) + + number(estimate.unclassified_receipt_count) + + number(estimate.invalid_estimate_count), + }; + } + + function renderSavingsOverview(payload) { + const { estimate, eligible, excluded } = savingsCounts(payload); + const scopeLabel = savingsScopeLabel(payload); + const persistentValue = byId('context-savings-persistent-value'); + const persistentMeta = byId('context-savings-persistent-meta'); + const persistentRate = byId('context-savings-persistent-rate'); + const setPersistent = (value, meta, rate = '—') => { + if (persistentValue) persistentValue.textContent = value; + if (persistentMeta) persistentMeta.textContent = meta; + if (persistentRate) persistentRate.textContent = rate; + }; + if (!eligible) { + setPersistent('—', excluded ? `${excluded} excluded or unclassified deliveries so far.` : 'Tracking starts with the first eligible delivery.'); + return; + } + const ratio = savingsRatio(estimate.savings_ratio); + setPersistent( + formatSavingsTokens(estimate.saved_tokens), + `Across ${eligible.toLocaleString()} eligible context deliveries${scopeLabel} · ${estimate.confidence || 'unknown'} confidence`, + `${(ratio * 100).toFixed(1)}% estimated reduction`, + ); + } + + function renderSavingsDetail(payload) { + const target = byId('savings-detail'); + if (!target) return; + const { estimate, eligible, excluded } = savingsCounts(payload); + const scopeLabel = savingsScopeLabel(payload); + target.replaceChildren(); + const header = node('div', 'savings-detail-header'); + header.append( + node('strong', 'savings-number', `${formatSavingsTokens(estimate.saved_tokens)} tokens`), + node('span', '', eligible + ? `${eligible} eligible deliveries${scopeLabel} · ${(number(estimate.savings_ratio) * 100).toFixed(1)}% estimated reduction` + : 'No eligible estimates in this range.'), + ); + const presets = node('div', 'savings-presets'); + [ + ['since', 'Since tracking started'], + ['current', 'Current release'], + ['7d', 'Last 7 days'], + ['all', 'All time'], + ].forEach(([value, label]) => { + const control = button(label, '', () => { + state.savingsPreset = value; + loadAudit(); + }); + control.classList.toggle('active', state.savingsPreset === value); + control.setAttribute('aria-pressed', String(state.savingsPreset === value)); + presets.append(control); + }); + header.append(presets); + target.append(header); + if (eligible) { + target.append(node('p', 'field-note', `Baseline ${formatSavingsTokens(estimate.baseline_tokens)} → emitted ${formatSavingsTokens(estimate.emitted_tokens)} · confidence: ${text(estimate.confidence || 'unknown')}`)); + target.append(node('p', 'field-note', 'Packed context is packing savings; adaptive history is estimated avoided prompt context.')); + const basisTitle = node('h3', '', 'Savings basis'); + const basisRows = node('div', 'savings-breakdown'); + (estimate.by_basis || []).forEach(row => { + const item = node('div', 'savings-breakdown-row'); + item.append( + node('span', '', `${text(row.basis || 'unclassified').replaceAll('_', ' ')} · ${text(row.confidence || 'unknown')}`), + node('span', '', `${formatSavingsTokens(row.baseline_tokens)} → ${formatSavingsTokens(row.emitted_tokens)} · ${formatSavingsTokens(row.saved_tokens)} saved`), + ); + basisRows.append(item); + }); + target.append(basisTitle, basisRows); + if ((estimate.by_token_counter || []).length) { + target.append(node('h3', '', 'Token counters')); + const counterRows = node('div', 'savings-breakdown'); + (estimate.by_token_counter || []).forEach(row => { + const item = node('div', 'savings-breakdown-row'); + item.append( + node('span', '', text(row.token_counter || 'unknown')), + node('span', '', `${formatSavingsTokens(row.saved_tokens)} saved · ${row.receipt_count || 0} eligible deliver${number(row.receipt_count) === 1 ? 'y' : 'ies'}`), + ); + counterRows.append(item); + }); + target.append(counterRows); + } + } + target.append(node('p', 'savings-note', `${excluded} excluded or unclassified deliver${excluded === 1 ? 'y' : 'ies'}. Measures estimated prompt-context reduction; it does not measure provider billing.`)); + } + + function renderReviewInbox(result) { + const target = byId('decision-list'); + target.replaceChildren(); + const items = result.items; + const partial = result.has_more || result.truncated; + byId('review-status').textContent = `${items.length} review ${items.length === 1 ? 'item' : 'items'} shown.` + + (partial ? ' This is a partial list; more records may need review.' : ''); + if (!items.length) { + target.append(empty(partial ? 'No review items in this sample. More records may remain.' + : 'No records currently need review in this context.')); + return; + } + items.forEach((item, index) => { + const card = node(item.id ? 'button' : 'article', 'decision-card memory-link-card'); + if (item.id) { + card.type = 'button'; + card.dataset.memoryId = item.id; + card.addEventListener('click', () => openMemory(item)); + } + const states = []; + if (item.quarantined) states.push('Quarantined'); + if (item.conflict_with) states.push('Conflicting evidence'); + if (item.review_state === 'pending') states.push('Source review pending'); + const header = node('div', 'decision-card-header'); + header.append( + node('span', 'tag', states.join(' · ') || 'Review required'), + node('h3', '', 'Inspect review item ' + (index + 1)), + ); + card.append(header, node('p', '', item.excerpt || 'Open the record to review its source and history.')); + target.append(card); + }); + } + + async function loadReviewInbox() { + if (!state.workspace) return; + const request = beginScopedRequest('reviews'); + const target = byId('decision-list'); + target.replaceChildren(empty('Loading review state…')); + target.setAttribute('aria-busy', 'true'); + byId('review-status').textContent = ''; + byId('review-refresh').disabled = true; + try { + const result = await api('/review-inbox?' + memoryQuery(request.workspace, request.project) + '&limit=6', { signal: request.signal }); + if (!isCurrentScopedRequest(request)) return; + if (!result || !Array.isArray(result.items)) throw new Error('No review state was returned.'); + renderReviewInbox(result); + } catch (error) { + if (!isCurrentScopedRequest(request)) return; + target.replaceChildren(empty('Review state is unavailable: ' + error.message)); + byId('review-status').textContent = 'Review status is unknown. Refresh reviews to try again.'; + } finally { + if (isCurrentScopedRequest(request)) { + target.setAttribute('aria-busy', 'false'); + byId('review-refresh').disabled = false; + } + } + } + + function auditItems(payload) { + if (Array.isArray(payload)) return payload; + return payload.audit || payload.entries || payload.records || payload.events || []; + } + + function receiptItems(payload) { + if (Array.isArray(payload)) return payload; + return payload.receipts || payload.entries || payload.records || []; + } + + function provenanceTimestampMs(item) { + // Audit rows use seconds (`ts`), while receipts use milliseconds (`ts_ms`). + // Normalize before merging so both the newest-first order and 120-row cap are + // chronological across the two independently paginated feeds. + const raw = item && (item.ts_ms ?? item.ts ?? item.timestamp ?? item.created_at); + const numeric = Number(raw); + if (Number.isFinite(numeric)) return numeric < 1e12 ? numeric * 1000 : numeric; + const parsed = Date.parse(raw); + return Number.isFinite(parsed) ? parsed : 0; + } + + function auditField(item, ...names) { + for (const name of names) { + if (item && item[name] != null && item[name] !== '') return item[name]; + } + return ''; + } + + function renderActivity(items) { + const target = byId('activity-body'); + target.replaceChildren(); + if (!items.length) { + const row = node('tr'); + const cell = node('td', '', 'No audit entries yet.'); + cell.colSpan = 5; + row.append(cell); + target.append(row); + return; + } + items.slice(0, 8).forEach(item => { + const row = node('tr'); + const timestamp = auditField(item, 'ts', 'timestamp', 'created_at', 'valid_from'); + const values = [ + relative(timestamp), + auditField(item, 'actor', 'source') || 'local operator', + auditField(item, 'action', 'operation', 'event') || 'recorded', + auditField(item, 'scope', 'workspace', 'target') || state.workspace, + truncate(auditField(item, 'hash', 'id', 'receipt_id'), 14) || '—', + ]; + values.forEach(value => row.append(node('td', '', value))); + target.append(row); + }); + } + + function renderProactive(memories, unavailableMessage = '') { + const target = byId('proactive-list'); + target.replaceChildren(); + if (!memories.length) { + target.append(empty(unavailableMessage || 'No proactive context is available.')); + return; + } + memories.slice(0, 5).forEach(memory => { + const row = node('button', 'compact-row'); + row.type = 'button'; + if (memory.id) row.dataset.memoryId = memory.id; + row.append( + node('strong', '', memoryTitle(memory)), + node('span', '', truncate(memory.summary || memory.content, 140)), + ); + row.addEventListener('click', () => openMemory(memory)); + target.append(row); + }); + } + + async function loadStats(workspace, epoch) { + try { + const stats = await api(`/stats?${query(workspace)}`); + if (epoch !== state.refreshEpoch) return; + state.stats = stats; + renderMetricValues(stats); + renderTypeBars(stats); + renderFirstMemoryJourney(); + } catch (error) { + if (epoch === state.refreshEpoch) byId('type-bars').replaceChildren(empty('Workspace composition is unavailable.')); + throw error; + } + } + + async function loadSavings(epoch) { + try { + const payload = await api(`/context-savings${savingsQuery()}`); + if (epoch !== state.refreshEpoch) return; + renderSavingsOverview(payload); + } catch (error) { + if (epoch !== state.refreshEpoch) return; + const persistentValue = byId('context-savings-persistent-value'); + const persistentMeta = byId('context-savings-persistent-meta'); + const persistentRate = byId('context-savings-persistent-rate'); + if (persistentValue) persistentValue.textContent = 'Unavailable'; + if (persistentMeta) persistentMeta.textContent = 'Receipt-backed estimate could not be loaded.'; + if (persistentRate) persistentRate.textContent = '—'; + } + } + + async function loadMemories(workspace, epoch, page = 0) { + resetMoveSelection(); + const request = beginScopedRequest('library'); + const params = new URLSearchParams({ workspace, limit: '100' }); + if (request.project) params.set('repo', request.project); + const search = byId('library-filter').value.trim(); + const type = byId('library-type').value; + if (search) params.set('q', search); + if (type) params.set('mtype', type); + const cursor = state.libraryCursors[page]; + if (cursor) params.set('cursor', cursor); + state.libraryLoading = true; + renderLibraryPaging(); + try { + const payload = await api(`/memories?${params}`, { signal: request.signal }); + if (epoch !== state.refreshEpoch || !isCurrentScopedRequest(request)) return; + state.memories = payload.memories || []; + state.libraryTotal = number(payload.total_count == null ? state.memories.length : payload.total_count); + state.libraryNextCursor = payload.next_cursor || null; + state.libraryPage = page; + if (!page) state.libraryCursors = [null]; + if (state.libraryNextCursor) state.libraryCursors[page + 1] = state.libraryNextCursor; + renderLibrary(); + renderFirstMemoryJourney(); + } catch (error) { + if (!isCurrentScopedRequest(request)) return; + if (cursor && error.status === 409 && error.code === 'cursor_stale') { + state.libraryCursors = [null]; + showNotice('Memory changed. Showing the first page with your filters preserved.'); + return await loadMemories(workspace, epoch); + } + byId('library-list').replaceChildren(empty(`Library is unavailable: ${error.message}`)); + throw error; + } finally { + if (isCurrentScopedRequest(request)) { + state.libraryLoading = false; + renderLibraryPaging(); + } + } + } + + function renderLibraryPaging() { + byId('library-previous').disabled = state.libraryLoading || state.libraryPage === 0; + byId('library-next').disabled = state.libraryLoading || !state.libraryNextCursor; + byId('library-refresh').disabled = state.libraryLoading || !state.workspace; + byId('library-list').setAttribute('aria-busy', String(state.libraryLoading)); + byId('library-list').querySelectorAll('.memory-card').forEach(card => { + card.disabled = state.librarySelecting && state.libraryLoading; + }); + renderMoveSelection(); + } + + function refreshLibrary(page = 0) { + window.clearTimeout(librarySearchTimer); + if (!state.workspace) return; + loadMemories(state.workspace, state.refreshEpoch, page).catch(error => showNotice(error.message)); + } + + function renderFirstMemoryJourney() { + const journey = byId('first-memory-journey'); + if (!journey) return; + journey.hidden = Boolean(state.workspace) && (state.stats.memories == null + || number(state.stats.memories) > 0 || state.libraryTotal > 0); + byId('first-memory-add').textContent = state.workspace ? 'Add your first memory' : 'Create your first workspace'; + } + + async function loadToday(workspace, epoch) { + const [proactiveResult, auditResult] = await Promise.allSettled([ + api(`/proactive?${query(workspace)}&k=8`), + api(`/audit?${query(workspace)}&limit=12`), + ]); + if (epoch !== state.refreshEpoch) return; + const proactive = proactiveResult.status === 'fulfilled' + ? (proactiveResult.value.memories || proactiveResult.value.results || []) + : []; + renderProactive(proactive, proactiveResult.status === 'rejected' + ? 'Strongest memories are unavailable. Try refreshing this workspace.' : ''); + renderActivity(auditResult.status === 'fulfilled' ? auditItems(auditResult.value) : []); + if (auditResult.status === 'rejected') { + const cell = byId('activity-body').querySelector('td'); + if (cell) cell.textContent = 'Activity is unavailable. Try refreshing this workspace.'; + } + } + + function renderWorkspaceNames() { + all('[data-workspace-name]').forEach(element => { + element.textContent = state.workspace || 'this workspace'; + }); + } + + function workspaceName(item) { + return typeof item === 'string' ? item : item.name; + } + function resetScopedPanels() { + const messages = { + 'answer-panel': 'Ask a question to receive a grounded answer with citations.', + 'retrieval-list': 'Retrieved memories will appear here.', + 'why-result': 'Trace a claim to inspect live and superseded support.', + 'timeline-result': 'Search a topic to inspect its temporal history.', + 'supersession-list': 'Search a topic to compare closed and current records.', + 'audit-list': 'Open Audit to load this workspace’s records and receipts.', + 'savings-detail': 'Open Audit to load this workspace’s receipt-backed estimate.', + 'analytics-result': 'Open this tab to check availability.', + 'automation-result': 'Open this tab to check availability.', + 'team-result': 'Open this tab to check connection state.', + }; + Object.entries(messages).forEach(([id, message]) => { + const target = byId(id); + if (target) target.replaceChildren(empty(message)); + }); + } + + const graphLifecycle = window.EngraphisGraphLifecycle.create({ + isVisible: () => state.view === 'relations', + onStatus: ({ visible, capability }) => { + const status = byId('graph-lifecycle-status'); + status.dataset.lifecycle = capability === 'drawing' ? (visible ? 'active' : 'paused') : capability; + status.textContent = capability === 'unloaded' ? 'The graph loads when you open Explore.' + : capability === 'physics' ? 'This renderer can pause physics only when you leave.' + : capability === 'unsupported' ? 'Automatic pause is unavailable for this renderer.' + : visible ? 'Select a node to inspect its evidence. Drawing pauses when you leave.' + : 'Graph drawing paused. Camera and selection are kept.'; + }, + onError: () => showNotice('A graph resource could not be paused or released. Reload this page if it stays active.'), + }); + const narrowSidebar = window.matchMedia('(max-width: 860px)'); + const syncSidebarOptions = () => { byId('sidebar-options').open = !narrowSidebar.matches; }; + if (narrowSidebar.addEventListener) narrowSidebar.addEventListener('change', syncSidebarOptions); + else narrowSidebar.addListener(syncSidebarOptions); + syncSidebarOptions(); + + const processingControls = window.EngraphisProcessingControls.create(api); + const askRequests = window.EngraphisAskRequests.create({ renderAnswer, renderPreview }); + const workflow = window.EngraphisWorkflow.create({ + api, + onProjectChange: () => { void selectWorkspace(state.workspace); }, + onNavigate: view => switchView(view), + onNewMemory: () => { switchView('library'); openEditor(); }, + }); + + async function loadProjects(workspace, epoch) { + const request = beginScopedRequest('projects'); + try { + const result = await api('/repos?' + query(workspace), { signal: request.signal }); + if (epoch === state.refreshEpoch && isCurrentScopedRequest(request)) { + workflow.setProjects(result && result.repos); + refreshMemoryOwnershipLabels(); + } + } catch (_) { + if (isCurrentScopedRequest(request)) { + workflow.projectsUnavailable(); + refreshMemoryOwnershipLabels(); + } + } + } + + async function selectWorkspace(name) { + if (!name) return; + resetMoveSelection(true); + invalidateConsolidationReview(); + const epoch = ++state.refreshEpoch; + invalidateScopedRequests(); + askRequests.reset(); + window.clearTimeout(librarySearchTimer); + state.libraryCursors = [null]; + state.libraryPage = 0; + state.libraryNextCursor = null; + state.libraryTotal = 0; + state.memories = []; + renderLibrary(); + renderLibraryPaging(); + closeGraphConnections(); + state.workspace = name; + state.stats = {}; + renderMetricValues({}); + byId('type-bars').replaceChildren(empty('Loading workspace composition…')); + renderFirstMemoryJourney(); + workflow.selectWorkspace(name); + state.project = workflow.project(); + void processingControls.selectWorkspace(name); + state.graphWorkspace = ''; + state.graphData = null; + state.graphDataPreset = 'galaxy'; + state.graphDataIncludeCode = false; + state.graphDataShowUnlinked = false; + state.graphDataRepo = ''; + state.selectedMemory = ''; + // Detail/editor handlers close over a memory record. Clear both before the + // workspace fetches begin so a stale form cannot write that record into the + // newly selected workspace. + state.editorMemory = null; + resetEditorSession(); + destroyMemoryHistory(); + byId('memory-editor').hidden = true; + const memoryDetail = byId('memory-detail'); + memoryDetail.replaceChildren(); + memoryDetail.hidden = true; + resetScopedPanels(); + state.syncStatus = null; + graphLifecycle.clear(); + state.graphSpacetimeOverlay = null; + state.graphEngine = null; + byId('workspace-select').value = name; + renderWorkspaceNames(); + try { + localStorage.setItem('engraphis-workspace', name); + } catch (_) {} + showNotice(''); + try { + const results = await Promise.allSettled([ + loadStats(name, epoch), + loadProjects(name, epoch), + loadMemories(name, epoch), + loadReviewInbox(), + loadToday(name, epoch), + ]); + if (epoch !== state.refreshEpoch) return; + const failed = results.find(result => result.status === 'rejected'); + if (failed) showNotice(`Some workspace panels could not refresh: ${failed.reason.message}`); + renderWorkspaceList(); + if (state.view === 'relations') await loadGraph(); + if (state.view === 'provenance' && state.provenanceTab === 'audit') await loadAudit(); + if (state.view === 'manage') { + await loadSavings(epoch); + await loadManageTab(state.manageTab); + } + } catch (error) { + if (epoch === state.refreshEpoch) showNotice(`Could not refresh ${name}: ${error.message}`); + } + } + + function memoryCard(memory) { + const card = node('button', 'memory-card'); + card.type = 'button'; + card.setAttribute('role', 'option'); + card.dataset.memoryId = memory.id; + const selected = state.librarySelecting ? state.moveMemoryIds.has(memory.id) : state.selectedMemory === memory.id; + card.setAttribute('aria-selected', String(selected)); + if (selected) card.classList.add('selected'); + if (state.librarySelecting) { + const marker = node('span', 'memory-selection-marker', selected ? 'Selected for move' : 'Select for move'); + marker.setAttribute('aria-hidden', 'true'); + card.append(marker); + } + card.append( + node('h2', '', memoryTitle(memory)), + node('p', '', truncate(memory.content || memory.summary, 240)), + memoryMeta(memory), + ); + card.addEventListener('click', () => { + if (!state.librarySelecting) { openMemory(memory); return; } + if (state.libraryLoading) return; + if (state.moveMemoryIds.has(memory.id)) state.moveMemoryIds.delete(memory.id); + else state.moveMemoryIds.add(memory.id); + closeMemoryMove(); + renderLibrary(); + byId('library-list').querySelectorAll('[data-memory-id]').forEach(item => { + if (item.dataset.memoryId === memory.id) { item.tabIndex = 0; item.focus(); } + else item.tabIndex = -1; + }); + }); + return card; + } + + function filteredMemories() { + return state.memories; + } + + function renderLibrary() { + const target = byId('library-list'); + target.setAttribute('aria-multiselectable', String(state.librarySelecting)); + renderMoveSelection(); + if (!target.dataset.keyboardBound) { + target.dataset.keyboardBound = 'true'; + target.addEventListener('keydown', event => { + const cards = [...target.querySelectorAll('[role="option"]')]; + const current = event.target.closest('[role="option"]'); + if (!current || !cards.length) return; + let index = cards.indexOf(current); + if (event.key === 'Home') index = 0; + else if (event.key === 'End') index = cards.length - 1; + else if (event.key === 'ArrowDown' || event.key === 'ArrowRight') index = Math.min(cards.length - 1, index + 1); + else if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') index = Math.max(0, index - 1); + else return; + event.preventDefault(); + cards.forEach((card, cardIndex) => { card.tabIndex = cardIndex === index ? 0 : -1; }); + cards[index].focus(); + }); + } + target.replaceChildren(); + const memories = filteredMemories(); + const total = state.libraryTotal; + const start = state.libraryPage * 100 + (memories.length ? 1 : 0); + byId('library-count').textContent = total > memories.length + ? `${start.toLocaleString()}–${(start + memories.length - 1).toLocaleString()} of ${total.toLocaleString()} memories` + : `${total.toLocaleString()} ${total === 1 ? 'memory' : 'memories'}`; + if (!memories.length) { + target.append(empty(byId('library-filter').value.trim() || byId('library-type').value + ? 'No memories match these filters.' : 'No active memories in this workspace. Add a fact or import local documents to begin.')); + return; + } + memories.forEach(memory => target.append(memoryCard(memory))); + const cards = [...target.querySelectorAll('[role="option"]')]; + const selectedIndex = cards.findIndex(card => card.getAttribute('aria-selected') === 'true'); + cards.forEach((card, index) => { card.tabIndex = index === (selectedIndex >= 0 ? selectedIndex : 0) ? 0 : -1; }); + } + + function renderMoveSelection() { + const toggle = byId('library-selection-toggle'); + toggle.disabled = state.libraryLoading || !state.workspace || !state.memories.length; + toggle.setAttribute('aria-pressed', String(state.librarySelecting)); + toggle.textContent = state.librarySelecting ? 'Cancel selection' : 'Select memories'; + byId('library-selection-status').textContent = state.librarySelecting + ? `${state.moveMemoryIds.size} selected on this page. Changing results clears the selection.` + : 'Select memories on this page to move them to another workspace.'; + byId('library-move').disabled = state.libraryLoading || !state.moveMemoryIds.size; + } + + function resetMoveSelection(finish = false) { + closeMemoryMove(); + state.moveMemoryIds.clear(); + if (finish) state.librarySelecting = false; + renderMoveSelection(); + } + + function closeMemoryMove() { + const dialog = byId('memory-move-dialog'); + const move = state.memoryMove; + state.memoryMove = null; + beginScopedRequest('memory-move'); + if (dialog.open) dialog.close(); + if (move && move.returnFocus && move.returnFocus.isConnected) move.returnFocus.focus(); + } + + function invalidateMemoryMovePreview() { + const move = state.memoryMove; + if (!move || move.applying) return; + beginScopedRequest('memory-move'); + move.preview = null; + move.loading = false; + byId('memory-move-error').hidden = true; + byId('memory-move-preview').replaceChildren(empty('Choose a destination, then preview the move.')); + renderMemoryMoveControls(); + } + + function renderMemoryMoveControls() { + const move = state.memoryMove; + if (!move) return; + const busy = move.loading || move.applying; + const target = byId('memory-move-target'); + target.disabled = move.applying; + byId('memory-move-cancel').disabled = move.applying; + byId('memory-move-preview-button').disabled = busy || !target.value; + byId('memory-move-apply').disabled = busy || !move.preview || move.preview.can_move !== true; + byId('memory-move-form').setAttribute('aria-busy', String(busy)); + } + + function openMemoryMove() { + if (!state.moveMemoryIds.size || state.libraryLoading) return; + closeMemoryMove(); + state.memoryMove = { + workspace: state.workspace, project: state.project, + ids: [...state.moveMemoryIds].sort(), preview: null, loading: false, applying: false, + returnFocus: document.activeElement, + }; + const target = byId('memory-move-target'); + target.replaceChildren(option('', 'Choose a workspace')); + state.workspaces.filter(item => workspaceName(item) && workspaceName(item) !== state.workspace) + .sort((a, b) => workspaceName(a).localeCompare(workspaceName(b))).forEach(item => { + const name = workspaceName(item); + const access = item.visibility === 'personal' ? 'Personal' + : item.visibility === 'shared' ? 'Shared' : 'Access shown in preview'; + target.append(option(name, `${name} · ${access}`)); + }); + byId('memory-move-source').textContent = `From ${JSON.stringify(state.workspace)} · ${state.moveMemoryIds.size} selected`; + invalidateMemoryMovePreview(); + if (target.options.length === 1) { + byId('memory-move-preview').replaceChildren(empty('Create another accessible workspace in Settings before moving memories.')); + } + byId('memory-move-dialog').showModal(); + target.focus(); + } + + function memoryMoveBody(move) { + return { workspace: move.workspace, target_workspace: byId('memory-move-target').value, memory_ids: move.ids }; + } + + function renderMemoryMovePreview(preview, move) { + const target = byId('memory-move-preview'); + const rows = [ + ['From workspace', preview.source], ['To workspace', preview.target], + ['Selected memories', move.ids.length], ['Total records to move', preview.count], + ['Related memories and history', preview.related_count], + ]; + if (preview.sessions != null) rows.push(['Closed sessions included', preview.sessions]); + if (preview.graph_edges != null) rows.push(['Graph relationships included', preview.graph_edges]); + if (Array.isArray(preview.repos) && preview.repos.length) rows.push(['Projects preserved', preview.repos.join(', ')]); + target.replaceChildren(definitionList(rows.map(([label, value]) => [label, text(value)]))); + const visibilityLabel = value => value === 'personal' ? 'Personal' : value === 'shared' ? 'Shared' : 'Unknown'; + target.append(node('p', 'project-help', `Access: ${visibilityLabel(preview.source_visibility)} → ${visibilityLabel(preview.target_visibility)}.`)); + if (preview.target_visibility === 'shared') { + target.append(node('p', 'project-help', 'The destination is shared. Other users with workspace access can read the moved workspace and project memories. Session ownership restrictions still apply.')); + } else if (preview.target_visibility === 'personal') { + target.append(node('p', 'project-help', 'The destination is personal. Its owner controls access to the moved memories.')); + } + target.append(node('p', 'project-help', 'Related memories and their history, closed sessions and their events move together. Original identifiers and preserved history stay attached to the memories.')); + if (Array.isArray(preview.memories) && preview.memories.length) { + const details = node('details', 'memory-move-records'); + details.append(node('summary', '', `Review all ${preview.memories.length} records`)); + const list = node('ul'); + preview.memories.forEach(memory => { + const item = node('li'); + item.append(node('strong', '', memory.title || memory.id), + node('span', '', ` · ${memory.id}${memory.related ? ' · related record' : ' · selected'}`)); + list.append(item); + }); + details.append(list); + target.append(details); + } + const blockers = Array.isArray(preview.blockers) ? preview.blockers : []; + if (blockers.length) { + target.append(node('p', 'form-error', 'These records cannot be moved yet:')); + const list = node('ul', 'memory-move-blockers'); + blockers.forEach(blocker => list.append(node('li', '', blocker.message || blocker.code || 'Move is blocked.'))); + target.append(list); + } else if (preview.can_move === true) { + target.append(node('p', 'project-help', 'Review this destination and complete record list, then choose Move memories.')); + } + } + + async function previewMemoryMove() { + const move = state.memoryMove; + if (!move || move.loading || move.applying || !byId('memory-move-target').value) return; + const request = beginScopedRequest('memory-move'); + const body = memoryMoveBody(move); + move.preview = null; + move.loading = true; + byId('memory-move-error').hidden = true; + byId('memory-move-preview').replaceChildren(empty('Checking the complete move and its related history…')); + renderMemoryMoveControls(); + try { + const preview = await api('/memories/move-preview', { method: 'POST', body, signal: request.signal }); + if (state.memoryMove !== move || !isCurrentScopedRequest(request) || body.target_workspace !== byId('memory-move-target').value) return; + const sameIds = Array.isArray(preview.requested_ids) + && JSON.stringify([...preview.requested_ids].sort()) === JSON.stringify(move.ids); + if (preview.source !== body.workspace || preview.target !== body.target_workspace || !sameIds + || (preview.can_move === true && (typeof preview.preview_token !== 'string' || !preview.preview_token))) { + throw new Error('The preview does not match this selection. Request a new preview.'); + } + if (Array.isArray(preview.blockers) && preview.blockers.length) preview.can_move = false; + move.preview = preview; + renderMemoryMovePreview(preview, move); + } catch (error) { + if (state.memoryMove !== move || !isCurrentScopedRequest(request)) return; + byId('memory-move-preview').replaceChildren(empty('No move has been submitted.')); + byId('memory-move-error').textContent = `Could not preview the move: ${error.message}`; + byId('memory-move-error').hidden = false; + } finally { + if (state.memoryMove === move && isCurrentScopedRequest(request)) { + move.loading = false; + renderMemoryMoveControls(); + } + } + } + + async function applyMemoryMove(event) { + event.preventDefault(); + const move = state.memoryMove; + if (!move || move.loading || move.applying || !move.preview || move.preview.can_move !== true) return; + const request = beginScopedRequest('memory-move'); + const body = { ...memoryMoveBody(move), preview_token: move.preview.preview_token, confirmed: true }; + if (body.target_workspace !== move.preview.target || body.workspace !== move.preview.source) { + invalidateMemoryMovePreview(); + return; + } + move.applying = true; + byId('memory-move-error').hidden = true; + renderMemoryMoveControls(); + try { + const result = await api('/memories/move', { method: 'POST', body, signal: request.signal }); + if (state.memoryMove !== move || !isCurrentScopedRequest(request)) return; + closeMemoryMove(); + await selectWorkspace(move.workspace); + if (state.workspace === move.workspace) showNotice(`Moved ${number(result.count)} records to ${JSON.stringify(result.workspace)}. History is preserved.`); + } catch (error) { + if (state.memoryMove !== move || !isCurrentScopedRequest(request)) return; + move.preview = null; + byId('memory-move-error').textContent = error.status === 409 + ? 'Memory or workspace state changed. Preview the move again before continuing.' + : `The move could not be confirmed: ${error.message} Preview again to check the current records before retrying.`; + byId('memory-move-error').hidden = false; + } finally { + if (state.memoryMove === move && isCurrentScopedRequest(request)) { + move.applying = false; + renderMemoryMoveControls(); + } + } + } + + function definitionList(entries) { + const list = node('dl', 'definition-list'); + entries.forEach(([term, value]) => { + const row = node('div'); + row.append(node('dt', '', term), node('dd', '', value || '—')); + list.append(row); + }); + return list; + } + + async function selectMemory(id) { + const workspace = state.workspace; + const project = state.project; + const request = beginScopedRequest('memory-detail'); + state.selectedMemory = id; + resetEditorSession(); + destroyMemoryHistory(); + renderLibrary(); + const target = byId('memory-detail'); + target.hidden = false; + byId('memory-editor').hidden = true; + target.replaceChildren(empty('Loading memory…')); + try { + const payload = await api(`/memory/${encodeURIComponent(id)}?${query(workspace)}`, { signal: request.signal }); + const memory = payload.memory || state.memories.find(item => item.id === id); + if (!memory || state.selectedMemory !== id || !isCurrentScopedRequest(request)) return; + state.editorMemory = memory; + target.replaceChildren(); + const title = node('h2', '', memoryTitle(memory)); + title.id = 'memory-detail-title'; + target.append( + node('p', 'eyebrow', `${memoryType(memory)} · ${memory.scope || 'workspace'}`), + title, + node('p', '', memory.content || memory.summary || 'No content.'), + memoryMeta(memory), + definitionList([ + ['Memory id', memory.id], + ['Workspace id', memory.workspace_id], + ['Project id', memory.repo_id], + ['Session id', memory.session_id], + ['Importance', memory.importance == null ? '—' : number(memory.importance).toFixed(2)], + ['Valid from', relative(memory.valid_from)], + ['Valid to', memory.valid_to ? relative(memory.valid_to) : 'current'], + ['Source', memory.provenance && (memory.provenance.source || memory.provenance.kind)], + ['Review', memory.provenance && (memory.provenance.review_state || 'pending')], + ]), + ); + const actions = node('div', 'detail-actions'); + const provenance = memory.provenance || {}; + if (provenance.review_state !== 'approved' || provenance.trusted !== true) { + actions.append(button('Approve for prompt…', 'primary-button', () => approveForPrompt(memory))); + } + actions.append( + button('Review with Jev', 'secondary-button', () => openJevReview(memory)), + button(memory.can_revise === false ? 'Review saved versions' : 'Edit', 'secondary-button', () => openEditor(memory)), + button(memory.pinned ? 'Unpin' : 'Pin', 'secondary-button', () => togglePin(memory)), + button('Search topic timeline', 'secondary-button', () => openMemoryTimeline(memory)), + button('Retire', 'danger-button', () => retireMemory(memory)), + button('Secure erase leak', 'danger-button', () => secureEraseMemory(memory)), + ); + target.append(actions); + state.memoryHistory = window.EngraphisMemoryHistory.create({ + api, id, workspace, repo: project, + isCurrent: () => isCurrentScopedRequest(request) && state.selectedMemory === id, + onOpen: openMemory, + }); + target.append(state.memoryHistory.element); + void state.memoryHistory.load(); + } catch (error) { + if (state.selectedMemory === id && isCurrentScopedRequest(request)) { + target.replaceChildren(empty(`Could not inspect memory: ${error.message}`)); + } + } + } + + function openJevReview(memory) { + const target = byId('memory-detail'); + const prior = byId('jev-review-panel'); + if (prior) { prior.remove(); return; } + const panel = node('section', 'jev-review-panel'); + panel.id = 'jev-review-panel'; + panel.setAttribute('aria-labelledby', 'jev-review-title'); + panel.append( + node('h3', '', 'Review selected evidence with Jev'), + node('p', 'project-help', 'This advisory checks whether the selected memory supports your claim and, if you choose a second memory, whether the two conflict. It never changes stored memories.'), + ); + panel.querySelector('h3').id = 'jev-review-title'; + const form = node('form'); + const claimLabel = node('label'); + claimLabel.append(node('span', '', 'Question for the selected evidence')); + const claim = node('textarea'); + claim.rows = 3; + claim.maxLength = 1200; + claim.required = true; + claim.value = memory.title || ''; + claim.placeholder = 'What should this memory support?'; + claimLabel.append(claim); + const peerLabel = node('label'); + peerLabel.append(node('span', '', 'Compare with another memory (optional)')); + const peer = node('select'); + peer.append(option('', 'No contradiction comparison')); + state.memories.filter(item => item && item.id && item.id !== memory.id).forEach(item => { + peer.append(option(item.id, memoryTitle(item))); + }); + peerLabel.append(peer); + const remoteLabel = node('label', 'check-row'); + const remote = node('input'); + remote.type = 'checkbox'; + remoteLabel.append(remote, document.createTextNode(' Allow this action to send the question and selected excerpts to Jev')); + const classificationLabel = node('label'); + classificationLabel.append(node('span', '', 'Data classification for this action')); + const classification = node('select'); + classification.append(option('internal', 'Internal', true), option('public', 'Public')); + classificationLabel.append(classification); + const status = node('p', 'project-help'); + status.setAttribute('role', 'status'); + status.setAttribute('aria-live', 'polite'); + const result = node('div', 'jev-review-result'); + const submit = node('button', 'primary-button', 'Run read-only review'); + submit.type = 'submit'; + form.append(claimLabel, peerLabel, remoteLabel, classificationLabel, submit); + form.addEventListener('submit', event => { event.preventDefault(); void runJevReview(); }); + panel.append(form, status, result); + target.append(panel); + + async function runJevReview() { + const claimText = claim.value.trim(); + if (!claimText) { + status.textContent = 'Enter a question before reviewing.'; + claim.focus(); + return; + } + if (!state.workspace || state.selectedMemory !== memory.id) return; + const request = beginScopedRequest('jev-review'); + const memoryIds = [memory.id, ...(peer.value ? [peer.value] : [])]; + submit.disabled = true; + status.textContent = 'Running a read-only advisory review…'; + result.replaceChildren(); + try { + const response = await api('/jev/review', { + method: 'POST', signal: request.signal, + body: { + workspace: request.workspace, + ...(request.project ? { repo: request.project } : {}), + memory_ids: memoryIds, + claim: claimText, + allow_remote: remote.checked, + data_classification: classification.value, + }, + }); + if (!isCurrentScopedRequest(request) || state.selectedMemory !== memory.id) return; + renderJevReviewResult(result, response); + status.textContent = response.remote_blocked_reason + ? `Remote consent was granted, but sending was blocked (${response.remote_blocked_reason.replaceAll('_', ' ')}). See the fallback result.` + : response.remote_consent_granted + ? 'Remote consent was granted for this action only. This does not confirm a provider request; check each result for fallback or uncertainty.' + : 'Remote consent was not granted for this action. Check each result for fallback or uncertainty.'; + } catch (error) { + if (isCurrentScopedRequest(request) && state.selectedMemory === memory.id) { + status.textContent = `Review unavailable: ${error.message}`; + } + } finally { + submit.disabled = false; + } + } + } + + function renderJevReviewResult(target, response) { + const fallbackLabels = { + allowance_exhausted: 'The included Jev allowance is currently used up. Check account usage for the next available window.', + provider_protection_limit: 'Jev is temporarily paused by a service-protection limit. Try again later.', + remote_timeout: 'Jev did not respond before the request deadline. You can retry later.', + session_changed: 'The Cloud session changed during the request. Sign in again before retrying.', + managed_operation_unsupported: 'This Jev operation is unavailable in the managed service.', + remote_unavailable: 'The Jev service is unavailable. You can retry later.', + malformed_response: 'Jev returned a response that could not be validated. Review the sources manually.', + }; + const labelStatus = item => item.status === 'decision' ? 'Advisory decision' + : item.status === 'uncertain' ? 'Uncertain; no conclusion' + : item.status === 'not_requested' ? 'Not checked' + : 'Fallback; review manually'; + const describe = (name, item) => { + const card = node('article'); + card.append(node('strong', '', `${name} · ${labelStatus(item)}`)); + if (item.status === 'decision') { + const value = item.value === true ? 'Evidence appears sufficient to answer the question.' + : item.value === false ? 'Evidence appears insufficient to answer the question.' + : item.value === 'contradicts_and_supersedes' ? 'The memories may contradict.' + : item.value === 'reinforces' ? 'The memories appear consistent.' + : item.value === 'orthogonal' ? 'No direct relationship was found.' + : 'Jev returned an advisory result.'; + card.append(node('p', '', value)); + } else if (item.reason) card.append(node('p', '', item.reason.replaceAll('_', ' '))); + if (item.fallback_reason) card.append(node('p', '', fallbackLabels[item.fallback_reason] + || 'Jev could not complete this check. Review the sources manually.')); + if (Number.isFinite(item.probability)) card.append(node('p', '', `Support probability: ${item.probability.toFixed(2)} · advisory only`)); + if (Number.isFinite(item.confidence)) card.append(node('p', '', `Confidence: ${item.confidence.toFixed(2)} · advisory only`)); + target.append(card); + }; + target.replaceChildren(); + describe('Evidence support', response.support || {}); + describe('Contradiction check', response.contradiction || {}); + target.append(node('p', 'project-help', 'Jev cannot authorize a memory change, grounded answer, or command. Review the sources yourself.')); + } + + function openMemory(memory) { + if (!memory || !memory.id) { + showNotice('This result no longer identifies a memory to inspect.'); + return; + } + switchView('library'); + selectMemory(memory.id); + } + + function simpleMemoryCard(memory, className = 'memory-card') { + const interactive = Boolean(memory && memory.id); + const card = node(interactive ? 'button' : 'article', `${className}${interactive ? ' memory-link-card' : ''}`); + if (interactive) { + card.type = 'button'; + card.dataset.memoryId = memory.id; + card.addEventListener('click', () => openMemory(memory)); + } + card.append( + node('h3', '', memoryTitle(memory)), + node('p', '', truncate(memory.content || memory.summary, 500)), + memoryMeta(memory), + ); + return card; + } + + function openEditor(memory = null) { + resetEditorSession(); + state.editorSession = { + revision: window.EngraphisMemoryRevision.create(memory), busy: false, + conflicted: Boolean(memory && memory.can_revise === false), + }; + state.editorMemory = memory; + state.editorReturnFocus = document.activeElement instanceof HTMLElement + ? document.activeElement : byId('new-memory-button'); + byId('memory-detail').hidden = true; + const editor = byId('memory-editor'); + editor.hidden = false; + byId('editor-title').textContent = memory ? 'Revise memory' : 'New memory'; + byId('editor-memory-title').value = memory ? (memory.title || '') : ''; + byId('editor-memory-type').value = memory ? memoryType(memory) : 'semantic'; + const scope = byId('editor-memory-scope'); + scope.querySelector('[value="repo"]').disabled = !state.project; + scope.querySelector('[value="repo"]').textContent = state.project + ? 'Project: ' + state.project : 'Select a project first'; + scope.value = state.project ? 'repo' : 'workspace'; + scope.disabled = Boolean(memory); + byId('editor-scope-control').hidden = Boolean(memory); + byId('editor-scope-note').textContent = memory + ? 'This revision preserves the existing ' + (memory.scope || 'workspace') + ' scope.' + : 'Workspace: ' + state.workspace + '. Choose where this new memory belongs.'; + byId('editor-memory-content').value = memory ? (memory.content || memory.summary || '') : ''; + byId('editor-memory-content').removeAttribute('aria-invalid'); + byId('editor-error').hidden = true; + byId('editor-error').textContent = ''; + const importanceControl = byId('editor-memory-importance'); + const storedImportance = memory && memory.importance != null ? memory.importance : 0.5; + importanceControl.value = String(graphSliderInputValue( + 'editor-memory-importance', storedImportance, 0.5, + )); + const respVal = graphSliderResponseValue( + 'editor-memory-importance', importanceControl.value, 0.5, + ); + importanceControl.setAttribute('aria-valuetext', `${respVal.toFixed(2)} importance`); + const importanceOutput = byId('editor-memory-importance-output'); + if (importanceOutput) importanceOutput.textContent = respVal.toFixed(2); + if (state.editorSession.conflicted) { + byId('editor-error').textContent = 'This version cannot be revised. Refresh saved versions and choose an editing base; your draft will be retained.'; + byId('editor-error').hidden = false; + byId('editor-refresh').hidden = false; + setEditorBusy(false); + } + byId('editor-memory-title').focus(); + } + + function destroyMemoryHistory() { + if (state.memoryHistory) state.memoryHistory.destroy(); + state.memoryHistory = null; + } + + function resetEditorSession() { + const session = state.editorSession; + if (session && session.history) session.history.destroy(); + state.editorSession = null; + byId('editor-refresh').hidden = true; + byId('editor-history').replaceChildren(); + byId('editor-history').hidden = true; + setEditorBusy(false); + } + + function setEditorBusy(busy) { + const editor = byId('memory-editor'); + editor.setAttribute('aria-busy', String(busy)); + editor.querySelectorAll('input, select, textarea, button').forEach(control => { control.disabled = busy; }); + byId('editor-memory-scope').disabled = busy || Boolean(state.editorMemory); + editor.querySelector('button[type="submit"]').disabled = busy || Boolean(state.editorSession && state.editorSession.conflicted); + } + + function refreshEditorVersions() { + const session = state.editorSession; + const original = state.editorMemory; + const workspace = state.workspace; + const project = state.project; + if (!session || !original || session.busy) return; + if (session.history) session.history.destroy(); + const target = byId('editor-history'); + target.hidden = false; + session.history = window.EngraphisMemoryHistory.create({ + api, id: original.id, workspace, repo: project, original, + isCurrent: () => state.editorSession === session && state.workspace === workspace && state.project === project, + onUseBase: record => { + state.editorMemory = record; + session.revision = window.EngraphisMemoryRevision.create(record); + session.conflicted = false; + session.history.destroy(); + target.replaceChildren(); + target.hidden = true; + byId('editor-refresh').hidden = true; + byId('editor-error').textContent = 'Editing base refreshed. Your draft is unchanged; review it before saving.'; + byId('editor-error').hidden = false; + setEditorBusy(false); + byId('editor-memory-content').focus(); + }, + }); + target.replaceChildren(session.history.element); + void session.history.load(); + } + + function closeEditor() { + const returnFocus = state.editorReturnFocus; + resetEditorSession(); + byId('memory-editor').hidden = true; + byId('memory-detail').hidden = false; + state.editorMemory = null; + state.editorReturnFocus = null; + if (returnFocus && document.contains(returnFocus) && !returnFocus.hidden + && !returnFocus.disabled) returnFocus.focus(); + else byId('new-memory-button').focus(); + } + + async function saveMemory(event) { + event.preventDefault(); + const session = state.editorSession; + if (!session || session.busy || session.conflicted) return; + const current = state.editorMemory; + let savedId = current && current.id; + const workspace = state.workspace; + const epoch = state.refreshEpoch; + const title = byId('editor-memory-title').value.trim(); + const memoryTypeValue = byId('editor-memory-type').value; + const content = byId('editor-memory-content').value.trim(); + const importance = graphSliderResponseValue( + 'editor-memory-importance', number(byId('editor-memory-importance').value), 0.5, + ); + const creationScope = byId('editor-memory-scope').value; + const creationProject = creationScope === 'repo' ? state.project : ''; + const currentImportance = current && current.importance != null + ? number(current.importance) : 0.5; + const contentField = byId('editor-memory-content'); + const editorError = byId('editor-error'); + contentField.removeAttribute('aria-invalid'); + editorError.hidden = true; + editorError.textContent = ''; + if (!current && creationScope === 'repo' && !creationProject) { + editorError.textContent = 'Select a project before saving a project memory.'; + editorError.hidden = false; + return; + } + if (!content) { + contentField.setAttribute('aria-invalid', 'true'); + editorError.textContent = 'Enter memory content before saving.'; + editorError.hidden = false; + showNotice('Enter memory content before saving.'); + contentField.focus(); + return; + } + try { + session.busy = true; + setEditorBusy(true); + if (current) { + if (content !== (current.content || current.summary || '') + || title !== (current.title || '') || memoryTypeValue !== memoryType(current) + || importance !== currentImportance) { + const body = session.revision.body({ workspace, content, title, mtype: memoryTypeValue, importance, reason: 'revised in Ledger' }); + const revised = await api('/memory/revise', { method: 'POST', body }); + if (!revised || !revised.id || !revised.version || !revised.receipt + || revised.receipt.status !== 'committed' || revised.receipt.operation !== 'revise' + || revised.receipt.operation_id !== body.operation_id) { + throw new Error('The save could not be confirmed. Retry the unchanged draft to check this operation.'); + } + savedId = revised.id; + if (state.editorSession === session) { + state.editorMemory = { ...current, id: revised.id, version: revised.version, content, title, memory_type: memoryTypeValue, importance }; + } + } + if (workspace === state.workspace && epoch === state.refreshEpoch) { + showNotice('Memory revision recorded with temporal history preserved.'); + } + } else { + const saved = await api('/remember', { + method: 'POST', + body: { + workspace, + content, + title, + mtype: memoryTypeValue, + scope: creationScope, + ...(creationProject ? { repo: creationProject } : {}), + importance, + source: 'human:ledger', + trusted: true, + }, + }); + savedId = saved && saved.id; + if (workspace === state.workspace && epoch === state.refreshEpoch) { + showNotice('Memory saved locally. Review its source before approving it for model context.'); + } + } + if (workspace !== state.workspace || epoch !== state.refreshEpoch || state.editorSession !== session) return; + closeEditor(); + await selectWorkspace(workspace); + if (savedId && workspace === state.workspace && state.refreshEpoch === epoch + 1) await selectMemory(savedId); + } catch (error) { + if (workspace === state.workspace && epoch === state.refreshEpoch && state.editorSession === session) { + session.conflicted = Boolean(current && (error.status === 409 || !current.version)); + editorError.textContent = session.conflicted + ? 'This saved record changed. Your draft is retained. Refresh saved versions and choose an editing base before saving.' + : `Could not save memory: ${error.message} Your draft is retained.`; + editorError.hidden = false; + byId('editor-refresh').hidden = !session.conflicted; + showNotice(editorError.textContent); + } + } finally { + session.busy = false; + if (state.editorSession === session) setEditorBusy(false); + } + } + + async function togglePin(memory) { + try { + await api('/pin', { + method: 'POST', + body: { id: memory.id, workspace: state.workspace, pinned: !memory.pinned }, + }); + showNotice(memory.pinned ? 'Memory unpinned.' : 'Memory pinned against decay.'); + await selectWorkspace(state.workspace); + selectMemory(memory.id); + } catch (error) { + showNotice(`Could not change pin: ${error.message}`); + } + } + + async function retireMemory(memory) { + if (!window.confirm(`Retire “${memory.title || memory.id}”? The record stays in temporal history but leaves live recall.`)) return; + try { + await api('/retire', { + method: 'POST', + body: { id: memory.id, workspace: state.workspace, reason: 'retired in Ledger' }, + }); + state.selectedMemory = ''; + byId('memory-detail').replaceChildren(empty('Memory moved out of live recall. Its history is retained.')); + showNotice('Memory retired without hard deletion.'); + await selectWorkspace(state.workspace); + } catch (error) { + showNotice(`Could not retire memory: ${error.message}`); + } + } + + async function secureEraseMemory(memory) { + const name = memory.title || memory.id; + if (!window.confirm(`Securely erase “${name}”? This destroys temporal history and local index copies. Rotate the leaked credential; copied exports, snapshots, remote peers, and an already-compromised agent cannot be erased here.`)) return; + try { + const result = await api('/secure-erase', { + method: 'POST', body: { id: memory.id, workspace: state.workspace }, + }); + state.selectedMemory = ''; + byId('memory-detail').replaceChildren(empty('Memory securely erased from this local store. Review the reported backup limitations and rotate the credential.')); + showNotice(result.vector_index_cleanup === 'deleted' + ? 'Memory securely erased from local persistence.' + : 'Memory removed locally; configured vector index needs separate remediation.'); + await selectWorkspace(state.workspace); + } catch (error) { + showNotice(`Could not securely erase memory: ${error.message}`); + } + } + + function openMemoryTimeline(memory) { + switchView('provenance'); + switchProvenanceTab('timeline'); + byId('timeline-input').value = memory.title || truncate(memory.content, 80); + byId('timeline-form').requestSubmit(); + } + + async function importFiles(files) { + if (!files.length) return; + const form = new FormData(); + form.append('workspace', state.workspace); + form.append('memory_type', 'semantic'); + form.append('derive_facts', 'false'); + [...files].forEach(file => form.append('files', file)); + try { + showNotice(`Importing ${files.length} ${files.length === 1 ? 'file' : 'files'} locally…`); + const result = await api('/workspaces/import-files', { method: 'POST', body: form }); + showNotice(`Import complete${result.count != null ? ` · ${result.count} memories` : ''}.`); + await selectWorkspace(state.workspace); + } catch (error) { + showNotice(`Import failed: ${error.message}`); + } finally { + byId('import-files').value = ''; + } + } + + const obsidianImport = { + preview: null, job: null, poll: null, selection: null, sources: [], + jobWorkspace: '', running: false, reviewGeneration: 0, + }; + let documentExtensions = null; + + async function obsidianApi(path, options = {}) { + const attempt = async () => { + const csrf = await reviewCsrfToken(); + return api(path, { + ...options, + headers: { ...(options.headers || {}), 'X-Engraphis-Review-CSRF': csrf }, + }); + }; + try { + return await attempt(); + } catch (error) { + if (error && error.status === 403) { + // A dashboard restart or re-login invalidates the cached process-local + // nonce. Refresh it once so the wizard recovers without a page reload. + state.reviewCsrf = ''; + return attempt(); + } + throw error; + } + } + + function obsidianSelection() { + const files = [ + ...byId('obsidian-import-files').files, + ...byId('obsidian-import-folder').files, + ]; + const sourceMode = byId('obsidian-source-mode').value; + const markdown = files.filter(file => /\.md$/i.test(file.name)); + const documents = files.filter(file => { + const suffix = (file.name.split('.').pop() || '').toLowerCase(); + // The format endpoint is an owner-only convenience hint. The server still + // enforces its registry for every byte if the hint is temporarily unavailable. + return !documentExtensions || documentExtensions.has(suffix); + }); + const uploadFiles = sourceMode === 'obsidian' ? markdown : documents; + const attachments = sourceMode === 'obsidian' + ? files.filter(file => !/\.md$/i.test(file.name)).map(file => ({ + path: file.webkitRelativePath || file.name, size: file.size, + })) : []; + const unsupported = sourceMode === 'obsidian' + ? 0 : files.length - uploadFiles.length; + const fields = { + workspace: byId('obsidian-workspace').value.trim(), + repo: byId('obsidian-repo').value.trim(), + session_id: byId('obsidian-session').value.trim(), + scope: byId('obsidian-scope').value.trim(), + memory_type: byId('obsidian-memory-type').value, + source_id: byId('obsidian-vault-id').value, + source_label: byId('obsidian-vault-label').value.trim(), + on_conflict: byId('obsidian-conflict').value, + source_mode: sourceMode, + }; + return { uploadFiles, attachments, unsupported, sourceMode, fields }; + } + + function obsidianFormData(selection, { confirmed = false, reviewToken = '' } = {}) { + const form = new FormData(); + Object.entries(selection.fields).forEach(([name, value]) => form.append(name, value)); + form.append('confirmed', confirmed ? 'true' : 'false'); + if (reviewToken) form.append('review_token', reviewToken); + form.append('attachment_manifest', JSON.stringify(selection.attachments)); + selection.uploadFiles.forEach(file => ( + form.append('files', file, file.webkitRelativePath || file.name) + )); + return form; + } + + function invalidateDocumentImportPreview(message = 'Selection changed. Preview again before importing.') { + obsidianImport.reviewGeneration += 1; + obsidianImport.preview = null; + obsidianImport.selection = null; + byId('obsidian-confirmed').checked = false; + byId('obsidian-run').disabled = true; + if (obsidianImport.running) return; + obsidianImport.job = null; + obsidianImport.jobWorkspace = ''; + byId('obsidian-cancel').hidden = true; + delete byId('obsidian-cancel').dataset.jobId; + renderObsidianReport(null); + if (message) byId('obsidian-import-progress').textContent = message; + } + + function updateDocumentImportMode() { + const obsidian = byId('obsidian-source-mode').value === 'obsidian'; + byId('obsidian-files-label').textContent = obsidian ? 'Individual Markdown notes' : 'Individual documents'; + byId('obsidian-folder-label').textContent = obsidian ? 'Obsidian vault folder' : 'Document folder'; + byId('obsidian-import-description').textContent = obsidian + ? 'Choose an Obsidian vault folder. Engraphis previews Markdown note bytes and attachment metadata before it writes anything; attachment bytes are never uploaded.' + : 'Choose individual files or a folder. Engraphis previews supported document formats before it writes anything; uploaded bytes are processed locally and are not kept as dashboard upload copies.'; + byId('obsidian-run').textContent = obsidian ? 'Import vault notes' : 'Import documents'; + byId('obsidian-import-files').value = ''; + byId('obsidian-import-folder').value = ''; + invalidateDocumentImportPreview('Choose files or a folder to preview its import.'); + } + + function updateSourceLabelRequirement() { + const label = byId('obsidian-vault-label'); + const isNewSource = !byId('obsidian-vault-id').value; + label.required = isNewSource; + label.setAttribute('aria-required', isNewSource ? 'true' : 'false'); + label.placeholder = isNewSource ? 'Required for a new source' : 'Saved source label'; + } + + function prefillNewSourceLabelFromFolder() { + if (byId('obsidian-vault-id').value || byId('obsidian-vault-label').value.trim()) return; + const firstFolderFile = [...byId('obsidian-import-folder').files] + .find(file => file.webkitRelativePath && file.webkitRelativePath.includes('/')); + if (!firstFolderFile) return; + const folderName = firstFolderFile.webkitRelativePath.split('/')[0].trim(); + if (folderName) byId('obsidian-vault-label').value = folderName; + } + + function requireNewSourceLabel() { + if (byId('obsidian-vault-id').value || byId('obsidian-vault-label').value.trim()) return true; + byId('obsidian-import-progress').textContent = 'Enter a Source label before creating a new source.'; + byId('obsidian-vault-label').focus(); + return false; + } + + function obsidianRows(result) { + const rows = result && (result.files || result.details || result.entries || []); + return Array.isArray(rows) ? rows : []; + } + + function renderObsidianReport(result) { + const target = byId('obsidian-import-report'); + const wanted = byId('obsidian-report-filter').value; + target.replaceChildren(); + const rows = obsidianRows(result).filter(row => { + const status = String(row.status || row.action || row.result || '').toLowerCase(); + if (wanted === 'all') return true; + if (wanted === 'reject') return /reject|error|warn|conflict/.test(status) || Boolean(row.warning || row.error); + return status.includes(wanted); + }); + if (!rows.length) { + target.append(empty(wanted === 'all' ? 'No per-file details were returned.' : 'No files match this filter.')); + return; + } + const list = node('ul'); + rows.forEach(row => { + const status = String(row.status || row.action || row.result || 'reported').toLowerCase(); + const action = row.action && String(row.action).toLowerCase() !== status + ? ` · action: ${row.action}` : ''; + const format = row.format || row.format_name ? ` · format: ${row.format || row.format_name}` : ''; + const warning = row.warning || row.error || row.reason + || (Number(row.warning_count) ? `${row.warning_count} warning(s)` : ''); + const item = node('li', '', `${status.toUpperCase()} · ${row.path || row.file || row.relative_path || 'unnamed document'}${format}${action}${warning ? ` · ${warning}` : ''}`); + item.dataset.status = /reject|error/.test(status) || row.error || row.reason ? 'reject' : status; + list.append(item); + }); + target.append(list); + } + + function obsidianSummary(result, prefix = 'Preview') { + const counts = result && (result.counts || result); + const keys = ['documents', 'markdown', 'formats', 'imported', 'updated', 'renamed', 'skipped', 'rejected', 'conflict', 'missing', 'error']; + const summary = keys.filter(key => Number.isFinite(Number(counts && counts[key]))) + .map(key => `${key.replace('_', ' ')}: ${counts[key]}`); + const unsupported = obsidianImport.selection && obsidianImport.selection.unsupported; + const warning = unsupported ? ` · warning: ${unsupported} unsupported files were not uploaded` : ''; + byId('obsidian-import-progress').textContent = summary.length ? `${prefix} · ${summary.join(' · ')}${warning}` : `${prefix} ready.${warning}`; + } + + async function loadObsidianVaults() { + const select = byId('obsidian-vault-id'); + try { + const result = await obsidianApi(`/workspaces/import-documents/sources?${query(state.workspace)}`); + const vaults = result.sources || result.vaults || result || []; + obsidianImport.sources = Array.isArray(vaults) ? vaults : []; + select.replaceChildren(option('', 'New source')); + obsidianImport.sources.forEach(vault => select.append(option(vault.id, vault.label || vault.name || vault.id))); + } catch (_) { + // A first-run vault list is optional; preview/import still present a useful error. + select.replaceChildren(option('', 'New source')); + obsidianImport.sources = []; + } + } + + async function loadDocumentFormats() { + try { + const result = await obsidianApi('/workspaces/import-documents/formats'); + const extensions = Array.isArray(result.extensions) ? result.extensions : []; + documentExtensions = new Set(extensions.map(extension => String(extension).replace(/^\./, '').toLowerCase())); + } catch (_) { + // Server-side validation remains authoritative; do not invent a stale client registry. + documentExtensions = null; + } + } + + function applySelectedDocumentSource() { + const source = obsidianImport.sources.find(item => item.id === byId('obsidian-vault-id').value); + if (!source) { + byId('obsidian-vault-label').value = ''; + updateSourceLabelRequirement(); + invalidateDocumentImportPreview(); + return; + } + byId('obsidian-vault-label').value = source.label || source.name || ''; + if (source.repo != null) byId('obsidian-repo').value = source.repo; + if (source.session_id != null) byId('obsidian-session').value = source.session_id; + if (source.scope) byId('obsidian-scope').value = source.scope; + if (source.memory_type) byId('obsidian-memory-type').value = source.memory_type; + byId('obsidian-source-mode').value = source.adapter === 'obsidian' || source.kind === 'obsidian' + ? 'obsidian' : 'documents'; + updateSourceLabelRequirement(); + updateDocumentImportMode(); + } + + async function previewObsidianImport() { + if (obsidianImport.running) return; + if (!requireNewSourceLabel()) return; + const selection = obsidianSelection(); + if (!selection.uploadFiles.length) { + byId('obsidian-import-progress').textContent = selection.sourceMode === 'obsidian' + ? 'Choose a folder containing Markdown notes.' + : 'Choose supported documents to import.'; + return; + } + invalidateDocumentImportPreview(''); + const generation = obsidianImport.reviewGeneration; + const type = selection.sourceMode === 'obsidian' ? 'Markdown notes' : 'supported documents'; + const ignored = selection.unsupported ? ` · ${selection.unsupported} unsupported files will not be uploaded` : ''; + byId('obsidian-import-progress').textContent = `Previewing ${selection.uploadFiles.length} ${type}${selection.attachments.length ? ` and ${selection.attachments.length} attachment manifests` : ''}${ignored}…`; + byId('obsidian-preview').disabled = true; + try { + const preview = await obsidianApi('/workspaces/import-documents/preview', { + method: 'POST', body: obsidianFormData(selection), + }); + if (generation !== obsidianImport.reviewGeneration) return; + if (!preview || typeof preview.review_token !== 'string' || !preview.review_token) { + throw new Error('The server did not bind this preview. Preview again.'); + } + selection.reviewToken = preview.review_token; + obsidianImport.selection = selection; + obsidianImport.preview = preview; + byId('obsidian-confirmed').checked = false; + renderObsidianReport(obsidianImport.preview); + obsidianSummary(obsidianImport.preview); + byId('obsidian-run').disabled = false; + } catch (error) { + if (generation !== obsidianImport.reviewGeneration) return; + obsidianImport.selection = null; + obsidianImport.preview = null; + byId('obsidian-import-progress').textContent = `Preview failed: ${error.message}`; + byId('obsidian-run').disabled = true; + } finally { + byId('obsidian-preview').disabled = false; + } + } + + async function pollObsidianImport(jobId, workspace) { + try { + const result = await obsidianApi(`/workspaces/import-documents/jobs/${encodeURIComponent(jobId)}?${query(workspace)}`); + obsidianImport.job = result; + renderObsidianReport(result); + obsidianSummary(result, 'Import'); + if (!['complete', 'completed', 'partial', 'failed', 'cancelled'].includes(String(result.state || result.status || '').toLowerCase())) { + obsidianImport.poll = window.setTimeout(() => pollObsidianImport(jobId, workspace), 750); + return; + } + obsidianImport.running = false; + obsidianImport.poll = null; + obsidianImport.selection = null; + obsidianImport.preview = null; + byId('obsidian-confirmed').checked = false; + byId('obsidian-cancel').hidden = true; + byId('obsidian-run').disabled = true; + byId('obsidian-preview').disabled = false; + showNotice('Document import finished.'); + await selectWorkspace(state.workspace); + } catch (error) { + byId('obsidian-import-progress').textContent = `Could not read import progress: ${error.message}`; + byId('obsidian-run').disabled = true; + } + } + + async function runObsidianImport(event) { + event.preventDefault(); + if (!requireNewSourceLabel()) return; + if (!byId('obsidian-confirmed').checked) { + byId('obsidian-import-progress').textContent = 'Confirm the selected scope before importing.'; + byId('obsidian-confirmed').focus(); + return; + } + const selection = obsidianImport.selection; + if (!selection || !selection.reviewToken) { + byId('obsidian-import-progress').textContent = 'Preview this exact selection before importing.'; + byId('obsidian-run').disabled = true; + return; + } + const workspace = selection.fields.workspace; + const runBody = obsidianFormData(selection, { + confirmed: true, reviewToken: selection.reviewToken, + }); + // The server token is one-time. Clear the client copy before the request so + // a double submit or ambiguous network failure cannot reuse it. + selection.reviewToken = ''; + byId('obsidian-run').disabled = true; + byId('obsidian-preview').disabled = true; + byId('obsidian-import-progress').textContent = 'Starting local document import…'; + obsidianImport.running = true; + obsidianImport.jobWorkspace = workspace; + try { + const result = await obsidianApi('/workspaces/import-documents/run', { + method: 'POST', + body: runBody, + }); + obsidianImport.job = result; + renderObsidianReport(result); + obsidianSummary(result, 'Import'); + const jobId = result.job_id || result.id; + if (jobId) { + byId('obsidian-cancel').hidden = false; + byId('obsidian-cancel').dataset.jobId = jobId; + byId('obsidian-cancel').dataset.workspace = workspace; + await pollObsidianImport(jobId, workspace); + } + else { + obsidianImport.running = false; + obsidianImport.selection = null; + obsidianImport.preview = null; + byId('obsidian-confirmed').checked = false; + byId('obsidian-run').disabled = true; + byId('obsidian-preview').disabled = false; + showNotice('Document import finished.'); + await selectWorkspace(state.workspace); + } + } catch (error) { + obsidianImport.running = false; + obsidianImport.selection = null; + obsidianImport.preview = null; + byId('obsidian-confirmed').checked = false; + byId('obsidian-import-progress').textContent = `Import failed: ${error.message} Preview again before retrying.`; + byId('obsidian-run').disabled = true; + byId('obsidian-preview').disabled = false; + } + } + + async function cancelObsidianImport() { + const button = byId('obsidian-cancel'); + const jobId = button.dataset.jobId; + const workspace = button.dataset.workspace || obsidianImport.jobWorkspace; + if (!jobId || !workspace) return; + button.disabled = true; + const form = new FormData(); + form.append('workspace', workspace); + try { + await obsidianApi(`/workspaces/import-documents/jobs/${encodeURIComponent(jobId)}/cancel`, { method: 'POST', body: form }); + byId('obsidian-import-progress').textContent = 'Cancellation requested; finishing the current document safely…'; + } catch (error) { + byId('obsidian-import-progress').textContent = `Could not cancel import: ${error.message}`; + } finally { + button.disabled = false; + } + } + + async function openObsidianImport() { + const dialog = byId('obsidian-import-dialog'); + byId('obsidian-confirmed').checked = false; + if (!obsidianImport.running) { + if (obsidianImport.poll) window.clearTimeout(obsidianImport.poll); + obsidianImport.preview = null; + obsidianImport.job = null; + obsidianImport.poll = null; + obsidianImport.selection = null; + obsidianImport.jobWorkspace = ''; + delete byId('obsidian-cancel').dataset.jobId; + delete byId('obsidian-cancel').dataset.workspace; + } + byId('obsidian-workspace').value = state.workspace; + byId('obsidian-repo').value = state.project; + byId('obsidian-scope').value = state.project ? 'repo' : 'workspace'; + byId('obsidian-session').value = ''; + byId('obsidian-vault-label').value = ''; + if (!obsidianImport.running) { + byId('obsidian-import-progress').textContent = 'Choose individual files or a folder to preview its import.'; + } + byId('obsidian-run').disabled = true; + byId('obsidian-preview').disabled = obsidianImport.running; + byId('obsidian-cancel').hidden = !obsidianImport.running; + if (!obsidianImport.running) renderObsidianReport(null); + await Promise.all([loadObsidianVaults(), loadDocumentFormats()]); + byId('obsidian-vault-id').value = ''; + updateSourceLabelRequirement(); + updateDocumentImportMode(); + dialog.showModal(); + byId('obsidian-import-files').focus(); + } + + function renderAnswer(result) { + const target = byId('answer-panel'); + target.replaceChildren(); + const meta = node('div', 'answer-meta'); + const grounded = Boolean(result.grounded); + meta.append( + node('span', `support-pill ${grounded ? 'grounded' : 'abstained'}`, grounded ? 'Grounded' : 'Abstained'), + node('span', 'support-pill', Number.isFinite(result.support) ? `Support ${result.support.toFixed(2)}` : 'Support unavailable'), + node('span', 'support-pill', `${(result.citations || []).length} ${(result.citations || []).length === 1 ? 'citation' : 'citations'}`), + ); + target.append(meta); + const advisory = result.planning_advisory; + if (advisory && typeof advisory === 'object') { + const reasonLabels = { + route_selected: 'Jev prioritized a locally generated alternate route. A 40-task synthetic check found no retrieval-metric change; user-workload benefit is unproven.', + jev_uncertain: 'Jev was uncertain, so the deterministic route order was used.', + remote_consent_required: 'Remote consent was not granted, so deterministic route order was used.', + planning_disabled: 'Query planning is disabled, so the original query route was used.', + backend_unavailable: 'Jev is unavailable, so deterministic route order was used.', + allowance_exhausted: 'The included Jev allowance is currently used up, so deterministic route order was used. Check account usage for the next available window.', + provider_protection_limit: 'Jev is temporarily paused by a service-protection limit, so deterministic route order was used. Try again later.', + remote_timeout: 'Jev did not respond before the request deadline, so deterministic route order was used. You can retry later.', + session_changed: 'The Cloud session changed during the request, so deterministic route order was used. Sign in again before retrying.', + managed_operation_unsupported: 'This Jev operation is unavailable in the managed service, so deterministic route order was used.', + planner_timeout: 'Route planning reached its deadline, so deterministic route order was used.', + remote_unavailable: 'The Jev request failed, so deterministic route order was used.', + malformed_response: 'Jev returned an invalid response, so deterministic route order was used.', + sensitive_content: 'The route text was not sent because it matched a sensitive-data filter; deterministic route order was used.', + }; + const message = reasonLabels[advisory.reason] + || (advisory.status === 'decision' + ? 'Jev prioritized a locally generated route. A 40-task synthetic check found no retrieval-metric change; user-workload benefit is unproven.' + : 'The Jev advisory did not select a route; deterministic route order was used.'); + target.append(node('p', 'project-help jev-recall-status', message)); + } + const coverage = ['unknown', 'partial', 'complete'].includes(result.answer_coverage) ? result.answer_coverage : 'unknown'; + const coverageNote = { + unknown: 'This answer has not been checked against every part of your question.', + partial: 'The answering service reports that some requested information is missing.', + complete: 'The answering service reports that every part of the question is covered.', + }; + const coveragePanel = node('div', 'answer-coverage'); + coveragePanel.dataset.coverage = coverage; + coveragePanel.append(node('strong', '', 'Question coverage: ' + coverage), + node('p', '', coverageNote[coverage] + ' Support and citations describe the returned evidence.')); + target.append(coveragePanel); + if (!grounded) { + target.append( + node('h2', '', 'Insufficient evidence'), + node('p', 'answer-copy', result.reason || 'The active workspace does not support a grounded answer.'), + ); + return; + } + target.append(node('h2', '', 'Answer'), node('p', 'answer-copy', result.answer || 'The cited memories support this answer.')); + const citations = node('div', 'citation-list'); + (result.citations || []).forEach(citation => { + const card = node(citation.id ? 'button' : 'article', 'citation-card memory-link-card'); + if (citation.id) { + card.type = 'button'; + card.dataset.memoryId = citation.id; + card.addEventListener('click', () => openMemory(citation)); + } + card.append( + node('h3', '', `[${citation.n || citation.number || '•'}] ${memoryTitle(citation)}`), + node('p', '', citation.content || citation.summary || ''), + node('div', 'memory-meta', `support ${number(citation.support || citation.score).toFixed(2)} · ${citation.id || ''}`), + ); + citations.append(card); + }); + target.append(citations); + } + + async function askMemory(event) { + event.preventDefault(); + const input = byId('ask-input'); + const question = input.value; + if (!question.trim()) { + showNotice('Enter a question before requesting a grounded answer.'); + input.focus(); + return; + } + if (!state.workspace) { + showNotice('Choose a workspace before requesting a grounded answer.'); + return; + } + const request = beginScopedRequest('ask'); + const workspace = request.workspace; + showNotice(''); + const k = number(byId('ask-k').value) || 5; + const jevAssisted = byId('ask-jev-assisted').checked; + const allowRemote = jevAssisted && byId('ask-jev-remote').checked; + const body = { + query: question, + workspace, + ...(request.project ? { repo: request.project } : {}), + k: Math.max(8, k), + max_citations: k, + planning: jevAssisted ? 'auto' : 'off', + jev_assisted: jevAssisted, + allow_remote: allowRemote, + include_retrieval_preview: true, + ...(jevAssisted ? { data_classification: byId('ask-jev-classification').value } : {}), + }; + await askRequests.start({ + question, + scopeLabel: workspace + (request.project ? ' / ' + request.project : ' / all projects'), + isCurrent: () => isCurrentScopedRequest(request), + answer: signal => api('/answer', { + method: 'POST', signal, body, + }), + }); + } + + function renderPreview(retrieval) { + const target = byId('retrieval-list'); + target.replaceChildren(); + const memories = retrieval.memories || []; + if (!memories.length) target.append(empty('No raw candidates were returned.')); + else memories.forEach(memory => target.append(simpleMemoryCard(memory))); + } + + function graphCommunityIndex(value) { + const numeric = Number(value); + if (Number.isFinite(numeric)) return numeric; + const source = text(value); + let hash = 0; + for (let index = 0; index < source.length; index += 1) hash = ((hash * 31) + source.charCodeAt(index)) | 0; + return Math.abs(hash); + } + + function optionalGraphNumber(value) { + return value == null || value === '' ? undefined : number(value); + } + + function graphNodes(payload) { + const source = payload.nodes || payload.entities || []; + return source.map(item => ({ + ...item, + id: item.id, + name: item.label || item.name || item.id, + label: item.label || item.name || item.id, + etype: item.etype || item.type || 'person_or_concept', + nodeKind: item.node_kind || item.kind || '', + degree: number(item.degree != null ? item.degree : item.weighted_degree), + community: item.community_id != null ? graphCommunityIndex(item.community_id) + : (item.community != null ? graphCommunityIndex(item.community) : undefined), + community_id: item.community_id == null ? item.community : item.community_id, + gravity_mass: optionalGraphNumber(item.gravity_mass), + evidence_mass: optionalGraphNumber(item.evidence_mass ?? item.gravity_mass ?? item.mass_score), + visual_radius: optionalGraphNumber(item.visual_radius), + anchor_role: item.anchor_role || '', + x: Number.isFinite(Number(item.x)) ? Number(item.x) : undefined, + y: Number.isFinite(Number(item.y)) ? Number(item.y) : undefined, + repo_names: Array.isArray(item.repo_names) ? item.repo_names.filter(name => typeof name === 'string') : [], + // The legacy engine reads `repo`; scene-aware engines use `repo_names`. Keeping both + // makes filtering work during an asset-cache transition without mutating scene data. + repo: item.repo || (Array.isArray(item.repo_names) ? item.repo_names.join(' ') : ''), + topic: item.topic || '', + valid_from: item.valid_from, + valid_to: item.valid_to, + ghost: item.ghost === true, + member_count: optionalGraphNumber(item.member_count), + visible_by_default: item.visible_by_default !== false, + })); + } + + function graphEndpoint(value) { + if (value && typeof value === 'object') return value.id ?? value; + return value; + } + + function graphLinks(payload) { + const source = payload.edges || payload.links || []; + return source.map((item, index) => ({ + ...item, + id: item.id || `edge-${index}`, + source: item.from ?? graphEndpoint(item.source), + target: item.to ?? graphEndpoint(item.target), + label: item.label || item.relation || 'related', + layer: item.layer || 'semantic', + valid_from: item.valid_from, + valid_to: item.valid_to, + rest_length: optionalGraphNumber(item.rest_length), + spring_strength: optionalGraphNumber(item.spring_strength), + physics_strength: optionalGraphNumber(item.physics_strength), + strength: optionalGraphNumber(item.strength), + ghost: item.ghost === true, + bridge: item.bridge === true, + visible_by_default: item.visible_by_default !== false, + })).filter(item => item.source !== undefined && item.source !== null + && item.target !== undefined && item.target !== null); + } + + function revealGraphNode(id, label = 'Selected entity') { + const engine = state.graphEngine; + if (!engine) return; + let attempts = 0; + const reveal = () => { + if (state.graphEngine !== engine) return; + if (engine.reveal(id)) return; + attempts += 1; + if (attempts < 8) { + window.requestAnimationFrame(reveal); + return; + } + showNotice(`${label} is outside the current graph scope.`); + }; + reveal(); + } + + function focusGraphNode(id, label = 'Selected entity') { + const engine = state.graphEngine; + if (!engine) return; + let attempted = false; + const focus = () => { + if (state.graphEngine !== engine || attempted) return; + attempted = true; + if (typeof engine.focus === 'function' && engine.focus(id)) return; + if (typeof engine.reveal === 'function' && engine.reveal(id)) return; + showNotice(`${label} is outside the current graph scope.`); + }; + focus(); + } + + function cancelGraphConnectionMemoryLoad() { + state.graphConnectionsRequest += 1; + if (state.graphConnectionsController) state.graphConnectionsController.abort(); + state.graphConnectionsController = null; + } + + function closeGraphConnections() { + cancelGraphConnectionMemoryLoad(); + const dialog = byId('graph-connections-dialog'); + if (dialog.open) dialog.close(); + } + + function graphMemoryCard(evidence) { + return { + id: evidence.memory_id || evidence.id, + title: evidence.title || evidence.label || evidence.memory_id || evidence.id, + content: evidence.excerpt || evidence.content || evidence.summary || '', + mtype: evidence.memory_type || evidence.mtype, + valid_from: evidence.valid_from, + valid_to: evidence.valid_to, + ingested_at: evidence.ingested_at, + provenance: evidence.provenance, + }; + } + + function graphMemoryEvidenceCard(memory) { + const card = node('article', 'graph-memory-evidence'); + card.append( + node('h4', '', memoryTitle(memory)), + node('p', '', truncate(memory.content || memory.summary, 500)), + memoryMeta(memory), + ); + if (memory.id) { + card.append(button('Open in Library', 'secondary-button', () => { + closeGraphConnections(); + openMemory(memory); + })); + } + return card; + } + + function renderGraphConnectionMemories(memories, message) { + const target = byId('graph-connection-memory-list'); + target.replaceChildren(); + if (!memories.length) { + const placeholder = empty(message); + placeholder.setAttribute('role', 'listitem'); + target.append(placeholder); + return; + } + memories.forEach(memory => { + const card = graphMemoryEvidenceCard(memory); + card.setAttribute('role', 'listitem'); + target.append(card); + }); + } + + function isGraphMemoryNode(item) { + const kind = String(item.nodeKind || '').toLowerCase(); + const type = String(item.etype || '').toLowerCase(); + return kind === 'memory' || type === 'memory' || type.startsWith('memory_'); + } + + function graphConnectionEntries(item) { + const graph = state.graphEngine && state.graphEngine.exportData + ? state.graphEngine.exportData() : state.graphData; + if (!graph) return []; + const nodes = new Map(graph.nodes.map(candidate => [candidate.id, candidate])); + const connections = new Map(); + graph.links.forEach(link => { + const source = link.source; + const target = link.target; + if (source !== item.id && target !== item.id) return; + const otherId = source === item.id ? target : source; + const other = nodes.get(otherId); + if (!other || other.id === item.id) return; + const entry = connections.get(other.id) || { + item: other, relations: new Set(), includeHistory: false, + }; + if (link.label) entry.relations.add(link.label); + entry.includeHistory = entry.includeHistory || link.ghost === true; + connections.set(other.id, entry); + }); + return [...connections.values()].sort((left, right) => { + const degree = number(right.item.degree) - number(left.item.degree); + return degree || left.item.name.localeCompare(right.item.name); + }); + } + + async function showGraphConnectionMemories(item, includeHistory = false) { + if (!item || item.id === undefined || item.id === null || !state.workspace) return; + cancelGraphConnectionMemoryLoad(); + const request = ++state.graphConnectionsRequest; + const workspace = state.workspace; + const repo = (byId('graph-repo-filter').value || '').trim(); + const title = item.name || item.label || item.id; + const historicalMemberId = includeHistory && item.ghost && Array.isArray(item.member_ids) + ? item.member_ids.find(value => typeof value === 'string' && value) || '' + : ''; + const historyQuery = includeHistory + ? `&include_history=true${historicalMemberId ? `&member_id=${encodeURIComponent(historicalMemberId)}` : ''}` + : ''; + byId('graph-connection-memory-title').textContent = `Memories for ${title}`; + renderGraphConnectionMemories([], 'Loading memory evidence…'); + if (isGraphMemoryNode(item)) { + const known = state.memories.find(memory => memory.id === item.id); + if (request !== state.graphConnectionsRequest || workspace !== state.workspace) return; + renderGraphConnectionMemories( + [known || graphMemoryCard(item)], 'No memory details are available for this node.', + ); + return; + } + const controller = new AbortController(); + state.graphConnectionsController = controller; + const timeout = window.setTimeout(() => controller.abort(), GRAPH_CONNECTION_MEMORIES_TIMEOUT_MS); + try { + const detail = await api( + `/graph/entities/${encodeURIComponent(item.id)}/memories?${query(workspace)}${repo ? `&repo=${encodeURIComponent(repo)}` : ''}${graphAsOfQuery()}${historyQuery}`, + { signal: controller.signal }, + ); + if (request !== state.graphConnectionsRequest || workspace !== state.workspace) return; + const evidence = detail.evidence || []; + const total = number(detail.totals && detail.totals.evidence) || evidence.length; + byId('graph-connection-memory-title').textContent = `${total} ${total === 1 ? 'memory' : 'memories'} for ${title}`; + renderGraphConnectionMemories( + evidence.map(graphMemoryCard), + 'No active memories support this connected node.', + ); + } catch (error) { + if (request !== state.graphConnectionsRequest || workspace !== state.workspace) return; + byId('graph-connection-memory-title').textContent = `Memories for ${title}`; + renderGraphConnectionMemories([], error && error.name === 'AbortError' + ? 'Memory evidence loading timed out. Choose this node again to retry.' + : `Could not load memory evidence: ${error.message}`); + } finally { + window.clearTimeout(timeout); + if (state.graphConnectionsController === controller) state.graphConnectionsController = null; + } + } + + function graphConnectionRow(entry) { + const item = entry.item; + const row = node('article', 'graph-connection-row'); + row.setAttribute('role', 'listitem'); + const details = node('div'); + const relations = [...entry.relations]; + const relationLabel = relations.length ? ` · ${relations.join(', ')}` : ''; + details.append( + node('h3', '', item.name), + node('p', '', `${number(item.degree)} connections · ${item.etype}${relationLabel}`), + ); + const actions = node('div', 'graph-connection-actions'); + actions.append( + button('Focus graph', 'secondary-button', () => { + closeGraphConnections(); + revealGraphNode(item.id, item.name); + }), + button('Memories', 'secondary-button', () => ( + showGraphConnectionMemories(item, entry.includeHistory) + )), + ); + row.append(details, actions); + return row; + } + + function openGraphConnections(item) { + if (!item || item.id === undefined || item.id === null) return; + cancelGraphConnectionMemoryLoad(); + state.graphConnectionsFocusId = String(item.id); + state.graphConnectionsFocusLabel = item.name || item.label || item.id; + const focusButton = byId('graph-connections-focus'); + if (focusButton) focusButton.hidden = !state.graphEngine; + const dialog = byId('graph-connections-dialog'); + const entries = graphConnectionEntries(item); + const title = item.name || item.label || item.id; + byId('graph-connections-title').textContent = `Connected to ${title}`; + byId('graph-connections-meta').textContent = `${entries.length} direct ${entries.length === 1 ? 'connection' : 'connections'} visible in this graph view`; + const target = byId('graph-connections-list'); + target.replaceChildren(); + if (!entries.length) target.append(empty('No connected nodes are visible in this graph view.')); + else entries.forEach(entry => target.append(graphConnectionRow(entry))); + byId('graph-connection-memory-title').textContent = 'Memories'; + renderGraphConnectionMemories([], 'Choose a connected node to inspect its memory evidence.'); + if (!dialog.open) dialog.showModal(); + } + + function updateGraphFacts(data) { + const stats = byId('graph-stats'); + stats.replaceChildren(); + const degrees = data.nodes.map(item => number(item.degree)).sort((a, b) => a - b); + const values = [ + ['Entities', data.nodes.length], + ['Relations', data.links.length], + ['Unlinked', data.nodes.filter(item => !number(item.degree)).length], + ['Median links', degrees.length ? degrees[Math.floor(degrees.length / 2)] : 0], + ]; + values.forEach(([label, value]) => { + const item = node('div', 'stat-item'); + item.append(node('span', '', label), node('strong', '', number(value).toLocaleString())); + stats.append(item); + }); + const top = byId('graph-top'); + top.replaceChildren(); + [...data.nodes].sort((a, b) => number(b.degree) - number(a.degree)).slice(0, 7).forEach(item => { + const control = node('button', 'compact-row'); + control.type = 'button'; + control.append(node('strong', '', item.name), node('span', '', `${number(item.degree)} connections · ${item.etype}`)); + control.addEventListener('click', () => openGraphConnections(item)); + top.append(control); + }); + } + + function updateGraphModeControls() { + const full = state.graphMode === 'full'; + const repoFilter = byId('graph-repo-filter'); + const repoLabel = document.querySelector('label[for="graph-repo-filter"]'); + if (repoFilter) { + repoFilter.placeholder = full + ? 'Filter by exact repository name…' + : 'Filter to a repository or topic…'; + repoFilter.title = full + ? 'All Nodes accepts an exact repository name from this workspace.' + : ''; + } + if (repoLabel) repoLabel.textContent = full + ? 'Filter by exact repository name' + : 'Filter to a repository or topic'; + ['graph-min-degree', 'graph-tune-min-degree', 'graph-collapse', 'graph-depth', + 'graph-show-unlinked', 'graph-flow', 'graph-flow-speed'].forEach(id => { + const control = byId(id); + if (control) { control.disabled = false; control.title = ''; } + }); + all('[data-graph-layer="code"]').forEach(control => { + control.disabled = false; + control.title = full + ? 'Choose an exact repository first, then add its code overlay within the All Nodes capacity.' + : ''; + }); + /* Focus-depth and auto-collapse are not implemented in the Every-node engine yet. + Disable them honestly instead of leaving controls that silently do nothing. */ + if (full && byId('graph-preset').value === 'every') { + ['graph-collapse', 'graph-depth'].forEach(id => { + const control = byId(id); + if (!control) return; + control.disabled = true; + control.title = 'Not yet available in the Every-node view.'; + }); + } + const lodNote = byId('graph-lod-note'); + if (lodNote) lodNote.hidden = !full; + byId('graph-reheat').textContent = full ? 'Reflow layout' : 'Reheat layout'; + const freezeRow = byId('graph-freeze-row'); + if (freezeRow) freezeRow.hidden = full; + const orbitPause = byId('graph-orbit-pause-row'); + const galaxy = graphIsGalaxy(); + const orbitCapable = galaxy && (!full || state.graphGalaxyQuality); + if (orbitPause) orbitPause.hidden = !orbitCapable; + const style = byId('graph-style').value; + const styleNotes = full ? GRAPH_LOD_STYLE_NOTES : GRAPH_STYLE_NOTES; + byId('graph-style-note').textContent = styleNotes[style] || styleNotes.classic; + updateGraphGalaxyControls(); + const preset = GRAPH_PRESET_LABELS[byId('graph-preset').value] || 'Galaxy gravity'; + byId('graph-mode').textContent = `${full ? 'All nodes · LOD' : 'High quality'} · ${preset}`; + } + + function graphIsGalaxy() { + return byId('graph-preset').value === 'galaxy'; + } + + function graphOverlayEnabled(galaxyQuality = state.graphGalaxyQuality) { + // Every can use the authored Galaxy renderer even though its toolbar preset is 'every'. + return graphIsGalaxy() || (galaxyQuality && byId('graph-preset').value === 'every'); + } + + function graphSizeBy() { + return graphIsGalaxy() && state.graphMode !== 'full' + ? 'evidence_mass' : byId('graph-size').value; + } + + function updateGraphGalaxyControls() { + const galaxy = graphIsGalaxy(); + const full = state.graphMode === 'full'; + const orbitCapable = galaxy && (!full || state.graphGalaxyQuality); + const size = byId('graph-size'); + if (galaxy && !full) { + if (['degree', 'betweenness'].includes(size.value)) size.dataset.legacyValue = size.value; + size.value = 'evidence_mass'; + size.disabled = true; + size.title = 'Galaxy gravity sizes stars by evidence mass.'; + } else { + size.disabled = false; + size.title = ''; + if (size.value === 'evidence_mass') size.value = size.dataset.legacyValue || 'degree'; + } + const labels = full + ? ['Repel force', 'Link distance', 'Centre gravity'] + : galaxy + ? ['Orbital speed', 'Link distance · tight ↔ loose', 'Galactic gravity · loose ↔ tight'] + : ['Repel force', 'Link distance', 'Centre gravity']; + ['graph-repel-label', 'graph-link-label', 'graph-gravity-label'].forEach((id, index) => { + const label = byId(id); + if (label) label.textContent = labels[index]; + }); + /* Keep the shared spacetime controls available for every renderer. Galaxy-specific rows + are gated below, while the normalized force controls remain useful to compact and + community layouts as well. */ + byId('graph-spacetime-tuning').hidden = false; + const forceLabels = full + ? ['Core attraction', 'Core mass', 'Cluster cohesion', 'Settling resistance', 'Link spring'] + : ['Galactic gravity', 'Black hole mass', 'Local solar gravity', 'Space friction', 'Spring stiffness']; + ['graph-gravitational-constant-label', 'graph-black-hole-mass-label', + 'graph-local-gravitational-constant-label', 'graph-space-damping-label', +'graph-spring-stiffness-label'].forEach((id, index) => { + const label = byId(id); + if (label) label.textContent = forceLabels[index]; + }); + const springLabel = byId('graph-spring-stiffness-label'); + const springCapable = galaxy || (full && !state.graphGalaxyQuality); + if (springLabel && springLabel.parentElement) { + springLabel.parentElement.hidden = !springCapable; + } + const galaxyRenderer = galaxy && (!full || state.graphGalaxyQuality); + const everyRenderer = full && !state.graphGalaxyQuality; + byId('graph-spacetime-summary').textContent = galaxyRenderer + ? 'Spacetime · black-hole orbit controls' + : everyRenderer ? 'All-node force refinement' : 'Responsive force controls'; + byId('graph-spacetime-note').textContent = galaxyRenderer + ? 'Drag and release a node to slingshot it into a new orbit.' + : everyRenderer + ? 'These values refine the settled worker layout.' + : 'These values tune the responsive force layout.'; + byId('graph-orbits-pause-label').textContent = 'Pause orbits'; + byId('graph-orbits-pause-detail').textContent = 'physics'; + byId('graph-orbits-pause').setAttribute('aria-label', 'Pause orbital physics'); + const orbitPauseRow = byId('graph-orbit-pause-row'); + if (orbitPauseRow) orbitPauseRow.hidden = !orbitCapable; + } + + function setChoicePressed(selector, dataKey, selected) { + all(selector).forEach(control => { + const active = control.dataset[dataKey] === selected; + control.classList.toggle('active', active); + control.setAttribute('aria-pressed', String(active)); + }); + } + + function syncGraphChoices() { + const preset = byId('graph-preset').value; + const style = byId('graph-style').value; + const color = byId('graph-color').value; + const palette = byId('graph-palette').value; + setChoicePressed('[data-graph-preset-choice]', 'graphPresetChoice', preset); + setChoicePressed('[data-graph-style-choice]', 'graphStyleChoice', style); + setChoicePressed('[data-graph-color-choice]', 'graphColorChoice', color); + setChoicePressed('[data-graph-palette-choice]', 'graphPaletteChoice', palette); + const styleNotes = state.graphMode === 'full' ? GRAPH_LOD_STYLE_NOTES : GRAPH_STYLE_NOTES; + byId('graph-style-note').textContent = styleNotes[style] || styleNotes.classic; + updateGraphGalaxyControls(); + syncGraphSavedViews(); + } + + function setGraphSwitch(id, on) { + const control = byId(id); + control.classList.toggle('on', on); + control.setAttribute('aria-checked', String(on)); + } + + function graphValueInRange(id, value, fallback) { + const control = byId(id); + const raw = Number(value); + const safe = Number.isFinite(raw) ? raw : fallback; + if (!control) return safe; + const min = Number(control.min); + const max = Number(control.max); + return Math.min(Number.isFinite(max) ? max : safe, Math.max(Number.isFinite(min) ? min : safe, safe)); + } + /* Controls keep their human-readable ranges and defaults. The engine value is computed by + a clamped identity 1:1 mapping: the slider's raw position flows straight to the engine, + bounded by the HTML min/max. The earlier 2x response saturated against the HTML bounds + for slider values near the loose and tight ends, producing visible plateaus where the + user dragged the slider but the engine value didn't change. Identity 1:1 with + [min, max] clamping gives every integer tick in the slider's full HTML range a + strictly distinct engine value. */ + function graphSliderResponseBaseline(item) { + if (!item) return 0; + if (item.id === 'graph-flow-speed') return 45; + const preset = byId('graph-preset'); + const tuning = preset ? graphPresetTuning(preset.value) : null; + const candidate = tuning && tuning[item.key]; + return Number.isFinite(Number(candidate)) ? Number(candidate) : item.fallback; + } + function graphSliderResponseValue(id, value, baseline) { + const control = byId(id); + if (!control) return Number.isFinite(Number(value)) ? Number(value) : baseline; + /* Flow speed is a linear control: the 2x response centered at 45 would + clamp every visible value in the lower quarter of the slider to + 0, which the compat engine then treats as "stop" and the user + sees an inert slider end. Use the raw value for flow-speed and + keep the 2x response for the geometry controls (gravity, repel, + link, etc.) where the centered calibration is intentional. */ + if (id === 'graph-flow-speed') { + const rawFlow = graphValueInRange(id, value, baseline); + return Number.isFinite(Number(rawFlow)) ? Number(rawFlow) : baseline; + } + const raw = graphValueInRange(id, value, baseline); + const min = Number(control.min); + const max = Number(control.max); + return Math.min(Number.isFinite(max) ? max : raw, + Math.max(Number.isFinite(min) ? min : raw, raw)); + } + function graphSliderInputValue(id, value, baseline) { + const control = byId(id); + if (!control) return Number.isFinite(Number(value)) ? Number(value) : baseline; + const min = Number(control.min); + const max = Number(control.max); + const safe = graphValueInRange(id, value, baseline); + /* Identity inverse of the clamped identity response. */ + return Math.min(Number.isFinite(max) ? max : safe, + Math.max(Number.isFinite(min) ? min : safe, safe)); + } + + function graphScopeValue(id, value, fallback) { + const control = byId(id); + const raw = Number(value); + const safe = Number.isFinite(raw) ? raw : fallback; + if (!control) return Math.round(safe); + const min = Number(control.min); + const max = Number(control.max); + return Math.round(Math.min(Number.isFinite(max) ? max : safe, + Math.max(Number.isFinite(min) ? min : safe, safe))); + } + + + function graphTuningEngineSettings() { + return GRAPH_TUNING.reduce((settings, item) => { + const raw = number(byId(item.id).value); + settings[item.key] = graphSliderResponseValue( + item.id, raw, graphSliderResponseBaseline(item), + ); + return settings; + }, { + flowSpeed: graphSliderResponseValue( + 'graph-flow-speed', number(byId('graph-flow-speed').value), 45, + ), + }); + } + + function graphSpacetimeEngineSettings() { + const controls = GRAPH_SPACETIME_TUNING.reduce((settings, item) => { + const raw = number(byId(item.id).value); + settings[item.key] = graphSliderResponseValue(item.id, raw, item.fallback); + return settings; + }, {}); + return { + gravitationalConstant: controls.gravitationalConstant / 30, + blackHoleMass: graphBlackHoleMassMultiplier(controls.blackHoleMass), + localGravitationalConstant: controls.localGravitationalConstant / 30, + damping: controls.damping, + springStiffness: controls.springStiffness / 32, + orbitPaused: state.graphOrbitPaused, + }; + } + + function graphScopeEngine() { + return { + minDegree: graphScopeValue('graph-min-degree', byId('graph-min-degree').value, 1), + showUnlinked: state.graphShowUnlinked, + depth: graphScopeValue('graph-depth', byId('graph-depth').value, 2), + }; + } + + + let graphPreferencesSaveScheduled = false; + let graphPreferencesSaveCancel = null; + function scheduleGraphPreferencesSave() { + if (graphPreferencesSaveScheduled) return; + graphPreferencesSaveScheduled = true; + const flush = () => { + if (!graphPreferencesSaveScheduled) return; + graphPreferencesSaveScheduled = false; + graphPreferencesSaveCancel = null; + saveGraphPreferences(); + }; + if (typeof requestAnimationFrame === 'function') { + const frame = requestAnimationFrame(flush); + graphPreferencesSaveCancel = () => cancelAnimationFrame(frame); + } else { + const timer = setTimeout(flush, 0); + graphPreferencesSaveCancel = () => clearTimeout(timer); + } + } + + function flushGraphPreferencesSave() { + if (!graphPreferencesSaveScheduled) return; + graphPreferencesSaveScheduled = false; + const cancel = graphPreferencesSaveCancel; + graphPreferencesSaveCancel = null; + if (cancel) cancel(); + saveGraphPreferences(); + } + + function graphPresetTuning(preset) { + const available = window.EngraphisGraph && window.EngraphisGraph.PRESETS; + const source = (available && available[preset]) || GRAPH_PRESET_TUNING[preset] || GRAPH_PRESET_TUNING.communities; + return GRAPH_TUNING.reduce((settings, item) => { + settings[item.key] = source && Number.isFinite(Number(source[item.key])) + ? Number(source[item.key]) : item.fallback; + return settings; + }, {}); + } + + function setGraphTuningControl(item, value) { + const control = byId(item.id); + const next = graphValueInRange(item.id, value, item.fallback); + control.value = String(next); + const rendered = item.precision ? next.toFixed(item.precision) : String(Math.round(next)); + const output = byId(`${item.id}-output`); + output.value = rendered; + output.textContent = rendered; + return next; + } + + function graphTuningSettings() { + return GRAPH_TUNING.reduce((settings, item) => { + settings[item.key] = number(byId(item.id).value); + return settings; + }, { flowSpeed: number(byId('graph-flow-speed').value) }); + } + + function setGraphSpacetimeControl(item, value) { + const control = byId(item.id); + const next = graphValueInRange(item.id, value, item.fallback); + control.value = String(next); + const rendered = item.precision ? next.toFixed(item.precision) : String(Math.round(next)); + const output = byId(`${item.id}-output`); + output.value = rendered; + output.textContent = rendered; + return next; + } + + + const GRAPH_BLACK_HOLE_MASS_BASELINE = 96; + function graphBlackHoleMassMultiplier(controlValue) { + const value = number(controlValue); + /* Below the 96 baseline the multiplier scales linearly. Above it, every +10 slider units + adds exactly +0.10 to the compact central-mass multiplier: 96→1.0, 106→1.1, 116→1.2. + Local stellar wells remain owned exclusively by Local solar gravity. */ + return value <= GRAPH_BLACK_HOLE_MASS_BASELINE + ? Math.max(0, value / GRAPH_BLACK_HOLE_MASS_BASELINE) + : 1 + (value - GRAPH_BLACK_HOLE_MASS_BASELINE) / 100; + } + + + + function syncGraphSpacetimeTuning(settings) { + GRAPH_SPACETIME_TUNING.forEach(item => setGraphSpacetimeControl(item, + settings && settings[item.key])); + setGraphSwitch('graph-orbits-pause', settings && settings.orbitPaused === true); + } + + function syncGraphTuning(settings) { + GRAPH_TUNING.forEach(item => setGraphTuningControl(item, settings && settings[item.key])); + const flowSpeed = graphValueInRange('graph-flow-speed', settings && settings.flowSpeed, 45); + byId('graph-flow-speed').value = String(flowSpeed); + byId('graph-flow-speed-output').value = String(Math.round(flowSpeed)); + byId('graph-flow-speed-output').textContent = String(Math.round(flowSpeed)); + } + + function graphScope() { + return graphScopeEngine(); + } + + function applyGraphScope() { + if (state.graphEngine) state.graphEngine.setScope(graphScope()); + } + + function setGraphMinDegree(value, apply = true) { + const next = graphScopeValue('graph-min-degree', value, 1); + byId('graph-min-degree').value = String(next); + byId('graph-min-degree-output').value = String(Math.round(next)); + byId('graph-min-degree-output').textContent = String(Math.round(next)); + byId('graph-tune-min-degree').value = String(next); + byId('graph-tune-min-degree-output').value = String(Math.round(next)); + byId('graph-tune-min-degree-output').textContent = String(Math.round(next)); + if (apply) applyGraphScope(); + } + + function setGraphDepth(value, apply = true) { + const next = graphScopeValue('graph-depth', value, 2); + byId('graph-depth').value = String(next); + byId('graph-depth-output').value = String(Math.round(next)); + byId('graph-depth-output').textContent = String(Math.round(next)); + if (apply) applyGraphScope(); + } + + function setGraphShowUnlinked(on, apply = true) { + const next = on === true; + state.graphShowUnlinked = next; + const control = byId('graph-show-unlinked'); + control.textContent = 'Unlinked nodes'; + control.setAttribute('aria-pressed', String(next)); + control.title = next + ? 'Hide entities that have no relations in this graph view' + : 'Show entities that have no relations in this graph view'; + if (apply) applyGraphScope(); + } + + function graphLayerState() { + return all('[data-graph-layer]').reduce((layers, control) => { + layers[control.dataset.graphLayer] = control.getAttribute('aria-pressed') === 'true'; + return layers; + }, {}); + } + + function setGraphLayers(layers) { + const source = layers && typeof layers === 'object' ? layers : GRAPH_DEFAULT_LAYERS; + all('[data-graph-layer]').forEach(control => { + const active = source[control.dataset.graphLayer] !== false; + control.classList.toggle('active', active); + control.setAttribute('aria-pressed', String(active)); + }); + } + + function updateGraphLayerCounts(data, supplied) { + const counts = GRAPH_LAYERS.reduce((result, layer) => { result[layer] = 0; return result; }, {}); + if (Array.isArray(supplied)) supplied.forEach(item => { + if (item && GRAPH_LAYERS.includes(item.layer)) counts[item.layer] = number(item.count); + }); + else (data.links || []).forEach(link => { + if (GRAPH_LAYERS.includes(link.layer)) counts[link.layer] += 1; + }); + GRAPH_LAYERS.forEach(layer => { byId(`graph-layer-${layer}-count`).textContent = counts[layer].toLocaleString(); }); + } + + function syncGraphSavedViews() { + all('[data-graph-saved-view]').forEach(control => { + const active = control.dataset.graphSavedView === state.graphSavedView; + control.classList.toggle('active', active); + control.setAttribute('aria-pressed', String(active)); + }); + } + + function clearGraphSavedView() { + if (!state.graphSavedView) return; + state.graphSavedView = ''; + syncGraphSavedViews(); + } + + function graphPreference(name, fallback, allowed) { + try { + const saved = JSON.parse(localStorage.getItem(GRAPH_PREFERENCES_KEY) || '{}'); + const value = saved && typeof saved === 'object' ? saved[name] : undefined; + return allowed && !allowed.includes(value) ? fallback : value === undefined ? fallback : value; + } catch (_) { + return fallback; + } + } + + function graphPreferenceSnapshot() { + const layers = graphLayerState(); + return { + physicsVersion: GRAPH_PHYSICS_VERSION, + preset: byId('graph-preset').value, + style: byId('graph-style').value, + color: byId('graph-color').value, + palette: byId('graph-palette').value, + flow: byId('graph-flow').getAttribute('aria-checked') === 'true', + labels: byId('graph-labels').getAttribute('aria-checked') === 'true', + tuning: graphTuningSettings(), + /* Pause is a session action, like Freeze. Persist the numeric spacetime tuning without + silently reopening a future dashboard with every orbit stopped. */ + spacetimeTuning: GRAPH_SPACETIME_TUNING.reduce((settings, item) => { + settings[item.key] = number(byId(item.id).value); + return settings; + }, {}), + minDegree: number(byId('graph-min-degree').value), + depth: number(byId('graph-depth').value), + showUnlinked: state.graphShowUnlinked, + layers, + includeCode: state.graphIncludeCode, + savedView: state.graphSavedView, + bridges: byId('graph-bridges').checked, + collapse: byId('graph-collapse').checked, + asOf: byId('graph-as-of').value, + ghosts: byId('graph-ghosts').checked, + size: byId('graph-size').value, + repoFilter: byId('graph-repo-filter').value.slice(0, 200), + }; + } + + function saveGraphPreferences() { + try { + localStorage.setItem(GRAPH_PREFERENCES_KEY, JSON.stringify(graphPreferenceSnapshot())); + } catch (_) {} + } + + function restoreGraphPreferences() { + let hasSavedPreferences = false; + try { hasSavedPreferences = localStorage.getItem(GRAPH_PREFERENCES_KEY) !== null; } catch (_) {} + const preset = graphPreference('preset', byId('graph-preset').value, + ['original', 'compact', 'communities', 'radial', 'constellation', 'galaxy']); + const style = graphPreference('style', byId('graph-style').value, + ['classic', 'galaxy', 'solar', 'cyber']); + const color = graphPreference('color', byId('graph-color').value, + ['community', 'connections', 'type']); + const palette = graphPreference('palette', byId('graph-palette').value, + ['theme', 'aurora', 'ocean', 'ember', 'contrast', 'custom']); + byId('graph-preset').value = preset; + byId('graph-style').value = style; + byId('graph-color').value = color; + byId('graph-palette').value = palette; + + const savedTuning = graphPreference('tuning', {}); + const savedPhysicsVersion = Number(graphPreference('physicsVersion', 0)); + const sourcePhysicsVersion = Number.isFinite(savedPhysicsVersion) ? savedPhysicsVersion : 0; + const needsPhysicsMigration = version => hasSavedPreferences + && sourcePhysicsVersion < version; + const legacyPhysics = needsPhysicsMigration(GRAPH_PHYSICS_VERSION); + const needsPhysicsV3Migration = needsPhysicsMigration(3); + const needsPhysicsV4Migration = needsPhysicsMigration(4); + const effectiveTuning = savedTuning && typeof savedTuning === 'object' + ? { ...savedTuning } : {}; + const savedSpacetimeTuning = graphPreference('spacetimeTuning', {}); + /* A failed physics-control experiment could persist every attractive force at its maximum, + friction at zero, and the Galaxy spacing control at 400. That exact vector is not a + useful custom preset: it collapses the visible graph and can reduce hundreds of loaded + entities to a small central knot. Physics v3 resets only this known-bad snapshot. */ + const staleMaxedPhysics = needsPhysicsV3Migration && Number(effectiveTuning.gravity) === 400 + && Number(savedSpacetimeTuning && savedSpacetimeTuning.gravitationalConstant) === 200 + && Number(savedSpacetimeTuning && savedSpacetimeTuning.blackHoleMass) === 500 + && Number(savedSpacetimeTuning && savedSpacetimeTuning.localGravitationalConstant) === 200 + && Number(savedSpacetimeTuning && savedSpacetimeTuning.damping) === 0 + && Number(savedSpacetimeTuning && savedSpacetimeTuning.springStiffness) === 100; + if (staleMaxedPhysics) { + delete effectiveTuning.repel; + delete effectiveTuning.link; + delete effectiveTuning.gravity; + } + /* Older preferences persisted 48 and then 60 as Galaxy's default orbital speed. Physics v4 + defines the control as a percentage with 100 as neutral, so migrate only those exact + retired defaults. Every other custom speed and every unrelated preference remains intact. */ + if (needsPhysicsV4Migration && preset === 'galaxy' + && [48, 60].includes(Number(effectiveTuning.repel))) { + effectiveTuning.repel = 100; + } + /* Physics v5 made 120 the Galaxy gravity default. Migrate only the exact retired default; + a saved 96 in an already-versioned v5 snapshot remains an intentional user choice. */ + if (needsPhysicsMigration(5) && preset === 'galaxy' && Number(effectiveTuning.gravity) === 96) { + effectiveTuning.gravity = 120; + } + /* Physics v6 recalibrates Galaxy gravity to 72 and the spacetime panel to 60/96/60. Move + only the exact retired default vector, so a value the person actually chose survives. */ + if (needsPhysicsMigration(6)) { + if (preset === 'galaxy' && Number(effectiveTuning.gravity) === 120) { + effectiveTuning.gravity = 72; + } + const retiredSpacetimeDefaults = { + gravitationalConstant: 100, blackHoleMass: 160, localGravitationalConstant: 100, + }; + const savedSpacetime = savedSpacetimeTuning && typeof savedSpacetimeTuning === 'object' + ? savedSpacetimeTuning : {}; + const retiredKeys = Object.keys(retiredSpacetimeDefaults); + const hasRetiredVector = retiredKeys.every(key => + Number(savedSpacetime[key]) === retiredSpacetimeDefaults[key]); + if (hasRetiredVector) { + retiredKeys.forEach(key => { + savedSpacetime[key] = GRAPH_SPACETIME_TUNING.find(item => item.key === key).fallback; + }); + } + } + syncGraphTuning({ + ...graphPresetTuning(preset), + ...effectiveTuning, + }); + /* Pause orbits is deliberately session-only. Old snapshots may contain orbitPaused=true; + ignore it so a fresh dashboard always starts with live galactic motion. */ + state.graphOrbitPaused = false; + syncGraphSpacetimeTuning({ + ...(!staleMaxedPhysics && savedSpacetimeTuning + && typeof savedSpacetimeTuning === 'object' + ? savedSpacetimeTuning : {}), + orbitPaused: false, + }); + + const savedMin = Number(graphPreference('minDegree', number(byId('graph-min-degree').value))); + const minDegree = Number.isFinite(savedMin) ? Math.max(0, Math.min(12, Math.round(savedMin))) : 1; + setGraphMinDegree(minDegree); + setGraphDepth(graphPreference('depth', 2)); + const savedRepo = graphPreference('repoFilter', ''); + byId('graph-repo-filter').value = typeof savedRepo === 'string' ? savedRepo.slice(0, 200) : ''; + const savedAsOf = graphPreference('asOf', ''); + byId('graph-as-of').value = typeof savedAsOf === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(savedAsOf) + ? savedAsOf : ''; + setGraphShowUnlinked(staleMaxedPhysics + || graphPreference('showUnlinked', state.graphShowUnlinked) === true); + byId('graph-bridges').checked = graphPreference('bridges', byId('graph-bridges').checked) === true; + byId('graph-collapse').checked = graphPreference('collapse', byId('graph-collapse').checked) === true; + byId('graph-ghosts').checked = graphPreference('ghosts', byId('graph-ghosts').checked) !== false; + byId('graph-size').value = graphPreference('size', byId('graph-size').value, + ['degree', 'betweenness', 'evidence_mass']); + // Freeze is deliberately session-only. A previously frozen arrangement must not make a + // freshly opened graph look broken; physics starts live until the person clicks Freeze. + state.graphFrozen = false; + setGraphSwitch('graph-freeze', state.graphFrozen); + setGraphSwitch('graph-flow', graphPreference('flow', true) !== false); + setGraphSwitch('graph-labels', graphPreference('labels', false) === true); + const savedLayers = graphPreference('layers', GRAPH_DEFAULT_LAYERS); + setGraphLayers(GRAPH_LAYERS.reduce((layers, layer) => { + layers[layer] = !savedLayers || typeof savedLayers !== 'object' || savedLayers[layer] !== false; + return layers; + }, {})); + state.graphIncludeCode = graphPreference('includeCode', false) === true; + state.graphSavedView = graphPreference('savedView', '', ['', ...Object.keys(GRAPH_SAVED_VIEWS)]); + syncGraphSavedViews(); + if (legacyPhysics) saveGraphPreferences(); + } + + function savedGraphView(id) { + if (id === 'custom') { + try { + const custom = JSON.parse(localStorage.getItem(GRAPH_CUSTOM_VIEW_KEY) || 'null'); + return custom && typeof custom === 'object' ? custom : null; + } catch (_) { + return null; + } + } + return GRAPH_SAVED_VIEWS[id] || null; + } + + function applyGraphView(id) { + const view = savedGraphView(id); + if (!view) { + showNotice(id === 'custom' ? 'No locally saved graph view yet.' : 'That saved graph view is unavailable.'); + return; + } + const preset = Object.prototype.hasOwnProperty.call(GRAPH_PRESET_LABELS, view.preset) + ? view.preset : byId('graph-preset').value; + const style = ['classic', 'galaxy', 'solar', 'cyber'].includes(view.style) ? view.style : byId('graph-style').value; + const color = ['community', 'connections', 'type'].includes(view.color) ? view.color : byId('graph-color').value; + const palette = ['theme', 'aurora', 'ocean', 'ember', 'contrast', 'custom'].includes(view.palette) + ? view.palette : byId('graph-palette').value; + const previousIncludeCode = state.graphIncludeCode; + const previousShowUnlinked = state.graphShowUnlinked; + const previousAsOf = byId('graph-as-of').value; + const previousRepo = (byId('graph-repo-filter').value || '').trim(); + const asOf = typeof view.asOf === 'string' ? view.asOf : previousAsOf; + const repoFilter = typeof view.repoFilter === 'string' + ? view.repoFilter.slice(0, 200) : byId('graph-repo-filter').value; + const nextRepo = repoFilter.trim(); + state.graphIncludeCode = typeof view.includeCode === 'boolean' + ? view.includeCode : state.graphIncludeCode; + byId('graph-preset').value = preset; + byId('graph-style').value = style; + byId('graph-color').value = color; + byId('graph-palette').value = palette; + byId('graph-as-of').value = asOf; + byId('graph-repo-filter').value = repoFilter; + if (typeof view.ghosts === 'boolean') byId('graph-ghosts').checked = view.ghosts; + if (['degree', 'betweenness', 'evidence_mass'].includes(view.size)) byId('graph-size').value = view.size; + if (typeof view.bridges === 'boolean') byId('graph-bridges').checked = view.bridges; + if (typeof view.collapse === 'boolean') byId('graph-collapse').checked = view.collapse; + if (typeof view.flow === 'boolean') setGraphSwitch('graph-flow', view.flow); + if (typeof view.labels === 'boolean') setGraphSwitch('graph-labels', view.labels); + setGraphSwitch('graph-freeze', state.graphFrozen); + syncGraphTuning({ + ...graphPresetTuning(preset), + ...(view.tuning && typeof view.tuning === 'object' ? view.tuning : {}), + }); + syncGraphSpacetimeTuning( + view.spacetimeTuning && typeof view.spacetimeTuning === 'object' + ? view.spacetimeTuning : {} + ); + setGraphMinDegree(view.minDegree == null ? 1 : view.minDegree, false); + setGraphDepth(view.depth == null ? 2 : view.depth, false); + setGraphShowUnlinked(view.showUnlinked === true, false); + setGraphLayers(view.layers); + state.graphSavedView = id === 'custom' ? '' : id; + syncGraphChoices(); + if (state.graphEngine) { + state.graphEngine.apply(graph => { + graph.setPreset(preset); + graph.setStyle(style); + graph.setColorBy(color); + applyGraphPalette(palette); + graph.setSettings({ + ...graphTuningEngineSettings(), + ...graphSpacetimeEngineSettings(), + flow: byId('graph-flow').getAttribute('aria-checked') === 'true', + labels: byId('graph-labels').getAttribute('aria-checked') === 'true', + frozen: state.graphFrozen, + }); + graph.setScope(graphScope()); + graph.setLayers(graphLayerState()); + graph.setRepoFilter(repoFilter); + graph.setAsOf(graphAsOfTimestamp()); + graph.setSizeBy(graphSizeBy()); + graph.setBridges(byId('graph-bridges').checked); + graph.setCollapse(byId('graph-collapse').checked ? 'auto' : false); + graph.setGhosts(byId('graph-ghosts').checked); + }, false, !state.graphFrozen); + state.graphEngine.freeze(state.graphFrozen); + } + graphLifecycle.sync(); + saveGraphPreferences(); + if (previousIncludeCode !== state.graphIncludeCode + || previousShowUnlinked !== state.graphShowUnlinked || previousAsOf !== asOf + || previousRepo !== nextRepo) { + loadGraph({ force: true }); + } + const label = all('[data-graph-saved-view]').find(control => control.dataset.graphSavedView === id); + showNotice(`${id === 'custom' ? 'Saved' : (label ? label.textContent : 'Saved')} graph view applied.`); + } + + function saveCurrentGraphView() { + try { + localStorage.setItem(GRAPH_CUSTOM_VIEW_KEY, JSON.stringify(graphPreferenceSnapshot())); + byId('graph-saved-view-status').textContent = 'Current graph view saved locally.'; + showNotice('Current graph view saved locally.'); + } catch (_) { + showNotice('Could not save this graph view in local storage.'); + } + } + + function resetGraphTuning() { + const preset = byId('graph-preset').value; + const previousIncludeCode = state.graphIncludeCode; + const previousShowUnlinked = state.graphShowUnlinked; + state.graphIncludeCode = false; + syncGraphTuning({ ...graphPresetTuning(preset), flowSpeed: 45 }); + state.graphOrbitPaused = false; + syncGraphSpacetimeTuning({}); + setGraphMinDegree(1, false); + setGraphDepth(2, false); + setGraphShowUnlinked(true, false); + setGraphLayers(GRAPH_DEFAULT_LAYERS); + clearGraphSavedView(); + if (state.graphEngine) { + state.graphEngine.apply(graph => { + graph.setPreset(preset); + graph.setSettings({ + ...graphTuningEngineSettings(), + ...graphSpacetimeEngineSettings(), + frozen: state.graphFrozen, + }); + graph.setScope(graphScope()); + graph.setLayers(graphLayerState()); + }, false, !state.graphFrozen); + state.graphEngine.freeze(state.graphFrozen); + } + saveGraphPreferences(); + if (previousIncludeCode || previousShowUnlinked) loadGraph({ force: true }); + showNotice('Graph tuning reset to the selected layout defaults.'); + } + + function applyGraphPalette(name) { + const graph = state.graphEngine; + if (!graph) return; + graph.setPalette(name); + if (name === 'custom') graph.setTypeColors(GRAPH_CUSTOM_PALETTE); + } + + function graphThemeColors() { + const css = getComputedStyle(document.body); + return { + accent: css.getPropertyValue('--c-acc').trim() || '#a39bf1', + surface: css.getPropertyValue('--c-surface').trim() || '#16191f', + canvas: css.getPropertyValue('--c-bg').trim() || '#0e1014', + label: css.getPropertyValue('--c-fg').trim() || '#e7e9ee', + relation_label: css.getPropertyValue('--c-dim').trim() || '#929baa', + }; + } + + function setGraphTab(tab) { + all('[data-graph-tab]').forEach(control => { + const active = control.dataset.graphTab === tab; + control.classList.toggle('active', active); + control.setAttribute('aria-selected', String(active)); + control.tabIndex = active ? 0 : -1; + }); + all('[data-graph-tab-panel]').forEach(panel => { + panel.hidden = panel.dataset.graphTabPanel !== tab; + }); + } + + function downloadGraphFile(blob, name) { + const href = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = href; + link.download = name; + document.body.append(link); + link.click(); + link.remove(); + window.setTimeout(() => URL.revokeObjectURL(href), 0); + } + + function setGraphExportMenuOpen(open, restoreTriggerFocus = false) { + const trigger = byId('graph-export'); + const menu = byId('graph-export-menu'); + if (open) { + menu.hidden = false; + trigger.setAttribute('aria-expanded', 'true'); + byId('graph-export-png').focus(); + return; + } + trigger.setAttribute('aria-expanded', 'false'); + if (restoreTriggerFocus || menu.contains(document.activeElement)) trigger.focus(); + menu.hidden = true; + } + + function exportGraphJson() { + const graph = state.graphEngine && state.graphEngine.exportData + ? state.graphEngine.exportData() + : state.graphData || { nodes: [], links: [] }; + const payload = { + workspace: state.workspace, + exported_at: new Date().toISOString(), + nodes: graph.nodes, + links: graph.links, + }; + // Pretty-print normal exports for readability. An All Nodes payload stays compact + // to avoid the indentation expansion and extra main-thread work at the release limit. + const indentation = state.graphMode === 'full' ? undefined : 2; + downloadGraphFile(new Blob([JSON.stringify(payload, null, indentation)], { type: 'application/json' }), 'engraphis-graph.json'); + showNotice('Graph data exported as JSON.'); + } + + function exportGraphPng() { + const canvas = state.graphEngine && typeof state.graphEngine.exportImageCanvas === 'function' + ? state.graphEngine.exportImageCanvas() + : byId('graph-canvas').querySelector('canvas'); + if (!canvas || !canvas.toBlob) { + showNotice('The graph image is not ready yet. Export JSON data instead.'); + return; + } + canvas.toBlob(blob => { + if (!blob) { + showNotice('Could not capture the graph image. Export JSON data instead.'); + return; + } + downloadGraphFile(blob, 'engraphis-graph.png'); + showNotice('Graph image exported as PNG.'); + }, 'image/png'); + } + + function graphCountText(nodes, links, drawnLinks = null, visibleNodes = null) { + const available = number(state.graphMeta && state.graphMeta.nodes_available) || nodes; + const prefix = state.graphMode === 'full' ? 'All nodes · LOD' : 'High quality'; + const entityText = visibleNodes != null && number(visibleNodes) < number(nodes) + ? `${number(visibleNodes).toLocaleString()} visible of ${number(nodes).toLocaleString()} entities` + : available > nodes + ? `${number(nodes).toLocaleString()} of ${available.toLocaleString()} entities` + : `${number(nodes).toLocaleString()} entities`; + const totalRelations = state.graphMeta && (state.graphMeta.relations_available != null + ? state.graphMeta.relations_available : state.graphMeta.total_edges); + const hiddenRelations = drawnLinks == null + ? (totalRelations == null ? null : Math.max(0, number(totalRelations) - number(links))) + : Math.max(0, number(links) - number(drawnLinks)); + const hidden = state.graphMode === 'full' && hiddenRelations != null + ? ` · ${hiddenRelations.toLocaleString()} hidden relationships` + : ''; + return `${prefix} · ${entityText} · ${number(links).toLocaleString()} relations${hidden}`; + } + + function graphStatsChanged(stats) { + if (!stats) return; + const nodes = stats.nodes == null ? state.graphData.nodes.length : stats.nodes; + const links = stats.links == null ? state.graphData.links.length : stats.links; + byId('graph-count').textContent = graphCountText( + nodes, links, stats.drawnLinks, stats.visibleNodes, + ); + if (state.graphMode === 'full') { + const note = byId('graph-lod-note'); + const detail = note && note.querySelector('span'); + if (detail) detail.textContent = stats.layoutPending + ? 'Reflowing the complete graph in the background…' + : stats.collapsed + ? 'Clusters are condensed into representative nodes. Zoom in to expand them.' + : 'Layout, forces, scope, colour and relation flow update without reloading the complete graph.'; + } + } + + function graphMetricsChanged(metrics) { + state.graphMetrics = metrics || {}; + byId('graph-bridge-count').textContent = metrics && metrics.bridges != null + ? `${metrics.bridges} bridge ${metrics.bridges === 1 ? 'edge' : 'edges'}` + : ''; + } + + function graphAsOfTimestamp() { + const value = byId('graph-as-of').value; + if (!value) return null; + // A date picker represents the complete selected day, not midnight at its start. + const timestamp = Date.parse(`${value}T23:59:59.999Z`); + return Number.isFinite(timestamp) ? timestamp : null; + } + + function graphAsOfQuery() { + const timestamp = graphAsOfTimestamp(); + return timestamp === null ? '' : `&as_of=${encodeURIComponent(timestamp / 1000)}`; + } + + function graphLoadKey(workspace, mode, includeCode, showUnlinked, asOf, repo) { + return JSON.stringify([workspace, mode, includeCode, showUnlinked, asOf, repo || '']); + } + + function graphRepositoryNames() { + const names = new Set(); + const add = value => { + const name = text(value).trim(); + if (name) names.add(name); + }; + if (state.graphData && Array.isArray(state.graphData.repositories)) { + state.graphData.repositories.forEach(add); + } + const workspace = state.workspaces.find(item => workspaceName(item) === state.workspace); + if (workspace && Array.isArray(workspace.repos)) workspace.repos.forEach(add); + if (state.graphData && Array.isArray(state.graphData.nodes)) { + state.graphData.nodes.forEach(item => { + if (item && Array.isArray(item.repo_names)) item.repo_names.forEach(add); + }); + } + return names; + } + + function validatedGraphRepository(value) { + const candidate = text(value).trim().toLowerCase(); + if (!candidate) return ''; + for (const name of graphRepositoryNames()) { + if (name.toLowerCase() === candidate) return name; + } + return ''; + } + + function cancelGraphRepositoryReload() { + if (graphRepoLoadTimer === null) return; + window.clearTimeout(graphRepoLoadTimer); + graphRepoLoadTimer = null; + } + + function scheduleGraphRepositoryReload() { + cancelGraphRepositoryReload(); + graphRepoLoadTimer = window.setTimeout(() => { + graphRepoLoadTimer = null; + if (state.view === 'relations' + && (state.graphIncludeCode || state.graphMode === 'full')) { + loadGraph({ force: true }); + } + }, 250); + } + + function isCurrentGraphLoad(request) { + return Boolean(request + && request.id === state.graphLoadRequest + && request.key === state.graphLoadKey + && request.workspace === state.workspace + && request.mode === state.graphMode + && request.includeCode === state.graphIncludeCode + && request.showUnlinked === state.graphShowUnlinked + && request.asOf === graphAsOfTimestamp() + && request.repo === (byId('graph-repo-filter').value || '').trim()); + } + + function setGraphLoadControlsBusy(busy, disableRetry = true) { + const controls = disableRetry ? ['graph-retry'] : []; + controls.forEach(id => { + const control = byId(id); + if (control) control.disabled = busy; + }); + const every = document.querySelector('[data-graph-preset-choice="every"]'); + if (every) every.disabled = busy; + const retry = byId('graph-retry'); + if (retry && ((busy && disableRetry) || !busy)) { + retry.textContent = busy ? 'Reloading graph…' : 'Reload data'; + } + } + + function retryGraphLoad() { + // A Retry click starts a new request rather than inheriting a timed-out promise. Keep its + // pending state local to the button so rapid clicks cannot repeatedly cancel fresh work. + if (state.graphRetryPending) return; + state.graphRetryPending = true; + Promise.resolve(loadGraph({ force: true })).finally(() => { + state.graphRetryPending = false; + }); + } + + async function loadGraph({ force = false } = {}) { + if (!state.workspace) return; + const currentRepo = (byId('graph-repo-filter').value || '').trim(); + if (!force && state.graphWorkspace === state.workspace + && state.graphDataMode === state.graphMode + && state.graphDataIncludeCode === state.graphIncludeCode + && state.graphDataShowUnlinked === state.graphShowUnlinked + && state.graphDataAsOf === graphAsOfTimestamp() + && state.graphDataRepo === currentRepo && state.graphData) { + if (state.graphEngine) state.graphEngine.resize(); + graphLifecycle.sync(); + return; + } + const targetWorkspace = state.workspace; + const targetMode = state.graphMode; + const targetIncludeCode = state.graphIncludeCode; + const targetShowUnlinked = state.graphShowUnlinked; + const targetAsOf = graphAsOfTimestamp(); + const targetRepo = currentRepo; + const fullGraph = targetMode === 'full'; + const key = graphLoadKey( + targetWorkspace, targetMode, targetIncludeCode, targetShowUnlinked, targetAsOf, targetRepo, + ); + if (!force && state.graphLoadPromise && state.graphLoadKey === key) { + return state.graphLoadPromise; + } + const request = { + id: state.graphLoadRequest + 1, + key, + workspace: targetWorkspace, + mode: targetMode, + includeCode: targetIncludeCode, + showUnlinked: targetShowUnlinked, + asOf: targetAsOf, + repo: targetRepo, + }; + const controller = new AbortController(); + const previousController = state.graphLoadController; + // Publish the new identity before cancelling the old request. Its timeout/error handler + // then becomes a no-op even when the next request has identical filters (a true retry). + state.graphLoadRequest = request.id; + state.graphLoadKey = key; + state.graphLoadWorkspace = targetWorkspace; + state.graphLoadMode = targetMode; + state.graphLoadIncludeCode = targetIncludeCode; + state.graphLoadShowUnlinked = targetShowUnlinked; + state.graphLoadAsOf = targetAsOf; + state.graphLoadRepo = targetRepo; + state.graphLoadController = controller; + if (previousController && !previousController.signal.aborted) previousController.abort(); + // The initial load remains retryable; once a user explicitly starts a replacement, lock + // the retry control until that transaction settles so repeated clicks cannot churn it. + setGraphLoadControlsBusy(true, force); + const oldEngine = state.graphEngine; + /* Loading is a transaction: freeze the committed renderer before fetching its replacement. + A failed request restores it; a successful request destroys it immediately before commit. */ + if (state.graphEngine && typeof state.graphEngine.freeze === 'function') { + state.graphEngine.freeze(true); + } + if (state.graphSpacetimeOverlay) state.graphSpacetimeOverlay.setEnabled(false); + const canvasEl = byId('graph-canvas'); + if (canvasEl) canvasEl.setAttribute('aria-busy', 'true'); + byId('graph-empty').hidden = false; + byId('graph-empty').textContent = fullGraph + ? 'Loading all nodes with progressive level of detail…' + : 'Loading the responsive evidence graph…'; + const task = (async () => { + const assets = ensureGraphAssets(fullGraph); + const deadline = fullGraph ? GRAPH_FULL_LOAD_TIMEOUT_MS : GRAPH_LOAD_TIMEOUT_MS; + let rejectTimeout; + const timeoutPromise = new Promise((_, reject) => { + rejectTimeout = reject; + }); + // Transactional candidate tracking: host/engine/overlay live here until commit. + // destroyCandidate() is the single cleanup path for stale, error, and timeout outcomes. + const restoreCommittedRenderer = () => { + /* A newer request owns the committed renderer while it is replacing this one. Do not + thaw that renderer from a stale response; its own transaction will settle it. */ + if (state.graphEngine !== oldEngine + || (state.graphLoadController && state.graphLoadController !== controller)) return; + if (oldEngine && typeof oldEngine.freeze === 'function') { + oldEngine.freeze(state.graphFrozen); + } + graphLifecycle.sync(); + }; + let candidateHost = null; + let candidateEngine = null; + let candidateOverlay = null; + let candidateStats = null; + let candidateMetrics = null; + let candidatePhysicsSnapshot = null; + const destroyCandidate = () => { + if (!candidateEngine) { + candidateOverlay = null; + if (candidateHost && candidateHost.parentNode) { + candidateHost.remove(); + } + candidateHost = null; + return; + } + // Keep committed references live: renderer callbacks close over candidateEngine. + // Nulling it here would make every post-readiness stats/metrics callback look stale. + if (state.graphEngine === candidateEngine) return; + graphLifecycle.release(candidateEngine, candidateOverlay); + candidateOverlay = null; + candidateEngine = null; + if (candidateHost && candidateHost.parentNode) { + candidateHost.remove(); + } + candidateHost = null; + }; + const timeout = window.setTimeout(() => { + if (!window.ForceGraph || !window.EngraphisGraph || !window.EngraphisSpacetime) { + releaseGraphAssetsAttempt(graphAssetsPromise); + } + if (fullGraph && !window.EngraphisEveryGraph) { + releaseGraphAllAssetsAttempt(graphAllAssetsPromise); + } + if (!controller.signal.aborted) controller.abort(); + const error = new Error('graph loading timed out'); + error.name = 'AbortError'; + rejectTimeout(error); + }, deadline); + try { + const level = fullGraph ? 'complete' : 'overview'; + const presentation = fullGraph ? '&presentation=all' : '&presentation=quality'; + const limits = fullGraph ? '' + : `&node_limit=${GRAPH_INITIAL_NODE_LIMIT}&edge_limit=${GRAPH_INITIAL_EDGE_LIMIT}`; + const connectedOnly = !fullGraph && !targetShowUnlinked ? '&connected_only=true' : ''; + const includeCode = targetIncludeCode ? '&include_code=true' : ''; + const validatedRepo = targetIncludeCode || fullGraph + ? validatedGraphRepository(targetRepo) : ''; + const scopedRepo = validatedRepo + ? `&repo=${encodeURIComponent(validatedRepo)}` : ''; + const asOf = targetAsOf === null ? '' : `&as_of=${encodeURIComponent(targetAsOf / 1000)}`; + const history = targetAsOf === null ? '' : '&include_history=true'; + // Complete Ledger views are canonical entity projections. Memory nodes remain available + // to compatible callers, but must not change the existing entity evidence click path. + const memoryProjection = fullGraph ? '&include_memory_nodes=false' : ''; + const [payload] = await Promise.race([ + Promise.all([ + api(`/graph/scene?${query(targetWorkspace)}&level=${level}${presentation}${limits}${connectedOnly}${includeCode}${scopedRepo}${asOf}${history}${memoryProjection}`, { + signal: controller.signal, + timeoutMs: deadline, + }), + assets, + ]), + timeoutPromise, + ]); + if (!isCurrentGraphLoad(request)) { + restoreCommittedRenderer(); + return; + } + if (payload && payload.error) throw new Error(String(payload.error)); + const scene = payload.scene && typeof payload.scene === 'object' ? payload.scene : payload; + const data = { + nodes: graphNodes(scene), + links: graphLinks(scene), + repositories: Array.isArray(scene.repos) + ? scene.repos.filter(repo => typeof repo === 'string') : [], + suggestions: scene.suggestions || [], + communities: scene.communities || [], + community_bridges: scene.community_bridges || scene.bridges || [], + meta: scene.meta || payload.meta || {}, + metadata: scene.metadata || payload.metadata || {}, + layout_seed: scene.layout_seed ?? (scene.meta && scene.meta.layout_seed) ?? (payload.meta && payload.meta.layout_seed), + }; + const sceneMeta = scene.meta || payload.meta || {}; + const nextMeta = { + ...sceneMeta, + nodes_available: sceneMeta.nodes_available == null ? (sceneMeta.total_nodes == null + ? data.nodes.length : sceneMeta.total_nodes) : sceneMeta.nodes_available, + nodes_complete: sceneMeta.nodes_complete == null + ? (sceneMeta.truncated == null ? fullGraph : !sceneMeta.truncated) + : sceneMeta.nodes_complete, + }; + const responseIncludeCode = sceneMeta.include_code === false ? false : targetIncludeCode; + const codeOverlayDegraded = targetIncludeCode && !responseIncludeCode; + const oldHost = byId('graph-canvas'); + if (!oldHost) throw new Error('graph canvas container missing'); + // oldEngine was captured before the first await so the failure path + // can restore the exact committed renderer even when candidate setup never begins. + candidateHost = oldHost.cloneNode(false); + candidateHost.id = `graph-canvas-candidate-${request.id}`; + candidateHost.classList.add('graph-canvas-candidate'); + candidateHost.setAttribute('aria-hidden', 'true'); + oldHost.insertAdjacentElement('afterend', candidateHost); + /* Authored-Galaxy detection must key off scene markers, not the toolbar preset: + entering Every via its chip sets the preset to 'every', but a complete scene + with system anchors still needs the hierarchical orbit engine and overlay. + The live-limit guard keeps very large complete scenes on the lightweight + Every renderer instead of the full physics galaxy. */ + const galaxyWithinLiveLimit = data.nodes.length <= GRAPH_INITIAL_NODE_LIMIT + && data.links.length <= GRAPH_INITIAL_EDGE_LIMIT; + const galaxyQuality = fullGraph && galaxyWithinLiveLimit + && data.nodes.some(node => node.anchor_role === 'community' + && (node.system_anchor_id !== undefined + || Number.isFinite(Number(node.galactic_radius)))); + const graphFactory = galaxyQuality ? window.EngraphisGraph + : fullGraph ? window.EngraphisEveryGraph : window.EngraphisGraph; + if (!graphFactory || typeof graphFactory.create !== 'function') { + throw new Error(fullGraph + ? 'all-node graph engine asset is unavailable' + : 'graph engine asset is unavailable'); + } + candidateEngine = graphFactory.create(candidateHost, { + renderMode: fullGraph ? 'all' : 'overview', + onNodeClick: item => openGraphConnections(item), + onBackgroundClick: () => candidateEngine.clearFocus(), + onStats: stats => { + if (state.graphEngine === candidateEngine + && state.graphLoadRequest === request.id) graphStatsChanged(stats); + else if (state.graphLoadRequest === request.id) candidateStats = stats; + }, + onMetrics: metrics => { + if (state.graphEngine === candidateEngine + && state.graphLoadRequest === request.id) graphMetricsChanged(metrics); + else if (state.graphLoadRequest === request.id) candidateMetrics = metrics; + }, + onPhysicsFrame: snapshot => { + candidatePhysicsSnapshot = snapshot; + if (candidateOverlay) candidateOverlay.setSnapshot(snapshot); + }, + onError: error => { + if (state.graphEngine !== candidateEngine || !fullGraph + || state.graphLoadRequest !== request.id || state.graphMode !== 'full') return; + byId('graph-empty').hidden = false; + byId('graph-empty').textContent = error && error.code === 'GRAPH_CAPACITY' + ? `All nodes exceed renderer capacity. Enter an exact repository filter or reduce the workspace graph. (${error.message})` + : 'The all-node renderer stopped. Choose Reload data to start a fresh worker.'; + byId('graph-canvas').setAttribute('aria-busy', 'false'); + }, + onCollapseChange: collapsed => { + if (targetMode === 'overview') showNotice(collapsed ? 'Clusters collapsed for overview.' : ''); + else { + const note = byId('graph-lod-note'); + const detail = note && note.querySelector('span'); + if (detail) detail.textContent = collapsed + ? 'Clusters are condensed into representative nodes. Zoom in to expand them.' + : 'Layout, forces, scope, colour and relation flow update without reloading the complete graph.'; + } + }, + onSlingshotRelease: () => { + if (state.graphSpacetimeOverlay && state.graphEngine + && typeof state.graphEngine.getPhysicsSnapshot === 'function') { + state.graphSpacetimeOverlay.setSnapshot(state.graphEngine.getPhysicsSnapshot()); + } + }, + }); + candidateEngine.apply(graph => { + const preset = byId('graph-preset').value; + graph.setPreset(galaxyQuality && preset === 'every' ? 'galaxy' : preset); + graph.setStyle(byId('graph-style').value); + graph.setColorBy(byId('graph-color').value); + graph.setThemeColors(graphThemeColors()); + const paletteName = byId('graph-palette').value; + graph.setPalette(paletteName); + if (paletteName === 'custom') graph.setTypeColors(GRAPH_CUSTOM_PALETTE); + graph.setSettings({ + ...graphTuningEngineSettings(), + ...graphSpacetimeEngineSettings(), + flow: byId('graph-flow').getAttribute('aria-checked') === 'true', + labels: byId('graph-labels').getAttribute('aria-checked') === 'true', + frozen: fullGraph ? false : state.graphFrozen, + }); + graph.setScope(graphScope()); + graph.setLayers(graphLayerState()); + graph.setRepoFilter(byId('graph-repo-filter').value); + graph.setAsOf(graphAsOfTimestamp()); + graph.setSizeBy(graphSizeBy()); + graph.setBridges(byId('graph-bridges').checked); + graph.setCollapse(byId('graph-collapse').checked ? 'auto' : false); + graph.setGhosts(byId('graph-ghosts').checked); + }, false, false); + candidateEngine.setData(data); + candidateEngine.freeze(fullGraph ? false : state.graphFrozen); + if ((!fullGraph || galaxyQuality) && window.EngraphisSpacetime + && window.EngraphisSpacetime.create) { + candidateOverlay = window.EngraphisSpacetime.create( + candidateHost, candidateEngine + ); + candidateOverlay.setEnabled(galaxyQuality || graphIsGalaxy()); + if (candidatePhysicsSnapshot) candidateOverlay.setSnapshot(candidatePhysicsSnapshot); + } + // Pending renderers also follow visibility while finite worker preparation finishes. + graphLifecycle.track(candidateEngine, candidateOverlay, () => graphOverlayEnabled(galaxyQuality)); + if (typeof candidateEngine.whenReady === 'function') { + await Promise.race([candidateEngine.whenReady(), timeoutPromise]); + if (!isCurrentGraphLoad(request)) { + destroyCandidate(); + restoreCommittedRenderer(); + return; + } + } + oldHost.id = `graph-canvas-retired-${request.id}`; + candidateHost.id = 'graph-canvas'; + candidateHost.classList.remove('graph-canvas-candidate'); + candidateHost.removeAttribute('aria-hidden'); + oldHost.replaceWith(candidateHost); + candidateHost = null; + state.graphEngine = candidateEngine; + state.graphSpacetimeOverlay = candidateOverlay; + state.graphData = data; + state.graphWorkspace = targetWorkspace; + state.graphDataMode = targetMode; + state.graphGalaxyQuality = galaxyQuality; + state.graphDataPreset = byId('graph-preset').value; + state.graphDataIncludeCode = responseIncludeCode; + state.graphDataShowUnlinked = targetShowUnlinked; + state.graphDataAsOf = targetAsOf; + state.graphDataRepo = targetRepo; + state.graphMeta = nextMeta; + if (codeOverlayDegraded) { + state.graphIncludeCode = false; + const layers = { ...graphLayerState(), code: false }; + setGraphLayers(layers); + candidateEngine.setLayers(layers); + clearGraphSavedView(); + saveGraphPreferences(); + showNotice(sceneMeta.degraded_reason === 'code_overlay_requires_repository_filter' + ? 'Code overlay needs a repository filter; showing entity relationships only.' + : 'Code overlay is unavailable; showing entity relationships only.'); + } + graphLifecycle.replace(candidateEngine, candidateOverlay, + () => graphOverlayEnabled()); + byId('graph-empty').hidden = Boolean(data.nodes.length); + if (!data.nodes.length) byId('graph-empty').textContent = 'No entities exist in this workspace yet.'; + updateGraphModeControls(); + updateGraphFacts(data); + // Candidate stats emitted before commit are retained, then published only after the + // renderer transaction commits. Raw payload counts are wrong when a frozen renderer + // has already applied repository, layer, ghost, or collapse visibility filters. + graphStatsChanged(candidateStats || { nodes: data.nodes.length, links: data.links.length }); + if (candidateMetrics) graphMetricsChanged(candidateMetrics); + updateGraphLayerCounts(data, scene.layers || payload.layers); + } catch (error) { + if (!isCurrentGraphLoad(request)) { + destroyCandidate(); + restoreCommittedRenderer(); + return; + } + destroyCandidate(); + byId('graph-empty').hidden = false; + byId('graph-empty').textContent = error && error.name === 'AbortError' + ? `${fullGraph ? 'All-node graph' : 'High-quality graph'} loading timed out. Choose Reload data to try again.` + : fullGraph && (error.status === 413 || error.code === 'GRAPH_CAPACITY') + ? `All nodes exceed the server capacity. Enter an exact repository filter or reduce the workspace graph. (${error.message})` + : `Graph unavailable: ${error.message}. Choose Reload data to try again.`; + if (state.graphData) { + if (state.graphDataMode !== targetMode) state.graphMode = state.graphDataMode; + /* The toolbar preset changes before a replacement request begins. Restore the + committed preset together with the committed renderer so a failed Every-node + transition cannot leave aria-pressed and the active engine disagreeing. */ + byId('graph-preset').value = state.graphDataPreset || 'galaxy'; + updateGraphModeControls(); + syncGraphChoices(); + } + // Restore the committed renderer's freeze/overlay state. The old engine survived + // because we never mutated state.graphEngine on the failure path. + if (oldEngine && typeof oldEngine.freeze === 'function') { + oldEngine.freeze(state.graphFrozen); + } + graphLifecycle.sync(); + } finally { + // Safety net: if control left the try/catch without committing or cleaning up + // (e.g. an unexpected throw in finally itself), ensure no candidate leaks. + destroyCandidate(); + window.clearTimeout(timeout); + if (state.graphLoadRequest === request.id && state.graphLoadController === controller) { + const canvasEl = byId('graph-canvas'); + if (canvasEl) canvasEl.setAttribute('aria-busy', 'false'); + setGraphLoadControlsBusy(false); + } + if (state.graphLoadController === controller) state.graphLoadController = null; + } + })(); + state.graphLoadPromise = task; + try { + return await task; + } finally { + if (state.graphLoadPromise === task) { + state.graphLoadPromise = null; + state.graphLoadWorkspace = ''; + state.graphLoadMode = ''; + state.graphLoadIncludeCode = false; + state.graphLoadShowUnlinked = false; + state.graphLoadAsOf = null; + state.graphLoadRepo = ''; + state.graphLoadKey = ''; + } + } + } + + function searchGraph(value) { + const target = byId('graph-search-results'); + target.replaceChildren(); + const needle = value.trim().toLowerCase(); + if (!needle || !state.graphData) return; + state.graphData.nodes + .filter(item => item.name.toLowerCase().includes(needle)) + .slice(0, 8) + .forEach(item => { + target.append(button(`${item.name} · ${item.degree}`, 'search-result', () => { + revealGraphNode(item.id, item.name); + target.replaceChildren(); + openGraphConnections(item); + })); + }); + } + + function renderMemoryCollection(target, memories, message) { + target.replaceChildren(); + if (!memories.length) { + target.append(empty(message)); + return; + } + memories.forEach(memory => target.append(simpleMemoryCard(memory))); + } + + function switchProvenanceTab(tab) { + state.provenanceTab = tab; + all('[data-provenance-tab]').forEach(control => { + const active = control.dataset.provenanceTab === tab; + control.classList.toggle('active', active); + control.setAttribute('aria-selected', String(active)); + control.tabIndex = active ? 0 : -1; + }); + all('[data-provenance-panel]').forEach(panel => panel.classList.toggle('active', panel.dataset.provenancePanel === tab)); + if (tab === 'audit') loadAudit(); + } + + async function whySearch(event) { + event.preventDefault(); + const question = byId('why-input').value.trim(); + if (!question) { + showNotice('Enter a claim or topic before tracing belief.'); + byId('why-input').focus(); + return; + } + const request = beginScopedRequest('why'); + showNotice(''); + const target = byId('why-result'); + target.replaceChildren(empty('Tracing the live belief and supersession chain…')); + try { + const payload = await api(`/why?q=${encodeURIComponent(question)}&${query(request.workspace)}&k=8`); + if (!isCurrentScopedRequest(request)) return; + target.replaceChildren(); + const live = payload.answer || []; + const superseded = payload.supersedes || []; + target.append(node('h2', '', 'Live support')); + if (!live.length) target.append(empty('No live supporting memory was found.')); + else live.forEach(memory => target.append(simpleMemoryCard(memory))); + target.append(node('h2', '', 'Superseded history')); + if (!superseded.length) target.append(empty('No superseded versions were found.')); + else superseded.forEach(memory => target.append(simpleMemoryCard(memory, 'timeline-card'))); + } catch (error) { + if (!isCurrentScopedRequest(request)) return; + target.replaceChildren(empty(`Could not trace belief: ${error.message}`)); + } + } + + async function timelineSearch(event, supersessionsOnly = false) { + event.preventDefault(); + const input = byId(supersessionsOnly ? 'supersession-input' : 'timeline-input'); + const target = byId(supersessionsOnly ? 'supersession-list' : 'timeline-result'); + const question = input.value.trim(); + if (!question) { + showNotice(`Enter a topic before ${supersessionsOnly ? 'finding supersessions' : 'showing history'}.`); + input.focus(); + return; + } + const request = beginScopedRequest(supersessionsOnly ? 'supersessions' : 'timeline'); + showNotice(''); + target.replaceChildren(empty('Loading temporal history…')); + try { + const payload = await api(`/timeline?q=${encodeURIComponent(question)}&${query(request.workspace)}&limit=50`); + if (!isCurrentScopedRequest(request)) return; + let history = payload.history || []; + if (supersessionsOnly) history = history.filter(item => item.valid_to || item.expired_at); + renderMemoryCollection(target, history, supersessionsOnly ? 'No closed versions were found for this topic.' : 'No temporal history was found.'); + } catch (error) { + if (!isCurrentScopedRequest(request)) return; + target.replaceChildren(empty(`Could not load history: ${error.message}`)); + } + } + + function renderAuditCards(audit, receipts) { + const target = byId('audit-list'); + target.replaceChildren(); + const combined = [ + ...audit.map(item => ({ ...item, _kind: 'audit' })), + ...receipts.map(item => ({ ...item, _kind: 'receipt' })), + ].sort((a, b) => provenanceTimestampMs(b) - provenanceTimestampMs(a)); + if (!combined.length) { + target.append(empty('No audit records or receipts yet.')); + return; + } + combined.slice(0, 120).forEach(item => { + const card = node('article', 'audit-card'); + card.append( + node('span', '', relative(provenanceTimestampMs(item))), + node('strong', '', item.actor || item.source || 'local operator'), + node('span', 'tag', item.operation || item.action || item.event || item._kind), + node('span', '', item.scope || item.workspace || item.status || state.workspace), + node('code', '', truncate(item.hash || item.id || item.receipt_id, 24) || '—'), + ); + target.append(card); + }); + } + + async function loadAudit() { + const request = beginScopedRequest('audit'); + const target = byId('audit-list'); + target.replaceChildren(empty('Loading audit records and receipts…')); + byId('savings-detail').replaceChildren(empty('Loading receipt-backed estimate…')); + const [auditResult, receiptsResult, savingsResult] = await Promise.allSettled([ + api(`/audit?${query(request.workspace)}&limit=100`), + api(`/receipts?${query(request.workspace)}&limit=100`), + api(`/context-savings${savingsQuery(state.savingsPreset)}`), + ]); + if (!isCurrentScopedRequest(request)) return; + if (savingsResult.status === 'fulfilled') { + renderSavingsDetail(savingsResult.value); + } else { + byId('savings-detail').replaceChildren(empty(`Could not load context savings: ${savingsResult.reason.message}`)); + } + const audit = auditResult.status === 'fulfilled' ? auditItems(auditResult.value) : []; + const receipts = receiptsResult.status === 'fulfilled' ? receiptItems(receiptsResult.value) : []; + if (auditResult.status === 'rejected' && receiptsResult.status === 'rejected') { + target.replaceChildren(empty('Could not load audit records or receipts. Try again.')); + } else { + renderAuditCards(audit, receipts); + } + if (auditResult.status === 'rejected' || receiptsResult.status === 'rejected') { + showNotice('Some provenance data could not be loaded; available records remain visible.'); + } + } + + async function verifyReceipts() { + try { + const result = await api(`/receipts/verify?${query()}`); + const valid = result.valid != null ? result.valid : result.verified; + showNotice(valid === false ? 'Receipt verification found a broken chain.' : 'Receipt chain verified.'); + } catch (error) { + showNotice(`Could not verify receipts: ${error.message}`); + } + } + + async function exportReceipts() { + try { + const receipts = await api(`/receipts/export?${query()}`); + const blob = new Blob([JSON.stringify(receipts, null, 2)], { type: 'application/json' }); + const link = document.createElement('a'); + const url = URL.createObjectURL(blob); + link.href = url; + link.download = `engraphis-receipts-${state.workspace || 'workspace'}.json`; + document.body.append(link); + link.click(); + link.remove(); + URL.revokeObjectURL(url); + showNotice('Privacy-safe receipts exported.'); + } catch (error) { + showNotice(`Could not export receipts: ${error.message}`); + } + } + + function switchManageTab(tab) { + state.manageTab = tab; + all('[data-manage-tab]').forEach(control => { + const active = control.dataset.manageTab === tab; + control.classList.toggle('active', active); + control.setAttribute('aria-selected', String(active)); + control.tabIndex = active ? 0 : -1; + }); + all('[data-manage-panel]').forEach(panel => panel.classList.toggle('active', panel.dataset.managePanel === tab)); + loadManageTab(tab); + } + + async function loadManageTab(tab) { + if (tab === 'workspaces') renderWorkspaceList(); + if (tab === 'settings') await loadSettings(); + if (tab === 'plans') await loadPlans(); + if (tab === 'analytics') await loadHosted('analytics'); + if (tab === 'automation') await loadHosted('automation'); + if (tab === 'team') await loadHosted('team'); + if (tab === 'sync') await loadSync(); + } + + function renderWorkspaceList() { + const target = byId('workspace-list'); + target.replaceChildren(); + if (!state.workspaces.length) { + target.append(empty('Create the first workspace to begin.')); + return; + } + state.workspaces.forEach(item => { + const name = workspaceName(item); + const card = node('article', `workspace-card${name === state.workspace ? ' active' : ''}`); + const copy = node('div'); + copy.append( + node('h3', '', name), + ...(item.description ? [node('p', '', item.description)] : []), + node('p', '', `${number(item.memories).toLocaleString()} memories · ${item.visibility === 'personal' + ? 'Personal · excluded from Cloud Sync' + : item.visibility === 'shared' + ? 'Shared · eligible for encrypted Cloud Sync' + : 'Local workspace'}`), + ); + const actions = node('div', 'workspace-card-actions'); + if (name !== state.workspace) actions.append(button('Switch to', 'secondary-button', () => selectWorkspace(name))); + actions.append( + button('Rename', 'secondary-button', () => renameWorkspace(name)), + button('Copy', 'secondary-button', () => copyWorkspace(name)), + ); + if (item.visibility === 'personal' && item.can_change_access === true) { + actions.append(button('Share for Cloud Sync', 'secondary-button', () => changeWorkspaceVisibility(name, 'shared'))); + } else if (item.visibility === 'shared' && item.can_change_access === true) { + actions.append(button('Make personal', 'secondary-button', () => changeWorkspaceVisibility(name, 'personal'))); + } + if (name !== state.workspace) actions.append(button('Delete', 'danger-button', () => deleteWorkspace(name))); + card.append(copy, actions); + target.append(card); + }); + } + + async function createWorkspace(event) { + event.preventDefault(); + const name = byId('new-workspace-name').value.trim(); + const description = byId('new-workspace-description').value.trim(); + if (!name) { + showNotice('Enter a workspace name before creating it.'); + byId('new-workspace-name').focus(); + return; + } + showNotice(''); + try { + await api('/workspaces/create', { + method: 'POST', + body: { workspace: name, description, visibility: 'personal', confirmed: false }, + }); + showNotice(`Workspace ${name} created.`); + byId('create-workspace-form').reset(); + byId('create-workspace-form').hidden = true; + await refreshBootstrap(name); + } catch (error) { + showNotice(`Could not create workspace: ${error.message}`); + } + } + + async function changeWorkspaceVisibility(name, visibility) { + const sharing = visibility === 'shared'; + const question = sharing + ? `Share “${name}” with signed-in Team members and make it eligible for encrypted Cloud Sync? Other Team members with access can read this workspace. This changes its visibility from personal to shared.` + : `Make “${name}” personal? Other Team members will lose access and future Cloud Sync rounds will exclude it. This does not erase data already uploaded to the encrypted relay.`; + if (!window.confirm(question)) return; + try { + await api('/workspaces/visibility', { + method: 'POST', + body: { workspace: name, visibility, confirmed: true }, + }); + await refreshBootstrap(state.workspace); + showNotice(sharing + ? `Workspace ${name} is shared and eligible for Cloud Sync. Run Sync now to upload eligible changes.` + : `Workspace ${name} is personal and excluded from future Cloud Sync rounds.`); + } catch (error) { + showNotice(`Could not change access for workspace ${name}: ${error.message}`); + } + } + + async function renameWorkspace(name) { + const next = window.prompt(`Rename ${name} to:`, name); + if (!next || next === name) return; + try { + await api('/workspaces/rename', { method: 'POST', body: { workspace: name, new_name: next } }); + showNotice(`Workspace renamed to ${next}.`); + await refreshBootstrap(name === state.workspace ? next : state.workspace); + } catch (error) { + showNotice(`Could not rename workspace: ${error.message}`); + } + } + + async function copyWorkspace(name) { + try { + const result = await api('/workspaces/copy', { method: 'POST', body: { workspace: name } }); + showNotice(`Workspace copied${result.name ? ` to ${result.name}` : ''}.`); + await refreshBootstrap(state.workspace); + } catch (error) { + showNotice(`Could not copy workspace: ${error.message}`); + } + } + + async function deleteWorkspace(name) { + if (!window.confirm(`Delete workspace “${name}”? Its memories are retired through the governed workspace operation.`)) return; + try { + await api('/workspaces/delete', { method: 'POST', body: { workspace: name } }); + showNotice(`Workspace ${name} deleted.`); + await refreshBootstrap(state.workspace); + } catch (error) { + showNotice(`Could not delete workspace: ${error.message}`); + } + } + + function renderObject(target, payload, title = 'Result') { + target.replaceChildren(); + target.append(node('h3', '', title)); + const entries = Object.entries(payload || {}).filter(([, value]) => ['string', 'number', 'boolean'].includes(typeof value)).slice(0, 12); + if (entries.length) target.append(definitionList(entries.map(([key, value]) => [key.replaceAll('_', ' '), text(value)]))); + else target.append(node('p', '', 'The operation completed.')); + } + + function runFreshAnalytics(workspace) { + if (workspace !== state.workspace) return; + state.analyticsJobs.delete(workspace); + state.analyticsResults.delete(workspace); + state.analyticsStartErrors.delete(workspace); + void loadHosted('analytics'); + } + + function renderAnalyticsStartError(target, workspace, message) { + target.replaceChildren( + node('h3', '', 'Analytics request unconfirmed'), + node('p', 'automation-policy-note', + `Could not confirm the Analytics run: ${message} It may still be processing in Cloud. Starting a fresh analysis creates another run.`), + button('Run fresh analysis', 'secondary-button', () => runFreshAnalytics(workspace)), + ); + } + + function hasAnalyticsMetrics(result) { + const totals = result && result.totals; + const forecast = result && result.decay_forecast; + return result && result.kind === 'analytics' + && Number.isInteger(result.memory_count) && result.memory_count >= 0 + && totals && Number.isInteger(totals.live) && totals.live >= 0 + && Number.isInteger(totals.pinned) && totals.pinned >= 0 + && Number.isFinite(totals.avg_retention) + && forecast && Number.isInteger(forecast.at_risk_7d); + } + + function renderAnalyticsResult(target, result, workspace, stale = false) { + const totals = result.totals; + const live = totals.live; + target.replaceChildren(node('h3', '', live === 0 ? 'Analytics has no eligible memories' : 'Analytics result')); + if (live === 0) { + target.append(node('p', 'automation-policy-note', + 'This result has no eligible memories. Add a workspace memory, then run a fresh analysis.')); + } else { + const metrics = [ + ['Live memories', live.toLocaleString()], + ['Average retention', `${Math.round(totals.avg_retention * 100)}%`], + ['At risk within 7 days', result.decay_forecast.at_risk_7d.toLocaleString()], + ['Pinned memories', totals.pinned.toLocaleString()], + ]; + const grid = node('div', 'stat-grid'); + metrics.forEach(([label, value]) => { + const item = node('div', 'stat-item'); + item.append(node('span', '', label), node('strong', '', value)); + grid.append(item); + }); + target.append(grid); + } + if (stale) target.append(node('p', 'automation-policy-note', + 'A newer workspace snapshot exists. Run a fresh analysis to see current data.')); + target.append(button('Run fresh analysis', 'secondary-button', () => runFreshAnalytics(workspace))); + } + + function renderAnalyticsPending(target, workspace, jobId, status, error = '') { + target.replaceChildren( + node('h3', '', error ? 'Analytics result unavailable' : 'Analytics is processing'), + node('p', 'automation-policy-note', error || + `The Cloud job is ${status === 'running' ? 'running' : 'queued'}. Check this result again without starting another run.`), + button('Refresh result', 'secondary-button', () => { void refreshAnalyticsResult(workspace, jobId); }), + ); + if (error) target.append(button('Run fresh analysis', 'secondary-button', + () => runFreshAnalytics(workspace))); + } + + async function refreshAnalyticsResult(workspace, jobId) { + if (workspace !== state.workspace || state.analyticsJobs.get(workspace) !== jobId) return; + const request = beginScopedRequest('hosted-analytics'); + const target = byId('analytics-result'); + target.replaceChildren(empty('Checking the existing Analytics job…')); + try { + const status = await api(`/analytics/job-result?${query(workspace)}&job_id=${encodeURIComponent(jobId)}`, + { signal: request.signal, timeoutMs: 45_000 }); + if (!isCurrentScopedRequest(request)) return; + if (status.pending === true) { + renderAnalyticsPending(target, workspace, jobId, status.state); + } else if (status.failed === true) { + state.analyticsJobs.delete(workspace); + target.replaceChildren( + node('p', 'automation-policy-note', 'This Analytics run did not complete.'), + button('Run fresh analysis', 'secondary-button', () => runFreshAnalytics(workspace)), + ); + } else if (hasAnalyticsMetrics(status.result)) { + state.analyticsJobs.delete(workspace); + state.analyticsResults.set(workspace, { result: status.result, stale: status.state === 'stale' }); + renderAnalyticsResult(target, status.result, workspace, status.state === 'stale'); + } else { + throw new Error('The Analytics result is unavailable.'); + } + } catch (error) { + if (isCurrentScopedRequest(request)) { + renderAnalyticsPending(target, workspace, jobId, '', `Could not check this result: ${error.message}`); + } + } + } + + function consolidationOptions() { + return { + workspace: state.workspace, + infer: false, + structured: byId('consolidate-structured').checked, + }; + } + + function sameConsolidationOptions(left, right) { + return Boolean(left && right) + && left.workspace === right.workspace + && left.infer === right.infer + && left.structured === right.structured; + } + + function invalidateConsolidationReview() { + state.consolidationReview = null; + byId('consolidate-commit').disabled = true; + } + + async function previewConsolidation(event) { + event.preventDefault(); + const options = consolidationOptions(); + invalidateConsolidationReview(); + const target = byId('consolidate-result'); + target.replaceChildren(empty('Scanning local memory without writing changes…')); + try { + const result = await api('/consolidate', { + method: 'POST', + body: { + ...options, + dry_run: true, + }, + }); + // The preview is an approval only for the exact workspace and choices that + // produced it; never let a late response authorize a changed form. + if (!sameConsolidationOptions(options, consolidationOptions())) return; + state.consolidationReview = options; + byId('consolidate-commit').disabled = false; + renderObject(target, result, 'Dry preview complete · nothing written'); + } catch (error) { + invalidateConsolidationReview(); + target.replaceChildren(empty(`Preview failed: ${error.message}`)); + } + } + + async function commitConsolidation() { + const options = consolidationOptions(); + if (!sameConsolidationOptions(state.consolidationReview, options)) { + invalidateConsolidationReview(); + showNotice('Run a new dry preview after changing the workspace or consolidation options.'); + return; + } + if (!window.confirm(`Commit the reviewed consolidation result for ${state.workspace}? Original records remain in temporal history.`)) return; + const target = byId('consolidate-result'); + target.replaceChildren(empty('Committing the reviewed local consolidation…')); + try { + const result = await api('/consolidate', { + method: 'POST', + body: { + ...options, + dry_run: false, + }, + }); + invalidateConsolidationReview(); + renderObject(target, result, 'Consolidation committed'); + await selectWorkspace(state.workspace); + } catch (error) { + target.replaceChildren(empty(`Commit failed: ${error.message}`)); + } + } + + function automationCheckbox(id, label, checked) { + const field = node('label', 'check-row'); + const input = node('input'); + input.id = id; + input.type = 'checkbox'; + input.checked = Boolean(checked); + field.htmlFor = id; + field.append(input, document.createTextNode(label)); + return field; + } + + function automationNumber(id, label, value, min, max) { + const field = node('label', '', label); + const input = node('input'); + input.id = id; + input.type = 'number'; + input.min = String(min); + input.max = String(max); + input.value = String(value); + field.htmlFor = id; + field.append(input); + return field; + } + + function renderAutomationPolicy(policy, workspace = state.workspace) { + const target = byId('automation-result'); + if (!target) return; + target.replaceChildren(); + const form = node('form', 'automation-policy-form'); + form.dataset.workspace = workspace; + form.dataset.lastRun = String(policy.last_run || ''); + if (policy.bootstrap_required) { + form.append( + node('p', 'automation-policy-note', 'Hosted automation is not initialized for this workspace. Initializing it uploads one bounded workspace snapshot and saves the default Cloud policy. No upload occurs until you choose this action.'), + ); + const actions = node('div', 'automation-policy-actions'); + const bootstrap = node('button', 'primary-button', 'Initialize hosted automation'); + bootstrap.type = 'button'; + bootstrap.addEventListener('click', () => bootstrapAutomation(workspace, bootstrap)); + actions.append(bootstrap); + form.append(actions); + target.append(form); + return; + } + const enabled = Boolean(policy.enabled); + const dreamEnabled = policy.dream_enabled != null ? policy.dream_enabled : policy.dream; + const lastRun = policy.last_run ? ` Last managed run: ${relative(policy.last_run)}.` : ''; + form.append( + node('p', 'automation-policy-note', enabled + ? `This workspace has an active hosted maintenance policy.${lastRun}` + : 'Hosted maintenance is paused for this workspace.'), + automationCheckbox('automation-enabled', 'Enable hosted maintenance', enabled), + automationNumber('automation-cadence', 'Run every (hours)', Math.max(1, Number(policy.cadence_hours) || 24), 1, 8760), + automationCheckbox('automation-dream', 'Enable Auto Dreaming after accumulation and idle time', dreamEnabled), + automationNumber('automation-dream-min', 'Minimum new memories', Math.max(1, Number(policy.dream_min_new) || 25), 1, 100000), + automationNumber('automation-dream-idle', 'Idle minutes before Dreaming', Math.max(0, Number(policy.dream_idle_minutes) || 0), 0, 10080), + automationCheckbox('automation-infer', 'Allow hosted relationship inference proposals', policy.infer), + node('p', 'automation-policy-note', `Cloud Sync: ${CLOUD_SYNC_PRIVACY_NOTICE} Managed compute: saving an enabled policy submits a bounded snapshot of this workspace’s normal and sensitive memory content to Engraphis Cloud. Cloud work returns proposals and never silently changes the local database.`), + ); + const actions = node('div', 'automation-policy-actions'); + const save = node('button', 'primary-button', enabled ? 'Save & send policy to Cloud' : 'Save hosted policy'); + save.type = 'submit'; + actions.append(save); + form.append(actions); + form.addEventListener('submit', saveAutomationPolicy); + target.append(form); + } + + async function bootstrapAutomation(workspace, control) { + if (!workspace || workspace !== state.workspace) return; + if (!window.confirm( + `Initialize hosted automation for ${workspace}? Engraphis will upload one bounded snapshot of that workspace's normal and sensitive memory content and save the default Cloud policy.`, + )) return; + const request = beginScopedRequest('automation-bootstrap'); + control.disabled = true; + control.textContent = 'Initializing…'; + try { + const policy = await api(`/automation/bootstrap?${query(workspace)}`, { method: 'POST' }); + if (!isCurrentScopedRequest(request) || !control.isConnected) return; + state.hostedLoaded.add(`automation:${workspace}`); + renderAutomationPolicy(policy, workspace); + showNotice('Hosted automation initialized.'); + } catch (error) { + if (!isCurrentScopedRequest(request) || !control.isConnected) return; + control.disabled = false; + control.textContent = 'Initialize hosted automation'; + showNotice(`Could not initialize hosted automation: ${error.message}`); + } + } + + async function saveAutomationPolicy(event) { + event.preventDefault(); + const form = event.currentTarget; + const workspace = form.dataset.workspace || ''; + if (!workspace || workspace !== state.workspace) { + showNotice('This policy belongs to a different workspace. Reloading the active workspace policy.'); + state.hostedLoaded.delete(`automation:${state.workspace}`); + await loadHosted('automation'); + return; + } + const request = beginScopedRequest('automation-save'); + const policy = { + enabled: byId('automation-enabled').checked, + cadence_hours: Math.max(1, Number(byId('automation-cadence').value) || 1), + dream_enabled: byId('automation-dream').checked, + dream_min_new: Math.max(1, Number(byId('automation-dream-min').value) || 1), + dream_idle_minutes: Math.max(0, Number(byId('automation-dream-idle').value) || 0), + infer: byId('automation-infer').checked, + }; + if (policy.enabled && !window.confirm( + `Save this hosted policy for ${workspace}? Engraphis will submit a bounded snapshot of that workspace’s normal and sensitive memory content to Cloud for managed compute.\n\nCloud Sync: ${CLOUD_SYNC_PRIVACY_NOTICE}`, + )) return; + const save = form.querySelector('button[type="submit"]'); + if (save) { + save.disabled = true; + save.textContent = 'Saving…'; + } + try { + const saved = await api(`/automation?${query(workspace)}`, { method: 'POST', body: policy }); + if (!isCurrentScopedRequest(request) || !form.isConnected) return; + state.hostedLoaded.add(`automation:${workspace}`); + renderAutomationPolicy({ ...saved, last_run: form.dataset.lastRun }, workspace); + showNotice('Hosted maintenance policy saved to Engraphis Cloud.'); + } catch (error) { + if (!isCurrentScopedRequest(request) || !form.isConnected) return; + if (save) { + save.disabled = false; + save.textContent = policy.enabled ? 'Save & send policy to Cloud' : 'Save hosted policy'; + } + showNotice(`Could not save the hosted policy: ${error.message}`); + } + } + + async function loadHosted(kind) { + if (kind === 'analytics' && state.analyticsStarting.has(state.workspace)) { + byId('analytics-result').replaceChildren(empty('An Analytics request is already running. Reopen this tab if it does not update.')); + return; + } + const request = beginScopedRequest(`hosted-${kind}`); + const workspace = request.workspace; + const cacheKey = `${kind}:${workspace}`; + const target = byId(`${kind}-result`); + if (kind === 'analytics') { + const completed = state.analyticsResults.get(workspace); + if (completed) { + renderAnalyticsResult(target, completed.result, workspace, completed.stale); + return; + } + const pendingJob = state.analyticsJobs.get(workspace); + if (pendingJob) { + await refreshAnalyticsResult(workspace, pendingJob); + return; + } + const previousError = state.analyticsStartErrors.get(workspace); + if (previousError) { + renderAnalyticsStartError(target, workspace, previousError); + return; + } + } else if (state.hostedLoaded.has(cacheKey)) return; + target.replaceChildren(empty(`Checking ${kind} availability…`)); + try { + if (kind === 'team') { + const [auth, license] = await Promise.all([api('/auth/state'), api('/license')]); + if (!isCurrentScopedRequest(request)) return; + state.license = license; + updatePlanBadge(); + renderSidebarCta(); + setDeploymentMode(auth.deployment_mode || 'local'); + renderObject(target, { + deployment_mode: auth.deployment_mode || 'local', + local_mode: auth.mode || 'open', + hosted_team: Boolean(auth.hosted_team), + local_invitations: Boolean(auth.local_invitations), + cloud_access: Boolean(license.cloud_access_active), + plan: license.plan || 'local', + }, 'Connection state'); + } else { + if (kind === 'analytics') state.analyticsStarting.add(workspace); + const result = await api(`/${kind}?${query(workspace)}`, + kind === 'analytics' ? { timeoutMs: 90_000 } : {}); + // A submitted Analytics job can outlive a workspace selection change. Keep its + // identifier/result for that workspace even when the visible panel has moved on. + if (kind !== 'analytics' && !isCurrentScopedRequest(request)) return; + if (kind === 'analytics' && !state.workspaces.some(item => workspaceName(item) === workspace)) return; + const showAnalytics = kind === 'analytics' && state.workspace === workspace + && state.view === 'manage' && state.manageTab === 'analytics'; + if (kind === 'automation') renderAutomationPolicy(result, workspace); + else if (kind === 'analytics' && result.pending === true) { + const jobId = text(result.job_id); + if (!/^[A-Za-z0-9_-]{1,64}$/.test(jobId)) { + throw new Error('Engraphis Cloud did not return a usable Analytics job.'); + } + state.analyticsJobs.set(workspace, jobId); + state.analyticsStartErrors.delete(workspace); + if (showAnalytics) renderAnalyticsPending(target, workspace, jobId, result.state); + } else if (kind === 'analytics') { + if (!hasAnalyticsMetrics(result)) { + throw new Error('Engraphis Cloud did not return a completed Analytics result.'); + } + const stale = result.state === 'stale'; + state.analyticsResults.set(workspace, { result, stale }); + state.analyticsStartErrors.delete(workspace); + if (showAnalytics) renderAnalyticsResult(target, result, workspace, stale); + } else renderObject(target, result, `${kind[0].toUpperCase()}${kind.slice(1)} status`); + } + if (kind !== 'analytics' && isCurrentScopedRequest(request)) state.hostedLoaded.add(cacheKey); + } catch (error) { + if (kind === 'analytics') { + state.analyticsStartErrors.set(workspace, error.message); + if (state.workspace === workspace && state.view === 'manage' && state.manageTab === 'analytics') { + renderAnalyticsStartError(target, workspace, error.message); + } + } else if (isCurrentScopedRequest(request)) { + target.replaceChildren(empty(`${kind[0].toUpperCase()}${kind.slice(1)} is not active: ${error.message}`)); + } + } finally { + if (kind === 'analytics') state.analyticsStarting.delete(workspace); + } + } + function syncSummaryMessage(summary) { + if (!summary) return 'No sync has run in this dashboard process.'; + const attempted = number(summary.attempted); + const succeeded = number(summary.succeeded); + const errors = Array.isArray(summary.errors) ? summary.errors : []; + const complete = summary.complete === true + || (summary.complete !== false && errors.length === 0 && succeeded >= attempted); + if (complete && attempted === 0) { + return 'Last sync found no eligible shared workspaces · 0/0 completed. Create a workspace, or share a personal Team workspace you own, then try again.'; + } + const counts = `${succeeded}/${attempted} eligible workspaces completed`; + const changes = `${number(summary.added)} added · ${number(summary.updated)} updated · ${number(summary.exported)} exported`; + return `${complete ? 'Last sync complete' : 'Last sync incomplete'} · ${counts} · ${changes}${errors.length ? ` · ${errors.length} ${errors.length === 1 ? 'error' : 'errors'}` : ''}.`; + } + + function renderSyncStatus(status, message = '') { + state.syncStatus = status || {}; + const connected = state.syncStatus.available === true; + const ready = connected && state.syncStatus.ready === true; + const hasKey = state.syncStatus.has_key === true; + const target = byId('sync-result'); + if (!target) return; + target.replaceChildren(); + if (message) target.append(empty(message, 'form-error')); + target.append( + node('p', 'automation-policy-note', syncSummaryMessage(state.syncStatus.last)), + definitionList([ + ['Connection', connected ? 'Connected' : 'Not connected'], + ['Encryption key', hasKey ? 'Valid' : state.syncStatus.key_state === 'invalid' + ? 'Invalid' : 'Not configured'], + ['Sync readiness', ready ? 'Ready' : 'Needs setup'], + ['Mode', state.syncStatus.read_only ? 'Read only · pull without upload' : 'Push and pull'], + ['Credential', state.syncStatus.has_cloud_session + ? 'Managed Cloud session' + : (state.syncStatus.has_user_token ? 'Local sync token' : 'None')], + ]), + node('p', 'automation-policy-note', CLOUD_SYNC_PRIVACY_NOTICE), + ); + const actions = node('div', 'automation-policy-actions'); + const run = button('Sync now', 'primary-button', runCloudSync); + run.id = 'sync-now'; + run.disabled = !ready; + actions.append(run); + if (state.syncStatus.last && number(state.syncStatus.last.attempted) === 0) { + actions.append(button('Review workspaces', 'secondary-button', () => switchManageTab('workspaces'))); + } + if (!connected) { + const url = safeUrl(state.syncStatus.upgrade_url) || hostedAccountUrl('sync'); + if (url) { + const connect = node('a', 'secondary-button', 'Connect Engraphis Cloud'); + connect.href = url; + connect.target = '_blank'; + connect.rel = 'noopener'; + actions.append(connect); + } + } else if (!ready) { + const missingKey = !hasKey; + 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.' + : 'Cloud Sync encryption support is unavailable. Install engraphis[cloud-sync] and restart this dashboard.'; + target.append(node('p', 'automation-policy-note', detail)); + 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'; + guide.rel = 'noopener'; + actions.append(guide); + } + target.append(actions); + } + + async function loadSync() { + const request = beginScopedRequest('sync-status'); + const target = byId('sync-result'); + if (!target) return; + target.replaceChildren(empty('Checking Cloud Sync connection…')); + try { + const status = await api('/sync/status'); + if (!isCurrentScopedRequest(request)) return; + renderSyncStatus(status); + } catch (error) { + if (!isCurrentScopedRequest(request)) return; + target.replaceChildren(empty(`Could not load Cloud Sync status: ${error.message}`, 'form-error')); + } + } + + async function runCloudSync() { + const request = beginScopedRequest('sync-run'); + const buttonNode = byId('sync-now'); + if (buttonNode) { + buttonNode.disabled = true; + buttonNode.textContent = 'Syncing…'; + } + try { + const result = await api('/sync/run', { method: 'POST' }); + if (!isCurrentScopedRequest(request)) return; + const summary = result && result.summary ? result.summary : {}; + const responseOk = Boolean(result) && result.ok !== false; + const displayedSummary = responseOk ? summary : { ...summary, complete: false }; + renderSyncStatus({ ...(state.syncStatus || {}), last: displayedSummary }); + const errors = Array.isArray(summary.errors) ? summary.errors : []; + const complete = responseOk && (summary.complete === true + || (summary.complete !== false && errors.length === 0 + && number(summary.succeeded) >= number(summary.attempted))); + showNotice(complete && number(summary.attempted) === 0 + ? 'No eligible shared workspaces were synced.' + : complete + ? 'Cloud Sync completed for every eligible workspace.' + : 'Cloud Sync is incomplete. Review the status before retrying.'); + } catch (error) { + if (!isCurrentScopedRequest(request)) return; + renderSyncStatus(state.syncStatus || {}, `Cloud Sync failed: ${error.message}`); + showNotice(`Cloud Sync failed: ${error.message}`); + } + } + + function planPrices() { + const annual = byId('billing-select').value === 'annual'; + return annual + ? { free: '$0', pro: '$100 / owner / year', team: '$200 / seat / year' } + : { free: '$0', pro: '$10 / owner / month', team: '$20 / seat / month' }; + } + + function renderPlans() { + const target = byId('plan-cards'); + target.replaceChildren(); + const prices = planPrices(); + const plans = [ + { id: 'free', name: 'Free', price: prices.free, note: 'The complete local memory engine and every core operation.', action: 'Current local plan' }, + { id: 'pro', name: 'Pro', price: prices.pro, note: 'Cloud sync, managed automation and portfolio analytics.' }, + { id: 'team', name: 'Team', price: prices.team, note: 'Shared workspaces, member roles, seats and remote agents.' }, + ]; + plans.forEach(plan => { + const card = node('article', `plan-card${plan.id === 'pro' ? ' featured' : ''}`); + card.append( + node('p', 'eyebrow', plan.id === (state.license && state.license.plan) ? 'Current plan' : plan.id), + node('h2', '', plan.name), + node('div', 'price', plan.price), + node('p', '', plan.note), + ); + if (plan.id === 'pro') { + card.append( + node('p', 'plan-support', 'Support continued Engraphis development with Pro. Your subscription helps cover hosted infrastructure and ongoing development.'), + node('p', 'plan-benefits', 'Cloud Sync, Analytics, Auto Consolidation, and Auto Dreaming across your installations.'), + ); + } + if (plan.id === 'free') { + const status = node('span', 'secondary-button', plan.action); + card.append(status); + } else { + const interval = byId('billing-select').value === 'annual' ? 'annual' : 'monthly'; + const cta = hostedCta(plan.id, 'plans', interval); + const action = node('a', 'primary-button', cta.label); + const url = cta.href; + action.dataset.proCta = plan.id; + action.href = url || '#'; + if (url) { + action.target = '_blank'; + action.rel = 'noopener'; + } else { + action.addEventListener('click', event => { + event.preventDefault(); + showNotice('Connect this installation to Engraphis Cloud to open hosted plan options.'); + }); + } + card.append(action); + } + target.append(card); + }); + } + + async function loadPlans() { + const request = beginScopedRequest('plans'); + try { + const license = await api(`/license?${query(request.workspace)}`); + if (!isCurrentScopedRequest(request)) return; + state.license = license; + } catch (_) { + if (!isCurrentScopedRequest(request)) return; + state.license = { plan: 'free' }; + } + updatePlanBadge(); + renderSidebarCta(); + renderPlans(); + } + + function llmSnippet(provider, model, keySet) { + return [ + `ENGRAPHIS_LLM_PROVIDER=${provider}`, + `ENGRAPHIS_LLM_MODEL=${model}`, + 'ENGRAPHIS_LLM_API_KEY=', + keySet ? 'ENGRAPHIS_EXTRACTOR=llm_structured' : '# set ENGRAPHIS_EXTRACTOR=llm_structured to use it', + 'ENGRAPHIS_LLM_AUTO_EXTRACT=1', + ].join('\n'); + } + + function setLlmTestResult(message, tone = '') { + const target = byId('llm-test-result'); + if (!target) return; + target.textContent = message; + target.dataset.tone = tone; + } + + function updateLlmSnippet(status) { + const provider = byId('llm-provider').value; + const model = byId('llm-model').value; + byId('llm-env-snippet').value = llmSnippet(provider, model, Boolean(status.key_set)); + } + + function renderLlmSettings(status) { + const target = byId('llm-connection'); + target.replaceChildren(); + const defaults = status.default_models || {}; + const provider = status.provider || 'openai'; + const model = status.model || defaults[provider] || ''; + const providers = [...new Set([...Object.keys(defaults), provider])]; + const models = [...new Set([model, ...Object.values(defaults)].filter(Boolean))]; + const configured = Boolean(status.configured); + const extractionEnabled = Boolean(status.extractor_enabled); + const stateLabel = status.working ? 'verified' : (configured ? 'configured' : 'not configured'); + + const overview = node('div', 'llm-status-line'); + overview.append( + node('span', '', 'Provider · Model'), + node('span', `llm-status-badge ${configured ? 'ready' : 'muted'}`, stateLabel), + ); + + const pickerGrid = node('div', 'llm-picker-grid'); + const providerLabel = node('label', '', 'Provider'); + const providerSelect = node('select'); + providerSelect.id = 'llm-provider'; + providers.forEach(value => providerSelect.append(option(value, value, value === provider))); + providerLabel.htmlFor = providerSelect.id; + providerLabel.append(providerSelect); + const modelLabel = node('label', '', 'Model'); + const modelSelect = node('select'); + modelSelect.id = 'llm-model'; + models.forEach(value => modelSelect.append(option(value, value, value === model))); + modelLabel.htmlFor = modelSelect.id; + modelLabel.append(modelSelect); + pickerGrid.append(providerLabel, modelLabel); + + const keyState = node('p', 'llm-key-state', status.key_set ? 'API key set' : 'No API key set'); + keyState.append(node('span', '', ` · extractor: ${status.extractor || 'none'}`)); + const setupNote = node('p', 'llm-setup-note', 'Choose a provider and model for the copyable .env snippet. Update it locally, then restart Engraphis to apply the change.'); + const snippetLabel = node('label', 'llm-snippet-label', 'Local .env setup'); + const snippet = node('textarea', 'llm-env-snippet'); + snippet.id = 'llm-env-snippet'; + snippet.readOnly = true; + snippet.rows = 5; + snippet.value = llmSnippet(provider, model, Boolean(status.key_set)); + snippetLabel.htmlFor = snippet.id; + snippetLabel.append(snippet); + const copy = button('Copy', 'secondary-button', copyLlmSnippet); + copy.classList.add('llm-copy-button'); + const snippetWrap = node('div', 'llm-snippet-wrap'); + snippetWrap.append(snippetLabel, copy); + + const extraction = node('div', 'llm-status-line'); + extraction.append( + node('span', '', 'LLM extraction'), + node('span', `llm-status-badge ${extractionEnabled ? 'ready' : 'muted'}`, extractionEnabled ? 'ON' : 'OFF'), + ); + const extractionNote = node('p', 'llm-extraction-note', 'While ON, ingested memory content is sent to your configured provider for schema-validated extraction. OFF disables extraction transfers only; retention supervision is configured separately.'); + const retentionUsesLlm = text(status.retention_supervisor).toLowerCase() === 'llm'; + const retentionNote = node( + 'p', + 'llm-extraction-note', + retentionUsesLlm + ? 'Retention supervision is ON. New memories may send their title and a bounded excerpt to the configured provider.' + : 'Retention supervision is OFF.', + ); + const extractionActions = node('div', 'llm-actions'); + const turnOn = button('Turn on', 'primary-button', () => setLlmExtractor(true)); + turnOn.disabled = extractionEnabled || !configured; + const turnOff = button('Turn off', 'secondary-button', () => setLlmExtractor(false)); + turnOff.disabled = !extractionEnabled; + extractionActions.append(turnOn, turnOff); + + const testActions = node('div', 'llm-actions'); + testActions.append(button('Test connection', 'secondary-button', testLlm)); + const testResult = node('p', 'llm-test-result'); + testResult.id = 'llm-test-result'; + testResult.setAttribute('role', 'status'); + testResult.setAttribute('aria-live', 'polite'); + testActions.append(testResult); + + providerSelect.addEventListener('change', () => { + const defaultModel = defaults[providerSelect.value]; + if (defaultModel && models.includes(defaultModel)) modelSelect.value = defaultModel; + updateLlmSnippet(status); + }); + modelSelect.addEventListener('change', () => updateLlmSnippet(status)); + target.append(overview, pickerGrid, keyState, setupNote, snippetWrap, extraction, extractionNote, retentionNote, extractionActions, testActions); + } + + async function copyLlmSnippet() { + const snippet = byId('llm-env-snippet'); + try { + await navigator.clipboard.writeText(snippet.value); + showNotice('Copied the local .env setup snippet.'); + } catch (_) { + snippet.focus(); + snippet.select(); + if (document.execCommand('copy')) showNotice('Copied the local .env setup snippet.'); + else showNotice('Select the snippet and copy it manually.'); + } + } + + async function loadSettings() { + try { + state.license = await api('/license'); + updatePlanBadge(); + renderSidebarCta(); + } catch (_) {} + renderCloudAccountSettings(); + try { + renderLlmSettings(await api('/llm/status')); + } catch (error) { + byId('llm-connection').replaceChildren(empty(`Model status unavailable: ${error.message}`)); + } + } + + async function setLlmExtractor(enabled) { + if (enabled && !window.confirm(`Turn on LLM extraction? ${EXTERNAL_LLM_PRIVACY_NOTICE}`)) return; + setLlmTestResult(enabled ? 'Verifying the configured provider…' : 'Turning extraction off…'); + try { + const result = await api('/llm/extractor', { method: 'POST', body: { enabled } }); + await loadSettings(); + const state = result.extractor_enabled ? 'LLM extraction is on for new ingested memories.' : 'LLM extraction is off for new ingested memories.'; + setLlmTestResult(`${state}${result.persisted === false ? ' The restart setting could not be saved.' : ''}`, result.extractor_enabled ? 'ready' : 'muted'); + } catch (error) { + setLlmTestResult(`Could not change extraction: ${error.message}`, 'error'); + } + } + + async function testLlm() { + setLlmTestResult('Testing the configured model…'); + try { + const result = await api('/llm/test', { method: 'POST' }); + await loadSettings(); + if (result.ok) { + const suffix = result.auto_enabled ? ' Extraction is active for new ingested memories.' : ''; + setLlmTestResult(`Connected — ${result.provider}/${result.model}.${suffix}`, 'ready'); + } else { + setLlmTestResult(`Could not connect: ${result.error || 'Check the provider, model, API key, and network.'}`, 'error'); + } + } catch (error) { + setLlmTestResult(`Model connection failed: ${error.message}`, 'error'); + } + } + + function switchView(view, { pushHistory = true } = {}) { + const validViews = ['today', 'ask', 'library', 'connections', 'relations', 'provenance', 'manage']; + if (!validViews.includes(view)) view = 'today'; + if (pushHistory && state.view !== view) { + const url = new URL(location.href); + url.searchParams.set('view', view); + window.history.pushState({ view }, '', url); + } + state.view = view; + all('[data-view-panel]').forEach(panel => panel.classList.toggle('active', panel.dataset.viewPanel === view)); + all('[data-view]').forEach(control => { + const active = control.dataset.view === view; + control.classList.toggle('active', active); + if (active) control.setAttribute('aria-current', 'page'); + else control.removeAttribute('aria-current'); + }); + try { + localStorage.setItem('engraphis-ledger-view', view); + } catch (_) {} + graphLifecycle.sync(); + if (view === 'relations') loadGraph(); + if (view === 'provenance' && state.provenanceTab === 'audit') loadAudit(); + if (view === 'manage') { + loadSavings(state.refreshEpoch); + loadManageTab(state.manageTab); + } + window.scrollTo({ top: 0, behavior: 'instant' }); + const heading = byId(`${view}-title`); + if (heading) { + heading.setAttribute('tabindex', '-1'); + heading.focus({ preventScroll: true }); + } + } + + function applyTheme(theme) { + const valid = ['slate', 'midnight', 'paper', 'matrix']; + const selected = valid.includes(theme) ? theme : 'slate'; + document.body.dataset.theme = selected; + byId('theme-select').value = selected; + byId('sidebar-theme-select').value = selected; + try { + localStorage.setItem('engraphis-ledger-theme', selected); + localStorage.setItem('engraphis-theme', ({ slate: 'dark', paper: 'light', midnight: 'midnight', matrix: 'matrix' })[selected]); + } catch (_) {} + if (state.graphEngine) state.graphEngine.setThemeColors(graphThemeColors()); + } + + async function refreshBootstrap(preferred = '') { + const bootstrap = (await api('/bootstrap')) || {}; + renderUpdateBanner(bootstrap.update); + if (typeof bootstrap.version === 'string' && bootstrap.version.trim()) { + state.releaseVersion = bootstrap.version.trim(); + } + state.workspaces = bootstrap.workspaces || []; + state.license = bootstrap.license || state.license; + updatePlanBadge(); + renderSidebarCta(); + const select = byId('workspace-select'); + select.replaceChildren(); + state.workspaces.forEach(item => { + const name = workspaceName(item); + select.append(option(name, name)); + }); + if (!state.workspaces.length) { + select.append(option('', 'No workspace')); + select.disabled = true; + setConnection('Local engine connected · no workspace'); + state.workspace = ''; + state.project = ''; + workflow.selectWorkspace(''); + state.stats = {}; + state.libraryTotal = 0; + renderFirstMemoryJourney(); + renderWorkspaceNames(); + renderWorkspaceList(); + renderMetricValues({ memories: 0, total_rows: 0, workspaces: 0, sessions: 0 }); + byId('decision-list').replaceChildren(empty('Create a workspace in Manage to start reviewing memory.')); + byId('review-status').textContent = 'Create a workspace to load its review state.'; + byId('review-refresh').disabled = true; + const emptyActivity = node('tr'); + const emptyActivityCell = node('td', '', 'No workspace selected yet.'); + emptyActivityCell.colSpan = 5; + emptyActivity.append(emptyActivityCell); + byId('activity-body').replaceChildren(emptyActivity); + byId('proactive-list').replaceChildren(empty('Create a workspace to see proactive context.')); + byId('context-savings-persistent-value').textContent = '—'; + byId('context-savings-persistent-meta').textContent = 'Create a workspace to start tracking context savings.'; + byId('context-savings-persistent-rate').textContent = '—'; + return; + } + select.disabled = false; + let saved = preferred; + try { + saved = preferred || localStorage.getItem('engraphis-workspace') || ''; + } catch (_) {} + const names = state.workspaces.map(workspaceName); + const selected = names.includes(saved) + ? saved + : workspaceName([...state.workspaces].sort((a, b) => number(b.memories) - number(a.memories))[0]); + await selectWorkspace(selected); + setConnection('Local engine connected'); + } + + async function boot() { + if (localDashboardUrl()) { + const fragment = new URLSearchParams(location.hash.slice(1)); + if (fragment.has('token')) { + fragment.delete('token'); + history.replaceState(null, '', location.pathname + location.search + + (fragment.size ? '#' + fragment.toString() : '')); + } + } + byId('today-date').textContent = new Intl.DateTimeFormat(undefined, { dateStyle: 'long' }).format(new Date()); + let theme = 'slate'; + try { + theme = localStorage.getItem('engraphis-ledger-theme') || theme; + } catch (_) {} + applyTheme(theme); + try { + const entry = new URL(location.href); + // Classic links to this workspace's approval controls. Bootstrap still + // validates the name against the authorized workspace list. + await refreshBootstrap(entry.searchParams.get('workspace') || ''); + let view = 'today'; + try { + const saved = localStorage.getItem('engraphis-ledger-view'); + if (['today', 'ask', 'library', 'connections', 'relations', 'provenance', 'manage'].includes(saved)) view = saved; + } catch (_) {} + const urlView = new URL(location.href).searchParams.get('view'); + switchView(['today', 'ask', 'library', 'connections', 'relations', 'provenance', 'manage'].includes(urlView) ? urlView : view, { pushHistory: false }); + const requestedManageTab = entry.searchParams.get('tab'); + if (urlView === 'manage' && ['settings', 'sync', 'analytics'].includes(requestedManageTab)) { + switchManageTab(requestedManageTab); + } + } catch (error) { + if (error.status === 401 && error.code === 'local_browser_session_required' + && await authenticateBrowser(error)) { + location.reload(); + return; + } + setConnection('Local engine unavailable', false); + showNotice(`Ledger could not connect: ${error.message}`); + } + } + + all('[data-view]').forEach(control => control.addEventListener('click', () => switchView(control.dataset.view))); + all('[data-go]').forEach(control => control.addEventListener('click', () => switchView(control.dataset.go))); + all('[data-manage]').forEach(control => control.addEventListener('click', () => { + switchView('manage'); + switchManageTab(control.dataset.manage); + })); + const planBadge = byId('plan-badge'); + if (planBadge) { + planBadge.addEventListener('click', event => { + if (event.currentTarget.dataset.opensAccount === 'true') return; + event.preventDefault(); + switchView('manage'); + switchManageTab('plans'); + }); + } + all('[data-provenance]').forEach(control => control.addEventListener('click', () => { + switchView('provenance'); + switchProvenanceTab(control.dataset.provenance); + })); + all('[data-provenance-tab]').forEach(control => control.addEventListener('click', () => switchProvenanceTab(control.dataset.provenanceTab))); + all('[data-manage-tab]').forEach(control => control.addEventListener('click', () => switchManageTab(control.dataset.manageTab))); + function wireTabKeyboard(selector, dataKey, activate) { + const controls = all(selector); + controls.forEach((control, index) => { + control.tabIndex = control.getAttribute('aria-selected') === 'true' ? 0 : (index ? -1 : 0); + control.addEventListener('keydown', event => { + const direction = event.key === 'ArrowRight' || event.key === 'ArrowDown' ? 1 + : event.key === 'ArrowLeft' || event.key === 'ArrowUp' ? -1 : 0; + let nextIndex = index; + if (event.key === 'Home') nextIndex = 0; + else if (event.key === 'End') nextIndex = controls.length - 1; + else if (direction) nextIndex = (index + direction + controls.length) % controls.length; + else return; + event.preventDefault(); + const next = controls[nextIndex]; + next.focus(); + activate(next.dataset[dataKey]); + }); + }); + } + wireTabKeyboard('[data-graph-tab]', 'graphTab', setGraphTab); + wireTabKeyboard('[data-provenance-tab]', 'provenanceTab', switchProvenanceTab); + wireTabKeyboard('[data-manage-tab]', 'manageTab', switchManageTab); + window.addEventListener('popstate', event => { + const view = event.state && event.state.view + ? event.state.view + : new URL(location.href).searchParams.get('view') || 'today'; + switchView(view, { pushHistory: false }); + }); + + byId('workspace-select').addEventListener('change', event => selectWorkspace(event.target.value)); + byId('ask-form').addEventListener('submit', askMemory); + const updateJevRecallControls = () => { + const enabled = byId('ask-jev-assisted').checked; + byId('ask-jev-remote').disabled = !enabled; + byId('ask-jev-classification').disabled = !enabled; + if (!enabled) byId('ask-jev-remote').checked = false; + }; + ['ask-jev-assisted', 'ask-jev-remote', 'ask-jev-classification'].forEach(id => { + byId(id).addEventListener('change', () => { + updateJevRecallControls(); + beginScopedRequest('ask'); + askRequests.reset(); + byId('answer-panel').replaceChildren(empty('Submit the question with the current Jev settings.')); + byId('retrieval-list').replaceChildren(empty('The next answer will include its matching retrieval preview.')); + byId('ask-status').textContent = 'Jev settings changed. Submit the question to apply them.'; + }); + }); + updateJevRecallControls(); + byId('review-refresh').addEventListener('click', () => { void loadReviewInbox(); }); + byId('library-filter').addEventListener('input', () => { + resetMoveSelection(); + window.clearTimeout(librarySearchTimer); + // Invalidate immediately: an earlier query must not paint while the new one debounces. + beginScopedRequest('library'); + state.libraryLoading = Boolean(state.workspace); + renderLibraryPaging(); + librarySearchTimer = window.setTimeout(() => refreshLibrary(), 250); + }); + byId('library-type').addEventListener('change', () => refreshLibrary()); + byId('library-previous').addEventListener('click', () => refreshLibrary(state.libraryPage - 1)); + byId('library-next').addEventListener('click', () => refreshLibrary(state.libraryPage + 1)); + byId('library-refresh').addEventListener('click', () => refreshLibrary()); + byId('library-selection-toggle').addEventListener('click', () => { + state.librarySelecting = !state.librarySelecting; + resetMoveSelection(); + renderLibrary(); + }); + byId('library-move').addEventListener('click', openMemoryMove); + byId('memory-move-target').addEventListener('change', invalidateMemoryMovePreview); + byId('memory-move-preview-button').addEventListener('click', () => { void previewMemoryMove(); }); + byId('memory-move-form').addEventListener('submit', applyMemoryMove); + byId('memory-move-cancel').addEventListener('click', closeMemoryMove); + byId('memory-move-dialog').addEventListener('cancel', event => { + event.preventDefault(); + if (!state.memoryMove || !state.memoryMove.applying) closeMemoryMove(); + }); + byId('first-memory-add').addEventListener('click', () => { + if (!state.workspace) { + switchView('manage'); + switchManageTab('workspaces'); + byId('create-workspace-form').hidden = false; + byId('new-workspace-name').focus(); + return; + } + switchView('library'); + openEditor(); + }); + byId('new-memory-button').addEventListener('click', () => openEditor()); + byId('editor-close').addEventListener('click', closeEditor); + byId('editor-cancel').addEventListener('click', closeEditor); + byId('editor-refresh').addEventListener('click', refreshEditorVersions); + byId('memory-editor').addEventListener('submit', saveMemory); + byId('editor-memory-importance').addEventListener('input', event => { + const effective = graphSliderResponseValue( + 'editor-memory-importance', event.target.value, 0.5, + ); + event.target.setAttribute('aria-valuetext', `${effective.toFixed(2)} importance`); + const importanceOutput = byId('editor-memory-importance-output'); + if (importanceOutput) importanceOutput.textContent = effective.toFixed(2); + }); + byId('import-button').addEventListener('click', () => byId('import-files').click()); + byId('import-files').addEventListener('change', event => importFiles(event.target.files)); + byId('obsidian-import-button').addEventListener('click', openObsidianImport); + byId('obsidian-import-close').addEventListener('click', () => byId('obsidian-import-dialog').close()); + byId('obsidian-preview').addEventListener('click', previewObsidianImport); + byId('obsidian-cancel').addEventListener('click', cancelObsidianImport); + byId('obsidian-import-form').addEventListener('submit', runObsidianImport); + byId('obsidian-source-mode').addEventListener('change', updateDocumentImportMode); + byId('obsidian-vault-id').addEventListener('change', applySelectedDocumentSource); + byId('obsidian-import-files').addEventListener('change', () => invalidateDocumentImportPreview()); + byId('obsidian-import-folder').addEventListener('change', () => { + prefillNewSourceLabelFromFolder(); + invalidateDocumentImportPreview(); + }); + [ + ['obsidian-workspace', 'input'], + ['obsidian-repo', 'input'], + ['obsidian-session', 'input'], + ['obsidian-scope', 'change'], + ['obsidian-memory-type', 'change'], + ['obsidian-vault-label', 'input'], + ['obsidian-conflict', 'change'], + ].forEach(([id, eventName]) => { + byId(id).addEventListener(eventName, () => invalidateDocumentImportPreview()); + }); + byId('obsidian-report-filter').addEventListener('change', () => renderObsidianReport(obsidianImport.job || obsidianImport.preview)); + + all('[data-graph-tab]').forEach(control => control.addEventListener('click', () => setGraphTab(control.dataset.graphTab))); + byId('graph-fit').addEventListener('click', () => state.graphEngine && state.graphEngine.fit()); + byId('graph-reheat').addEventListener('click', () => state.graphEngine && state.graphEngine.reheat()); + byId('graph-clear-focus').addEventListener('click', () => { + if (state.graphEngine) state.graphEngine.clearFocus(); + }); + byId('graph-freeze').addEventListener('click', () => { + state.graphFrozen = !state.graphFrozen; + setGraphSwitch('graph-freeze', state.graphFrozen); + if (state.graphEngine) state.graphEngine.freeze(state.graphFrozen); + saveGraphPreferences(); + }); + byId('graph-flow').addEventListener('click', event => { + const on = event.currentTarget.getAttribute('aria-checked') !== 'true'; + setGraphSwitch('graph-flow', on); + if (state.graphEngine) state.graphEngine.setSettings({ flow: on }); + clearGraphSavedView(); + saveGraphPreferences(); + }); + byId('graph-labels').addEventListener('click', event => { + const on = event.currentTarget.getAttribute('aria-checked') !== 'true'; + setGraphSwitch('graph-labels', on); + if (state.graphEngine) state.graphEngine.setSettings({ labels: on }); + clearGraphSavedView(); + saveGraphPreferences(); + }); + byId('graph-flow-speed').addEventListener('input', event => { + const speed = graphValueInRange('graph-flow-speed', event.target.value, 45); + const effectiveSpeed = graphSliderResponseValue('graph-flow-speed', speed, 45); + byId('graph-flow-speed').value = String(speed); + byId('graph-flow-speed-output').value = String(Math.round(speed)); + byId('graph-flow-speed-output').textContent = String(Math.round(speed)); + if (state.graphEngine) state.graphEngine.setSettings({ flowSpeed: effectiveSpeed }); + clearGraphSavedView(); + scheduleGraphPreferencesSave(); + }); + byId('graph-search').addEventListener('input', event => searchGraph(event.target.value)); + byId('graph-repo-filter').addEventListener('input', event => { + if (state.graphEngine) state.graphEngine.setRepoFilter(event.target.value); + clearGraphSavedView(); + scheduleGraphPreferencesSave(); + // Repository-scoped payloads need a server reload, but do not issue a 20k-node request + // for every keystroke. The current input is still reflected immediately by the renderer. + if (state.graphMode === 'full') { + const candidate = (event.target.value || '').trim(); + if (candidate && !validatedGraphRepository(candidate)) { + cancelGraphRepositoryReload(); + return; + } + } + if (state.graphIncludeCode || state.graphMode === 'full') scheduleGraphRepositoryReload(); + }); + all('[data-graph-preset-choice]').forEach(control => control.addEventListener('click', () => { + const preset = control.dataset.graphPresetChoice; + /* Every node is its own presentation: selecting the Every node chip loads the + complete LOD scene; the Show-all toggle remains the canonical exit that + restores overview filters. Other layout presets while in Every-node re-run + the seeded Every-node layout without leaving the presentation. */ + if (preset === 'every') { + if (state.graphMode !== 'full') { + byId('graph-preset').value = preset; + clearGraphSavedView(); + syncGraphChoices(); + saveGraphPreferences(); + /* Every node means every node: entering the map clears the unlinked/degree + filters that the overview uses, but remembers them so leaving restores the + person's overview exactly as they had configured it. */ + if (!state.everyPriorFilters) { + state.everyPriorFilters = { + minDegree: Number(byId('graph-min-degree').value) || 0, + unlinked: byId('graph-show-unlinked').getAttribute('aria-pressed') === 'true', + }; + } + setGraphMinDegree(0, false); + setGraphShowUnlinked(true, false); + cancelGraphRepositoryReload(); + state.graphMode = 'full'; + updateGraphModeControls(); + loadGraph({ force: true }); + } else { + // Clicking Every node while already in Every-node exits back to overview, + // mirroring the old Show-all toggle but now via the layout chip. + if (state.everyPriorFilters) { + const prior = state.everyPriorFilters; + state.everyPriorFilters = null; + setGraphMinDegree(prior.minDegree, false); + setGraphShowUnlinked(prior.unlinked, false); + } + byId('graph-preset').value = 'galaxy'; + cancelGraphRepositoryReload(); + state.graphMode = 'overview'; + clearGraphSavedView(); + syncGraphChoices(); + saveGraphPreferences(); + updateGraphModeControls(); + loadGraph({ force: true }); + } + return; + } + if (state.graphMode === 'full') { + byId('graph-preset').value = preset; + clearGraphSavedView(); + syncGraphChoices(); + saveGraphPreferences(); + if (state.graphEngine && state.graphEngine.setPreset) { + const result = state.graphEngine.setPreset(preset); + if (result && typeof result === 'object') syncGraphTuning(result); + } else { + syncGraphTuning(graphPresetTuning(preset)); + } + updateGraphModeControls(); + if (state.graphEngine) state.graphEngine.setSizeBy(graphSizeBy()); + graphLifecycle.sync(); + return; + } + const resumeLayout = state.graphFrozen; + byId('graph-preset').value = preset; + if (state.graphEngine && resumeLayout) { + // Freeze is the safe default for arranging nodes by hand. Selecting a named layout is an + // explicit request to run physics, so make that transition visible and leave the switch + // truthful; the person can freeze the settled arrangement again when they are happy. + state.graphFrozen = false; + setGraphSwitch('graph-freeze', false); + state.graphEngine.freeze(false); + } + let settings = graphPresetTuning(preset); + if (state.graphEngine) settings = state.graphEngine.setPreset(preset); + syncGraphTuning(settings); + updateGraphModeControls(); + if (state.graphEngine) state.graphEngine.setSizeBy(graphSizeBy()); + graphLifecycle.sync(); + clearGraphSavedView(); + syncGraphChoices(); + saveGraphPreferences(); + if (resumeLayout) showNotice('Layout applied. Simulation resumed — freeze it to lock node positions.'); + })); + all('[data-graph-style-choice]').forEach(control => control.addEventListener('click', () => { + byId('graph-style').value = control.dataset.graphStyleChoice; + if (state.graphEngine) state.graphEngine.setStyle(control.dataset.graphStyleChoice); + clearGraphSavedView(); + syncGraphChoices(); + saveGraphPreferences(); + })); + all('[data-graph-color-choice]').forEach(control => control.addEventListener('click', () => { + byId('graph-color').value = control.dataset.graphColorChoice; + if (state.graphEngine) state.graphEngine.setColorBy(control.dataset.graphColorChoice); + clearGraphSavedView(); + syncGraphChoices(); + saveGraphPreferences(); + })); + all('[data-graph-palette-choice]').forEach(control => control.addEventListener('click', () => { + const palette = control.dataset.graphPaletteChoice; + byId('graph-palette').value = palette; + applyGraphPalette(palette); + clearGraphSavedView(); + syncGraphChoices(); + saveGraphPreferences(); + showNotice(`${control.textContent.trim()} palette applied to the graph.`); + })); + byId('graph-min-degree').addEventListener('input', event => { + setGraphMinDegree(event.target.value); + clearGraphSavedView(); + scheduleGraphPreferencesSave(); + }); + byId('graph-show-unlinked').addEventListener('click', event => { + setGraphShowUnlinked(event.currentTarget.getAttribute('aria-pressed') !== 'true'); + clearGraphSavedView(); + saveGraphPreferences(); + if (state.graphMode !== 'full') loadGraph({ force: true }); + }); + byId('graph-tune-min-degree').addEventListener('input', event => { + setGraphMinDegree(event.target.value); + clearGraphSavedView(); + scheduleGraphPreferencesSave(); + }); + byId('graph-depth').addEventListener('input', event => { + setGraphDepth(event.target.value); + clearGraphSavedView(); + scheduleGraphPreferencesSave(); + }); + GRAPH_TUNING.forEach(item => byId(item.id).addEventListener('input', event => { + const value = setGraphTuningControl(item, event.target.value); + const effectiveValue = graphSliderResponseValue( + item.id, value, graphSliderResponseBaseline(item), + ); + if (state.graphEngine) state.graphEngine.setSettings({ [item.key]: effectiveValue }); + clearGraphSavedView(); + scheduleGraphPreferencesSave(); + })); + GRAPH_SPACETIME_TUNING.forEach(item => byId(item.id).addEventListener('input', event => { + setGraphSpacetimeControl(item, event.target.value); + /* Controls use human-scale values (G=100, mass=160, spring=32), while the engine API is + normalized around 1. Apply the same conversion used during graph creation on every live + input event; passing the raw slider value would immediately clamp G to 8 and mass to 16. */ + if (state.graphEngine) { + const settings = graphSpacetimeEngineSettings(); + state.graphEngine.setSettings({ [item.key]: settings[item.key] }); + } + clearGraphSavedView(); + scheduleGraphPreferencesSave(); + })); + byId('graph-orbits-pause').addEventListener('click', event => { + state.graphOrbitPaused = event.currentTarget.getAttribute('aria-checked') !== 'true'; + setGraphSwitch('graph-orbits-pause', state.graphOrbitPaused); + if (state.graphEngine) state.graphEngine.setSettings({ orbitPaused: state.graphOrbitPaused }); + clearGraphSavedView(); + saveGraphPreferences(); + }); + all('[data-graph-layer]').forEach(control => control.addEventListener('click', () => { + const layers = graphLayerState(); + const layer = control.dataset.graphLayer; + const next = !layers[layer]; + if (layer === 'code' && next && state.graphMode === 'full' + && !validatedGraphRepository(byId('graph-repo-filter').value)) { + showNotice('Choose an exact repository before adding its code overlay to All nodes.'); + byId('graph-repo-filter').focus(); + return; + } + layers[layer] = next; + const previousIncludeCode = state.graphIncludeCode; + state.graphIncludeCode = layers.code === true; + setGraphLayers(layers); + if (state.graphEngine) state.graphEngine.setLayers(layers); + clearGraphSavedView(); + saveGraphPreferences(); + if (previousIncludeCode !== state.graphIncludeCode) loadGraph({ force: true }); + })); + all('[data-graph-saved-view]').forEach(control => control.addEventListener('click', () => applyGraphView(control.dataset.graphSavedView))); + byId('graph-save-view').addEventListener('click', saveCurrentGraphView); + byId('graph-reset-tuning').addEventListener('click', resetGraphTuning); + byId('graph-retry').addEventListener('click', retryGraphLoad); + byId('graph-bridges').addEventListener('change', event => { + clearGraphSavedView(); + if (state.graphEngine) state.graphEngine.setBridges(event.target.checked); + saveGraphPreferences(); + }); + byId('graph-collapse').addEventListener('change', event => { + clearGraphSavedView(); + if (state.graphEngine) state.graphEngine.setCollapse(event.target.checked ? 'auto' : false); + saveGraphPreferences(); + }); + byId('graph-as-of').addEventListener('change', event => { + clearGraphSavedView(); + if (state.graphEngine) state.graphEngine.setAsOf(graphAsOfTimestamp()); + saveGraphPreferences(); + loadGraph({ force: true }); + }); + byId('graph-ghosts').addEventListener('change', event => { + clearGraphSavedView(); + if (state.graphEngine) state.graphEngine.setGhosts(event.target.checked); + saveGraphPreferences(); + }); + byId('graph-size').addEventListener('change', event => { + clearGraphSavedView(); + if (state.graphEngine) state.graphEngine.setSizeBy(graphSizeBy()); + saveGraphPreferences(); + }); + const graphExportWrap = byId('graph-export').closest('.graph-export-wrap'); + byId('graph-export').addEventListener('click', () => { + setGraphExportMenuOpen(byId('graph-export-menu').hidden); + }); + graphExportWrap.addEventListener('keydown', event => { + if (event.key !== 'Escape' || byId('graph-export-menu').hidden) return; + event.preventDefault(); + event.stopPropagation(); + setGraphExportMenuOpen(false, true); + }); + document.addEventListener('focusin', event => { + if (!byId('graph-export-menu').hidden && !graphExportWrap.contains(event.target)) { + setGraphExportMenuOpen(false); + } + }); + document.addEventListener('pointerdown', event => { + if (!byId('graph-export-menu').hidden && !graphExportWrap.contains(event.target)) { + setGraphExportMenuOpen(false); + } + }); + byId('graph-export-png').addEventListener('click', () => { + setGraphExportMenuOpen(false, true); + exportGraphPng(); + }); + byId('graph-export-json').addEventListener('click', () => { + setGraphExportMenuOpen(false, true); + exportGraphJson(); + }); + byId('graph-connections-close').addEventListener('click', closeGraphConnections); + byId('graph-connections-dialog').addEventListener('close', cancelGraphConnectionMemoryLoad); + byId('graph-connections-focus').addEventListener('click', () => { + const id = state.graphConnectionsFocusId; + if (!id) return; + closeGraphConnections(); + focusGraphNode(id, state.graphConnectionsFocusLabel || 'Selected entity'); + }); + byId('graph-connections-dialog').addEventListener('click', event => { + if (event.target === event.currentTarget) closeGraphConnections(); + }); + restoreGraphPreferences(); + syncGraphChoices(); + + byId('why-form').addEventListener('submit', whySearch); + byId('timeline-form').addEventListener('submit', event => timelineSearch(event, false)); + byId('supersession-form').addEventListener('submit', event => timelineSearch(event, true)); + byId('verify-receipts').addEventListener('click', verifyReceipts); + byId('export-receipts').addEventListener('click', exportReceipts); + + byId('create-workspace-toggle').addEventListener('click', () => { + byId('create-workspace-form').hidden = !byId('create-workspace-form').hidden; + if (!byId('create-workspace-form').hidden) byId('new-workspace-name').focus(); + }); + window.addEventListener('pagehide', flushGraphPreferencesSave); + byId('create-workspace-form').addEventListener('submit', createWorkspace); + byId('consolidate-form').addEventListener('submit', previewConsolidation); + byId('consolidate-commit').addEventListener('click', commitConsolidation); + ['consolidate-structured'].forEach(id => { + byId(id).addEventListener('change', invalidateConsolidationReview); + }); + byId('billing-select').addEventListener('change', renderPlans); + byId('dashboard-select').addEventListener('change', event => { + location.assign(event.target.value === 'classic' ? '/classic' : '/'); + }); + byId('theme-select').addEventListener('change', event => applyTheme(event.target.value)); + byId('sidebar-theme-select').addEventListener('change', event => applyTheme(event.target.value)); + boot(); +})(); diff --git a/engraphis/local_auth.py b/engraphis/local_auth.py index 7a144982d..c32d6e53e 100644 --- a/engraphis/local_auth.py +++ b/engraphis/local_auth.py @@ -9,8 +9,10 @@ import base64 import hashlib import hmac +import ipaddress import time from typing import Optional +from urllib.parse import urlsplit BROWSER_SESSION_COOKIE = "engraphis_local_session" @@ -18,6 +20,69 @@ _BROWSER_SESSION_VERSION = "v1" +def local_browser_request(request, *, require_origin: bool = True) -> bool: + """Authenticate a same-origin browser bootstrap at the local-machine boundary. + + Unlike the open-mode network helper, this never trusts configured LAN peers or + forwarding proxies. Literal loopback, a loopback Host, an exact browser Origin, + and the dashboard's non-simple header are all required to mint a session. + """ + scope = request.scope + client = scope.get("client") + try: + peer = ipaddress.ip_address(client[0] if client else "") + except ValueError: + return False + if not (getattr(peer, "ipv4_mapped", None) or peer).is_loopback: + return False + + headers = {} + for name, value in scope.get("headers", ()): + name = name.decode("latin1").lower() + if name.startswith("x-forwarded-") or name in {"forwarded", "x-real-ip", "via"}: + return False + headers.setdefault(name, []).append(value.decode("latin1")) + if headers.get("x-engraphis-browser-session") != ["1"]: + return False + if "sec-fetch-site" in headers and headers["sec-fetch-site"] != ["same-origin"]: + return False + hosts = headers.get("host", []) + origins = headers.get("origin", []) + if len(hosts) != 1 or len(origins) > 1: + return False + host = hosts[0] + scheme = scope.get("scheme", "") + if scheme not in {"http", "https"} or any( + char.isspace() or ord(char) < 32 or char in "\\/@?#," for char in host + ): + return False + try: + target = urlsplit(scheme + "://" + host) + hostname = target.hostname or "" + if hostname.lower() != "localhost": + address = ipaddress.ip_address(hostname) + if not (getattr(address, "ipv4_mapped", None) or address).is_loopback: + return False + target_port = target.port if target.port is not None else (443 if scheme == "https" else 80) + if target_port < 1: + return False + if not origins: + return not require_origin + origin = origins[0] + if any(char.isspace() or ord(char) < 32 or char == "\\" for char in origin): + return False + parsed = urlsplit(origin) + origin_port = parsed.port if parsed.port is not None else (443 if parsed.scheme == "https" else 80) + return bool( + parsed.scheme == scheme and parsed.hostname == target.hostname + and origin_port == target_port + and parsed.username is None and parsed.password is None + and not parsed.path and not parsed.query and not parsed.fragment + ) + except ValueError: + return False + + def bearer_token(authorization: Optional[str]) -> str: """Return a stripped bearer credential, or an empty string for another scheme.""" value = str(authorization or "") diff --git a/engraphis/mcp_cli.py b/engraphis/mcp_cli.py index e0c7bc23e..185f9e567 100644 --- a/engraphis/mcp_cli.py +++ b/engraphis/mcp_cli.py @@ -33,7 +33,15 @@ def main(argv=None) -> None: epilog="Configuration comes from the environment / .env (ENGRAPHIS_DB_PATH, " "ENGRAPHIS_PORT, ...). Generate a client config with: engraphis-init", ) - ap.parse_args(argv) + ap.add_argument( + "--connect", metavar="LOOPBACK_MCP_URL", + help="bridge stdio to an existing loopback MCP HTTP service, sharing its warmed engine", + ) + args = ap.parse_args(argv) + if args.connect: + from engraphis.mcp_stdio_bridge import main as bridge_main + bridge_main(["--url", args.connect]) + return error = _dependency_error() if error: raise SystemExit(error) diff --git a/engraphis/mcp_http_cli.py b/engraphis/mcp_http_cli.py index 79740cb28..f92ec9e0f 100644 --- a/engraphis/mcp_http_cli.py +++ b/engraphis/mcp_http_cli.py @@ -1,168 +1,168 @@ -"""Console entry for a local loopback MCP-over-HTTP server. - -This is intentionally a generic MCP transport, not an integration with any particular -agent host. Remote MCP access belongs behind the authenticated dashboard ``/mcp`` -mount; a standalone FastMCP transport has no Engraphis authentication middleware. -""" -from __future__ import annotations - -import argparse -import importlib.util -import ipaddress -import logging -import os -import sys - -_TRANSPORTS = ("streamable-http", "sse") - - -def _configure_logging() -> None: - """Opt-in INFO-level logs for the standalone MCP HTTP launcher. - - The MCP SDK and ``engraphis_recall_context`` use ``logging.getLogger(__name__)``, - which falls back to a ``NullHandler`` when no root config is set. Operators who - want per-call visibility can set ``ENGRAPHIS_MCP_LOG=info`` (or any non-empty - truthy value); the launcher then wires ``logging.basicConfig(level=INFO)`` so - logs reach stderr. Default behaviour is silent to preserve the standalone - CLI's quietness. - """ - if os.environ.get("ENGRAPHIS_MCP_LOG", "").strip().lower() in { - "1", "true", "yes", "info", "on", - }: - if not logging.getLogger().handlers: - logging.basicConfig( - level=logging.INFO, - format="%(asctime)s %(levelname)s %(name)s: %(message)s", - ) - - -def _dependency_error() -> str: - if sys.version_info < (3, 10): - return ( - "The Engraphis MCP server requires Python 3.10 or newer.\n" - "Create a Python 3.10+ environment, then run: pip install \"engraphis[mcp]\"" - ) - if importlib.util.find_spec("mcp") is None: - return ( - "The 'mcp' package is required to run the Engraphis MCP server.\n" - "Install it with: pip install \"engraphis[mcp]\"" - ) - return "" - - -def _loopback_host(value: str) -> str: - host = value.strip() - # Accept 'localhost' as a synonym for 127.0.0.1 - standard network tool behavior. - if host.lower() == "localhost": - return "127.0.0.1" - try: - if ipaddress.ip_address(host).is_loopback: - return host - except ValueError: - pass - raise argparse.ArgumentTypeError( - "standalone MCP-over-HTTP accepts loopback hosts only; use the authenticated " - "dashboard /mcp endpoint for remote access" - ) - - -def _transport_security(host: str, port: int): - """Build the SDK's Host/Origin allowlist for the address this launcher binds.""" - from mcp.server.transport_security import TransportSecuritySettings - - address = ipaddress.ip_address(host) - authority = f"[{address.compressed}]" if address.version == 6 else address.compressed - return TransportSecuritySettings( - enable_dns_rebinding_protection=True, - allowed_hosts=[authority, f"{authority}:{port}"], - allowed_origins=[f"http://{authority}", f"http://{authority}:{port}"], - ) - - -def _port(value: str) -> int: - try: - port = int(value) - except ValueError as exc: - raise argparse.ArgumentTypeError("port must be an integer") from exc - if not 1 <= port <= 65535: - raise argparse.ArgumentTypeError("port must be between 1 and 65535") - return port - - -def main(argv=None) -> None: - ap = argparse.ArgumentParser( - prog="engraphis-mcp-http", - description="Run a loopback-only Engraphis MCP server over HTTP.", - epilog=( - "Use the authenticated dashboard /mcp endpoint for remote clients. " - "Configuration also honors ENGRAPHIS_DB_PATH and the normal .env settings." - ), - ) - ap.add_argument( - "--host", - type=_loopback_host, - default=os.environ.get("ENGRAPHIS_HTTP_HOST", "127.0.0.1"), - help="loopback address to bind (default: ENGRAPHIS_HTTP_HOST or 127.0.0.1)", - ) - ap.add_argument( - "--port", - type=_port, - default=os.environ.get("ENGRAPHIS_HTTP_PORT", "8711"), - help="TCP port to bind (default: ENGRAPHIS_HTTP_PORT or 8711)", - ) - ap.add_argument( - "--transport", - choices=_TRANSPORTS, - default=os.environ.get("ENGRAPHIS_HTTP_TRANSPORT", "streamable-http"), - help="MCP transport (default: ENGRAPHIS_HTTP_TRANSPORT or streamable-http)", - ) - ap.add_argument( - "--classic", - action="store_true", - help=( - "serve the legacy 34 direct-tool surface; normal use defaults to the compact " - "Smart gateway" - ), - ) - args = ap.parse_args(argv) - if args.transport not in _TRANSPORTS: - ap.error("ENGRAPHIS_HTTP_TRANSPORT must be streamable-http or sse") - - error = _dependency_error() - if error: - raise SystemExit(error) - - _configure_logging() - - # Import only after --help and dependency validation: FastMCP registers tools at - # module import time, so importing it eagerly would make even help unusable. - from engraphis.mcp_server import ( - _eager_exact_backend_check, - _preload_sentence_transformers, - mcp, - ) - - server = mcp - if args.classic: - from engraphis.mcp_server import classic_mcp - - server = classic_mcp - server.settings.host = args.host - server.settings.port = args.port - server.settings.transport_security = _transport_security(args.host, args.port) - # Restart-resilient transport. FastMCP's default *stateful* mode tracks MCP - # session ids in memory, so every service bounce (pm2 resurrect, watchdog, - # manual restart) invalidates all live session ids: the client's next request - # gets a 404, the mcp SDK raises "Session terminated", and Hermes' gateway - # client parks for its full retry interval with zero registered tools. - # Stateless mode makes each POST self-contained per the MCP spec, so any - # healthy process can answer any request. Spec-compliant clients handle the - # absent GET SSE stream (the server answers 405 and clients skip it). - server.settings.stateless_http = True - _preload_sentence_transformers() - _eager_exact_backend_check() - server.run(transport=args.transport) - - -if __name__ == "__main__": - main() +"""Console entry for a local loopback MCP-over-HTTP server. + +This is intentionally a generic MCP transport, not an integration with any particular +agent host. Remote MCP access belongs behind the authenticated dashboard ``/mcp`` +mount; a standalone FastMCP transport has no Engraphis authentication middleware. +""" +from __future__ import annotations + +import argparse +import importlib.util +import ipaddress +import logging +import os +import sys + +_TRANSPORTS = ("streamable-http", "sse") + + +def _configure_logging() -> None: + """Opt-in INFO-level logs for the standalone MCP HTTP launcher. + + The MCP SDK and ``engraphis_recall_context`` use ``logging.getLogger(__name__)``, + which falls back to a ``NullHandler`` when no root config is set. Operators who + want per-call visibility can set ``ENGRAPHIS_MCP_LOG=info`` (or any non-empty + truthy value); the launcher then wires ``logging.basicConfig(level=INFO)`` so + logs reach stderr. Default behaviour is silent to preserve the standalone + CLI's quietness. + """ + if os.environ.get("ENGRAPHIS_MCP_LOG", "").strip().lower() in { + "1", "true", "yes", "info", "on", + }: + if not logging.getLogger().handlers: + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)s %(name)s: %(message)s", + ) + + +def _dependency_error() -> str: + if sys.version_info < (3, 10): + return ( + "The Engraphis MCP server requires Python 3.10 or newer.\n" + "Create a Python 3.10+ environment, then run: pip install \"engraphis[mcp]\"" + ) + if importlib.util.find_spec("mcp") is None: + return ( + "The 'mcp' package is required to run the Engraphis MCP server.\n" + "Install it with: pip install \"engraphis[mcp]\"" + ) + return "" + + +def _loopback_host(value: str) -> str: + host = value.strip() + # Accept 'localhost' as a synonym for 127.0.0.1 - standard network tool behavior. + if host.lower() == "localhost": + return "127.0.0.1" + try: + if ipaddress.ip_address(host).is_loopback: + return host + except ValueError: + pass + raise argparse.ArgumentTypeError( + "standalone MCP-over-HTTP accepts loopback hosts only; use the authenticated " + "dashboard /mcp endpoint for remote access" + ) + + +def _transport_security(host: str, port: int): + """Build the SDK's Host/Origin allowlist for the address this launcher binds.""" + from mcp.server.transport_security import TransportSecuritySettings + + address = ipaddress.ip_address(host) + authority = f"[{address.compressed}]" if address.version == 6 else address.compressed + return TransportSecuritySettings( + enable_dns_rebinding_protection=True, + allowed_hosts=[authority, f"{authority}:{port}"], + allowed_origins=[f"http://{authority}", f"http://{authority}:{port}"], + ) + + +def _port(value: str) -> int: + try: + port = int(value) + except ValueError as exc: + raise argparse.ArgumentTypeError("port must be an integer") from exc + if not 1 <= port <= 65535: + raise argparse.ArgumentTypeError("port must be between 1 and 65535") + return port + + +def main(argv=None) -> None: + ap = argparse.ArgumentParser( + prog="engraphis-mcp-http", + description="Run a loopback-only Engraphis MCP server over HTTP.", + epilog=( + "Use the authenticated dashboard /mcp endpoint for remote clients. " + "Configuration also honors ENGRAPHIS_DB_PATH and the normal .env settings." + ), + ) + ap.add_argument( + "--host", + type=_loopback_host, + default=os.environ.get("ENGRAPHIS_HTTP_HOST", "127.0.0.1"), + help="loopback address to bind (default: ENGRAPHIS_HTTP_HOST or 127.0.0.1)", + ) + ap.add_argument( + "--port", + type=_port, + default=os.environ.get("ENGRAPHIS_HTTP_PORT", "8711"), + help="TCP port to bind (default: ENGRAPHIS_HTTP_PORT or 8711)", + ) + ap.add_argument( + "--transport", + choices=_TRANSPORTS, + default=os.environ.get("ENGRAPHIS_HTTP_TRANSPORT", "streamable-http"), + help="MCP transport (default: ENGRAPHIS_HTTP_TRANSPORT or streamable-http)", + ) + ap.add_argument( + "--classic", + action="store_true", + help=( + "serve the legacy 34 direct-tool surface; normal use defaults to the compact " + "Smart gateway" + ), + ) + args = ap.parse_args(argv) + if args.transport not in _TRANSPORTS: + ap.error("ENGRAPHIS_HTTP_TRANSPORT must be streamable-http or sse") + + error = _dependency_error() + if error: + raise SystemExit(error) + + _configure_logging() + + # Import only after --help and dependency validation: FastMCP registers tools at + # module import time, so importing it eagerly would make even help unusable. + from engraphis.mcp_server import ( + _eager_exact_backend_check, + _preload_sentence_transformers, + mcp, + ) + + server = mcp + if args.classic: + from engraphis.mcp_server import classic_mcp + + server = classic_mcp + server.settings.host = args.host + server.settings.port = args.port + server.settings.transport_security = _transport_security(args.host, args.port) + # Restart-resilient transport. FastMCP's default *stateful* mode tracks MCP + # session ids in memory, so every service bounce (pm2 resurrect, watchdog, + # manual restart) invalidates all live session ids: the client's next request + # gets a 404, the mcp SDK raises "Session terminated", and Hermes' gateway + # client parks for its full retry interval with zero registered tools. + # Stateless mode makes each POST self-contained per the MCP spec, so any + # healthy process can answer any request. Spec-compliant clients handle the + # absent GET SSE stream (the server answers 405 and clients skip it). + server.settings.stateless_http = True + _preload_sentence_transformers() + _eager_exact_backend_check() + server.run(transport=args.transport) + + +if __name__ == "__main__": + main() diff --git a/engraphis/mcp_server.py b/engraphis/mcp_server.py index 7abc99d5c..a9b9e3981 100644 --- a/engraphis/mcp_server.py +++ b/engraphis/mcp_server.py @@ -38,6 +38,7 @@ from collections import OrderedDict from dataclasses import dataclass +from functools import partial, wraps from typing import Any, Annotated, Callable, List, Optional try: @@ -49,6 +50,7 @@ BeforeValidator = None # type: ignore[assignment,misc] try: + import anyio from mcp.server.fastmcp import FastMCP from mcp.types import CallToolResult, TextContent try: @@ -101,6 +103,24 @@ # ``classic_mcp`` retains the public, named-tool protocol for integrations that pinned a # tool name. ``mcp`` temporarily refers to it while the legacy decorators below execute; # it is rebound to the small Smart MCP surface after every classic tool has registered. +def _threaded_tool(server, **options): + """Keep blocking memory work off the shared MCP transport's event loop. + + AnyIO copies the bound service, caller and MCP request ContextVars into the + worker. Its default cancellation shielding lets an in-flight mutation finish + its transaction. Direct Python callers and gateway dispatch stay synchronous. + """ + def register(function): + @wraps(function) + async def run(*args, **kwargs): + return await anyio.to_thread.run_sync(partial(function, *args, **kwargs)) + + run._engraphis_sync_handler = function + server.tool(**options)(run) + return function + return register + + classic_mcp = FastMCP("engraphis_mcp", instructions=_SESSION_PROTOCOL, log_level="WARNING") mcp = classic_mcp @@ -469,7 +489,7 @@ def minimum_role(tool_name: str) -> str: return "member" -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_list_workspaces", annotations={"title": "List available memory workspaces", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, @@ -482,7 +502,7 @@ def engraphis_list_workspaces() -> str: return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_get_workspace_routing", annotations={"title": "Read a project's workspace choice", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, @@ -498,7 +518,7 @@ def engraphis_get_workspace_routing( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_set_workspace_routing", annotations={"title": "Save a project's workspace choice", "readOnlyHint": False, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, @@ -518,7 +538,7 @@ def engraphis_set_workspace_routing( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_remember", annotations={"title": "Remember a fact", "readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, "openWorldHint": False}, @@ -593,7 +613,8 @@ def engraphis_remember( rationale, a bug's cause and fix, a user preference, or a reusable procedure. Returns: - str: JSON ``{"id","workspace","repo","scope","mtype","stored":true,"op"}`` where + str: JSON with ``id``, resolved scope, ``stored``, ``op``, ``review_state`` and + ``prompt_eligible``. Stored memories may still require review before recall. Here ``op`` is ``"add"`` (new), ``"noop"`` (matched an existing memory almost exactly — that one was reinforced, ``id`` points to it), or ``"invalidate"`` (superseded an existing memory on the same subject — see ``superseded`` for the old id(s); history @@ -627,7 +648,7 @@ def engraphis_remember( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_remember_many", annotations={"title": "Remember a batch of facts", "readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, "openWorldHint": False}, @@ -699,7 +720,7 @@ def engraphis_remember_many( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_recall", annotations={"title": "Recall relevant memories", "readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, @@ -811,7 +832,7 @@ def engraphis_recall( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_recall_context", annotations={"title": "Recall token-efficient context", "readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, @@ -1018,7 +1039,7 @@ def engraphis_recall_context( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_recall_grounded", annotations={"title": "Grounded recall (cited answer, or abstain)", "readOnlyHint": False, "destructiveHint": False, @@ -1126,7 +1147,7 @@ def engraphis_recall_grounded( pass -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_answer", annotations={"title": "Grounded answer (compatibility alias)", "readOnlyHint": False, "destructiveHint": False, @@ -1184,7 +1205,7 @@ def engraphis_answer( ) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_why", annotations={"title": "Explain the rationale behind a fact", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, @@ -1218,7 +1239,7 @@ def engraphis_why( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_timeline", annotations={"title": "Bi-temporal history of a fact", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, @@ -1248,7 +1269,7 @@ def engraphis_timeline( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_recall_proactive", annotations={"title": "What should I know right now", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, @@ -1281,7 +1302,7 @@ def engraphis_recall_proactive( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_proactive_context", annotations={"title": "Agent-ready proactive context", "readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, @@ -1344,7 +1365,7 @@ def _require_local_operator_attestation(tool_name: str, confirmed: bool) -> Opti return None -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_retire", annotations={"title": "Retire a memory", "readOnlyHint": False, "destructiveHint": True, "idempotentHint": False, "openWorldHint": False}, @@ -1387,7 +1408,7 @@ def engraphis_retire( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_forget", annotations={"title": "Forget a memory (deprecated; use retire)", "readOnlyHint": False, "destructiveHint": True, "idempotentHint": False, "openWorldHint": False}, @@ -1421,7 +1442,7 @@ def engraphis_forget( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_secure_erase", annotations={"title": "Securely erase a leaked memory", "readOnlyHint": False, "destructiveHint": True, "idempotentHint": False, "openWorldHint": False}, @@ -1458,7 +1479,7 @@ def engraphis_secure_erase( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_pin", annotations={"title": "Pin or unpin a memory", "readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, @@ -1491,7 +1512,7 @@ def engraphis_pin( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_correct", annotations={"title": "Correct a memory", "readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, "openWorldHint": False}, @@ -1541,7 +1562,7 @@ def engraphis_correct( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_promote", annotations={"title": "Promote a memory to a wider scope", "readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, @@ -1583,7 +1604,7 @@ def engraphis_promote( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_link", annotations={"title": "Link two memories", "readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, "openWorldHint": False}, @@ -1625,7 +1646,7 @@ def engraphis_link( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_record_event", annotations={"title": "Log an episodic event", "readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, "openWorldHint": False}, @@ -1657,7 +1678,7 @@ def engraphis_record_event( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_index_repo", annotations={"title": "Index a repository's code graph", "readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, @@ -1709,7 +1730,7 @@ def engraphis_index_repo( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_search_code", annotations={"title": "Search the code symbol graph", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, @@ -1748,7 +1769,7 @@ def engraphis_search_code( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_code_path", annotations={"title": "Find a path through the code graph", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, @@ -1786,7 +1807,7 @@ def engraphis_code_path( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_code_impact", annotations={"title": "Estimate change impact from the code graph", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, @@ -1817,7 +1838,7 @@ def engraphis_code_impact( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_export_code_graph", annotations={"title": "Export the indexed code graph", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, @@ -1844,7 +1865,7 @@ def engraphis_export_code_graph( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_link_symbol", annotations={"title": "Link a code symbol to a memory", "readOnlyHint": False, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, @@ -1886,7 +1907,7 @@ def engraphis_link_symbol( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_start_session", annotations={"title": "Start a memory session", "readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, @@ -1943,7 +1964,7 @@ def engraphis_start_session( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_end_session", annotations={"title": "End a memory session", "readOnlyHint": False, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, @@ -1974,7 +1995,7 @@ def engraphis_end_session( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_receipts", annotations={"title": "List privacy-safe operation receipts", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, @@ -1992,7 +2013,7 @@ def engraphis_receipts( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_context_savings", annotations={"title": "Summarize context savings", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, @@ -2032,7 +2053,7 @@ def engraphis_context_savings( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_verify_receipts", annotations={"title": "Verify an operation receipt chain", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, @@ -2061,7 +2082,7 @@ def engraphis_verify_receipts( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_export_receipts", annotations={"title": "Export operation receipts", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, @@ -2077,7 +2098,7 @@ def engraphis_export_receipts( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_stats", annotations={"title": "Memory store stats", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, @@ -2097,7 +2118,7 @@ def engraphis_stats( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_check_update", annotations={"title": "Check for an Engraphis update", "readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, @@ -2127,7 +2148,7 @@ def engraphis_check_update( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_ingest", annotations={"title": "Ingest raw text (extract facts first)", "readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, "openWorldHint": False}, @@ -2177,7 +2198,7 @@ def engraphis_ingest( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_ingest_postgres_schema", annotations={"title": "Ingest a live PostgreSQL schema", "readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, @@ -2209,7 +2230,7 @@ def engraphis_ingest_postgres_schema( return _err(exc) -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_consolidate", annotations={"title": "Consolidate memories (sleep-time sweep)", "readOnlyHint": False, "destructiveHint": True, "idempotentHint": False, @@ -2552,7 +2573,7 @@ def _heuristic_decision( } -@mcp.tool( +@_threaded_tool(mcp, name="engraphis_decide", annotations={ "title": "System 1 decision gating (Jev / TypeSafe AI)", @@ -2833,6 +2854,7 @@ class ActionSpec: # yet. The TTL and size cap bound a long-lived stdio process's memory and stop a # leaked capability id from remaining usable indefinitely. _CAPABILITY_INDEX: "OrderedDict[str, tuple[str, str, str, str, float]]" = OrderedDict() +_CAPABILITY_LOCK = threading.RLock() _GATEWAY_RESULT_COUNTER = RegexTokenCounter() @@ -3081,12 +3103,13 @@ def _rank_actions(task: str, *, category: str = "", intent: str = "") -> list[Ac def _prune_capabilities(now: float) -> None: """Drop expired capabilities so a leaked id stops resolving after its TTL.""" - expired = [ - capability_id for capability_id, entry in _CAPABILITY_INDEX.items() - if now - entry[4] > _CAPABILITY_TTL_SECONDS - ] - for capability_id in expired: - del _CAPABILITY_INDEX[capability_id] + with _CAPABILITY_LOCK: + expired = [ + capability_id for capability_id, entry in _CAPABILITY_INDEX.items() + if now - entry[4] > _CAPABILITY_TTL_SECONDS + ] + for capability_id in expired: + del _CAPABILITY_INDEX[capability_id] def _issue_capability(spec: ActionSpec) -> str: @@ -3096,13 +3119,14 @@ def _issue_capability(spec: ActionSpec) -> str: signature = hmac.new(_CAPABILITY_SECRET, body, hashlib.sha256).hexdigest()[:24] capability_id = f"cap_{signature}" now = time.time() - _prune_capabilities(now) - while len(_CAPABILITY_INDEX) >= _CAPABILITY_MAX_ENTRIES: - _CAPABILITY_INDEX.popitem(last=False) # evict oldest first (LRU) - _CAPABILITY_INDEX[capability_id] = ( - spec.canonical_id, spec.schema_digest, _CAPABILITY_VERSION, _DEPLOYMENT_POLICY, - now, - ) + with _CAPABILITY_LOCK: + _prune_capabilities(now) + while len(_CAPABILITY_INDEX) >= _CAPABILITY_MAX_ENTRIES: + _CAPABILITY_INDEX.popitem(last=False) # evict oldest first (LRU) + _CAPABILITY_INDEX[capability_id] = ( + spec.canonical_id, spec.schema_digest, _CAPABILITY_VERSION, _DEPLOYMENT_POLICY, + now, + ) return capability_id @@ -3290,34 +3314,35 @@ def _bounded_gateway_success(spec: ActionSpec, payload: dict[str, Any]) -> str: def _resolve_capability(capability_id: str, schema_digest: str) -> Optional[ActionSpec]: - entry = _CAPABILITY_INDEX.get(str(capability_id or "")) - if entry is None: - return None - action_id, issued_digest, issued_version, issued_policy, issued_at = entry - if time.time() - issued_at > _CAPABILITY_TTL_SECONDS: - # Expired: forget the id so it can never resolve again, even within a - # long-lived stdio process that outlives the TTL. - del _CAPABILITY_INDEX[str(capability_id or "")] - return None - spec = ACTION_SPECS.get(action_id) - expected_body = ( - f"{_CAPABILITY_VERSION}:{_DEPLOYMENT_POLICY}:{action_id}:{schema_digest}" - ).encode("utf-8") - expected_id = "cap_" + hmac.new( - _CAPABILITY_SECRET, expected_body, hashlib.sha256, - ).hexdigest()[:24] - if ( - spec is None - or issued_digest != schema_digest - or spec.schema_digest != schema_digest - or issued_version != _CAPABILITY_VERSION - or issued_policy != _DEPLOYMENT_POLICY - or not hmac.compare_digest(str(capability_id), expected_id) - or not spec.availability_predicate() - ): - return None - _CAPABILITY_INDEX.move_to_end(str(capability_id or "")) # LRU refresh - return spec + with _CAPABILITY_LOCK: + entry = _CAPABILITY_INDEX.get(str(capability_id or "")) + if entry is None: + return None + action_id, issued_digest, issued_version, issued_policy, issued_at = entry + if time.time() - issued_at > _CAPABILITY_TTL_SECONDS: + # Expired: forget the id so it can never resolve again, even within a + # long-lived stdio process that outlives the TTL. + del _CAPABILITY_INDEX[str(capability_id or "")] + return None + spec = ACTION_SPECS.get(action_id) + expected_body = ( + f"{_CAPABILITY_VERSION}:{_DEPLOYMENT_POLICY}:{action_id}:{schema_digest}" + ).encode("utf-8") + expected_id = "cap_" + hmac.new( + _CAPABILITY_SECRET, expected_body, hashlib.sha256, + ).hexdigest()[:24] + if ( + spec is None + or issued_digest != schema_digest + or spec.schema_digest != schema_digest + or issued_version != _CAPABILITY_VERSION + or issued_policy != _DEPLOYMENT_POLICY + or not hmac.compare_digest(str(capability_id), expected_id) + ): + return None + _CAPABILITY_INDEX.move_to_end(str(capability_id or "")) # LRU refresh + # Availability may consult a bound service; never hold the index lock over I/O. + return spec if spec.availability_predicate() else None _MAX_GATEWAY_ARGUMENT_BYTES = 256 * 1024 @@ -3375,7 +3400,8 @@ def _run_action(spec: ActionSpec, arguments: dict[str, Any]) -> tuple[bool, Any, except Exception: # noqa: BLE001 - validation errors stay content-free return False, "invalid_arguments", {} try: - raw = tool.fn(**validated_arguments) + handler = getattr(tool.fn, "_engraphis_sync_handler", tool.fn) + raw = handler(**validated_arguments) except Exception as exc: # noqa: BLE001 - handler errors stay content-free # Surface the exception object (never its internals) so the Smart gateway can # tag timeouts / locked-store contention as E_RETRYABLE while keeping the @@ -3461,7 +3487,7 @@ def _normalize_session_action(value: Any) -> Any: return value -@smart_mcp.tool( +@_threaded_tool(smart_mcp, name="engraphis_session", annotations={"title": "Start or end a memory session", "readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, "openWorldHint": False}, @@ -3536,7 +3562,7 @@ def engraphis_session( return _ok(payload) -@smart_mcp.tool( +@_threaded_tool(smart_mcp, name="engraphis_recall_context", annotations={"title": "Recall compact project context", "readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, "openWorldHint": True}, @@ -3574,7 +3600,7 @@ def smart_recall_context( return result -@smart_mcp.tool( +@_threaded_tool(smart_mcp, name="engraphis_remember", annotations={"title": "Remember a durable fact", "readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, "openWorldHint": False}, @@ -3604,7 +3630,7 @@ def smart_remember( description="Literal type: literal, string, identifier, path, number, date, enum, or json.", max_length=32)] = "literal", ) -> str: - """Store a durable fact with safe default provenance and deduplication.""" + """Store a fact; check prompt_eligible and review_state before recall.""" result = engraphis_remember( content=content, workspace=workspace, repo=repo, session_id=session_id, mtype=mtype, importance=importance, @@ -3616,7 +3642,7 @@ def smart_remember( return result -@smart_mcp.tool( +@_threaded_tool(smart_mcp, name="engraphis_discover_actions", annotations={"title": "Discover an advanced Engraphis capability", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, @@ -3661,7 +3687,7 @@ def _execute_gateway(capability_id: str, schema_digest: str, arguments: dict[str }) -@smart_mcp.tool( +@_threaded_tool(smart_mcp, name="engraphis_execute_read", annotations={"title": "Execute a discovered read action", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, @@ -3678,7 +3704,7 @@ def engraphis_execute_read( return _execute_gateway(capability_id, schema_digest, arguments, expected="read") -@smart_mcp.tool( +@_threaded_tool(smart_mcp, name="engraphis_execute_action", annotations={"title": "Execute a discovered stateful action", "readOnlyHint": False, "destructiveHint": True, "idempotentHint": False, "openWorldHint": False}, @@ -3695,7 +3721,7 @@ def engraphis_execute_action( return _execute_gateway(capability_id, schema_digest, arguments, expected="action") -@smart_mcp.tool( +@_threaded_tool(smart_mcp, name="engraphis_get_memory", annotations={"title": "Read one memory's governed record", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, @@ -3788,7 +3814,7 @@ def engraphis_get_memory( return _classify_gateway_exception(exc) -@smart_mcp.tool( +@_threaded_tool(smart_mcp, name="engraphis_update_memory", annotations={"title": "Edit a memory's metadata fields", "readOnlyHint": False, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, @@ -3828,7 +3854,7 @@ def engraphis_update_memory( return _classify_gateway_exception(exc) -@smart_mcp.tool( +@_threaded_tool(smart_mcp, name="engraphis_conflict_review", annotations={"title": "List pending/quarantined/conflicting memories", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False}, diff --git a/engraphis/mcp_stdio_bridge.py b/engraphis/mcp_stdio_bridge.py new file mode 100644 index 000000000..2900647c7 --- /dev/null +++ b/engraphis/mcp_stdio_bridge.py @@ -0,0 +1,157 @@ +"""Dependency-free stdio bridge to an already running local Engraphis MCP service. + +Run this file directly with Python, including ``python -I /path/to/this/file.py``. +Desktop clients can share one warmed engine instead of each loading PyTorch. +Only numeric loopback HTTP endpoints are accepted; proxies and redirects are disabled. +""" +from __future__ import annotations + +import argparse +import ipaddress +import json +import math +import sys +from urllib.parse import urlsplit +from urllib.request import HTTPRedirectHandler, ProxyHandler, Request, build_opener + + +MAX_MESSAGE_BYTES = 4_000_000 + + +class _NoRedirect(HTTPRedirectHandler): + def redirect_request(self, req, fp, code, msg, headers, newurl): + return None + + +def loopback_url(value: str) -> str: + try: + parsed = urlsplit(value) + address = ipaddress.ip_address(parsed.hostname or "") + port = parsed.port + except ValueError as exc: + raise argparse.ArgumentTypeError("use a numeric loopback HTTP MCP URL") from exc + if ( + parsed.scheme != "http" or not address.is_loopback + or parsed.username is not None or parsed.password is not None + or parsed.query or parsed.fragment or port == 0 + ): + raise argparse.ArgumentTypeError("use a numeric loopback HTTP MCP URL without credentials") + return value + + +def response_message(body: bytes, request_id): + """Decode JSON or bounded Streamable HTTP SSE, preserving protocol errors.""" + text = body.decode("utf-8") + try: + messages = [json.loads(text)] if text.strip() else [] + except ValueError: + messages = [] + data = [] + for line in [*text.splitlines(), ""]: + if line.startswith("data:"): + data.append(line[5:].lstrip(" ")) + elif not line and data: + messages.append(json.loads("\n".join(data))) + data = [] + for message in messages: + if ( + isinstance(message, dict) and message.get("jsonrpc") == "2.0" + and message.get("id") == request_id + and ("result" in message or "error" in message) + ): + return message + raise ValueError("local MCP response did not contain the requested result") + + +def read_response(response, request_id): + """Finish at the matching SSE result, without waiting for a stream to close.""" + content_type = response.headers.get("Content-Type", "").split(";", 1)[0].strip().lower() + if content_type != "text/event-stream": + body = response.read(MAX_MESSAGE_BYTES + 1) + if len(body) > MAX_MESSAGE_BYTES: + raise ValueError("local MCP response exceeded the message limit") + return response_message(body, request_id) + + size = 0 + data = [] + while True: + line = response.readline(MAX_MESSAGE_BYTES - size + 1) + size += len(line) + if size > MAX_MESSAGE_BYTES: + raise ValueError("local MCP response exceeded the message limit") + if not line: + break + text = line.decode("utf-8").rstrip("\r\n") + if text.startswith("data:"): + data.append(text[5:].lstrip(" ")) + elif not text and data: + message = json.loads("\n".join(data)) + data = [] + if (isinstance(message, dict) and message.get("jsonrpc") == "2.0" + and message.get("id") == request_id + and ("result" in message or "error" in message)): + return message + raise ValueError("local MCP response did not contain the requested result") + + +class LocalBridge: + def __init__(self, url: str, timeout: float = 120.0): + self.url = loopback_url(url) + self.timeout = timeout + self.session_id = None + self.protocol_version = None + self.opener = build_opener(ProxyHandler({}), _NoRedirect()) + + def forward(self, message: dict): + headers = {"Content-Type": "application/json", + "Accept": "application/json, text/event-stream"} + if self.session_id: + headers["Mcp-Session-Id"] = self.session_id + if self.protocol_version: + headers["MCP-Protocol-Version"] = self.protocol_version + request = Request(self.url, data=json.dumps(message).encode("utf-8"), + headers=headers, method="POST") + with self.opener.open(request, timeout=self.timeout) as response: + self.session_id = response.headers.get("Mcp-Session-Id") or self.session_id + if "id" not in message: + return None + result = read_response(response, message["id"]) + if message.get("method") == "initialize" and "result" in result: + self.protocol_version = result["result"].get("protocolVersion") + return result + + +def main(argv=None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--url", type=loopback_url, default="http://127.0.0.1:8711/mcp") + parser.add_argument("--timeout", type=float, default=120.0) + args = parser.parse_args(argv) + if not math.isfinite(args.timeout) or not 0 < args.timeout <= 300: + parser.error("timeout must be finite and between 0 and 300 seconds") + bridge = LocalBridge(args.url, args.timeout) + while True: + line = sys.stdin.readline(MAX_MESSAGE_BYTES + 1) + if not line: + return 0 + message = None + try: + if len(line.encode("utf-8")) > MAX_MESSAGE_BYTES: + raise ValueError("request exceeds the message limit") + message = json.loads(line) + if not isinstance(message, dict) or message.get("jsonrpc") != "2.0": + raise ValueError("invalid JSON-RPC envelope") + result = bridge.forward(message) + except Exception as exc: + # Never echo request content, URLs or backend exception strings. + print("Engraphis local MCP bridge failed: " + type(exc).__name__, file=sys.stderr) + if isinstance(message, dict) and "id" not in message: + continue + result = {"jsonrpc": "2.0", "id": message.get("id") if isinstance(message, dict) else None, + "error": {"code": -32000, "message": "Local Engraphis MCP service unavailable"}} + if result is not None: + sys.stdout.write(json.dumps(result, ensure_ascii=True) + "\n") + sys.stdout.flush() + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/engraphis/routes/v2_api.py b/engraphis/routes/v2_api.py index 60ff18b5b..bc725f9f5 100644 --- a/engraphis/routes/v2_api.py +++ b/engraphis/routes/v2_api.py @@ -23,7 +23,7 @@ from dataclasses import replace from pathlib import Path from typing import Optional -from urllib.parse import quote +from urllib.parse import quote, urlsplit from fastapi import APIRouter, HTTPException, Query, Request from pydantic import BaseModel, Field, StrictBool, StrictInt @@ -2395,16 +2395,14 @@ def export(workspace: Optional[str] = None, signed: bool = False, # ── automated maintenance (Pro) ─────────────────────────────────────────────── class _AutomationReq(BaseModel): + model_config = {"extra": "forbid"} enabled: Optional[bool] = None - cadence_hours: Optional[int] = None - consolidate: Optional[bool] = None - min_cluster: Optional[int] = None - archive_below: Optional[float] = None + cadence_hours: Optional[int] = Field(None, ge=1, le=720) workspaces: Optional[list] = None dream: Optional[bool] = None dream_enabled: Optional[bool] = None - dream_min_new: Optional[int] = None - dream_idle_minutes: Optional[int] = None + dream_min_new: Optional[int] = Field(None, ge=2, le=10000) + dream_idle_minutes: Optional[int] = Field(None, ge=0, le=10080) infer: Optional[bool] = None @@ -2434,9 +2432,6 @@ def automation_get(workspace: Optional[str] = None): return { "enabled": bool(policy.get("enabled")), "cadence_hours": max(1, int(policy.get("cadence_minutes") or 1440) // 60), - "consolidate": True, - "min_cluster": 3, - "archive_below": 0.05, "workspaces": [ws], "dream": bool(policy.get("dream_enabled", True)), "dream_min_new": int(policy.get("dream_min_new") or 25), @@ -2560,7 +2555,6 @@ def automation_set(req: _AutomationReq, workspace: Optional[str] = None): **saved, "cadence_hours": policy["cadence_minutes"] // 60, "dream": policy["dream_enabled"], - "consolidate": True, } @@ -4289,11 +4283,27 @@ def _sync_all(svc) -> dict: attempted += 1 try: cloud_access = None + relay_url = _relay_url() if not legacy_token_configured: + target = urlsplit(relay_url) + canonical = urlsplit(DEFAULT_RELAY_URL) + if ( + target.scheme != canonical.scheme + or target.hostname != canonical.hostname + or (443 if target.port is None else target.port) + != (443 if canonical.port is None else canonical.port) + or target.username is not None or target.password is not None + ): + errors.append({ + "workspace": name, + "error": "custom relay requires an explicitly origin-bound sync token", + "status": 409, + }) + continue cloud_access, _, _ = access_for_workspace(name, require_compute=False) transport = get_transport( "relay", - base_url=_relay_url(), + base_url=relay_url, workspace_id=name, access_token=cloud_access, ) @@ -4340,11 +4350,18 @@ def _sync_all(svc) -> dict: else: round_errors = rep.get("errors") failed_items = len(round_errors) if isinstance(round_errors, list) else 1 - errors.append({ + failure = { "workspace": name, "error": "sync round incomplete", "failed_items": max(1, min(failed_items, 10_000)), - }) + } + if isinstance(round_errors, list) and round_errors and all( + isinstance(item, dict) and item.get("status") in {401, 402, 403} + for item in round_errors + ): + failure["error"] = "cloud relay synchronization failed" + failure["status"] = round_errors[0]["status"] + errors.append(failure) for k in totals: totals[k] += int((rep.get("totals") or {}).get(k, 0) or 0) diff --git a/engraphis/service.py b/engraphis/service.py index 2b4ba518e..e841c2aad 100644 --- a/engraphis/service.py +++ b/engraphis/service.py @@ -78,6 +78,7 @@ ) from engraphis.core.secrets import SecretDetectedError, redact_secrets as _redact_secrets, reject_secrets from engraphis.core.store import ( + _memory_temporal_visibility_sql, _loads, _merge_edge_provenance, _public_receipt_row, @@ -544,6 +545,14 @@ def _rollback_service_transaction(method): def wrapped(self, *args, **kwargs): conn = self.store.conn owns_transaction = not conn.transaction_owned_by_current_thread() + # Engine writes take this lock before reserving the SQLite connection. + # Batch/lifecycle writes must use the same order. A caller that already + # owns SQLite may only reenter/acquire an uncontended engine lock: waiting + # here would invert the order and deadlock another engine writer. + write_lock = self.engine._write_lock + if not write_lock.acquire(blocking=owns_transaction): + raise sqlite3.OperationalError( + "engine write lock busy during caller-owned transaction") try: if owns_transaction: conn.execute("BEGIN IMMEDIATE") @@ -559,6 +568,8 @@ def wrapped(self, *args, **kwargs): except Exception: # noqa: BLE001 - preserve the original failure pass raise + finally: + write_lock.release() return wrapped @@ -2113,6 +2124,16 @@ def remember(self, content: str, *, workspace: Optional[str] = None, repo: Optio "workspace_source": route["source"], "scope": sc.value, "mtype": mt.value, "stored": True, "op": result["op"], } + # A successful write may still require review. Report the persisted + # target's state (also for dedupe/noop), rather than implying that stored + # content is necessarily available to the next agent's prompt. + stored_record = self.store.get_memory(result["id"]) + out["prompt_eligible"] = bool( + stored_record and prompt_eligible(stored_record.provenance, stored_record.metadata) + ) + out["review_state"] = ( + str(stored_record.provenance.get("review_state") or "") if stored_record else "" + ) if result["op"] in ("noop", "invalidate", "relate"): out["resolution"] = result.get("reason", "") if result.get("exact_value_bound"): @@ -6944,6 +6965,11 @@ def _remap_memory_ids_in_text(raw: Any) -> str: m["valid_to_recorded_at"], m["ingested_at"], m["expired_at"], _remap_copy_text(m["subject_key"]), m["claim_kind"], m["pinned"], m["sensitivity"], _remap_json_memory_ids(m["provenance"]), m["sort_order"])) + c.execute( + "INSERT OR IGNORE INTO memory_closures(memory_id,valid_to,recorded_at) " + "SELECT ?,valid_to,recorded_at FROM memory_closures WHERE memory_id=?", + (nmid, m["id"]), + ) fts_row = c.execute( "SELECT title, content, keywords FROM mem_fts WHERE id=?", (m["id"],)).fetchone() if fts_row: @@ -8164,6 +8190,15 @@ def _export_workspace_snapshot(self, *, workspace: str, recovery: bool = False, else: memories = all_memories memory_ids = {str(row["id"]) for row in memories} + memory_closures = [ + dict(row) for row in conn.execute( + "SELECT mc.memory_id,mc.valid_to,mc.recorded_at FROM memory_closures mc " + "JOIN memories m ON m.id=mc.memory_id WHERE m.workspace_id=? " + "ORDER BY mc.memory_id,mc.valid_to,mc.recorded_at", + (wid,), + ).fetchall() + if str(row["memory_id"]) in memory_ids + ] all_source_vaults = [dict(row) for row in conn.execute( "SELECT * FROM source_vaults WHERE workspace_id=? ORDER BY id", @@ -8598,6 +8633,7 @@ def scrub_json(raw: Any, default: Any) -> Any: "repos": repos, "sessions": sessions, "memories": memories, + "memory_closures": memory_closures, "source_vaults": source_vaults, "source_imports": source_imports, "entities": entities, @@ -8626,6 +8662,7 @@ def scrub_json(raw: Any, default: Any) -> Any: "repos": ["id"], "sessions": ["id"], "memories": ["id"], + "memory_closures": ["memory_id", "valid_to", "recorded_at"], "source_vaults": ["id"], "source_imports": ["vault_id", "source_key", "id"], "entities": ["id"], @@ -9523,6 +9560,24 @@ def temporal_ghost(row: Any) -> bool: except (TypeError, ValueError): return False + closed_memory_ids: set[str] = set() + + def load_memory_ghosts(memory_ids: list[str]) -> None: + for start in range(0, len(memory_ids), 500): + chunk = memory_ids[start:start + 500] + marks = ",".join("?" for _ in chunk) + closed_memory_ids.update( + str(row["memory_id"]) for row in self.store.conn.execute( + "SELECT DISTINCT memory_id FROM memory_closures " + f"WHERE memory_id IN ({marks}) AND valid_to<=? " + "AND (recorded_at IS NULL OR recorded_at<=?)", + (*chunk, t, known_t), + ) + ) + + def memory_ghost(memory_id: str) -> bool: + return memory_id in closed_memory_ids + try: lower_time = float(time_from) if time_from is not None else None upper_time = float(time_to) if time_to is not None else None @@ -9592,7 +9647,7 @@ def temporal_ghost(row: Any) -> bool: "AND (visibility_support.expired_at IS NULL " "OR ? bool: ) if not include_history: visibility_sql += ( - "AND visibility_memory.workspace_id=? " - "AND (visibility_memory.valid_from IS NULL " - "OR visibility_memory.valid_from<=?) " - "AND (visibility_memory.valid_to IS NULL " - "OR ? bool: "AND (visibility_edge.expired_at IS NULL " "OR ? bool: "AND (touching_edge.expired_at IS NULL " "OR ? bool: "AND (s.expired_at IS NULL " "OR ? bool: ) if not include_history: touching_sql += ( - "AND touching_memory.workspace_id=? " - "AND (touching_memory.valid_from IS NULL " - "OR touching_memory.valid_from<=?) " - "AND (touching_memory.valid_to IS NULL " - "OR ? bool: entity_params.extend((wid, wid, known_t)) if not include_history: entity_sql += ( - "WHERE ((public_edge.workspace_id=? AND public_edge.src=entity.id) " - "OR (public_edge.workspace_id=? AND public_edge.dst=entity.id)) " - "AND (public_edge.valid_from IS NULL OR public_edge.valid_from<=?) " - "AND (public_edge.valid_to IS NULL OR ? bool: "WHERE any_graph_support.edge_id=edges.id) OR " ) edge_sql += ( - "EXISTS (SELECT 1 FROM edge_supports graph_support " - "JOIN memories graph_memory ON graph_memory.id=graph_support.memory_id " - "WHERE graph_support.edge_id=edges.id " - "AND (graph_support.valid_from IS NULL OR graph_support.valid_from<=?) " - "AND (graph_support.valid_to IS NULL OR ? bool: "OR (historical_edge.valid_to IS NOT NULL " "AND historical_edge.valid_to<=? " "AND (historical_edge.valid_to_recorded_at IS NULL " - "OR historical_edge.valid_to_recorded_at<=?))) " + "OR historical_edge.valid_to_recorded_at<=?)) " + "OR EXISTS (SELECT 1 FROM memory_closures historical_memory_closure " + "WHERE historical_memory_closure.memory_id=memory.id " + "AND historical_memory_closure.valid_to<=? " + "AND (historical_memory_closure.recorded_at IS NULL " + "OR historical_memory_closure.recorded_at<=?))) " # An older version of a currently visible evidence key belongs # to the live arm already. Exclude it before LIMIT so duplicate # keys cannot consume the global historical-support budget. @@ -10460,13 +10489,18 @@ def temporal_ghost(row: Any) -> bool: "AND ? bool: str(row.get("memory_id") or "") for row in support_rows if row.get("memory_id") }) + load_memory_ghosts(support_memory_ids) support_memory_meta: dict[str, dict[str, Any]] = {} for start in range(0, len(support_memory_ids), 500): chunk = support_memory_ids[start:start + 500] @@ -10544,10 +10579,10 @@ def temporal_ghost(row: Any) -> bool: memory_params.extend([known_t, known_t]) else: memory_sql += ( - "AND (valid_to IS NULL OR ? bool: support_memory_meta[str(memory["id"])] = { "memory_type": str(memory["mtype"] or ""), "support_time": float(memory["support_time"] or 0.0), - "ghost": temporal_ghost(memory), + "ghost": memory_ghost(str(memory["id"])), } enriched_supports = [] for support in support_rows: @@ -10579,6 +10614,16 @@ def temporal_ghost(row: Any) -> bool: enriched["support_time"] = metadata["support_time"] enriched["memory_ghost"] = metadata["ghost"] enriched_supports.append(enriched) + if include_history: + support_ghosts: dict[str, list[bool]] = {} + for support in enriched_supports: + support_ghosts.setdefault(str(support["edge_id"]), []).append( + bool(support.get("ghost") or support.get("memory_ghost")) + ) + for edge in edge_rows: + evidence_ghosts = support_ghosts.get(str(edge["id"])) + if evidence_ghosts and all(evidence_ghosts): + edge["ghost"] = True if prune_entities: matching_edge_ids = { str(support.get("edge_id") or "") @@ -10626,8 +10671,9 @@ def temporal_ghost(row: Any) -> bool: memory_where = [ "workspace_id=?", "(valid_from IS NULL OR valid_from<=?)", - "(valid_to IS NULL OR ? bool: count=MAX_GRAPH_COMPLETE_MEMORY_CANDIDATES + 1, limit=MAX_GRAPH_COMPLETE_MEMORY_CANDIDATES, ) + load_memory_ghosts([str(row["id"]) for row in raw_memory_rows]) for row in raw_memory_rows: memory = dict(row) metadata = _loads(memory.pop("metadata", None), {}) @@ -10700,16 +10747,7 @@ def temporal_ghost(row: Any) -> bool: last_memory_id = str(raw_memory_rows[-1]["id"]) if include_history: for memory in memory_rows: - valid_to_value = memory.get("valid_to") - recorded_at = memory.get("valid_to_recorded_at") - try: - memory["ghost"] = bool( - valid_to_value is not None - and float(valid_to_value) <= t - and (recorded_at is None or float(recorded_at) <= known_t) - ) - except (TypeError, ValueError): - memory["ghost"] = False + memory["ghost"] = memory_ghost(str(memory["id"])) if len(memory_rows) > MAX_GRAPH_COMPLETE_MEMORIES: raise GraphSceneCapacityExceeded( resource="memory nodes", count=len(memory_rows), @@ -11225,7 +11263,11 @@ def graph_suggest(self, query: str, *, workspace: str, limit: int = 8, like = f"%{escaped}%" prefix = f"{escaped}%" - def visible_sql(alias: str) -> tuple[str, list[float]]: + def visible_sql(alias: str) -> tuple[str, list[Any]]: + if alias in {"memory", "memories", "suggest_memory"}: + return _memory_temporal_visibility_sql( + alias, SearchFilter(valid_at=suggestion_at, known_at=suggestion_known_at), + ) prefix = f"{alias}." return ( f"({prefix}valid_from IS NULL OR {prefix}valid_from<=?) " @@ -11673,9 +11715,16 @@ def graph_entity(self, canonical_id: str, *, workspace: str, "ON history_memory.id=history_support.memory_id " "WHERE history_support.edge_id=edges.id " "AND history_memory.workspace_id=? " + "AND (history_memory.ingested_at IS NULL OR history_memory.ingested_at<=?) " + "AND (history_memory.expired_at IS NULL OR ? tuple[str, list[float]]: + ) -> tuple[str, list[Any]]: """Parameterized world/system visibility for graph-owned SQL. ``history`` retains world-time closures for the Time view, but never relaxes system time: future ingestion and later expiry stay invisible. """ + if not history and alias in {"visibility_memory", "left_memory", "right_memory"}: + return _memory_temporal_visibility_sql( + alias, SearchFilter(valid_at=world_anchor, known_at=system_anchor), + ) prefix = f"{alias}." if history: world_sql = f"({prefix}valid_from IS NULL OR {prefix}valid_from<=?)" @@ -12297,7 +12347,8 @@ def visible_entities(): ) history_sql = ( "SELECT relation.id, relation.src, relation.dst, relation.relation, " - "relation.layer, relation.valid_from, relation.valid_to " + "relation.layer, relation.valid_from, relation.valid_to, " + "relation.valid_to_recorded_at " "FROM edges relation WHERE relation.workspace_id=? " f"AND {relation_visibility} AND {public_visibility}" ) @@ -12330,6 +12381,58 @@ def visible_entities(): dict(row) for row in conn.execute(history_sql, history_params).fetchall() if row["src"] in node_ids and row["dst"] in node_ids ] + if include_relation_history and edgs: + # The physical edge keeps its first recorded interval. Derive the + # Time-view ending from every source observation known at this anchor; + # a later shortening must not make a closed relation appear live. + support_endings: dict[str, Optional[float]] = {} + for start in range(0, len(edgs), 500): + chunk = [str(edge["id"]) for edge in edgs[start:start + 500]] + marks = ",".join("?" for _ in chunk) + ending_sql = ( + "WITH source_endings AS (SELECT support.edge_id, " + "CASE WHEN support.valid_to_recorded_at IS NULL " + "OR support.valid_to_recorded_at<=? THEN support.valid_to " + "ELSE NULL END AS support_end, " + "(SELECT MIN(closure.valid_to) FROM memory_closures closure " + "WHERE closure.memory_id=memory.id AND (closure.recorded_at IS NULL " + "OR closure.recorded_at<=?)) AS memory_end " + "FROM edge_supports support JOIN memories memory " + "ON memory.id=support.memory_id " + f"WHERE support.edge_id IN ({marks}) " + "AND memory.workspace_id=? " + "AND COALESCE(memory.scope,'workspace')!='session' " + "AND (support.valid_from IS NULL OR support.valid_from<=?) " + "AND (support.ingested_at IS NULL OR support.ingested_at<=?) " + "AND (support.expired_at IS NULL OR ? system_anchor: + physical_end = None + source_end = support_endings.get(str(edge["id"])) + endings = [value for value in (physical_end, source_end) if value is not None] + edge["valid_to"] = min(endings) if endings else None for link in memory_link_fallback: if len(edgs) >= edge_cap: break @@ -13006,6 +13109,7 @@ def _mem_to_dict(rec: Any) -> dict: "subject_key": rec.subject_key, "claim_kind": rec.claim_kind, "valid_from": rec.valid_from, "valid_to": rec.valid_to, "valid_to_recorded_at": rec.valid_to_recorded_at, + "closure_observations": [list(pair) for pair in rec.closure_observations], "ingested_at": rec.ingested_at, "expired_at": rec.expired_at, "provenance": rec.provenance, } diff --git a/engraphis/static/dashboard.js b/engraphis/static/dashboard.js index 03f54e327..7992b2c05 100644 --- a/engraphis/static/dashboard.js +++ b/engraphis/static/dashboard.js @@ -1,1832 +1,1854 @@ -const API=location.origin+'/api'; -let WS=null, WORKSPACES=[], LIC=null, RELEASE_VERSION=''; -const TITLES={overview:'Overview',recall:'Recall',memories:'Memories','mem-editor':'Memory',proactive:'Proactive recall',why:'Why',timeline:'Timeline',audit:'Audit trail',graph:'Knowledge Graph',analytics:'Hosted Analytics',health:'Memory Health',consolidate:'Consolidate',automation:'Hosted Automation',workspaces:'Workspaces',team:'Team Cloud',settings:'Settings'}; -const ROUTE_SECTIONS={overview:'Operate',recall:'Operate',memories:'Operate','mem-editor':'Operate',proactive:'Operate',why:'History',timeline:'History',audit:'History',graph:'Relations',analytics:'Relations',health:'Relations',consolidate:'Engine',automation:'Engine',workspaces:'Operate',team:'Engine',settings:'Engine'}; -/* Per-view subtitle rendered in the topbar next to the view name. The body no longer - repeats the view title/description — the topbar is the single source for both. */ -const DESCS={overview:'',recall:'Hybrid semantic + retention search over this workspace.',memories:'Browse and curate the memories in this workspace.','mem-editor':'',proactive:'What matters right now: importance × recency × retention, plus the last session handoff.',why:'The current answer to a question, with the facts it superseded.',timeline:'Bi-temporal history: what was believed, when it was valid, and when it was recorded.',audit:'Local governance history or content-free, tamper-evident receipts for sharing.',graph:"Explore entities and their sourced relationships from this workspace's memories.",analytics:'Hosted growth, retention, decay, and entity insights for this workspace.',health:'Memory lifecycle metrics: age distribution, decay rates, and staleness.',consolidate:'Run the free local consolidation tool manually; dry-run first to preview changes.',automation:'Hosted maintenance policy: consolidate, dream, and review on a schedule.',workspaces:'Switch between workspaces or create a new one.',team:'Hosted organizations, roles, and seats for Engraphis Cloud.',settings:'Theme, update, and connection settings.'}; -let CURRENT_VIEW='overview'; -/* Loaders that compute a live subtitle (e.g. Overview's counts) call this instead of - writing to a body element, so the topbar stays authoritative. */ -function setViewDesc(v,text){DESCS[v]=text;if(CURRENT_VIEW===v){const s=document.getElementById('topbar-sub');if(s)s.textContent=text}} -/* Plan pills are async: a slow /analytics response must not repaint the topbar after - the user has already navigated elsewhere, so writes are dropped once the view changes. */ -function setPlanPill(el,text,cls){if(!el)return;const owner=el.id==='an-lock'?'analytics':'automation';if(CURRENT_VIEW!==owner)return;el.textContent=text;el.className=cls+' topbar-lock'} -function esc(s){if(s===undefined||s===null)return '';return (''+s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,''')} -/* Scheme-sanitize URLs interpolated into href attributes. esc() entity-encodes but - does not block javascript:/data:/vbscript: URIs; a compromised or misconfigured - license server could otherwise push a crafted upgrade_url that executes script - when clicked. Non-http(s) URLs collapse to '#' so the link stays inert. - Control characters are stripped before the scheme test: the WHATWG URL parser - removes tab/newline/C0 before resolving a scheme, so a split scheme would re-form - as javascript: after a naive scheme match had already failed. A scheme-less string - that still contains ':' therefore fails closed rather than passing through. */ -function safeUrl(u){if(!u||typeof u!=='string')return '#';const s=u.replace(/[\u0000-\u001F\u007F]/g,'').trim();if(/^#/.test(s))return s;if((s[0]==='/'||s[0]==='\\')&&(s[1]==='/'||s[1]==='\\'))return '#';const m=s.match(/^([a-z][a-z0-9+.-]*):/i);if(!m)return /:/.test(s)?'#':s;if(/^(https?|mailto|ftps?)$/i.test(m[1]))return s;return '#'} -function showAs(el,visible,mode){if(!el)return;el.classList.toggle('is-hidden',!visible);for(const name of ['is-flex','is-block','is-inline-flex'])el.classList.remove(name);if(visible&&mode)el.classList.add('is-'+mode)} -function setTone(el,tone){if(!el)return;for(const name of ['tone-red','tone-green','tone-muted'])el.classList.remove(name);if(tone)el.classList.add('tone-'+tone)} -function renderMd(md){try{return DOMPurify.sanitize(marked.parse(md||''))}catch(e){return esc(md)}} -let TOAST_TIMER=null; -function toast(m,t){const e=document.getElementById('toast');const kind=t||'ok';e.textContent=m;e.className='toast toast-'+kind+' show';e.setAttribute('role',kind==='err'?'alert':'status');e.setAttribute('aria-live',kind==='err'?'assertive':'polite');clearTimeout(TOAST_TIMER);TOAST_TIMER=setTimeout(()=>e.classList.remove('show'),3200)} -function fmtRel(ts){if(!ts)return '';if(typeof ts==='string'){const parsed=Date.parse(ts);if(!Number.isFinite(parsed))return '';ts=parsed/1000}const s=Math.max(0,Date.now()/1000-ts);if(s<60)return 'just now';if(s<3600)return Math.floor(s/60)+'m ago';if(s<86400)return Math.floor(s/3600)+'h ago';if(s<2592000)return Math.floor(s/86400)+'d ago';return new Date(ts*1000).toISOString().slice(0,10)} -async function api(p,o){o=o||{};o={...o,headers:{...(o.headers||{}),'X-Engraphis-Browser-Session':'1'}};const r=await fetch(p.startsWith('http')?p:API+p,o);const txt=await r.text();let d=null;try{d=txt?JSON.parse(txt):null}catch(e){} - if(!r.ok){let msg=(d&&d.detail&&(d.detail.error||d.detail))||(d&&d.error)||(''+r.status);if(typeof msg!=='string')msg=JSON.stringify(msg);const err=new Error(msg);err.status=r.status;err.detail=d&&d.detail;throw err}return d} -async function authenticateBrowser(){let token='';try{const fragment=new URLSearchParams(location.hash.slice(1));token=fragment.get('token')||'';if(token)history.replaceState(null,'',location.pathname+location.search)}catch(e){}if(!token)token=window.prompt('Enter this deployment’s ENGRAPHIS_API_TOKEN:')||'';if(!token)return false;try{await api('/auth/session',{method:'POST',body:JSON.stringify({token}),headers:{'Content-Type':'application/json'}});token='';return true}catch(e){token='';toast('Authentication failed: '+e.message,'err');return false}} -let ACTION_RESOLVE=null,ACTION_SPEC=null; -function actionDialog(spec){if(ACTION_RESOLVE)closeActionDialog(null);ACTION_SPEC=spec||{};document.getElementById('action-title').textContent=ACTION_SPEC.title||'Confirm action';document.getElementById('action-message').textContent=ACTION_SPEC.message||'';document.getElementById('action-error').textContent='';const fields=document.getElementById('action-fields');fields.replaceChildren();(ACTION_SPEC.fields||[]).forEach((f,i)=>{const wrap=document.createElement('div');wrap.className='field';const label=document.createElement('label');label.className='field-lbl';label.htmlFor='action-field-'+i;label.textContent=f.label||'Value';const input=f.options?document.createElement('select'):document.createElement(f.multiline?'textarea':'input');input.className=f.options?'select':'input';input.id='action-field-'+i;input.dataset.name=f.name||('field'+i);if(f.type==='password'){input.type='password';input.autocomplete='off'}else if(input.tagName==='INPUT'){input.type=f.type||'text'}if(f.placeholder)input.placeholder=f.placeholder;if(f.value!=null)input.value=f.value;if(f.required)input.required=true;(f.options||[]).forEach(opt=>{const option=document.createElement('option');option.value=opt.value==null?opt:opt.value;option.textContent=opt.label==null?opt:opt.label;input.appendChild(option)});wrap.append(label,input);fields.appendChild(wrap)});const submit=document.getElementById('action-submit');submit.textContent=ACTION_SPEC.submit||'Continue';submit.className='btn '+(ACTION_SPEC.danger?'btn-danger':'btn-primary');document.getElementById('action-overlay').classList.add('show');return new Promise(resolve=>{ACTION_RESOLVE=resolve})} -function closeActionDialog(value){const resolve=ACTION_RESOLVE;ACTION_RESOLVE=null;ACTION_SPEC=null;document.getElementById('action-overlay').classList.remove('show');if(resolve)resolve(value)} -function submitActionDialog(){const values={};for(const input of document.querySelectorAll('#action-fields input,#action-fields select,#action-fields textarea')){if(input.required&&!input.value.trim()){document.getElementById('action-error').textContent='Complete all required fields.';input.focus();return}values[input.dataset.name]=input.value}closeActionDialog(values)} -async function confirmAction(title,message,submit,danger){const result=await actionDialog({title,message,submit:submit||'Continue',danger:!!danger});return result!==null} -async function textAction(title,message,label,value,options){const result=await actionDialog({title,message,submit:(options&&options.submit)||'Save',danger:!!(options&&options.danger),fields:[{name:'value',label,value:value||'',required:!(options&&options.optional),multiline:!!(options&&options.multiline),options:(options&&options.options)||[]} ]});return result===null?null:result.value} -document.getElementById('action-close').addEventListener('click',()=>closeActionDialog(null)); -document.getElementById('action-cancel').addEventListener('click',()=>closeActionDialog(null)); -document.getElementById('action-submit').addEventListener('click',submitActionDialog); -document.getElementById('action-overlay').addEventListener('click',event=>{if(event.target===event.currentTarget)closeActionDialog(null)}); -let DIALOG_ACTIVE=null; -const DIALOG_RETURN=new WeakMap(); -function toggleMobileNav(force){const app=document.querySelector('.app'),btn=document.getElementById('mobile-nav-toggle'),side=document.getElementById('app-sidebar'),main=document.getElementById('main-content');if(!app||!btn||!side||!main)return;const mobile=matchMedia('(max-width:768px)').matches;const open=mobile&&(force===undefined?!app.classList.contains('mobile-nav-open'):!!force);app.classList.toggle('mobile-nav-open',open);btn.setAttribute('aria-expanded',String(open));btn.setAttribute('aria-label',open?'Close navigation':'Open navigation');side.inert=mobile&&!open;side.setAttribute('aria-hidden',String(mobile&&!open));main.inert=mobile&&open;if(open){const active=document.querySelector('.nav-item.active');setTimeout(()=>{if(active)active.focus()},0)}} -function closeMobileNav(returnFocus){toggleMobileNav(false);if(returnFocus)setTimeout(()=>document.getElementById('mobile-nav-toggle').focus(),0)} -function syncMobileNavMode(){toggleMobileNav(document.querySelector('.app').classList.contains('mobile-nav-open'))} -window.addEventListener('resize',syncMobileNavMode); -syncMobileNavMode(); -function dialogChanged(ov){const open=ov.classList.contains('show');ov.setAttribute('aria-hidden',String(!open));if(open){if(DIALOG_ACTIVE!==ov){DIALOG_RETURN.set(ov,document.activeElement);DIALOG_ACTIVE=ov;setTimeout(()=>{const first=ov.querySelector('.mm-body input:not([disabled]),.mm-body select:not([disabled]),.mm-body textarea:not([disabled])')||ov.querySelector('button:not([disabled]),[href],[tabindex="0"]');if(first)first.focus()},0)}}else if(DIALOG_ACTIVE===ov){DIALOG_ACTIVE=null;const back=DIALOG_RETURN.get(ov);if(back&&document.contains(back)&&back!==document.body)back.focus();else{const heading=document.getElementById('topbar-title');if(heading){heading.tabIndex=-1;heading.focus()}}}} -function trapDialog(e){const ov=DIALOG_ACTIVE;if(!ov||e.key!=='Tab')return;const els=Array.from(ov.querySelectorAll('input:not([disabled]),select:not([disabled]),textarea:not([disabled]),button:not([disabled]),[href],[tabindex="0"]')).filter(x=>x.offsetParent!==null);if(!els.length){e.preventDefault();return}const first=els[0],last=els[els.length-1];if(e.shiftKey&&document.activeElement===first){e.preventDefault();last.focus()}else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();first.focus()}} -function ensureDialogFocus(){if(!DIALOG_ACTIVE||DIALOG_ACTIVE.contains(document.activeElement))return;requestAnimationFrame(()=>{if(!DIALOG_ACTIVE)return;const first=DIALOG_ACTIVE.querySelector('.mm-body input:not([disabled]),.mm-body select:not([disabled]),.mm-body textarea:not([disabled])')||DIALOG_ACTIVE.querySelector('button:not([disabled]),[href],[tabindex="0"]');if(first)first.focus()})} -function controlName(el){const named={recall_q:'Recall query',recall_k:'Number of recall results',mem_q:'Memory filter',why_q:'Question to explain',tl_q:'Timeline topic',graph_repo_filter:'Repository filter',graph_search:'Find graph entity',import_path:'Local import path',import_pattern:'File pattern',code_repo:'Repository name',code_root:'Repository path',postgres_dsn:'PostgreSQL DSN',postgres_repo:'PostgreSQL repository scope'};return named[(el.id||'').replace(/-/g,'_')]||(el.placeholder||'').replace(/[…*]+$/,'').trim()||(el.id||el.type||'control').replace(/[-_]+/g,' ')} -function enhanceUi(root){const scope=root&&root.querySelectorAll?root:document;scope.querySelectorAll('.nav-item').forEach(el=>{el.setAttribute('role','link');el.tabIndex=0;el.setAttribute('aria-current',el.classList.contains('active')?'page':'false')});scope.querySelectorAll('button:not([type])').forEach(el=>el.type='button');scope.querySelectorAll('input,select,textarea').forEach((el,i)=>{if(el.labels&&el.labels.length)return;const field=el.closest('.field'),slider=el.closest('.gslider');const label=(field&&field.querySelector('label'))||(slider&&slider.querySelector('label'));if(label){if(!el.id)el.id='ui-control-'+i+'-'+Date.now();label.htmlFor=el.id}else if(!el.getAttribute('aria-label')&&!el.getAttribute('aria-labelledby'))el.setAttribute('aria-label',controlName(el))});scope.querySelectorAll('.recall-card,.gtop-row,.ep-edge,.mem-card').forEach(el=>{if(!el.hasAttribute('tabindex'))el.tabIndex=0;if(!el.hasAttribute('role'))el.setAttribute('role','button');if(!el.getAttribute('aria-label')){const txt=el.querySelector('.recall-title,.gtop-name,.mem-card-title')||el;el.setAttribute('aria-label',(txt.textContent||'Open item').trim())}});scope.querySelectorAll('.vault-card').forEach(el=>{el.setAttribute('role','group');const name=el.querySelector('.vault-card-name');if(name&&!el.getAttribute('aria-label'))el.setAttribute('aria-label','Workspace '+name.textContent.trim())});scope.querySelectorAll('.spinner').forEach(el=>{el.setAttribute('role','status');el.setAttribute('aria-label','Loading')});document.querySelectorAll('.status-region').forEach(el=>{const busy=!!el.querySelector('.spinner');el.setAttribute('aria-busy',String(busy));const text=(el.textContent||'').toLowerCase();el.setAttribute('role',/\b(error|failed|invalid|unavailable|offline)\b/.test(text)?'alert':'status')});document.querySelectorAll('.mm-overlay').forEach(dialogChanged)} -const enhanceUiBase=enhanceUi; -enhanceUi=function(root){ - enhanceUiBase(root); - const scope=root&&root.querySelectorAll?root:document; - scope.querySelectorAll('.nav-item').forEach(el=>{ - if(el.tagName==='BUTTON')el.removeAttribute('role'); - else{el.setAttribute('role','button');el.tabIndex=0} - }); -}; -function enhanceDynamicUi(){document.querySelectorAll('.vault-card-name,.tl-item.clickable,#mm-body>div[data-onclick]').forEach(el=>{el.setAttribute('role','button');el.tabIndex=0});document.querySelectorAll('.mem-card').forEach(el=>el.setAttribute('aria-describedby','memory-reorder-help'));document.querySelectorAll('.card-head:not(h1):not(h2):not(h3)').forEach(el=>{el.setAttribute('role','heading');el.setAttribute('aria-level','2')});['sync-status','llm-test-result','au-result','tok-created'].forEach(id=>{const el=document.getElementById(id);if(el){el.setAttribute('role','status');el.setAttribute('aria-live','polite')}})} -function enhanceAdjacentLabels(){document.querySelectorAll('input,select,textarea').forEach((el,i)=>{if(el.labels&&el.labels.length)return;const direct=el.previousElementSibling,parent=el.parentElement&&el.parentElement.previousElementSibling,label=(direct&&direct.matches('label')&&direct)||(parent&&parent.matches('label')&&parent);if(!label)return;if(!el.id)el.id='ui-adjacent-'+i+'-'+Date.now();label.htmlFor=el.id;el.removeAttribute('aria-label')})} -function enhanceDecorativeIcons(){document.querySelectorAll('.nav-icon,.brand-mark,.dropzone-icon,.empty-icon').forEach(el=>el.setAttribute('aria-hidden','true'))} -function enhanceExplorerSemantics(){document.querySelectorAll('#graph-entity-list [role="listitem"],#graph-relation-list [role="listitem"]').forEach(el=>el.removeAttribute('role'))} -function enhanceStatusContainers(){document.querySelectorAll('.status-region,#ov-analytics,#ed-history,#sync-body,#lic-body,#tokens-body,#llm-body,#au-result').forEach(el=>{const visible=el.getClientRects().length>0;el.setAttribute('aria-live','polite');el.setAttribute('aria-busy',String(visible&&(!!el.querySelector('.spinner')||/^\s*Loading/.test(el.textContent||''))))})} -// Coalesce the enhancement sweeps to at most one per frame. Each one re-queries the -// whole document, and the observer fires on every keystroke in graph search and every -// drag-over boundary crossing, so running them per mutation batch was quadratic-ish on -// exactly the interactions that need to stay responsive. Dialog bookkeeping stays -// immediate — it drives focus, which cannot wait a frame. -let UI_SWEEP=0; -function scheduleUiSweep(){if(UI_SWEEP)return;UI_SWEEP=requestAnimationFrame(()=>{UI_SWEEP=0;enhanceUi(document);enhanceAdjacentLabels();enhanceDynamicUi();enhanceDecorativeIcons();enhanceStatusContainers();enhanceExplorerSemantics();ensureDialogFocus()})} -const UI_OBSERVER=new MutationObserver(records=>{records.forEach(r=>{if(r.type==='attributes'&&r.target.classList.contains('mm-overlay'))dialogChanged(r.target)});scheduleUiSweep()}); -enhanceUi(document); -enhanceAdjacentLabels(); -enhanceDynamicUi(); -enhanceDecorativeIcons(); -enhanceStatusContainers(); -enhanceExplorerSemantics(); -document.querySelectorAll('.view').forEach(el=>el.setAttribute('aria-hidden',String(!el.classList.contains('active')))); -UI_OBSERVER.observe(document.body,{subtree:true,childList:true,attributes:true,attributeFilter:['class']}); -document.addEventListener('keydown',e=>{trapDialog(e);const t=e.target;if(t.matches('.mem-card')){if(e.altKey&&(e.key==='ArrowUp'||e.key==='ArrowDown')){e.preventDefault();memKeyboardMove(t.dataset.id,e.key==='ArrowUp'?-1:1);return}if(e.key==='Enter'||e.key===' '){e.preventDefault();openMem(t.dataset.id);return}}if((e.key==='Enter'||e.key===' ')&&t.matches('[role=button],.nav-item')){if(e.key===' '||t.matches('.nav-item'))e.preventDefault();t.click()}}); - -/* theme */ -const THEMES=[['dark','Dark','#15181e','#8c83e8'],['light','Light','#fbfbfc','#5547b8'],['midnight','Midnight','#111b2d','#79a6ef'],['solarized','Solarized','#073642','#58a7d8'],['sepia','Sepia','#f8f2e4','#925420'],['matrix','Matrix','#000403','#3ce072']]; -const THEME_GLYPH={dark:'◑',light:'☀',midnight:'☾',solarized:'◐',sepia:'❂',matrix:'▦'}; -function applyTheme(t){if(!THEME_GLYPH[t])t='dark';document.body.setAttribute('data-theme',t);window.__theme=t;try{localStorage.setItem('engraphis-theme',t)}catch(e){}const b=document.getElementById('theme-btn');if(b){b.textContent=THEME_GLYPH[t];b.setAttribute('aria-label','Change theme. Current theme: '+t)}const sel=document.getElementById('theme-select');if(sel&&sel.value!==t)sel.value=t;renderThemeMenu();if(typeof graphRecolor==='function')graphRecolor()} -function renderThemeMenu(){const m=document.getElementById('theme-menu');if(!m)return;m.innerHTML=THEMES.map(x=>{const on=(x[0]===window.__theme);return ``}).join('')} -function toggleThemeMenu(e){if(e)e.stopPropagation();const m=document.getElementById('theme-menu'),b=document.getElementById('theme-btn');if(!m)return;const show=!m.classList.contains('is-open');m.classList.toggle('is-open',show);if(b)b.setAttribute('aria-expanded',String(show));if(show){renderThemeMenu();setTimeout(()=>{const first=m.querySelector('button');if(first)first.focus();document.addEventListener('click',closeThemeMenu)},0)}} -function closeThemeMenu(){const m=document.getElementById('theme-menu'),b=document.getElementById('theme-btn');if(m)m.classList.remove('is-open');if(b)b.setAttribute('aria-expanded','false');document.removeEventListener('click',closeThemeMenu)} -function pickTheme(t){applyTheme(t);closeThemeMenu()} -function toggleTheme(){const ids=THEMES.map(x=>x[0]);const i=ids.indexOf(window.__theme);pickTheme(ids[(i+1)%ids.length])} -function initTheme(){let saved=null;try{saved=localStorage.getItem('engraphis-theme')}catch(e){}applyTheme(saved||window.__theme||'dark')} -function selectDashboard(value){window.location.assign(value==='classic'?'/classic':'/')} -function initDashboard(){const select=document.getElementById('dashboard-select');if(select)select.value=location.pathname==='/classic'?'classic':'primary'} - -/* nav */ -function selectView(v){ - document.querySelectorAll('.nav-item').forEach(n=>{const active=n.dataset.view===v;n.classList.toggle('active',active);n.setAttribute('aria-current',active?'page':'false')}); - document.querySelectorAll('.view').forEach(el=>{const active=el.id==='view-'+v;el.classList.toggle('active',active);el.setAttribute('aria-hidden',String(!active))}); - CURRENT_VIEW=v; - const heading=document.getElementById('topbar-title'); - heading.textContent=TITLES[v]||v; - const section=document.getElementById('route-section'); - if(section)section.textContent=ROUTE_SECTIONS[v]||'Operate'; - const sub=document.getElementById('topbar-sub'); - if(sub)sub.textContent=DESCS[v]||''; - /* Plan pills live in the topbar; clear them on navigation so only the active - view's loader can repopulate one. */ - ['an-lock','au-lock'].forEach(id=>{const p=document.getElementById(id);if(p){p.textContent='';p.className='pill pill-muted topbar-lock'}}); - if(v==='graph')graphEngineResume();else graphEnginePause(); - closeMobileNav(); - (LOADERS[v]||function(){})(); - heading.tabIndex=-1; - setTimeout(()=>heading.focus(),0); -} -function navTo(v){selectView(v)} -document.querySelectorAll('.nav-item').forEach(it=>it.addEventListener('click',()=>selectView(it.dataset.view))); - -/* workspace */ -function setWS(name){ - WS=name; - const shown=name||'—',sw=document.getElementById('vault-switcher'),top=document.getElementById('topbar-workspace'); - document.getElementById('ws-name').textContent=shown; - if(top)top.textContent=shown; - if(sw)sw.setAttribute('aria-label',name?'Choose active workspace. Current workspace: '+name:'Choose active workspace'); -} -async function loadWorkspaceList(){const d=await api('/workspaces');WORKSPACES=d.workspaces||[];if(!WS&&WORKSPACES.length){WORKSPACES.sort((a,b)=>(b.memories||0)-(a.memories||0));setWS(WORKSPACES[0].name)}} - -/* overview */ -function formatTokenCount(value){return Math.max(0,Math.round(Number(value)||0)).toLocaleString()} -async function loadOverview(){ - try{ - const st=await api('/stats?workspace='+encodeURIComponent(WS||'')); - setViewDesc('overview',(st.memories||0)+' memories · '+(st.workspaces||0)+' workspaces'); - const cards=[['Memories',st.memories],['Live rows',st.total_rows],['Workspaces',st.workspaces],['Sessions',st.sessions]]; - document.getElementById('stat-grid').innerHTML=cards.map(c=>`
${c[1]!=null?c[1]:'—'}
${c[0]}
`).join(''); - document.getElementById('nav-mem-count').textContent=st.memories||''; - const bt=st.by_type||{},tot=Object.values(bt).reduce((a,b)=>a+b,0)||1; - document.getElementById('ov-types').innerHTML=Object.keys(bt).length?Object.entries(bt).map(([k,v])=>`
${esc(k)}
${v}
`).join(''):'
No memories
'; - loadOverviewAnalytics(); - }catch(e){ - const msg='Overview unavailable: '+e.message; - setViewDesc('overview',msg); - document.getElementById('stat-grid').innerHTML='
'+esc(msg)+'
'; - document.getElementById('ov-types').innerHTML='
Memory types could not be loaded.
'; - document.getElementById('ov-analytics').innerHTML='
Analytics could not be loaded.
'; - toast(msg,'err'); - } -} -async function loadOverviewAnalytics(){ - const el=document.getElementById('ov-analytics'),lock=document.getElementById('ov-lock'); - try{ - const a=await api('/analytics?workspace='+encodeURIComponent(WS||'')); - lock.textContent=licTrialActive()?'TRIAL':''; - lock.className='pill pill-muted'; - const t=a.totals||{},f=a.decay_forecast||{}; - if(t.live==null){ - const nd=a.namespace_distribution||[],mx=Math.max(...nd.map(x=>x.count),1); - el.innerHTML='
Memories by workspace
'+nd.map(x=>barRow(x.namespace,x.count,mx,'var(--accent-dim)')).join(''); - return; - } - const weeks=a.growth_weekly||[],thisWeek=weeks.length?weeks[weeks.length-1]:0,avg=Math.round((t.avg_retention||0)*100); - const cell=(v,l,c)=>{const tone=c==='var(--red)'?' tone-red':(c==='var(--amber)'?' tone-amber':(c==='var(--green)'?' tone-green':''));return `
${v}
${l}
`}; - el.innerHTML=`
${cell(avg+'%','Avg retention',avg<40?'var(--red)':(avg<70?'var(--amber)':'var(--green)'))}${cell(f.at_risk_7d||0,'Fading ≤ 7 days',f.at_risk_7d>0?'var(--amber)':'')}${cell(thisWeek,'Written this week')}${cell(t.pinned||0,'Pinned')}
`; - }catch(e){ - // Same routing as loadAnalytics: consent first, entitlement second, everything else - // reported as the error it is. See hostedFeatureUnavailable for why 409 is not an - // entitlement status. - if(managedConsentRequired(e)){ - lock.textContent='CLOUD'; - lock.className='pill pill-accent'; - el.innerHTML=managedConsentHtml('Analytics'); - }else if(hostedFeatureUnavailable(e)){ - lock.textContent='PRO'; - lock.className='pill pill-muted'; - const offerTrial=licTrialAvailable(); - el.innerHTML='
Hosted growth, retention distribution, and decay forecast.
'+esc(lockReason(false))+'
'+(offerTrial?' ':'')+'
'; - }else el.innerHTML='
'+esc(e.message)+'
'; - } -} - -/* ── hosted access state ── - /api/license reports what the control plane said, never what this client guessed: - access_state is one of active | trial | trial_expired | lapsed | inactive, and each one - is a different thing to tell the customer. Reading it here is what stopped a customer - whose trial had ended, and a customer whose card had failed, both being shown a - confident PRO badge over rows of locks with no reason given. */ -function licAccessState(){const s=LIC&&LIC.access_state;return s==='active'||s==='trial'||s==='trial_expired'||s==='lapsed'?s:'inactive'} -function licAccessLive(){const s=licAccessState();return s==='active'||s==='trial'} -function licTrialActive(){return licAccessState()==='trial'} -/* The server refuses a second trial for any organization that already holds an - entitlement, so this is false for every connected customer — trialling, lapsed, or - paying — and the CTA that could only ever return 409 is not drawn. */ -function licTrialAvailable(){return !!(LIC&&LIC.trial&&LIC.trial.available)} -function licPlanName(){return licPlanKey()?licPlanKey().toUpperCase():'LOCAL'} -/* The plan the customer actually holds, as the wire spells it: '' when they hold none. - Renewal and billing actions must follow this, not a hardcoded 'pro'. */ -function licPlanKey(){const raw=String((LIC&&LIC.plan)||'local').toLowerCase();return raw==='pro'||raw==='team'?raw:''} -function licTrialEnds(){return fmtDay(LIC&&LIC.trial&&LIC.trial.ends_at)} -function fmtDay(epoch){const n=Number(epoch)||0;if(!(n>0))return '';try{const d=new Date(n*1000);return Number.isFinite(d.getTime())?d.toISOString().slice(0,10):''}catch(e){return ''}} - -/* ── shared hosted upgrade / trial CTA ── */ -/* The plan-neutral hosted entry point. An account portal is not a checkout for either - plan, so it must not carry the ?plan= that would reframe it as one — and it cannot be - LIC.upgrade_url either: that is licensing.upgrade_url(), which resolves plan="pro" and - prefers ENGRAPHIS_PRO_UPGRADE_URL, so where the portal and the checkout are configured - separately it is the Pro checkout under a neutral name. LIC.account_url resolves the - generic value directly; the fallback only matters against a build that predates it. */ -function withCtaAttribution(raw,content,medium){const safe=safeUrl(raw);if(!safe||safe==='#')return '#';try{const url=new URL(safe,location.href);url.searchParams.set('utm_source','engraphis');url.searchParams.set('utm_medium',medium||'product');url.searchParams.set('utm_campaign','pro_conversion');url.searchParams.set('utm_content',content||'plans');return url.href}catch(e){return safe}} -function hostedAccountUrl(content){return withCtaAttribution((LIC&&LIC.account_url)||(LIC&&LIC.upgrade_url),content||'account','product')} -function hostedPlanUrl(plan,trial,interval,content){const cadence=interval==='annual'?'annual':'monthly',key=plan+'_'+cadence+'_upgrade_url',raw=(LIC&&(LIC[key]||(plan==='team'?LIC.team_upgrade_url:LIC.pro_upgrade_url)))||(LIC&&LIC.upgrade_url);const safe=safeUrl(raw);if(!safe||safe==='#')return '#';try{const url=new URL(safe,location.href);if(plan==='pro'||plan==='team')url.searchParams.set('plan',plan);url.searchParams.set('interval',cadence);if(!url.hash)url.hash='billing';if(trial)url.searchParams.set('trial',plan);return withCtaAttribution(url.href,content||plan,'product')}catch(e){return safe}} -/* Why is this feature locked? One sentence per access state, so the panel never claims a - trial the customer cannot start nor blames billing for a trial that simply ran out. - This is DENIAL copy: every caller reaches it because a hosted request was refused. The - one surface that is not a denial — the Team tab, which renders for everyone on every - visit — must go through teamTeaserNote() instead. */ -function lockReason(team){const st=licAccessState(),ends=licTrialEnds(); - if(st==='trial')return `Your free trial is live${ends?` until ${esc(ends)}`:''}. ${team?'Team':'Pro'} needs a subscription of its own.`; - if(st==='trial_expired')return `Your free trial has ended${ends?` (${esc(ends)})`:''}, so hosted features are locked. The trial cannot be started again.`; - if(st==='lapsed')return `Your ${esc(licPlanName())} subscription is no longer active, so hosted features are locked until billing is up to date.`; - if(st==='active')return `Your ${esc(licPlanName())} subscription does not include this.`; - if(licTrialAvailable()){const days=licTrialDays(team?'team':'pro');return days?`The email-confirmed, no-card trial lasts exactly ${days} active days.`:'An email-confirmed, no-card trial is available; review its duration in Cloud.'} - return 'Your free trial has already been used.'} -/* The Team tab describes the hosted service; it is not an answer to a refused request, and - it renders for every customer including the ones who are paying for Team. Handing it - lockReason(true) told a live Team subscriber "Your TEAM subscription does not include - this" directly above an unlocked Team nav item and an Open Team Cloud button. A customer - whose live plan already grants Team gets the truth; everyone else still gets the denial - copy, which for them is accurate. */ -function teamTeaserNote(){const ends=licTrialEnds(); - if(licPlanKey()!=='team'||!licAccessLive())return lockReason(true); - if(licAccessState()==='trial')return `Your free trial includes Team${ends?` until ${esc(ends)}`:''}. Organizations, roles, and seats are managed in Engraphis Cloud.`; - return 'Your TEAM subscription includes this. Organizations, roles, and seats are managed in Engraphis Cloud.'} -function licTrialDays(plan){const trial=(LIC&&LIC.trial)||{},days=(trial.days_by_plan||{})[plan]??(plan==='pro'?trial.trial_days:null);return Number.isSafeInteger(days)&&days>0?days:null} -function hostedCta(plan,content,interval){const team=plan==='team',name=team?'Team':'Pro',state=licAccessState(),current=licPlanKey();if(state==='lapsed')return {label:'Update billing',href:hostedAccountUrl(content||'account'),kind:'account'};if(licAccessLive()&&(current===plan||(current==='team'&&plan==='pro')))return {label:current==='team'&&team?'Open Team Cloud':'Open Engraphis Cloud',href:hostedAccountUrl(content||'account'),kind:'account'};const trial=licTrialAvailable()&&state==='inactive',days=licTrialDays(plan);return {label:trial?`Start ${days?`${days}-day `:''}${name} trial`:`Subscribe to ${name}`,href:hostedPlanUrl(plan,trial,interval||'monthly',content||plan),kind:trial?'trial':'subscribe'} } -function ctaLinkHtml(cta,className,content){return `${esc(cta.label)}`} -function unlockHtml(feature,plan){const team=plan==='team',name=team?'Team':'Pro',featureKey=`feature_${String(feature).toLowerCase().replace(/[^a-z0-9]+/g,'_')}`,primary=hostedCta(plan,featureKey),annual=primary.kind==='account'?'':{label:`Annual ${name} option`,href:hostedPlanUrl(plan,false,'annual',`${featureKey}_annual`),kind:'subscribe'},price=team?'$20 per seat/month or $200 per seat/year':'$10/month or $100/year',detail=lockReason(team),benefits=team?['Everything in Pro','Hosted organizations, invitations, and named seats','Roles, scoped credentials, and Team audit history']:['Hosted Cloud Sync across your installations','Growth, retention, decay, and entity Analytics','Auto Consolidation with hosted retention policies','Auto Dreaming with reviewable managed proposals','Priority support'],lede=team?'Team adds shared workspaces, named seats, roles, and remote agent access.':'Support continued Engraphis development with Pro. Your subscription helps cover hosted infrastructure and ongoing development while unlocking Cloud Sync, Analytics, Auto Consolidation, and Auto Dreaming across your installations.';return `
ENGRAPHIS ${name.toUpperCase()}

Unlock ${esc(feature)} and more

${lede}

${price}
Your license unlocks
    ${benefits.map(item=>`
  • ${esc(item)}
  • `).join('')}

${detail}

${ctaLinkHtml(primary,'btn btn-primary',name.toLowerCase())}${annual.href&&annual.href!=='#'?ctaLinkHtml(annual,'btn btn-ghost',`${featureKey}_annual`):''}
`} -function startTrialPlan(plan){const url=hostedPlanUrl(plan,true);if(url==='#'){toast('Hosted signup URL is not configured','err');return}const link=document.createElement('a');link.href=url;link.target='_blank';link.rel='noopener';link.click()} -function startTrial(){return startTrialPlan('pro')} -function startTeamTrial(){return startTrialPlan('team')} -/* The badge follows the access state, not the plan name. A plan name alone told a trialist - they were a subscriber, and told a lapsed or expired customer nothing was wrong. */ -function updateLicBadge(){/* The side-menu header intentionally has no plan or upgrade badge. */} -function updateFeatureLocks(){ - const has=f=>LIC&&(LIC.features||[]).includes(f); - const apply=(id,feature,label,plan)=>{ - const badge=document.getElementById(id),item=badge&&badge.closest('.nav-item'),locked=!has(feature); - if(badge)badge.textContent=locked?plan:''; - if(item){ - item.setAttribute('aria-label',locked?`${label} — ${plan} plan; opens upgrade options`:label); - item.title=locked?`${plan} plan required; open for trial and upgrade options`:''; - } - }; - apply('nav-analytics-lock','analytics','Analytics','PRO'); - apply('nav-automation-lock','automation','Automation','PRO'); - apply('nav-team-lock','team','Team','TEAM'); -} - -/* ── analytics (Pro) ── */ -function barRow(label,val,peak,color){const tone=color==='var(--green)'?' analytics-bar-green':(color==='var(--blue)'?' analytics-bar-blue':(color==='var(--cyan)'?' analytics-bar-cyan':(color==='var(--accent-dim)'?' analytics-bar-dim':'')));return `
${esc(label)}
${val}
`} -function statMini(v,l,color){const tone=color==='var(--red)'?' tone-red':(color==='var(--amber)'?' tone-amber':(color==='var(--green)'?' tone-green':''));return `
${v}
${esc(l)}
`} -function renderAnalytics(a,isPortfolio){const t=a.totals||{},f=a.decay_forecast||{};const weeks=a.growth_weekly||[];const gp=Math.max(...weeks,1);const gitems=weeks.map((n,i)=>{const back=weeks.length-1-i;return barRow(back===0?'now':back+'w ago',n,gp,'var(--accent-dim)')}).join('')||'
No data
';const hist=a.retention_histogram||{};const hc=hist.counts||[],hb=hist.buckets||[];const hp=Math.max(...hc,1);const hitems=hb.map((b,i)=>barRow(b,hc[i]||0,hp,'var(--green)')).join('');const mix=a.resolver_mix||{};const mk=Object.keys(mix);const mp=Math.max(...Object.values(mix),1);const mitems=mk.length?mk.map(k=>barRow(k,mix[k],mp,'var(--blue)')).join(''):'
No resolver events yet.
';const bt=a.by_type||{};const btk=Object.keys(bt);const bp=Math.max(...Object.values(bt),1);const btitems=btk.length?btk.map(k=>barRow(k,bt[k],bp,'var(--accent)')).join(''):'
No memories yet.
';const ents=a.top_entities||[];const ep=Math.max(...ents.map(e=>e.n),1);const eitems=ents.length?ents.map(e=>barRow(e.name+(isPortfolio&&e.workspace?' · '+e.workspace:''),e.n,ep,'var(--cyan)')).join(''):'
No entities yet — they appear as the graph grows.
';const avg=Math.round((t.avg_retention||0)*100);let wsTable='';if(isPortfolio&&a.workspaces){wsTable=`
Per-workspace breakdown
${a.workspaces.map(w=>``).join('')}
WorkspaceLivePinnedAvg ret.Fading 7d
${esc(w.workspace)}${w.live}${w.pinned}${Math.round((w.avg_retention||0)*100)}%${w.at_risk_7d}
`}return `
${statMini(t.live!=null?t.live:'—','Live memories')}${statMini(avg+'%','Avg retention',avg<40?'var(--red)':(avg<70?'var(--amber)':'var(--green)'))}${statMini(f.at_risk_7d!=null?f.at_risk_7d:'—','Fading ≤ 7 days',f.at_risk_7d>0?'var(--amber)':'')}${statMini(f.at_risk_30d!=null?f.at_risk_30d:'—','Fading ≤ 30 days')}${statMini(t.pinned!=null?t.pinned:'—','Pinned (protected)')}${isPortfolio?statMini(t.workspaces||0,'Workspaces'):statMini(t.superseded!=null?t.superseded:'—','Superseded (history)')}
Memories written per week
${gitems}
Retention distribution
${hitems}
By type
${btitems}
Write-path resolver activity
${mitems}
Most connected entities
${eitems}
${wsTable}`} -/* A consent-required response is a valuable moment to show the job Pro can do, not a - dead end about configuration. A customer with live access must never be offered their - own plan again. Readable processing requires a separate workspace choice. */ -function managedConsentHtml(feature){const automation=/automation/i.test(feature),featureKey=`managed_${String(feature).toLowerCase().replace(/[^a-z0-9]+/g,'_')}`,live=licAccessLive(),trial=licTrialAvailable(),copy=automation?{eyebrow:'MEMORY MAINTENANCE',title:'Let your memory improve after you log off.',lede:'Turn repetitive cleanup into a steady, reviewable habit. Pro watches the rhythm of your workspace and brings the useful changes back for approval.',cards:[['CONSOLIDATE','Distill recurring work into durable knowledge on a cadence you control.'],['DREAM','Surface useful links after accumulation and idle time, before fresh context gets buried.'],['REVIEW','Every managed result is a proposal. Nothing silently rewrites your local memory.']]}:{eyebrow:'MEMORY INTELLIGENCE',title:'See the memory your team is about to lose.',lede:'Pro turns your local memory into an operating signal—so you can see what is growing, what is fading, and what is quietly shaping recall.',cards:[['GROWTH','Separate knowledge that compounds from activity that only accumulates.'],['RETENTION','Catch fading context before an important answer disappears from reach.'],['ENTITY SIGNAL','See the people, projects, and ideas organizing your workspace.']]};const primary=hostedCta('pro',featureKey),annual=primary.kind==='account'?'':{label:'Annual Pro option',href:hostedPlanUrl('pro',false,'annual',`${featureKey}_annual`),kind:'subscribe'},settings=`Review workspace processing`,actions=`${settings}${ctaLinkHtml(primary,'btn btn-ghost',featureKey)}${annual.href?ctaLinkHtml(annual,'btn btn-ghost',`${featureKey}_annual`):''}`,next=live?'Included in your plan. Readable managed processing is paused until you approve this workspace in Manage > Settings.':licAccessState()==='lapsed'?'Your subscription needs billing attention. Restore access, then confirm workspace processing before new readable uploads.':trial?`Start ${licTrialDays('pro')?`with ${licTrialDays('pro')} days of Pro`:'a Pro trial'}. After connecting, explicitly approve each workspace in Manage > Settings.`:'Subscribe to Pro, then explicitly approve each workspace in Manage > Settings.';return `
ENGRAPHIS PRO /${copy.eyebrow}

${copy.title}

${copy.lede}

${esc(next)}

${actions}
WHAT PRO IS WATCHING
${copy.cards.map(card=>`
${card[0]}

${card[1]}

`).join('')}
Your memory stays yours. Readable processing requires your workspace approval. Encrypted Cloud Sync is a separate choice. Secret and session-scoped memories stay local.
`} -function managedConsentRequired(error){return error&&error.status===409&&error.detail&&error.detail.code==='consent_required'} -const CLOUD_SYNC_PRIVACY_COPY='Cloud Sync encrypts eligible shared-workspace changes end-to-end before they leave this device. Engraphis Cloud cannot read their contents; secret and session-scoped memories stay local.'; -const EXTERNAL_LLM_PRIVACY_COPY='Memory text is sent to your configured LLM provider for processing under that provider’s terms. The provider must read that text to return extracted facts.'; -async function confirmCloudTransfer(title,summary,submit,privacyCopy){return confirmAction(title,summary+(privacyCopy?'\n\nPrivacy: '+privacyCopy:''),submit||'Continue')} -const managedConsentHtmlBase=managedConsentHtml; -managedConsentHtml=function(feature){return managedConsentHtmlBase(feature)}; -/* Only an unconfigured local installation may turn a 401 into trial signup. A revoked - or expired Cloud session is also a 401, but ``trial.available`` is false there and it - must remain a reconnect error instead of offering a trial the control plane rejects. */ -function cloudTrialSignupRequired(error){return !!(error&&error.status===401&&error.detail&&error.detail.code==='cloud_unconfigured'&&licTrialAvailable())} -/* Entitlement failure — and only that. 402 subscription not active and 501 feature not - offered here: each means the customer's plan does not include what they just opened, - so each may draw the purchase panel. - 401 is deliberately NOT here either. The cloud maps it to "the cloud session expired or - was revoked; connect again" — a credential problem, not a billing one. An already-paying - customer whose refresh credential lapsed was being sold a plan they own; 401 falls - through to the error branch so the reconnect instruction is what they actually read. - 409 is deliberately NOT here. Every 409 this API emits is a *conflict*, never a billing - answer: detail.code==='consent_required' means the customer IS entitled but managed - compute is off for this installation (managedConsentRequired, above), and the rest are - transient state conflicts (snapshot generation, cloud session state, rebuilding index). - Counting any of them as "unpaid" showed a paying Pro customer a panel selling Pro, so - every caller routes managedConsentRequired first and this second. - Neither predicate may branch on CURRENT_VIEW: doing so made both the consent branch and - the plain-error branch unreachable on the analytics and automation views, which is the - regression this comment previously described but the code did not implement. */ -function hostedFeatureUnavailable(error){return !!error&&(error.status===402||error.status===501)} -async function loadAnalytics(){const el=document.getElementById('analytics-body'),lock=document.getElementById('an-lock'),acts=document.getElementById('an-actions');el.innerHTML='
';try{const a=await api('/analytics?workspace='+encodeURIComponent(WS||''));setPlanPill(lock,licTrialActive()?'TRIAL':'CLOUD','pill pill-accent');showAs(acts,true,'flex');el.innerHTML=renderAnalytics(a,false)}catch(e){if(managedConsentRequired(e)||cloudTrialSignupRequired(e)){setPlanPill(lock,'CLOUD','pill pill-accent');showAs(acts,false);el.innerHTML=managedConsentHtml('Analytics')}else if(hostedFeatureUnavailable(e)){setPlanPill(lock,'PRO','pill pill-muted');showAs(acts,false);el.innerHTML=unlockHtml('Analytics','pro')}else{el.innerHTML='
'+esc(e.message)+'
'}}} - -/* ── hosted automation policy (Pro / Team) ── */ -async function loadAutomation(){const el=document.getElementById('automation-body'),lock=document.getElementById('au-lock'),ws='?workspace='+encodeURIComponent(WS||'');el.innerHTML='
';try{const p=await api('/automation'+ws);setPlanPill(lock,licTrialActive()?'TRIAL':'CLOUD','pill pill-accent');const last=p.last_run?fmtRel(p.last_run):'never',dream=p.dream_enabled!=null?p.dream_enabled:p.dream;el.innerHTML=`
Hosted maintenance policy
The cloud returns reviewable proposals. Pinned memories remain protected.
Cloud worker status
Status${p.enabled?'ENABLED':'OFF'}
Last run${esc(last)}
Requesting managed work uploads the selected workspace’s normal and sensitive memory content, excluding secret and session-scoped rows, capped at 16 MiB, over HTTPS without end-to-end encryption. Results are proposals and never automatically write the local database.
`}catch(e){if(managedConsentRequired(e)||cloudTrialSignupRequired(e)){setPlanPill(lock,'CLOUD','pill pill-accent');el.innerHTML=managedConsentHtml('Hosted Automation')}else if(hostedFeatureUnavailable(e)){setPlanPill(lock,'PRO','pill pill-muted');el.innerHTML=unlockHtml('Automation, Auto Consolidation, and Auto Dreaming','pro')}else{el.innerHTML='
'+esc(e.message)+'
'}}} -async function saveAutomation(){const body={enabled:document.getElementById('au-enabled').checked,cadence_hours:Number(document.getElementById('au-cadence').value)||24,consolidate:document.getElementById('au-consolidate').checked,min_cluster:Number(document.getElementById('au-mincluster').value)||3,archive_below:Number(document.getElementById('au-archive').value)||0.05,dream_enabled:document.getElementById('au-dream').checked,dream_min_new:Number(document.getElementById('au-dream-min').value)||20,dream_idle_minutes:Number(document.getElementById('au-dream-idle').value)};try{await api('/automation?workspace='+encodeURIComponent(WS||''),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});toast('Hosted policy saved','ok');loadAutomation()}catch(e){if(managedConsentRequired(e)){const result=document.getElementById('au-result');if(result)result.innerHTML=managedConsentHtml('Hosted Automation');toast('Approve workspace processing in Manage > Settings','err');return}toast((e.status===402||e.status===501)?'Hosted Automation requires Pro or Team':e.message,'err')}} -async function runMaintenance(){const el=document.getElementById('au-result');if(el)el.innerHTML='
';try{const d=await api('/maintenance/run?workspace='+encodeURIComponent(WS||''),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({dry_run:true})});if(el)el.innerHTML=`PROPOSAL Hosted work was submitted for review.
${esc(JSON.stringify(d,null,2))}
`;toast('Managed proposal requested','ok')}catch(e){if(el)el.innerHTML=managedConsentRequired(e)?managedConsentHtml('Hosted Automation'):'
'+esc(e.message)+'
';toast(managedConsentRequired(e)?'Approve workspace processing in Manage > Settings':((e.status===402||e.status===501)?'Hosted Automation requires Pro or Team':e.message),'err')}} - -const runMaintenanceBase=runMaintenance; -const saveAutomationBase=saveAutomation; -saveAutomation=async function(){const enabled=document.getElementById('au-enabled');if(enabled&&enabled.checked&&!await confirmCloudTransfer('Save hosted policy','Saving this enabled policy uploads this workspace’s normal and sensitive memory content to Engraphis Cloud; secret and session-scoped rows stay local.','Save policy'))return;return saveAutomationBase()} -let MAINTENANCE_PENDING=false; -runMaintenance=async function(dry){ - if(MAINTENANCE_PENDING)return; - if(!await confirmCloudTransfer('Request hosted proposal','This sends this workspace’s normal and sensitive memory content to Engraphis Cloud for a reviewable proposal; secret and session-scoped rows stay local.','Request proposal'))return; - const buttons=Array.from(document.querySelectorAll('#automation-body button[data-onclick="h90"]')),labels=buttons.map(button=>button.textContent); - MAINTENANCE_PENDING=true; - buttons.forEach(button=>{button.disabled=true}); - try{return await runMaintenanceBase(dry)} - finally{ - MAINTENANCE_PENDING=false; - buttons.forEach((button,index)=>{button.disabled=false;button.textContent=labels[index]}); - } -} -/* ── version-chain word diff (memory detail) ── */ -function tokenizeWords(s){return (s||'').split(/(\s+)/)} -function wordDiff(oldS,newS){const a=tokenizeWords(oldS),b=tokenizeWords(newS),n=a.length,m=b.length;if(n*m>200000)return ''+esc(newS)+'';const dp=Array.from({length:n+1},()=>new Uint32Array(m+1));for(let i=n-1;i>=0;i--)for(let j=m-1;j>=0;j--)dp[i][j]=a[i]===b[j]?dp[i+1][j+1]+1:Math.max(dp[i+1][j],dp[i][j+1]);let i=0,j=0,out='';while(i=dp[i][j+1]){out+=''+esc(a[i])+'';i++}else{out+=''+esc(b[j])+'';j++}}while(i'+esc(a[i])+'';i++}while(j'+esc(b[j])+'';j++}return out} -function renderChainDiff(chain){if(!chain||chain.length<2)return '';const c=chain.slice().sort((x,y)=>(x.valid_from||0)-(y.valid_from||0));let h='

Version history '+c.length+' versions · additions green, removals struck

';for(let k=0;k0?c[k-1]:null;const body=prev?wordDiff(prev.content||'',m.content||''):esc(m.content||'');const when=m.valid_from?new Date(m.valid_from*1000).toISOString().slice(0,10):'—';h+=`
${k===0?'original':'revision '+k} · ${when}${cur?' · current':''}
${body}
`}h+='
';return h} -function renderAuditMini(audit){const rows=(audit||[]).slice(0,20);if(!rows.length)return '';return '

Audit trail

'+rows.map(r=>`
${esc(r.action||r.op||r.kind||'edit')}${esc(r.reason||r.detail||'')}${esc(r.actor||'')}${(r.ts||r.at)?fmtRel(r.ts||r.at):''}
`).join('')+'
'} - -/* recall */ -function memCardHtml(m){const sc=m.score!=null?Math.round(Math.min(m.score>1?m.score/5:m.score,1)*100):null;const rc=m.retention!=null?Math.round(Math.min(m.retention,1)*100):null;return `
${esc(m.title||m.id)} ${m.pinned?'PINNED':''} ${esc(m.memory_type)}
${sc!=null?'Score '+sc+'%':''}${rc!=null?'Retention '+rc+'%':''}
${esc((m.content||'').slice(0,320))}
`} -let RECALL_PENDING=false; -async function doRecall(){ - if(RECALL_PENDING)return; - const q=document.getElementById('recall-q').value.trim(); - if(!q){toast('Enter a query','err');return} - if(!WS){workspaceRequired('recall-results','recall memories');return} - const k=document.getElementById('recall-k').value,el=document.getElementById('recall-results'),button=document.querySelector('#view-recall button[data-onclick="h8"]'); - RECALL_PENDING=true; - if(button){button.disabled=true;button.textContent='Recalling…'} - el.innerHTML='
'; - try{ - const d=await api(`/recall?q=${encodeURIComponent(q)}&workspace=${encodeURIComponent(WS||'')}&k=${k}`); - if(!d.count){el.innerHTML='
No matching memories.
';return} - el.innerHTML=`
${d.count} recalled
`+d.memories.map(memCardHtml).join(''); - }catch(e){ - el.innerHTML='
'+esc(e.message)+'
'; - }finally{ - RECALL_PENDING=false; - if(button){button.disabled=false;button.textContent='Recall'} - } -} - -/* memories */ -let MEM_LIST=[], MEM_DRAG=null; -async function loadMemories(){const q=document.getElementById('mem-q').value.trim();const el=document.getElementById('mem-cards');el.innerHTML='
';try{const d=await api(`/memories?q=${encodeURIComponent(q)}&workspace=${encodeURIComponent(WS||'')}&limit=200`);MEM_LIST=d.memories||[];if(!d.count){el.innerHTML='
No memories found.
';return}el.innerHTML=MEM_LIST.map(memRowHtml).join('')}catch(e){el.innerHTML='
'+esc(e.message)+'
'}} -function memRowHtml(m){return `
⠿
${esc(m.title||m.id)}
${esc(m.memory_type)}${m.pinned?'pin':''}${esc((m.content||'').slice(0,60))}
`} -/* drag-to-reorder: whole-list resend on drop keeps this simple and robust — see - MemoryService.reorder_memories for how sort_order is assigned server-side. */ -function memDragStart(e,id){MEM_DRAG=id;e.dataTransfer.effectAllowed='move';e.currentTarget.classList.add('dragging')} -function memDragOver(e,id){if(!MEM_DRAG||MEM_DRAG===id)return;e.preventDefault();const el=e.currentTarget,r=el.getBoundingClientRect(),before=(e.clientY-r.top)m.id===dragId);let to=MEM_LIST.findIndex(m=>m.id===id);if(from<0||to<0)return;const[moved]=MEM_LIST.splice(from,1);to=MEM_LIST.findIndex(m=>m.id===id);MEM_LIST.splice(before?to:to+1,0,moved);document.getElementById('mem-cards').innerHTML=MEM_LIST.map(memRowHtml).join('');try{await api('/memories/reorder',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({workspace:WS,ids:MEM_LIST.map(m=>m.id)})})}catch(err){toast('Reorder failed: '+err.message,'err');loadMemories()}} -function memDragEnd(e){e.currentTarget.classList.remove('dragging');document.querySelectorAll('#mem-cards .mem-card').forEach(c=>c.classList.remove('drag-over-top','drag-over-bottom'))} -async function memKeyboardMove(id,delta){const from=MEM_LIST.findIndex(m=>m.id===id),to=Math.max(0,Math.min(MEM_LIST.length-1,from+delta));if(from<0||from===to){toast(delta<0?'Memory is already first':'Memory is already last','ok');return}const[moved]=MEM_LIST.splice(from,1);MEM_LIST.splice(to,0,moved);document.getElementById('mem-cards').innerHTML=MEM_LIST.map(memRowHtml).join('');try{await api('/memories/reorder',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({workspace:WS,ids:MEM_LIST.map(m=>m.id)})});toast('Moved '+(moved.title||moved.id)+' to position '+(to+1),'ok');setTimeout(()=>{const card=document.querySelector('#mem-cards .mem-card[data-id="'+CSS.escape(id)+'"]');if(card)card.focus()},0)}catch(err){toast('Reorder failed: '+err.message,'err');loadMemories()}} - -/* proactive */ -async function loadProactive(){const el=document.getElementById('proactive-body');el.innerHTML='
';try{const d=await api('/proactive?workspace='+encodeURIComponent(WS||''));let h='';if(d.handoff)h+=`
Last session handoff
${esc(typeof d.handoff==='string'?d.handoff:JSON.stringify(d.handoff))}
`;h+=(d.memories&&d.memories.length)?d.memories.map(memCardHtml).join(''):'
Nothing pressing right now.
';el.innerHTML=h}catch(e){el.innerHTML='
'+esc(e.message)+'
'}} - -/* why */ -let WHY_PENDING=false; -async function doWhy(){ - if(WHY_PENDING)return; - const q=document.getElementById('why-q').value.trim(); - if(!q){toast('Enter a question','err');return} - if(!WS){workspaceRequired('why-body','explain current beliefs and their history');return} - const el=document.getElementById('why-body'),button=document.querySelector('#view-why button[data-onclick="h17"]'); - WHY_PENDING=true; - if(button){button.disabled=true;button.textContent='Explaining…'} - el.innerHTML='
'; - try{ - const d=await api(`/why?q=${encodeURIComponent(q)}&workspace=${encodeURIComponent(WS||'')}`); - let h=''; - if(d.answer.length){ - h+='
Current answer
'+d.answer.map(m=>`
${esc(m.title||m.id)}
${esc(m.content)}
`).join('')+'
'; - }else{ - h+='
No current answer found.
'; - } - if(d.supersedes.length){ - h+='
Superseded (no longer true)
'+d.supersedes.map(m=>`
${esc(m.title||m.id)}
${esc(m.content)}
until ${m.valid_to?new Date(m.valid_to*1000).toISOString().slice(0,10):'—'}
`).join('')+'
'; - } - el.innerHTML=h; - }catch(e){ - el.innerHTML='
'+esc(e.message)+'
'; - }finally{ - WHY_PENDING=false; - if(button){button.disabled=false;button.textContent='Explain'} - } -} - -/* timeline */ -let TIMELINE_PENDING=false; -async function doTimeline(){ - const q=document.getElementById('tl-q').value.trim(); - if(!q){toast('Enter a topic','err');return} - if(!WS){workspaceRequired('tl-body','trace memory history');return} - if(TIMELINE_PENDING)return; - const el=document.getElementById('tl-body'),button=document.querySelector('#view-timeline button[data-onclick="h19"]'); - TIMELINE_PENDING=true; - if(button){button.disabled=true;button.textContent='Tracing…'} - el.innerHTML='
'; - try{ - const d=await api(`/timeline?q=${encodeURIComponent(q)}&workspace=${encodeURIComponent(WS||'')}&limit=40`); - if(!d.history.length){el.innerHTML='
No history for this topic.
';return} - const exact=value=>value?new Date(value*1000).toISOString():'Unavailable'; - el.innerHTML='
'+d.history.map(m=>{ - const cur=!m.valid_to&&!m.expired_at,validEnd=m.valid_to||m.expired_at; - const valid=exact(m.valid_from)+(validEnd?' — '+exact(validEnd):' — current'); - const recorded=exact(m.ingested_at),source=(m.provenance&&m.provenance.source)||'Unknown source'; - return `
${esc(m.title||m.id)} ${cur?'current':'past'}
${esc(m.content)}
Valid time
${esc(valid)}
Recorded time
${esc(recorded)}
Source
${esc(source)}
`; - }).join('')+'
'; - }catch(e){ - el.innerHTML='
'+esc(e.message)+'
'; - }finally{ - TIMELINE_PENDING=false; - if(button){button.disabled=false;button.textContent='Trace'} - } -} - -/* audit */ -async function loadAudit(){const el=document.getElementById('audit-body');el.innerHTML='
';try{const d=await api('/audit?workspace='+encodeURIComponent(WS||'')+'&limit=200');const rows=d.entries||d.audit||[];if(!rows.length){el.innerHTML='
No governance actions recorded.
';return}el.innerHTML='
'+rows.map(r=>`
${esc(r.action||r.op||r.kind||'edit')}${esc(r.memory_id||r.target||r.detail||'')}${esc(r.actor||'')}${r.ts||r.at?fmtRel(r.ts||r.at):''}
`).join('')+'
'}catch(e){el.innerHTML='
'+esc(e.message)+'
'}} -async function downloadReceipts(){try{const d=await api('/receipts/export?workspace='+encodeURIComponent(WS||''));const blob=new Blob([JSON.stringify(d,null,2)],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='engraphis-receipts-'+(WS||'workspace')+'.json';a.click();URL.revokeObjectURL(a.href);toast('Privacy-safe receipts exported','ok')}catch(e){toast(e.message,'err')}} - -let SAVINGS_PRESET='all'; -function savingsPresetQuery(){const p=new URLSearchParams({workspace:WS||''});if(SAVINGS_PRESET==='current'&&RELEASE_VERSION)p.set('release_version',RELEASE_VERSION);if(SAVINGS_PRESET==='7d')p.set('from_ts',String(Date.now()/1000-604800));return p.toString()} -function renderSavingsDetail(s){const e=(s&&s.estimated)||{},eligible=Number(e.eligible_receipt_count)||0,excluded=(Number(e.excluded_receipt_count)||0)+(Number(e.unclassified_receipt_count)||0)+(Number(e.invalid_estimate_count)||0),basisRows=(e.by_basis||[]).map(x=>'
'+esc((x.basis||'unclassified').replaceAll('_',' '))+' · '+esc(x.confidence||'unknown')+''+formatTokenCount(x.baseline_tokens)+' → '+formatTokenCount(x.emitted_tokens)+' · '+formatTokenCount(x.saved_tokens)+' saved ('+(x.receipt_count||0)+' delivery)
').join(''),counterRows=(e.by_token_counter||[]).map(x=>'
'+esc(x.token_counter||'unknown')+''+formatTokenCount(x.saved_tokens)+' saved · '+(x.receipt_count||0)+' eligible delivery
').join(''),preset=SAVINGS_PRESET==='current'?'Current release':SAVINGS_PRESET==='7d'?'Last 7 days':SAVINGS_PRESET==='since'?'Since tracking started':'All time';const buttons=['since','current','7d','all'].map(x=>'').join('');return '
Estimated context saved
View'+buttons+'
'+(eligible?'
'+formatTokenCount(e.saved_tokens)+' tokens
Baseline '+formatTokenCount(e.baseline_tokens)+' → emitted '+formatTokenCount(e.emitted_tokens)+' · '+(Number(e.savings_ratio||0)*100).toFixed(1)+'% estimated reduction
'+eligible+' eligible deliveries · confidence: '+esc(e.confidence||'unknown')+' · range: '+preset+'
'+(basisRows||'
No basis breakdown available.
')+(counterRows?'
Token counters
'+counterRows:''):'
No eligible estimates in this range.
')+'
'+excluded+' excluded or unclassified delivery(s). Measures estimated prompt-context reduction; it does not measure provider billing.
'} -async function loadReceipts(){const el=document.getElementById('audit-body');el.innerHTML='
';try{if(!window.__savingsPresetBound){window.__savingsPresetBound=true;document.addEventListener('click',function(ev){const button=ev.target.closest('[data-savings-preset]');if(!button)return;SAVINGS_PRESET=button.getAttribute('data-savings-preset')||'all';loadReceipts()})}const q='workspace='+encodeURIComponent(WS||''),sq=savingsPresetQuery();const [d,v,s]=await Promise.all([api('/receipts?'+q+'&limit=500'),api('/receipts/verify?'+q),api('/context-savings?'+sq)]);const rows=d.entries||[],packed=(s.by_token_counter||[]).map(x=>'
'+esc(x.token_counter||'unknown')+''+formatTokenCount(x.context_tokens)+' packed / '+formatTokenCount(x.source_tokens)+' source · '+formatTokenCount(x.saved_tokens)+' legacy saved
').join('');const packedCard='
Packed context accounting
Packing savings compare retrieved source tokens with emitted context. They are not added again to adaptive history savings.
'+(packed||'
No complete context-usage receipts yet.
')+'
';el.innerHTML=renderSavingsDetail(s)+packedCard+'
Receipt chain '+(v.valid?'verified':'invalid')+'
'+(v.count||0)+' receipts · head '+esc((v.head||'').slice(0,24))+'
'+(rows.length?'
'+rows.map(r=>'
'+esc(r.operation||'operation')+''+esc((r.hash||'').slice(0,20))+' · '+esc(r.status||'ok')+' · '+(r.target_count||0)+' target(s)'+(r.ts_ms?fmtRel(r.ts_ms/1000):'')+'
').join('')+'
':'
No receipts yet.
')}catch(e){el.innerHTML='
'+esc(e.message)+'
'}} - -/* consolidate */ -async function runConsolidate(dry){const el=document.getElementById('consolidate-body');el.innerHTML='
';try{const d=await api('/consolidate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({workspace:WS,dry_run:dry})});el.innerHTML=`
${dry?'Dry run (nothing changed)':'Consolidation complete'}
${esc(JSON.stringify(d,null,2))}
`;if(!dry)toast('Consolidation done','ok')}catch(e){el.innerHTML='
'+esc(e.message)+'
'}} - -const runConsolidateBase=runConsolidate; -let CONSOLIDATE_PENDING=false; -runConsolidate=async function(dry){ - if(CONSOLIDATE_PENDING)return; - if(!WS){workspaceRequired('consolidate-body','preview or commit consolidation');return} - if(!dry&&!await confirmAction('Commit consolidation','This may create distilled semantic memories and archive decayed transients in workspace "'+(WS||'')+'". Choose the dry run first if you only want a preview.','Commit consolidation',true))return; - const buttons=Array.from(document.querySelectorAll('#view-consolidate button[data-onclick="h61"],#view-consolidate button[data-onclick="h62"]')),labels=buttons.map(button=>button.textContent); - CONSOLIDATE_PENDING=true; - buttons.forEach(button=>{button.disabled=true}); - try{return await runConsolidateBase(dry)} - finally{ - CONSOLIDATE_PENDING=false; - buttons.forEach((button,index)=>{button.disabled=false;button.textContent=labels[index]}); - } -} -/* local single-user workspaces */ -function canCreateWs(){return true} -function folderCardName(el){const card=el.closest('.vault-card');return card?card.dataset.workspace:''} -function folderOpen(el){const card=el.closest('.vault-card');if(card)wsSwitch(card.dataset.workspace)} -function folderCardHtml(w,opts){opts=opts||{};const manage=opts.manage!==false,a=w.name===WS,count=Number(w.memories)||0;const actions=manage?`
`:'';return `
${actions}
${esc(w.name)}${a?' active':''}
${w.description?`
${esc(w.description)}
`:''}
${count} memories${w.repos&&(Array.isArray(w.repos)?w.repos.length:w.repos)?''+(Array.isArray(w.repos)?w.repos.length:w.repos)+' repos':''}View memories →
`} -function tfMemories(name){setWS(name);navTo('memories')} -async function refreshFolders(){await loadWorkspaces()} -async function loadWorkspaces(){ - const el=document.getElementById('ws-cards'),canNew=true,canSources=true; - const nb=document.getElementById('ws-new-btn'),ic=document.getElementById('import-card'),cc=document.getElementById('code-import-card'),iwn=document.getElementById('import-ws-name'); - if(iwn)iwn.textContent=WS?('"'+WS+'"'):'the active folder'; - if(nb){nb.textContent='New folder';showAs(nb,canNew,'inline-flex')} - showAs(ic,canNew,'block'); - showAs(cc,canSources,'block'); - try{ - await loadWorkspaceList(); - if(!WORKSPACES.length){ - el.innerHTML='
No folders yet.
'; - return; - } - el.innerHTML='
'+WORKSPACES.map(w=>folderCardHtml(w,{manage:canNew})).join('')+'
'; - }catch(e){el.innerHTML='
'+esc(e.message)+'
'} -} -async function wsCreate(){ - const result=await actionDialog({title:'New folder',message:'Create a local workspace for related memories.',submit:'Create folder',fields:[{name:'name',label:'Folder name',required:true},{name:'description',label:'Description (optional)',optional:true}]}); - if(result===null)return; - const name=result.name.trim(); - if(!name){toast('Enter a folder name','err');return} - if(WORKSPACES.some(w=>w.name===name)){toast('A folder named "'+name+'" already exists','err');return} - try{ - await api('/workspaces/create',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({workspace:name,description:(result.description||'').trim()})}); - await loadWorkspaceList();setWS(name);toast('Folder "'+name+'" created — now the active folder','ok');refreshFolders(); - }catch(e){toast('Create failed: '+e.message,'err')} -} -function wsSwitch(name){setWS(name);toast('Switched to '+name,'ok');navTo('overview')} - -/* import (files/folders from this PC — see MemoryService.import_folder/import_files) */ -async function importUpload(items){if(!canCreateWs()){toast('Viewers can’t import','err');return}if(!WS){toast('Create or select a folder first','err');return}if(!items||!items.length)return;const fd=new FormData();fd.append('workspace',WS);fd.append('memory_type','semantic');fd.append('derive_facts',document.getElementById('import-derive').checked?'true':'false');for(const it of items)fd.append('files',it.file,it.name);const el=document.getElementById('import-status');el.textContent='Extracting and importing '+items.length+' file(s)…';try{const r=await api('/workspaces/import-files',{method:'POST',body:fd});const wc=(r.warnings||[]).length;el.textContent=r.imported+' imported, '+r.skipped+' skipped, '+r.errors+' error(s), '+(r.derived_facts||0)+' derived fact(s)'+(wc?', '+wc+' warning(s)':'')+(r.truncated?', TRUNCATED at '+r.scanned+' of '+r.matched_total+' matching files':'')+(r.unreadable?', '+r.unreadable+' unreadable':'');toast(r.imported+' resource'+(r.imported===1?'':'s')+' imported into "'+WS+'"'+(r.truncated?' (truncated at max '+r.scanned+')':''),'ok');refreshFolders()}catch(e){el.textContent='';toast('Import failed: '+e.message,'err')}} -function importFilesPicked(fileList,el){const items=Array.from(fileList||[]).map(f=>({file:f,name:f.webkitRelativePath||f.name}));if(el)el.value='';importUpload(items)} -async function importWalkEntry(entry,path,out){if(entry.isFile){await new Promise(res=>entry.file(f=>{out.push({file:f,name:(path?path+'/':'')+f.name});res()},()=>res()))}else if(entry.isDirectory){const reader=entry.createReader();const readBatch=()=>new Promise(res=>reader.readEntries(res,()=>res([])));let batch;do{batch=await readBatch();for(const e of batch)await importWalkEntry(e,(path?path+'/':'')+entry.name,out)}while(batch.length)}} -async function importDrop(e){e.preventDefault();e.currentTarget.classList.remove('drag');const items=e.dataTransfer.items;const out=[];if(items&&items.length&&items[0].webkitGetAsEntry){for(const it of items){const entry=it.webkitGetAsEntry&&it.webkitGetAsEntry();if(entry)await importWalkEntry(entry,'',out)}}else{for(const f of e.dataTransfer.files)out.push({file:f,name:f.name})}importUpload(out)} -async function importFromPath(){if(!canCreateWs()){toast('Viewers can’t import','err');return}if(!WS){toast('Create or select a folder first','err');return}const path=(document.getElementById('import-path').value||'').trim();const pattern=(document.getElementById('import-pattern').value||'*').trim()||'*';if(!path){toast('Enter a path','err');return}const el=document.getElementById('import-status');el.textContent='Extracting and importing…';try{const r=await api('/workspaces/import-folder',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({workspace:WS,path,file_pattern:pattern,memory_type:'semantic',derive_facts:document.getElementById('import-derive').checked})});const wc=(r.warnings||[]).length;el.textContent=r.imported+' imported, '+r.skipped+' skipped, '+r.errors+' error(s), '+(r.derived_facts||0)+' derived fact(s), scanned '+r.scanned+(wc?', '+wc+' warning(s)':'')+(r.truncated?', TRUNCATED at '+r.scanned+' of '+r.matched_total+' matching files':'')+(r.unreadable?', '+r.unreadable+' unreadable':'');toast(r.imported+' resource'+(r.imported===1?'':'s')+' imported into "'+WS+'"'+(r.truncated?' (truncated at max '+r.scanned+')':''),'ok');refreshFolders()}catch(e){el.textContent='';toast('Import failed: '+e.message,'err')}} -async function indexRepository(){if(!WS){toast('Select a workspace first','err');return}const repo=(document.getElementById('code-repo').value||'').trim(),root=(document.getElementById('code-root').value||'').trim(),el=document.getElementById('code-import-status');if(!repo||!root){toast('Enter a repository name and path','err');return}el.textContent='Incrementally indexing repository…';try{const r=await api('/code/index',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({workspace:WS,repo:repo,root_path:root})});el.textContent=`${r.files_indexed} changed, ${r.files_unchanged} unchanged · ${r.symbols} symbols · ${r.edges} edges · ${r.code_memory_links||0} memory links`;toast('Repository graph updated','ok')}catch(e){el.textContent='';toast(e.message,'err')}} -async function importPostgresSchema(){if(!WS){toast('Select a workspace first','err');return}const dsn=(document.getElementById('postgres-dsn').value||'').trim(),repo=(document.getElementById('postgres-repo').value||'').trim(),el=document.getElementById('code-import-status');if(!dsn){toast('Enter a PostgreSQL DSN','err');return}el.textContent='Reading PostgreSQL catalog…';try{const r=await api('/resources/postgres',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({workspace:WS,repo:repo||null,dsn:dsn})});document.getElementById('postgres-dsn').value='';el.textContent=`Imported ${r.schema.tables||0} tables, ${r.entities} entities, and ${r.relations} relations`;toast('Database schema imported','ok')}catch(e){el.textContent='';toast(e.message,'err')}} -async function wsRename(name){const nn=await textAction('Rename workspace','Choose a new name for "'+name+'".','Workspace name',name,{submit:'Rename'});if(nn===null)return;const v=nn.trim();if(!v||v===name)return;try{await api('/workspaces/rename',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({workspace:name,new_name:v})});if(WS===name)setWS(v);toast('Renamed','ok');refreshFolders()}catch(e){toast(e.message,'err')}} -async function wsDescribe(name){const cur=((WORKSPACES.find(w=>w.name===name)||{}).description)||'';const d=await textAction('Describe workspace','Update the optional description for "'+name+'".','Description',cur,{submit:'Save',optional:true,multiline:true});if(d===null)return;try{await api('/workspaces/describe',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({workspace:name,description:d})});toast('Saved','ok');refreshFolders()}catch(e){toast(e.message,'err')}} -async function wsMerge(name){const others=WORKSPACES.map(w=>w.name).filter(n=>n!==name);if(!others.length){toast('No other workspace to merge into','err');return}const result=await actionDialog({title:'Merge workspace',message:'All memories in "'+name+'" will move to the selected workspace and "'+name+'" will be removed. This cannot be undone.',submit:'Merge workspace',danger:true,fields:[{name:'target',label:'Destination workspace',required:true,options:others.map(value=>({value,label:value}))}]});if(result===null)return;const v=result.target;try{const r=await api('/workspaces/merge',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({source:name,target:v})});toast('Merged '+(r.memories_moved||0)+' memories into '+v,'ok');if(WS===name)setWS(v);refreshFolders()}catch(e){toast('Merge failed: '+e.message,'err')}} -async function wsCopy(name){try{const r=await api('/workspaces/copy',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({workspace:name})});toast('Copied to "'+r.workspace+'" ('+(r.memories_copied||0)+' memories)','ok');refreshFolders()}catch(e){toast('Copy failed: '+e.message,'err')}} -async function wsDelete(name,n){const active=name===WS?' This is the active workspace; another workspace will become active after deletion.':'';if(!await confirmAction('Delete workspace','Delete "'+name+'" and all '+n+' memories in it from this Engraphis store?'+active+' Export anything you need first. This cannot be undone.','Delete workspace',true))return;try{const r=await api('/workspaces/delete',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({workspace:name})});toast('Deleted ('+(r.memories_removed||0)+' memories)','ok');if(WS===name){WS=null;await loadWorkspaceList();if(WORKSPACES[0])setWS(WORKSPACES[0].name)}refreshFolders()}catch(e){toast(e.message,'err')}} - -/* memory detail + governance — a full-page editor (view-mem-editor), not a popup: - the raw content is always a live, editable textarea (Obsidian-style), with a - rendered preview alongside and the version history/audit trail below. */ -function setEditorActionsEnabled(enabled){['ed-save-btn','ed-pin-btn','ed-forget-btn'].forEach(id=>{const btn=document.getElementById(id);if(btn)btn.disabled=!enabled})} -async function openMem(id){window.CURMEM=null;setEditorActionsEnabled(false);navTo('mem-editor');const ta=document.getElementById('ed-content');document.getElementById('ed-title').value='';ta.value='';document.getElementById('ed-meta').innerHTML='';document.getElementById('ed-preview').innerHTML='';document.getElementById('ed-history').innerHTML='
';document.getElementById('topbar-title').textContent='Loading…';try{const d=await api('/memory/'+encodeURIComponent(id)+'?workspace='+encodeURIComponent(WS||''));const m=d.memory;if(!m){document.getElementById('topbar-title').textContent='Memory unavailable';document.getElementById('ed-history').innerHTML='
Memory not found.
';return false}window.CURMEM=m;document.getElementById('ed-title').value=m.title||'';document.getElementById('ed-type').value=m.memory_type||'semantic';ta.value=m.content||'';edPreviewUpdate();edRenderMeta();document.getElementById('topbar-title').textContent=m.title||m.id;document.getElementById('ed-history').innerHTML=renderChainDiff(d.chain)+renderAuditMini(d.audit);setEditorActionsEnabled(true);return true}catch(e){document.getElementById('topbar-title').textContent='Memory unavailable';document.getElementById('ed-history').innerHTML='
Could not load this memory: '+esc(e.message)+'
';return false}} -function closeMem(){navTo('memories')} -function edPreviewUpdate(){document.getElementById('ed-preview').innerHTML=renderMd(document.getElementById('ed-content').value)} -function edRenderMeta(){const m=window.CURMEM;if(!m)return;const btn=document.getElementById('ed-pin-btn');if(btn)btn.textContent=m.pinned?'Unpin':'Pin';document.getElementById('ed-meta').innerHTML=`${esc(m.memory_type)} ${esc(m.scope||'')} ${m.pinned?'pinned':''} ${esc((m.provenance&&m.provenance.source)||'')}${m.provenance&&m.provenance.trusted===false?' · untrusted':''} · id ${esc(m.id)}`} -async function edTogglePin(){const m=window.CURMEM;if(!m)return;try{await api('/pin',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:m.id,workspace:WS,pinned:!m.pinned})});m.pinned=!m.pinned;edRenderMeta();toast(m.pinned?'Pinned':'Unpinned','ok')}catch(e){toast(e.message,'err')}} -async function edSave(){const m=window.CURMEM;if(!m)return;const nt=document.getElementById('ed-title').value;const ntype=document.getElementById('ed-type').value;const nc=document.getElementById('ed-content').value;try{let meta=false,body=false,id=m.id;if(nt!==(m.title||'')||ntype!==(m.memory_type||'semantic')){await api('/memory/update',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:id,workspace:WS,title:nt,memory_type:ntype})});meta=true}if(nc!==(m.content||'')){const r=await api('/correct',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:id,workspace:WS,content:nc,reason:'dashboard edit'})});body=true;id=r.id}if(!meta&&!body){toast('No changes','ok');return}toast('Saved','ok');await openMem(id)}catch(e){toast(e.message,'err')}} -async function edForget(){const m=window.CURMEM;if(!m)return;if(!await confirmAction('Retire memory','Close the current validity of "'+(m.title||m.id)+'" in workspace "'+(WS||'')+'"? It will stop appearing as current truth but remain in bi-temporal history.','Retire memory',true))return;try{await api('/retire',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:m.id,workspace:WS,reason:'dashboard retirement'})});toast('Memory retired and retained in history','ok');closeMem()}catch(e){toast(e.message,'err')}} -let EDITOR_BASELINE='',EDITOR_FORCE_CLOSE=false; -function editorSnapshot(){return JSON.stringify({title:document.getElementById('ed-title').value,type:document.getElementById('ed-type').value,content:document.getElementById('ed-content').value})} -function editorIsDirty(){return !!window.CURMEM&&!!EDITOR_BASELINE&&editorSnapshot()!==EDITOR_BASELINE} -function editorRefreshDirty(){const state=document.getElementById('ed-save-state');if(!state)return;const dirty=editorIsDirty();state.textContent=dirty?'Unsaved changes':'Saved';state.classList.toggle('dirty',dirty)} -function editorCommitBaseline(){EDITOR_BASELINE=window.CURMEM?editorSnapshot():'';editorRefreshDirty()} -const openMemWithEditorState=openMem; -openMem=async function(id){const loaded=await openMemWithEditorState(id);if(loaded)editorCommitBaseline();else{EDITOR_BASELINE='';editorRefreshDirty()}return loaded} -const selectViewWithDirtyGuard=selectView; -selectView=async function(v){if(!EDITOR_FORCE_CLOSE&&v!=='mem-editor'&&document.getElementById('view-mem-editor').classList.contains('active')&&editorIsDirty()){const title=document.getElementById('ed-title').value||'this memory';if(!await confirmAction('Discard unsaved changes','Leave the Memory editor? Unsaved changes to "'+title+'" will be lost.','Discard changes',true))return false}selectViewWithDirtyGuard(v);return true} -const edForgetWithEditorState=edForget; -edForget=async function(){EDITOR_FORCE_CLOSE=true;try{return await edForgetWithEditorState()}finally{EDITOR_FORCE_CLOSE=false}} -;['ed-title','ed-type','ed-content'].forEach(id=>{const el=document.getElementById(id);el.addEventListener('input',editorRefreshDirty);el.addEventListener('change',editorRefreshDirty)}); -window.addEventListener('beforeunload',e=>{if(!editorIsDirty())return;e.preventDefault();e.returnValue=''}); -document.addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='s'&&document.getElementById('view-mem-editor').classList.contains('active')){e.preventDefault();edSave()}}); -/* license */ -async function loadLicense(){const el=document.getElementById('lic-body');try{const d=await api('/license');LIC=d;updateLicBadge();updateFeatureLocks();renderLicense(d)}catch(e){if(el)el.innerHTML='
'+esc(e.message)+'
'}} -/* Copy for the control plane's own entitlement status, when it named one. This is what - separates "your card was declined" from "you cancelled" inside a single lapsed state. */ -const LIC_STATUS_NOTE={past_due:'the last payment did not go through',canceled:'the subscription was cancelled',expired:'the billing period ended',revoked:'access was revoked'}; -const LIC_SOURCE_LABEL={environment:'operator override',session:'cloud handshake',cloud:'cloud entitlements read',connected:'not yet confirmed',local:'not connected',override:'operator override'}; -/* The whole point of the panel: when hosted features are locked, say why, and offer the - one action that fixes it. Never a plan badge over unexplained locks. */ -function licStateBanner(state,plan,ends,status){ - if(state==='trial_expired')return `
Your free trial has ended${ends?' on '+esc(ends):''}Hosted features are locked. Everything you have written is still in your local database and still fully usable — only the cloud capabilities stopped. The free trial runs once per account and cannot be started again, so restoring them means subscribing.
`; - if(state==='lapsed'){const note=LIC_STATUS_NOTE[status];return `
Your ${esc(plan||'hosted')} subscription is no longer active${note?esc(note.charAt(0).toUpperCase()+note.slice(1))+', so hosted':'Hosted'} features are locked until billing is up to date. Your local memories are unaffected. Open the account portal to restore access.
`} - if(state==='inactive')return `
No hosted plan on this installationThe local memory engine is free and complete on its own. Cloud Sync, Analytics, Automation, and Team administration run in Engraphis Cloud.
`; - return ''} -function licActionsHtml(state){if(state!=='active'&&state!=='lapsed'&&state!=='trial'&&state!=='inactive'&&state!=='trial_expired')return '';const pro=hostedCta('pro','license');return `
${ctaLinkHtml(pro,'btn btn-primary btn-sm','license')}
`} -function renderLicense(d){ - const el=document.getElementById('lic-body');if(!el)return; - const state=licAccessState(),raw=String(d.plan||'local').toLowerCase(); - const plan=raw==='pro'||raw==='team'?raw.toUpperCase():''; - const hosted=state!=='inactive',live=licAccessLive(),ends=licTrialEnds(); - const label=state==='trial'?(plan||'PRO')+' TRIAL':state==='trial_expired'?(plan||'PRO')+' TRIAL ENDED':state==='lapsed'?plan+' INACTIVE':state==='active'?plan:'LOCAL CORE'; - const known=d.known_features||{}; - const feats=hosted?Object.keys(known).map(f=>`${(d.features||[]).includes(f)?'✓':'○'} ${esc(known[f])}`).join(''):''; - let h=`
${hosted?'Hosted plan':'Local runtime'}${esc(label)}
`; - if(d.error)h+=`
Hosted authorization unavailable — ${esc(d.error)}
`; - h+=licStateBanner(state,plan,ends,d.entitlement_status); - if(hosted&&d.email&&d.email!=='trial')h+=`
Hosted account${esc(d.email)}
`; - if(state==='trial'&&ends)h+=`
Trial ends${esc(ends)}
`; - else if(hosted&&d.expires)h+=`
Authorization expires${esc(fmtDay(d.expires))}
`; - if(feats)h+=`
${feats}
`; - /* Support diagnostics: which rule produced this answer and when the cloud last confirmed - it. Emitted by /api/license since the plan resolver landed, and never shown until now — - so "the dashboard says PRO" and "the cloud says PRO" could not be told apart. */ - if(d.plan_source)h+=`
Plan source${esc(LIC_SOURCE_LABEL[d.plan_source]||d.plan_source)}${d.plan_checked_at?' · confirmed '+esc(fmtRel(d.plan_checked_at)):''}
`; - if(state==='active')h+=`
Thank you for supporting Engraphis. Your subscription helps fund hosted infrastructure and ongoing development.
`; - h+=licActionsHtml(state); - el.innerHTML=h; -} -async function exportWorkspace(){try{const d=await api('/export?workspace='+encodeURIComponent(WS||''));const blob=new Blob([JSON.stringify(d,null,2)],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='engraphis-export-'+Date.now()+'.json';a.click();URL.revokeObjectURL(a.href);toast('Exported','ok')}catch(e){toast(e.message,'err')}} - -/* Hosted Team is a service CTA; local identity and seat administration are not shipped. */ -async function loadTeam(){const el=document.getElementById('team-body'),teamCta=hostedCta('team','team_tab');try{const st=await api('/auth/state');if(teamCta.href==='#'&&st&&st.cloud_url)teamCta.href=safeUrl(st.cloud_url)}catch(e){}el.innerHTML=`
Engraphis Team Cloud HOSTED
Organizations, invitations, roles, named seats, scoped device credentials, and team audit run on the private hosted service. This local dashboard is intentionally single-user.
${esc(teamTeaserNote())} Private-service account grace is capped at 24 hours, never extends Team access, and never restricts the free local core.
${ctaLinkHtml(teamCta,'btn btn-primary btn-sm','team_tab')}
`} -/* health + settings */ -function connectionContext(){const host=(location.hostname||'').toLowerCase();return host==='localhost'||host==='127.0.0.1'||host==='::1'||host.endsWith('.localhost')?'Local engine':'Remote customer node'} -async function checkHealth(){const label=connectionContext();try{await api('/health');const d=document.getElementById('health-dot'),t=document.getElementById('health-text');if(d){d.classList.add('health-ok');d.classList.remove('health-error')}if(t)t.textContent=label+' connected'}catch(e){const d=document.getElementById('health-dot'),t=document.getElementById('health-text');if(d){d.classList.add('health-error');d.classList.remove('health-ok')}if(t)t.textContent=label+' unavailable'}try{const auth=await api('/auth/state');const m=document.getElementById('deployment-mode-indicator');if(m){const isLocal=auth.deployment_mode==='local';m.textContent=isLocal?'LOCAL':'HOSTED';m.title=isLocal?(auth.enabled?'Local API token required':'Local mode: no hosted cloud configured'):'Hosted mode: connected to Engraphis Cloud';m.className='deployment-mode '+(isLocal?'mode-local':'mode-hosted');m.hidden=false}}catch(e){}} -function loadSettings(){loadLicense();loadSyncStatus();loadHostedAgentAccess();loadLlmStatus();const s=document.getElementById('cfg-store');if(s)s.textContent=location.host;api('/info').then(function(d){var v=document.getElementById('cfg-version');if(v&&d&&d.version)v.textContent=d.version}).catch(function(){})} - -async function loadLlmStatus(){const el=document.getElementById('llm-body');if(!el)return;try{const st=await api('/llm/status');const ok=st.configured;const badge=ok?'configured':'not configured';const keyLine=st.key_set?'API key set ✓':'No API key set';let modelSel='';let provSel='';el.innerHTML=`
Provider · Model${badge}
${provSel}${modelSel}
${keyLine} · extractor: ${esc(st.extractor)}
Add this to your .env and restart Engraphis:
LLM extraction${st.extractor_enabled?'ON':'OFF'}
While ON, ingested memory content is sent to your LLM provider for schema-validated extraction. OFF disables extraction transfers only; retention supervision is configured separately.
`}catch(e){el.innerHTML='
'+esc(e.message)+'
'}} -function onLlmProvChange(){const p=document.getElementById('llm-prov').value;const sel=document.getElementById('llm-model');const defs={openai:'gpt-4o-mini',anthropic:'claude-sonnet-5-5',google:'gemini-1.5-flash',openrouter:'openai/gpt-4o-mini'};if(sel&&defs[p]){sel.value=defs[p]}updateLlmSnippet()} -function updateLlmSnippet(){const p=(document.getElementById('llm-prov')||{}).value||'openai';const m=(document.getElementById('llm-model')||{}).value||'';const ta=document.getElementById('llm-snippet');if(!ta)return;ta.value='ENGRAPHIS_LLM_PROVIDER='+p+'\nENGRAPHIS_LLM_MODEL='+m+'\nENGRAPHIS_LLM_API_KEY=\nENGRAPHIS_EXTRACTOR=llm_structured\n'} -function copyLlmSnippet(){const ta=document.getElementById('llm-snippet');if(!ta)return;ta.select();try{navigator.clipboard.writeText(ta.value);toast('Copied .env snippet','ok')}catch(e){toast('Copy failed — select and Ctrl+C','err')}} -async function setLlmExtractor(on){try{const d=await api('/llm/extractor',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({enabled:!!on})});const ok=!!d.extractor_enabled;toast(ok?'LLM extraction turned on — new memories will be sent to your provider':'LLM extraction turned off — extractor transfers are disabled'+(d.persisted===false?' (could not save for restart)':''),ok?'ok':'muted');loadLlmStatus()}catch(e){toast(e.message,'err')}} -async function testLlm(){const r=document.getElementById('llm-test-result');if(r){r.textContent='Testing…';setTone(r,'muted')}try{const d=await api('/llm/test',{method:'POST'});if(r){if(d.ok){const transient=d.auto_enabled&&d.persisted===false;r.textContent=(transient?'⚠ ':'✓ ')+'Connected — '+esc(d.provider)+'/'+esc(d.model)+(transient?' Extraction is active for this process, but the setting could not be saved for restart. Set ENGRAPHIS_EXTRACTOR=llm_structured and ENGRAPHIS_LLM_AUTO_EXTRACT=1 in the deployment environment.':'');setTone(r,transient?'red':'green')}else{r.textContent='✗ '+(d.error||'failed');setTone(r,'red')}}}catch(e){if(r){r.textContent='✗ '+esc(e.message);setTone(r,'red')}}} - -async function loadHostedAgentAccess(){const el=document.getElementById('tokens-body');if(!el)return;const teamCta=hostedCta('team','agent_access');try{const st=await api('/auth/state');if(teamCta.href==='#'&&st&&st.cloud_url)teamCta.href=safeUrl(st.cloud_url)}catch(e){}el.innerHTML=`
Per-member agent accounts, roles, named seats, and rotating device credentials are managed in Team Cloud, not by this local dashboard.
${ctaLinkHtml(teamCta,'btn btn-primary btn-sm','agent_access')}Agent Connect guide
`} -const setLlmExtractorBase=setLlmExtractor; -setLlmExtractor=async function(on){if(on&&!await confirmAction('Turn on LLM extraction',EXTERNAL_LLM_PRIVACY_COPY,'Turn on'))return;return setLlmExtractorBase(on)}; -/* Route by cause, exactly like loadAnalytics/loadAutomation. Rendering the purchase panel for - every failure told a paying customer to buy the plan they already own whenever the network - blipped or the cloud answered 5xx. Only 402 is an entitlement answer; 401/403 require reconnecting. */ -async function loadSyncStatus(){try{const d=await api('/sync/status');renderSync(d)}catch(e){const el=document.getElementById('sync-body');if(!el)return;if(managedConsentRequired(e))el.innerHTML=managedConsentHtml('Cloud Sync');else if(hostedFeatureUnavailable(e))el.innerHTML=unlockHtml('Cloud Sync','pro');else el.innerHTML='
'+esc(e.message)+'
'}} -function syncTotalAuthorizationDenial(last){if(!last||!(Number(last.attempted)>0)||Number(last.succeeded)!==0)return false;const errors=Array.isArray(last.errors)?last.errors:[];return errors.length===Number(last.attempted)&&errors.every(error=>[401,402,403].includes(Number(error&&error.status)))} -function syncRecoveryHtml(){return unlockHtml('Cloud Sync','pro')+`
`} -function syncReconnectHtml(){return `
Cloud Sync is not authorized for this installation. Reconnect in Engraphis Cloud or contact your administrator.
`} -function syncDenialHtml(last){const errors=Array.isArray(last&&last.errors)?last.errors:[];return errors.some(error=>[401,403].includes(Number(error&&error.status)))?syncReconnectHtml():syncRecoveryHtml()} -function renderSync(d){const el=document.getElementById('sync-body');if(!el)return;d=d||{};const last=d.last;if(!d.available){el.innerHTML=unlockHtml('Cloud Sync','pro');return}if (syncTotalAuthorizationDenial(last)) { el.innerHTML = syncDenialHtml(last); return; }let status='Cloud session connected; no sync recorded on this installation.';if(last){const when=new Date((last.at||0)*1000).toLocaleString();status='Last synced '+when+' — pushed '+(last.exported||0)+', +'+(last.added||0)+' received'+((last.errors&&last.errors.length)?' · '+last.errors.length+' issue(s)':'')+'.'}el.innerHTML=`
Hosted relayCONNECTED
Relay storage and authorization run in Engraphis Cloud. This package contains only the customer client; it does not run a local relay or background scheduler.
${esc(status)}
`} -async function syncNow(){const b=document.getElementById('sync-btn')||document.getElementById('sync-retry-btn');const original=b&&b.textContent;const s=document.getElementById('sync-status');if(b){b.disabled=true;b.textContent='Syncing…'}if(s)s.textContent='Contacting the cloud…';try{const d=await api('/sync/run',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'});const su=d.summary||{};toast('Synced — pushed '+(su.exported||0)+', '+(su.added||0)+' new from other devices','ok');await loadSyncStatus()}catch(e){const status=Number(e&&e.status);if(status===402){const el=document.getElementById('sync-body');if(el)el.innerHTML=syncRecoveryHtml();toast('Cloud Sync requires an active Pro or Team entitlement — open Engraphis Cloud to upgrade or renew.','err')}else if(status===401||status===403){const el=document.getElementById('sync-body');if(el)el.innerHTML=syncReconnectHtml();toast('Cloud Sync authorization is no longer active — reconnect in Engraphis Cloud.','err')}else{toast('Sync failed: '+e.message,'err');if(s)s.textContent='Sync failed — try again.'}}finally{if(b){b.disabled=false;b.textContent=original||'Sync now'}}} - -const syncNowBase=syncNow; -syncNow=async function(){if(!await confirmCloudTransfer('Sync shared workspaces','Cloud Sync sends eligible changes from your shared workspaces to Engraphis Cloud and receives authorized changes from your other installations; secret and session-scoped rows stay local.','Sync now',CLOUD_SYNC_PRIVACY_COPY))return;return syncNowBase()} - -/* ─── knowledge graph (force-graph + d3-force: compact defaults and selectable layouts) ─── */ -let GRAPH=null, FG=null, GRAPH_ENGINE=null, GRESIZE=false, GRESIZEFRAME=0, GADJ={}, GCOMM_ADJ={}, GCOMPONENTS={}, GCOMPONENT_LAYOUT=null, GHILITE=null, GHOVERSET=null, GLABELRANK={}, GLABELBOXES=[], GDATA_CACHE=null, GACTIVE_DATA=null, GREDRAWFRAME=0, GPERF={large:false,dense:false}, GRAPH_LOAD_REQUEST=0, GRAPH_LOAD_CONTROLLER=null; -const GRAPH_PRESETS={ - original:{label:'Original force',repel:120,link:30,gravity:14,font:13,size:3,linkw:1,labelDensity:40,curve:0,particles:0}, - compact:{label:'Compact clusters',repel:42,link:20,gravity:26,font:12,size:3,linkw:.7,labelDensity:30,curve:.08,particles:0}, - communities:{label:'Community islands',repel:48,link:16,gravity:48,font:12,size:3,linkw:.72,labelDensity:24,curve:.12,particles:0}, - radial:{label:'Radial orbit',repel:68,link:26,gravity:12,font:13,size:3,linkw:.75,labelDensity:55,curve:.22,particles:0}, - constellation:{label:'Constellation flow',repel:34,link:16,gravity:38,font:12,size:3,linkw:.65,labelDensity:35,curve:.32,particles:2}, - custom:{label:'Custom tuning',curve:.1,particles:0} -}; -window.GSET=window.GSET||{mode:'communities',font:12,size:3,repel:48,link:16,gravity:48,labels:false,linkw:.72,labelDensity:24,flow:true,frozen:false}; -// Freeze is session-only. Never let a pre-existing dashboard state make a newly opened graph -// look broken: every Classic graph begins with physics live until its visible switch is clicked. -window.GSET.frozen=false; -/* Keep legacy Classic geometry in the same compact world-space range as Ledger. The old - `size * sqrt(1 + degree)` rule let a highly connected entity become a giant disc, then - zoom-to-fit magnified that disc again. Degree still adds a restrained emphasis, but it is - normalized and bounded so material/theme painters cannot change node geometry. */ -function graphNodeRadius(node,base,metric){ - const size=Number.isFinite(+base)&&+base>0?+base:3; - const normalized=Math.max(0,Math.min(1,Number(metric)||0)); - const radius=size*.45*(.6+Math.min(1.6,normalized*1.9)); - return Math.max(.25,Math.min(size*1.1,radius)); -} -const ETYPE_TOKEN={person_or_concept:'--entity-concept',mention:'--entity-mention',hashtag:'--entity-hashtag',email:'--entity-email',organization:'--entity-organization',location:'--entity-location'}; -const GRAPH_PALETTES={ - theme:null, - aurora:{person_or_concept:'#8b7cf6',mention:'#2dd4bf',hashtag:'#fbbf24',email:'#60a5fa',organization:'#f472b6',location:'#a3e635'}, - ocean:{person_or_concept:'#38bdf8',mention:'#2dd4bf',hashtag:'#facc15',email:'#818cf8',organization:'#22d3ee',location:'#34d399'}, - ember:{person_or_concept:'#f97316',mention:'#fb7185',hashtag:'#facc15',email:'#a78bfa',organization:'#ef4444',location:'#84cc16'}, - contrast:{person_or_concept:'#0072b2',mention:'#009e73',hashtag:'#e69f00',email:'#56b4e9',organization:'#cc79a7',location:'#d55e00'} -}; -const GRAPH_COLOR_KEY='engraphis-graph-colors-v1'; -let GCOLOR_OVERRIDES={}, GCOLOR_PALETTE='theme'; -function cssvar(name,fallback){const value=getComputedStyle(document.body).getPropertyValue(name).trim();return value||fallback} -function graphValidColor(color){return /^#[0-9a-f]{6}$/i.test(color||'')} -function graphTypeLabel(type){return String(type||'entity').replace(/_/g,' ')} -function graphLoadColorPreferences(){ - try{ - const saved=JSON.parse(localStorage.getItem(GRAPH_COLOR_KEY)||'{}'),colors=saved&&saved.colors; - if(colors&&typeof colors==='object')Object.entries(colors).forEach(([type,color])=>{if(graphValidColor(color))GCOLOR_OVERRIDES[type]=color.toLowerCase()}); - if(saved&&saved.palette&&Object.prototype.hasOwnProperty.call(GRAPH_PALETTES,saved.palette))GCOLOR_PALETTE=saved.palette; - else if(Object.keys(GCOLOR_OVERRIDES).length)GCOLOR_PALETTE='custom'; - }catch(e){GCOLOR_OVERRIDES={};GCOLOR_PALETTE='theme'} -} -function graphSaveColorPreferences(){try{localStorage.setItem(GRAPH_COLOR_KEY,JSON.stringify({palette:GCOLOR_PALETTE,colors:GCOLOR_OVERRIDES}))}catch(e){}} -function graphTypeColor(type){if(GCOLOR_OVERRIDES[type])return GCOLOR_OVERRIDES[type];if(typeof GSTYLE!=='undefined'&&GSTYLE&&GSTYLE!=='classic'&&STYLE_PAL[GSTYLE]&&STYLE_PAL[GSTYLE][type])return STYLE_PAL[GSTYLE][type];return cssvar(ETYPE_TOKEN[type]||'--entity-concept',cssvar('--color-accent','#8c83e8'))} -/* The engine renders to a canvas, so it cannot read `--entity-*` itself the way the legend and - the controls do. Resolve the active theme's values here and hand them over; without this the - opt-in canvas keeps dark-theme node colours after a switch to Light/Solarized/Sepia. */ -function graphThemeTypeColors(){const colors={},fallback=cssvar('--color-accent','#8c83e8');Object.keys(ETYPE_TOKEN).forEach(type=>{colors[type]=cssvar(ETYPE_TOKEN[type],fallback)});colors.accent=fallback;colors.surface=cssvar('--color-panel','#15181e');colors.canvas=cssvar('--color-canvas','#0e1014');colors.relation_label=cssvar('--color-text-dim','#7e8795');colors.label=cssvar('--color-text','#e7e9ee');return colors} -/* Co-occurrence is implicit graph structure, not useful canvas text. */ -function graphShowRelationLabel(label){return !!label&&String(label).toLowerCase()!=='co_occurs'} -function graphContrastColor(color){if(!graphValidColor(color))return cssvar('--color-canvas','#0e1014');const n=parseInt(color.slice(1),16),lum=.2126*(n>>16)+.7152*((n>>8)&255)+.0722*(n&255);return lum>150?'#111827':'#f8fafc'} -const ETYPE_COLOR=new Proxy({},{get:(_,type)=>graphTypeColor(type)}); -graphLoadColorPreferences(); -function graphUpdateColorSwatches(){} -function graphRefreshNodeColors(){ - const nodes=FG&&FG.graphData?FG.graphData().nodes||[]:[]; - nodes.forEach(node=>{node.color=graphNodeColor(node);node.stroke=graphContrastColor(node.color)}); - graphUpdateColorSwatches(); - graphRedraw(); -} -function renderGraphColorControls(){ - const box=document.getElementById('graph-color-controls'),picker=document.getElementById('graph-palette'); - if(picker)picker.value=GCOLOR_PALETTE; - if(!box)return; - const types=GRAPH&&GRAPH.types&&GRAPH.types.length?GRAPH.types.map(item=>item.etype):Object.keys(ETYPE_TOKEN); - box.innerHTML=types.map(type=>{const label=graphTypeLabel(type),color=graphTypeColor(type);return ``}).join(''); -} -function graphSetTypeColor(type,color,persist){ - if(!type||!graphValidColor(color))return; - GCOLOR_OVERRIDES[type]=color.toLowerCase();GCOLOR_PALETTE='custom'; - graphClearMaterialCache(); - const picker=document.getElementById('graph-palette');if(picker)picker.value='custom'; - if(GRAPH_ENGINE){GRAPH_ENGINE.setTypeColor(type,color);if(persist)graphSaveColorPreferences();return} - graphRefreshNodeColors(); - if(persist)graphSaveColorPreferences(); -} -function graphApplyPalette(name){ - if(!Object.prototype.hasOwnProperty.call(GRAPH_PALETTES,name))name='theme'; - GCOLOR_PALETTE=name;GCOLOR_OVERRIDES=GRAPH_PALETTES[name]?{...GRAPH_PALETTES[name]}:{}; - graphSaveColorPreferences();graphRecolor(); -} -function graphResetColors(){graphApplyPalette('theme');toast('Node colors reset to the active theme','ok')} -function graphInjectCss(){} -function prefersReducedMotion(){return matchMedia('(prefers-reduced-motion: reduce)').matches} -function graphSetLayoutStatus(text,busy){ - for(const id of ['graph-layout-status','galaxy-layout-status']){const status=document.getElementById(id);if(status){status.textContent=text;status.classList.toggle('busy',!!busy)}} - for(const id of ['graph-net','galaxy-net']){const net=document.getElementById(id);if(net)net.setAttribute('aria-busy',String(!!busy))} -} -function graphSetSimulationStatus(text,busy){ - graphSetLayoutStatus(text,busy) -} -function graphUpdateHud(data){ - const mode=document.getElementById('graph-hud-mode'),count=document.getElementById('graph-hud-count'),badge=document.getElementById('graph-performance-badge'); - const preset=GRAPH_PRESETS[window.GSET.mode]||GRAPH_PRESETS.compact; - if(mode)mode.textContent=preset.label||'Custom graph'; - if(count&&data)count.textContent=data.nodes.length.toLocaleString()+' entities · '+data.links.length.toLocaleString()+' relations'; - if(badge)badge.textContent=GPERF.large?'Large graph mode':'Adaptive rendering'; -} -/* ── canonical graph renderer ──────────────────────────────────────────────────────────── - Classic now uses the same renderer as Ledger for its normal graph view. The legacy canvas - remains the rollback path: one engine failure latches GRAPH_ENGINE_FAILED and the graph - degrades to the legacy renderer instead of taking the view down. */ -let GRAPH_ENGINE_FAILED=false; -function graphEngineEnabled(){ - if(GRAPH_ENGINE_FAILED)return false; - try{return new URLSearchParams(window.location.search).get('graph-engine')==='next'||/(^|\/)classic\/?$/.test(window.location.pathname)}catch(e){return false} -} -function graphEngineFallback(error){ - GRAPH_ENGINE_FAILED=true; - try{if(GRAPH_ENGINE)GRAPH_ENGINE.destroy()}catch(e){} - GRAPH_ENGINE=null; - /* The classic renderer skips seeding when GACTIVE_DATA still points at the current data, - so a failure *after* a successful engine render would hand it an empty canvas. Clearing - the marker makes the very next graphRender() a full classic build. */ - GACTIVE_DATA=null;GCOMPONENT_LAYOUT=null;GHILITE=null;GHOVERSET=null; - if(window.console&&console.warn)console.warn('graph-engine=next failed; falling back to the classic renderer',error); -} -/* Explicit user-initiated Reload (boot()) must not be haunted by a prior failed attempt: - drop the quality-engine failure latch and any cached failed promises so the next render - retries with a fresh URL. Mirrors the per-loader cleanup that runs on each rejection. */ -function graphResetEngineFailure(){GRAPH_ENGINE_FAILED=false;FORCE_GRAPH_LOADING=null;FORCE_GRAPH_RETRY=0;GRAPH_ENGINE_LOADING=null;GRAPH_ENGINE_RETRY=0;try{if(GRAPH_ENGINE)GRAPH_ENGINE.destroy()}catch(e){}GRAPH_ENGINE=null;GACTIVE_DATA=null;} -function graphEngineEmptyMessage(){ - const total=(GRAPH&&GRAPH.nodes&&GRAPH.nodes.length)||0; - return total?('No connected entities — tick "Show unlinked" to see all '+total+'.'):'No entities in this workspace yet.'; -} -function graphRenderEngine(data,fit,reheat){ - const element=document.getElementById('graph-net'),empty=document.getElementById('graph-empty'); - if(!element||typeof EngraphisGraph==='undefined')return false; - try{ - if(!data.nodes.length){ - if(GRAPH_ENGINE)GRAPH_ENGINE.setData({nodes:[],links:[]}); - showAs(empty,true,'flex'); - if(empty)empty.textContent=graphEngineEmptyMessage(); - GACTIVE_DATA=null;graphSetLayoutStatus('No entities',false);return true; - } - showAs(empty,false);GPERF={large:data.nodes.length>600||data.links.length>2400,dense:data.links.length>1500}; - const created=!GRAPH_ENGINE; - if(created){ - GRAPH_ENGINE=EngraphisGraph.create(element,{ - renderMode:'overview', - reducedMotion:prefersReducedMotion, - onNodeClick:node=>{syncGraphExplorerSelection(node.id);graphNodeClick(node.label||node.name||node.id)}, - onBackgroundClick:()=>graphSetHighlight(null), - onStats:stats=>{const count=document.getElementById('graph-hud-count');if(count)count.textContent=stats.nodes.toLocaleString()+' entities · '+stats.links.toLocaleString()+' relations'} - }); - } - /* Re-seeding identical data would re-copy every node and throw its x/y away, restarting - the layout on each slider or preset change. The classic path guards the same way. */ - const dataChanged=created||GACTIVE_DATA!==data; - const layers={};document.querySelectorAll('#graph-layer-filters input').forEach(input=>{layers[input.value]=input.checked}); - /* "Show unlinked nodes" is applied twice: graphData() decides what is handed over, and the - engine re-filters by degree on its own state. Leaving the engine on its defaults - (showUnlinked:false, minDegree:1) drops every degree-zero entity graphData() just supplied, - so the checkbox appeared to do nothing under ?graph-engine=next. */ - const isolated=document.getElementById('graph-show-iso'),showUnlinked=!!(isolated&&isolated.checked); - GRAPH_ENGINE.apply(engine=>{ - engine.setSettings({...window.GSET}); - if(typeof engine.setRenderMode==='function')engine.setRenderMode('overview'); - engine.setStyle(typeof GSTYLE!=='undefined'?GSTYLE:'cyber'); - engine.setColorBy(typeof GCOLORBY!=='undefined'?GCOLORBY:'community'); - engine.setThemeColors(graphThemeTypeColors()); - engine.setPalette(typeof GCOLOR_PALETTE!=='undefined'?GCOLOR_PALETTE:'theme'); - engine.setTypeColors(GCOLOR_OVERRIDES||{}); - engine.setLayers(layers); - engine.setScope({showUnlinked,minDegree:showUnlinked?0:1}); - if(dataChanged)engine.setData(data); - },fit,reheat); - /* Mirror the engine's clustering back onto the dashboard's own node objects, or the - cluster legend (which reads GACTIVE_DATA) reports one community for the whole store. */ - const communityMap=GRAPH_ENGINE.communityMap(); - data.nodes.forEach(node=>{node.community=communityMap[node.id]||0}); - GACTIVE_DATA=data;graphSyncReadouts();graphUpdateEditedBadge();graphUpdateHud(data);graphRenderLegend(GRAPH); - if(dataChanged)graphSetHighlight(null); - if(window.GSET.frozen)GRAPH_ENGINE.freeze(true); - /* The renderer can be born after the user has already left the view: /graph and both lazy - scripts resolve asynchronously, and the pause on nav-away ran while GRAPH_ENGINE was still - null. Re-apply the parked state here so a renderer created against a hidden pane never - starts a rAF that nothing will stop. */ - if(GRAPH_ENGINE_PARKED)GRAPH_ENGINE.pause(); - graphSetSimulationStatus(window.GSET.frozen?'Layout frozen':'Adaptive layout',false); - return true; - }catch(error){ - graphEngineFallback(error); - return false; - } -} -/* Nav away from the graph view: park the engine's animation frame. Without this the opt-in - renderer keeps repainting a hidden canvas for the rest of the session. - The intent is *recorded* as well as applied, because pausing an engine that does not exist - yet is a no-op: leaving Graph before /graph (or either lazy script) resolves would otherwise - let the pending callback create and start a renderer against a hidden pane with no later - pause to stop it. graphRenderEngine() re-applies GRAPH_ENGINE_PARKED for that case. */ -let GRAPH_ENGINE_PARKED=false; -function graphEnginePause(){GRAPH_ENGINE_PARKED=true;try{if(GRAPH_ENGINE)GRAPH_ENGINE.pause()}catch(e){}} -function graphEngineResume(){GRAPH_ENGINE_PARKED=false;try{if(GRAPH_ENGINE)GRAPH_ENGINE.resume()}catch(e){}} -function graphInvalidateData(){ - if(GRAPH_ENGINE){try{GRAPH_ENGINE.destroy()}catch(e){}GRAPH_ENGINE=null} - GDATA_CACHE=null;GACTIVE_DATA=null;GCOMPONENT_LAYOUT=null;GHILITE=null;GHOVERSET=null -} -async function loadLegacyGraph(){ - const request=++GRAPH_LOAD_REQUEST; - const previousController=GRAPH_LOAD_CONTROLLER,controller=new AbortController();GRAPH_LOAD_CONTROLLER=controller; - if(previousController&&!previousController.signal.aborted)previousController.abort(); - /* Transactional reload: keep the existing graph visible until the replacement payload - succeeds. Only invalidate caches (not the rendered graph) so a network failure leaves - the user looking at the previous data rather than an error screen. */ - const previousGraph=GRAPH,previousEngine=GRAPH_ENGINE,previousActive=GACTIVE_DATA; - GDATA_CACHE=null; - const empty=document.getElementById('graph-empty'),net=document.getElementById('graph-net'),nodesBox=document.getElementById('graph-entity-list'),edgesBox=document.getElementById('graph-relation-list'); - showAs(empty,true,'flex');empty.textContent='Loading graph…';graphSetLayoutStatus('Loading data',true); - if(net)net.setAttribute('aria-busy','true'); - if(!GRESIZE){ - GRESIZE=true; - window.addEventListener('resize',()=>{ - if((!FG&&!GRAPH_ENGINE)||GRESIZEFRAME)return; - GRESIZEFRAME=requestAnimationFrame(()=>{GRESIZEFRAME=0;const element=document.getElementById('graph-net');if(GRAPH_ENGINE)GRAPH_ENGINE.resize();else if(FG&&element)FG.width(element.clientWidth).height(element.clientHeight)}); - }); - } - const layerInputs=Array.from(document.querySelectorAll('#graph-layer-filters input')),selectedLayers=layerInputs.filter(input=>input.checked).map(input=>input.value),layerFilter=selectedLayers.length===layerInputs.length?'':'&layers='+encodeURIComponent(selectedLayers.join(',')),includeCode=document.getElementById('graph-include-code').checked,repo=(document.getElementById('graph-repo-filter').value||'').trim(),showUnlinked=!!document.getElementById('graph-show-iso').checked; - try{ - const query=encodeURIComponent(WS||'')+(repo?'&repo='+encodeURIComponent(repo):'')+layerFilter; - const nextGraph=await api('/graph?workspace='+query+'&include_code='+(includeCode?'true':'false')+'&limit=1000&node_limit=1000&edge_limit=2000'+(showUnlinked?'':'&connected_only=true'),{signal:controller.signal}); - if(request!==GRAPH_LOAD_REQUEST)return; - /* Commit: the new payload arrived successfully. Now tear down the old renderer and - install the replacement graph. */ - if(previousEngine){try{previousEngine.destroy()}catch(e){}} - GRAPH_ENGINE=null;GACTIVE_DATA=null;GCOMPONENT_LAYOUT=null;GHILITE=null;GHOVERSET=null; - GRAPH=nextGraph; - renderGraphSide();renderGraphExplorer();graphRender(); - }catch(error){ - if(request!==GRAPH_LOAD_REQUEST||error.name==='AbortError')return; - /* Rollback: restore the previous graph so the user is not left staring at an error. - If there was no previous graph (first load), show the error message. */ - if(previousGraph){ - GRAPH=previousGraph;GRAPH_ENGINE=previousEngine;GACTIVE_DATA=previousActive; - showAs(empty,false);graphSetLayoutStatus('Reload failed — showing previous data',false); - toast('Reload data failed: '+error.message,'err'); - }else{ - showAs(empty,true,'flex');empty.textContent='Graph failed: '+error.message;graphSetLayoutStatus('Load failed',false); - } - }finally{ - if(request!==GRAPH_LOAD_REQUEST)return; - if(GRAPH_LOAD_CONTROLLER===controller)GRAPH_LOAD_CONTROLLER=null; - if(net)net.setAttribute('aria-busy','false'); - if(!GRAPH){ - if(FG)FG.graphData({nodes:[],links:[]}); - const message=empty.textContent||'Graph data unavailable.'; - if(nodesBox)nodesBox.innerHTML='
'+esc(message)+'
'; - if(edgesBox)edgesBox.innerHTML='
Relations are unavailable until graph data loads.
'; - } - } -} -function graphData(){ - const _si=document.getElementById('graph-show-iso');const hideIso=!(_si&&_si.checked); - if(GDATA_CACHE&&GDATA_CACHE.graph===GRAPH&&GDATA_CACHE.hideIso===hideIso)return GDATA_CACHE.data; - let sourceNodes=GRAPH.nodes;if(hideIso)sourceNodes=sourceNodes.filter(node=>node.degree>0); - const names=new Set(sourceNodes.map(node=>node.id)); - const nodes=sourceNodes.map(node=>({id:node.id,label:node.label||node.id,displayLabel:(node.label||node.id).length>30?(node.label||node.id).slice(0,29)+'…':(node.label||node.id),etype:node.etype,degree:node.degree||0,val:1+(node.degree||0)})); - const maxDegree=Math.max(1,...nodes.map(node=>node.degree||0)); - nodes.sort((a,b)=>b.degree-a.degree).forEach((node,index)=>{node.rank=index;node.hub=index<24;node.radius=graphNodeRadius(node,window.GSET.size,(node.degree||0)/maxDegree);node.color=graphTypeColor(node.etype);node.stroke=graphContrastColor(node.color)}); - const links=GRAPH.edges.filter(edge=>names.has(edge.from)&&names.has(edge.to)).map(edge=>({source:edge.from,target:edge.to,label:edge.label,layer:edge.layer||'semantic'})); - const data={nodes,links};GDATA_CACHE={graph:GRAPH,hideIso,data};return data; -} -function buildAdj(links){ - GADJ={};GCOMM_ADJ={}; - links.forEach(link=>{ - const source=(link.source&&link.source.id)||link.source,target=(link.target&&link.target.id)||link.target; - (GADJ[source]=GADJ[source]||new Set()).add(target);(GADJ[target]=GADJ[target]||new Set()).add(source); - GCOMM_ADJ[source]=GCOMM_ADJ[source]||new Set();GCOMM_ADJ[target]=GCOMM_ADJ[target]||new Set(); - // Influence links often connect otherwise distinct bodies of work. Keep them - // visible, but do not let a few such bridges collapse all communities into one. - if(link.label!=='influences'){ - (GCOMM_ADJ[source]=GCOMM_ADJ[source]||new Set()).add(target);(GCOMM_ADJ[target]=GCOMM_ADJ[target]||new Set()).add(source); - } - }); -} -function graphIndexComponents(nodes){ - const seen=new Set(),components=[]; - nodes.forEach(node=>{ - if(seen.has(node.id))return; - const ids=[],stack=[node.id];seen.add(node.id); - while(stack.length){const id=stack.pop();ids.push(id);(GADJ[id]||[]).forEach(next=>{if(!seen.has(next)){seen.add(next);stack.push(next)}})} - components.push(ids); - }); - components.sort((a,b)=>b.length-a.length); - GCOMPONENTS={}; - const cols=Math.max(1,Math.ceil(Math.sqrt(components.length))),gap=Math.max(84,window.GSET.link*4); - components.forEach((ids,index)=>{ - const row=Math.floor(index/cols),col=index%cols,used=Math.min(cols,components.length-row*cols); - const x=(col-(used-1)/2)*gap,y=(row-(Math.ceil(components.length/cols)-1)/2)*gap; - ids.forEach(id=>{GCOMPONENTS[id]={index,size:ids.length,x,y}}); - }); -} -function graphIndexCommunities(nodes){ - const groups={}; - nodes.forEach(node=>{const key=Number.isFinite(node.community)?node.community:0;(groups[key]=groups[key]||[]).push(node);}); - const communities=Object.entries(groups).sort((a,b)=>b[1].length-a[1].length); - const cols=Math.max(1,Math.ceil(Math.sqrt(communities.length))),gap=Math.max(150,window.GSET.link*9); - GCOMPONENTS={}; - communities.forEach(([key,members],index)=>{ - const row=Math.floor(index/cols),col=index%cols,used=Math.min(cols,communities.length-row*cols); - const x=(col-(used-1)/2)*gap,y=(row-(Math.ceil(communities.length/cols)-1)/2)*gap; - members.forEach(node=>{GCOMPONENTS[node.id]={index,size:members.length,x,y,community:Number(key)};}); - }); -} -function graphRefreshComponentCenters(nodes,force=false){ - const layout=window.GSET.mode+'|'+window.GSET.link; - if(!force&&GCOMPONENT_LAYOUT===layout)return; - if(window.GSET.mode==='communities')graphIndexCommunities(nodes);else graphIndexComponents(nodes);GCOMPONENT_LAYOUT=layout; -} -function graphAlpha(color,alpha){ - const hex=/^#([0-9a-f]{6})$/i.exec(color||''); - if(hex){const value=parseInt(hex[1],16);return `rgba(${value>>16},${(value>>8)&255},${value&255},${alpha})`} - const rgb=(color||'').match(/\d+(?:\.\d+)?/g); - return rgb&&rgb.length>=3?`rgba(${rgb[0]},${rgb[1]},${rgb[2]},${alpha})`:color; -} -function graphReadThemeColors(){ - const layers=(typeof GSTYLE!=='undefined'&&GSTYLE&&GSTYLE!=='classic'&&STYLE_LAYERS[GSTYLE])?Object.assign({},STYLE_LAYERS[GSTYLE]):{temporal:cssvar('--color-info','#6f9fd8'),entity:cssvar('--color-teal','#5aafb3'),causal:cssvar('--color-warning','#d7a84b'),semantic:cssvar('--color-accent','#8c83e8')}; - const baseAlpha=GPERF.dense?.18:.32,links={}; - Object.entries(layers).forEach(([layer,color])=>{links[layer]={base:graphAlpha(color,baseAlpha),active:graphAlpha(color,.82),dim:graphAlpha(color,.035)}}); - return {label:cssvar('--color-text','#e7e9ee'),dim:cssvar('--color-text-dim','#7e8795'),accent:cssvar('--color-accent','#8c83e8'),panel:cssvar('--color-panel','#15181e'),canvas:cssvar('--color-canvas','#0e1014'),layers,links}; -} -/* ─── graph aesthetic styles (Galaxy · Solar system · Cyberpunk), additive ─── */ -var STYLE_PAL={ - galaxy:{person_or_concept:'#b789ff',mention:'#7bb4ff',hashtag:'#ffcf6b',email:'#8aa2ff',organization:'#66e0d0',location:'#ff7ea8'}, - solar:{person_or_concept:'#ffb454',mention:'#3fd2c7',hashtag:'#ffd68a',email:'#8ea8ff',organization:'#5b9bff',location:'#ff8f6b'}, - cyber:{person_or_concept:'#ff3ea5',mention:'#b6ff3c',hashtag:'#ffe14d',email:'#8b7bff',organization:'#22e0ff',location:'#ff5c7a'} -}; -var STYLE_LAYERS={ - galaxy:{temporal:'#7bb4ff',entity:'#66e0d0',causal:'#ffcf6b',semantic:'#b789ff'}, - solar:{temporal:'#5b9bff',entity:'#3fd2c7',causal:'#ffb454',semantic:'#ffd68a'}, - cyber:{temporal:'#22e0ff',entity:'#b6ff3c',causal:'#ffe14d',semantic:'#ff3ea5'} -}; -var STYLE_BG={ - classic:'', - galaxy:'radial-gradient(58% 50% at 24% 22%,rgba(126,64,208,.30),transparent 66%),radial-gradient(52% 58% at 82% 78%,rgba(220,72,164,.20),transparent 68%),radial-gradient(46% 52% at 62% 42%,rgba(58,120,224,.16),transparent 70%),#06040f', - solar:'radial-gradient(40% 48% at 50% 50%,rgba(255,184,92,.16),transparent 60%),radial-gradient(90% 90% at 50% 50%,rgba(18,32,64,.55),transparent 82%),#05070d', - cyber:'linear-gradient(rgba(34,224,255,.055) 1px,transparent 1px) 0 0/30px 30px,linear-gradient(90deg,rgba(34,224,255,.055) 1px,transparent 1px) 0 0/30px 30px,radial-gradient(72% 60% at 50% 0%,rgba(255,62,165,.12),transparent 72%),#050810' -}; -var GSTYLE='cyber';try{var _gs=localStorage.getItem('engraphis-graph-style');if(_gs)GSTYLE=_gs;}catch(e){} -function graphMakeStars(){var a=[],c=['#dfe6ff','#dfe6ff','#c9b6ff','#a7c6ff','#ffd9ef'];for(var i=0;i<110;i++){a.push({x:(Math.random()-.5)*1200,y:(Math.random()-.5)*1200,r:Math.random()*1.1+.25,a:Math.random()*.7+.25,tw:Math.random()*1.6+.4,ph:Math.random()*6.28,c:c[i%c.length]});}return a;} -var GSTARS=graphMakeStars(); -function graphRgb(c){if(!c)return[140,131,232];if(c[0]==='#'){var h=c.length===4?c[1]+c[1]+c[2]+c[2]+c[3]+c[3]:c.slice(1,7),n=parseInt(h,16);if(Number.isFinite(n))return[n>>16&255,n>>8&255,n&255]}var m=c.match(/\d+/g);return m&&m.length>=3?[+m[0],+m[1],+m[2]]:[140,131,232]} -function graphMix(a,b,amount){var A=graphRgb(a),B=graphRgb(b),t=Math.max(0,Math.min(1,amount));return'rgb('+Math.round(A[0]+(B[0]-A[0])*t)+','+Math.round(A[1]+(B[1]-A[1])*t)+','+Math.round(A[2]+(B[2]-A[2])*t)+')'} -const GRAPH_MATERIAL_CACHE_LIMIT=192,GRAPH_MATERIAL_CACHE=new Map(); -const GRAPH_MATERIAL_RADIUS={signature:5,bezel:12,full:40}; -let GRAPH_MATERIAL_DPR=null; -function graphMaterialTier(screenRadius,forceLow){if(forceLow||!Number.isFinite(+screenRadius)||+screenRadius<6)return'signature';return+screenRadius<12?'bezel':'full'} -function graphMaterialProfile(style,col){ - var theme=window.GCOL||graphReadThemeColors(),surface=theme.panel||theme.surface||theme.canvas||'#0e1014'; - style=['cyber','galaxy','solar','classic'].includes(style)?style:'classic'; - var substrate=graphMix(surface,'#02050a',style==='classic'?.68:.78),identity='rgb('+graphRgb(col).join(',')+')',base={styleName:style,substrate:substrate,identity:identity,identityKey:graphRgb(col).join(','),substrateKey:graphRgb(substrate).join(',')}; - if(style==='cyber'){var cyber={cyan:'#21dff3',blue:'#367cff',violet:'#8d61ff',magenta:'#ec4fc4',teal:'#4ce4cf'};return Object.assign(base,{family:'iridescent-pvd',fixedPalette:cyber,film:cyber,outer:graphMix(substrate,'#01040a',.82),bezel:graphMix(substrate,'#101626',.46),face:graphMix(substrate,'#182237',.48),edge:'#677386',sheen:'#8d61ff'})} - if(style==='galaxy'){var galaxy={navy:'#111a3b',blue:'#3979e8',violet:'#8d68df',highlight:'#aab9ee'};return Object.assign(base,{family:'anodized-alloy',fixedPalette:galaxy,outer:graphMix(substrate,'#02040d',.76),bezel:graphMix(substrate,'#151a34',.54),face:graphMix(substrate,galaxy.navy,.68),edge:'#7587bb',sheen:galaxy.blue})} - if(style==='solar'){var solar={ember:'#713018',copper:'#b85c2f',amber:'#f18a32',gold:'#ffc46b',shadow:'#2b1008'};return Object.assign(base,{family:'brushed-copper',fixedPalette:solar,outer:graphMix(substrate,'#0a0402',.72),bezel:graphMix(substrate,'#351609',.62),face:graphMix(substrate,solar.copper,.48),edge:solar.amber,sheen:solar.gold})} - var classic={charcoal:'#242d36',steel:'#778593',highlight:'#c0c9cf',coolEdge:'#8aa7bd'};return Object.assign(base,{family:'satin-gunmetal',fixedPalette:classic,outer:graphMix(substrate,'#05080b',.68),bezel:graphMix(substrate,'#20272e',.52),face:graphMix(substrate,classic.charcoal,.72),edge:classic.coolEdge,sheen:classic.highlight}) -} -function graphMaterialFill(ctx,x,y,r,fill){ctx.beginPath();ctx.arc(x,y,Math.max(.1,r),0,6.2832);ctx.fillStyle=fill;ctx.fill()} -function graphMaterialStroke(ctx,x,y,r,stroke,width){ctx.beginPath();ctx.arc(x,y,Math.max(.1,r),0,6.2832);ctx.lineWidth=width;ctx.strokeStyle=stroke;ctx.stroke()} -function graphMaterialGradient(ctx,kind,args,stops){var maker=ctx[kind];if(typeof maker!=='function')return stops[Math.floor(stops.length/2)][1];var result=maker.apply(ctx,args);stops.forEach(stop=>result.addColorStop(stop[0],stop[1]));return result} -function graphMaterialIdentity(ctx,x,y,r,profile,strength){graphMaterialStroke(ctx,x,y,r*.955,graphAlpha(profile.identity,strength),Math.max(.32,r*.045))} -function graphMaterialHalo(ctx,x,y,r,tier,color,opacity,shiftX,shiftY){ - if(tier==='signature')return; - var reach=tier==='full'?1.12:1.14,halo=graphMaterialGradient(ctx,'createRadialGradient',[x+r*(shiftX||0),y+r*(shiftY||0),r*.48,x,y,r*reach],[[0,graphAlpha(color,opacity)],[.68,graphAlpha(color,opacity*.42)],[1,graphAlpha(color,0)]]); - graphMaterialFill(ctx,x,y,r*reach,halo); -} -function graphDirectionalBrush(ctx,x,y,r,angle,dark,light,strength){ - if(typeof ctx.moveTo!=='function'||typeof ctx.lineTo!=='function')return; - var alongX=Math.cos(angle),alongY=Math.sin(angle),normalX=-alongY,normalY=alongX,bound=r*.76; - for(var i=-13;i<=13;i++){var offset=i*r*.052,span=Math.sqrt(Math.max(0,bound*bound-offset*offset)),cx=x+normalX*offset,cy=y+normalY*offset;ctx.lineWidth=Math.max(.18,r*(.007+Math.abs(i%3)*.002));ctx.strokeStyle=graphAlpha(i%4===0?dark:light,strength*(.48+Math.abs(i%5)*.13));ctx.beginPath();ctx.moveTo(cx-alongX*span,cy-alongY*span);ctx.lineTo(cx+alongX*span,cy+alongY*span);ctx.stroke()} -} -function graphPaintCyberMaterial(ctx,x,y,r,p,tier){ - var f=p.fixedPalette;graphMaterialHalo(ctx,x,y,r,tier,f.cyan,.20,-.15,.12);graphMaterialHalo(ctx,x,y,r,tier,f.magenta,.17,.16,-.14);graphMaterialFill(ctx,x,y,r,p.outer);graphMaterialFill(ctx,x,y,r*.94,p.bezel); - if(tier==='signature'){graphMaterialFill(ctx,x,y,r*.79,graphMix(f.magenta,f.cyan,.58));graphMaterialStroke(ctx,x,y,r*.82,graphAlpha(f.violet,.84),Math.max(.35,r*.09));graphMaterialIdentity(ctx,x,y,r,p,.88);return} - var rimKind=typeof ctx.createConicGradient==='function'?'createConicGradient':'createLinearGradient',rimArgs=rimKind==='createConicGradient'?[-2.2,x,y]:[x-r*.8,y-r*.8,x+r*.8,y+r*.8]; - var rim=graphMaterialGradient(ctx,rimKind,rimArgs,[[0,f.cyan],[.20,f.blue],[.40,f.violet],[.61,f.magenta],[.80,f.teal],[1,f.cyan]]);graphMaterialFill(ctx,x,y,r*.89,rim); - var film=graphMaterialGradient(ctx,'createLinearGradient',[x-r*.16,y-r*.80,x+r*.22,y+r*.80],[[0,graphMix(p.face,f.magenta,.82)],[.22,graphMix(p.face,f.violet,.78)],[.48,graphMix(p.face,f.blue,.58)],[.73,graphMix(p.face,f.cyan,.82)],[1,graphMix(p.face,f.teal,.68)]]);graphMaterialFill(ctx,x,y,r*.81,film); - var band=graphMaterialGradient(ctx,'createLinearGradient',[x-r*.78,y+r*.48,x+r*.72,y-r*.56],[[0,graphAlpha(f.cyan,0)],[.31,graphAlpha(f.cyan,.16)],[.48,graphAlpha('#eef8ff',.28)],[.58,graphAlpha(f.magenta,.18)],[1,graphAlpha(f.magenta,0)]]);graphMaterialFill(ctx,x,y,r*.80,band); - var shade=graphMaterialGradient(ctx,'createRadialGradient',[x-r*.27,y-r*.34,r*.04,x,y,r*.82],[[0,graphAlpha('#f3f7ff',.38)],[.23,graphAlpha('#aebcff',.08)],[.66,graphAlpha('#02040a',.03)],[1,graphAlpha('#010207',.42)]]);graphMaterialFill(ctx,x,y,r*.80,shade); - if(tier==='full'){for(var i=0;i<13;i++){ctx.lineWidth=Math.max(.25,r*(.009+(i%3)*.003));ctx.strokeStyle=graphAlpha(i%3===0?f.cyan:(i%3===1?f.violet:f.magenta),.075+(i%4)*.018);ctx.beginPath();ctx.arc(x,y,r*(.16+i*.048),-2.88,.72);ctx.stroke()}} - ctx.lineWidth=Math.max(.36,r*.030);ctx.strokeStyle=graphAlpha('#f5fbff',.48);ctx.beginPath();ctx.arc(x,y,r*.73,-2.66,-1.14);ctx.stroke(); - graphMaterialIdentity(ctx,x,y,r,p,.78); -} -function graphPaintGalaxyMaterial(ctx,x,y,r,p,tier){ - var f=p.fixedPalette;graphMaterialHalo(ctx,x,y,r,tier,graphMix(f.blue,f.violet,.48),.11,-.10,-.10);graphMaterialFill(ctx,x,y,r,p.outer);graphMaterialFill(ctx,x,y,r*.93,p.bezel); - if(tier==='signature'){graphMaterialFill(ctx,x,y,r*.80,p.face);graphMaterialStroke(ctx,x,y,r*.84,graphAlpha(f.violet,.82),Math.max(.35,r*.08));graphMaterialIdentity(ctx,x,y,r,p,.82);return} - var face=graphMaterialGradient(ctx,'createLinearGradient',[x-r*.72,y-r*.72,x+r*.72,y+r*.72],[[0,graphMix(p.face,f.highlight,.34)],[.26,graphMix(p.face,f.blue,.40)],[.52,graphMix(p.face,f.violet,.28)],[.76,p.face],[1,graphMix(p.face,f.navy,.72)]]);graphMaterialFill(ctx,x,y,r*.83,face); - var sheen=graphMaterialGradient(ctx,'createLinearGradient',[x-r*.76,y+r*.64,x+r*.68,y-r*.70],[[0,graphAlpha(f.navy,0)],[.34,graphAlpha(f.blue,.07)],[.47,graphAlpha(f.violet,.34)],[.56,graphAlpha(f.highlight,.24)],[.68,graphAlpha(f.blue,.08)],[1,graphAlpha(f.navy,0)]]);graphMaterialFill(ctx,x,y,r*.82,sheen); - if(tier==='full'){graphDirectionalBrush(ctx,x,y,r,-.54,f.navy,f.highlight,.13);for(var i=0;i<14;i++){ctx.lineWidth=Math.max(.20,r*(.008+(i%2)*.003));ctx.strokeStyle=graphAlpha(i%2?f.blue:f.violet,.055+(i%4)*.018);ctx.beginPath();ctx.arc(x,y,r*(.14+i*.047),-2.94,.46);ctx.stroke()}} - ctx.lineWidth=Math.max(.34,r*.026);ctx.strokeStyle=graphAlpha(f.highlight,.38);ctx.beginPath();ctx.arc(x,y,r*.75,-2.70,-1.18);ctx.stroke(); - graphMaterialStroke(ctx,x,y,r*.88,graphAlpha(f.violet,.72),Math.max(.38,r*.046));graphMaterialIdentity(ctx,x,y,r,p,.76); -} -function graphPaintSolarMaterial(ctx,x,y,r,p,tier){ - var f=p.fixedPalette;graphMaterialHalo(ctx,x,y,r,tier,f.amber,.14,-.08,-.12);graphMaterialFill(ctx,x,y,r,p.outer);graphMaterialFill(ctx,x,y,r*.95,p.bezel); - if(tier==='signature'){graphMaterialFill(ctx,x,y,r*.78,f.copper);graphMaterialStroke(ctx,x,y,r*.84,f.amber,Math.max(.42,r*.10));graphMaterialIdentity(ctx,x,y,r,p,.70);return} - var copper=graphMaterialGradient(ctx,'createRadialGradient',[x-r*.20,y-r*.24,r*.025,x,y,r*.86],[[0,f.gold],[.15,f.amber],[.38,'#c66a38'],[.68,f.copper],[.86,f.ember],[1,f.shadow]]);graphMaterialFill(ctx,x,y,r*.82,copper); - var copperSheen=graphMaterialGradient(ctx,'createLinearGradient',[x-r*.74,y+r*.52,x+r*.70,y-r*.60],[[0,graphAlpha(f.shadow,0)],[.38,graphAlpha(f.amber,.08)],[.50,graphAlpha(f.gold,.34)],[.62,graphAlpha(f.ember,.10)],[1,graphAlpha(f.shadow,0)]]);graphMaterialFill(ctx,x,y,r*.80,copperSheen); - graphMaterialStroke(ctx,x,y,r*.90,f.gold,Math.max(.42,r*.055));graphMaterialStroke(ctx,x,y,r*.85,graphAlpha(f.ember,.94),Math.max(.34,r*.036)); - if(tier==='full'){for(var i=0;i<25;i++){var rr=r*(.12+i*.027);ctx.lineWidth=Math.max(.19,r*(.008+(i%3)*.0025));ctx.strokeStyle=graphAlpha(i%4===0?f.gold:f.shadow,.085+(i%5)*.018);ctx.beginPath();ctx.arc(x,y,rr,-3.02+(i%3)*.07,2.94-(i%4)*.05);ctx.stroke()}} - ctx.lineWidth=Math.max(.38,r*.030);ctx.strokeStyle=graphAlpha('#fff0c0',.48);ctx.beginPath();ctx.arc(x,y,r*.73,-2.70,-1.14);ctx.stroke(); - graphMaterialIdentity(ctx,x,y,r,p,.66); -} -function graphPaintClassicMaterial(ctx,x,y,r,p,tier){ - var f=p.fixedPalette;graphMaterialFill(ctx,x,y,r,p.outer);graphMaterialFill(ctx,x,y,r*.94,p.bezel); - if(tier==='signature'){graphMaterialFill(ctx,x,y,r*.79,p.face);graphMaterialStroke(ctx,x,y,r*.84,graphAlpha(f.coolEdge,.76),Math.max(.35,r*.08));graphMaterialIdentity(ctx,x,y,r,p,.68);return} - var steel=graphMaterialGradient(ctx,'createLinearGradient',[x-r*.72,y-r*.72,x+r*.72,y+r*.72],[[0,graphMix(p.face,f.highlight,.48)],[.24,graphMix(p.face,f.steel,.38)],[.50,p.face],[.76,graphMix(p.face,'#111820',.34)],[1,graphMix(p.face,'#05080b',.66)]]);graphMaterialFill(ctx,x,y,r*.83,steel); - var satin=graphMaterialGradient(ctx,'createRadialGradient',[x-r*.26,y-r*.31,r*.04,x,y,r*.86],[[0,graphAlpha(f.highlight,.26)],[.38,graphAlpha(f.steel,.03)],[.74,graphAlpha('#070a0d',.08)],[1,graphAlpha('#020304',.42)]]);graphMaterialFill(ctx,x,y,r*.82,satin); - if(tier==='full')graphDirectionalBrush(ctx,x,y,r,.04,'#020507',f.highlight,.16); - ctx.lineWidth=Math.max(.34,r*.026);ctx.strokeStyle=graphAlpha('#edf5fb',.34);ctx.beginPath();ctx.arc(x,y,r*.74,-2.70,-1.16);ctx.stroke(); - graphMaterialStroke(ctx,x,y,r*.88,graphAlpha(f.coolEdge,.62),Math.max(.34,r*.040));graphMaterialIdentity(ctx,x,y,r,p,.62); -} -function graphPaintMaterialDirect(ctx,x,y,r,p,tier){if(p.family==='iridescent-pvd')graphPaintCyberMaterial(ctx,x,y,r,p,tier);else if(p.family==='anodized-alloy')graphPaintGalaxyMaterial(ctx,x,y,r,p,tier);else if(p.family==='brushed-copper')graphPaintSolarMaterial(ctx,x,y,r,p,tier);else graphPaintClassicMaterial(ctx,x,y,r,p,tier)} -function graphClearMaterialCache(){GRAPH_MATERIAL_CACHE.clear();GRAPH_MATERIAL_DPR=null} -function graphMaterialDpr(){var d=window.devicePixelRatio||1;return Math.max(1,Math.min(3,Math.round(d*2)/2))} -function graphMaterialSprite(p,tier){ - var dpr=graphMaterialDpr();if(GRAPH_MATERIAL_DPR!==null&&GRAPH_MATERIAL_DPR!==dpr)graphClearMaterialCache();GRAPH_MATERIAL_DPR=dpr; - var key=[p.styleName,p.substrateKey,p.identityKey,tier,dpr].join('|');if(GRAPH_MATERIAL_CACHE.has(key)){var hit=GRAPH_MATERIAL_CACHE.get(key);GRAPH_MATERIAL_CACHE.delete(key);GRAPH_MATERIAL_CACHE.set(key,hit);return hit} - var radius=GRAPH_MATERIAL_RADIUS[tier]||24,padding=tier==='full'?3:1.5,half=radius+padding,pixels=Math.max(2,Math.ceil(half*2*dpr)),canvas=typeof OffscreenCanvas!=='undefined'?new OffscreenCanvas(pixels,pixels):document.createElement('canvas');canvas.width=pixels;canvas.height=pixels; - var spriteCtx=canvas.getContext('2d');if(!spriteCtx)return null;if(typeof spriteCtx.scale==='function'){spriteCtx.scale(dpr,dpr);graphPaintMaterialDirect(spriteCtx,half,half,radius,p,tier)}else graphPaintMaterialDirect(spriteCtx,half*dpr,half*dpr,radius*dpr,p,tier); - var value={canvas:canvas,half:half,radius:radius};GRAPH_MATERIAL_CACHE.set(key,value);if(GRAPH_MATERIAL_CACHE.size>GRAPH_MATERIAL_CACHE_LIMIT)GRAPH_MATERIAL_CACHE.delete(GRAPH_MATERIAL_CACHE.keys().next().value);return value; -} -function graphPaintMaterialSurface(ctx,x,y,r,scale,profile,large,paintDirect){ - var screenRadius=r*Math.max(.01,scale),tier=graphMaterialTier(screenRadius,large); - /* The full material sprite is intentionally bounded to 40 CSS pixels. On a focused or - high-rank node, enlarging that raster sprite is what produces the blocky/pixelated blob - users see in Classic. Paint only that exceptional node directly at its final size; keep - ordinary nodes on the cache so the large graph remains responsive. */ - if(paintDirect&&tier==='full'&&screenRadius>GRAPH_MATERIAL_RADIUS.full){graphPaintMaterialDirect(ctx,x,y,r,profile,tier);return tier} - var sprite=graphMaterialSprite(profile,tier); - if(sprite&&typeof ctx.drawImage==='function'){var half=r*sprite.half/sprite.radius;ctx.drawImage(sprite.canvas,x-half,y-half,half*2,half*2)}else graphPaintMaterialDirect(ctx,x,y,r,profile,tier); - return tier; -} -function graphStyleBackground(ctx,scale){ - if(GSTYLE==='galaxy'){ - if(GPERF.large)return;var t=performance.now()/1000,S=GSTARS;ctx.save();ctx.globalCompositeOperation='lighter'; - for(var i=0;i1,neighbor=focus&&GHOVERSET.has(node.id),dim=focus&&!neighbor; - var r=node.radius,col=node.color,profile,directMaterial=node.id===GHILITE||node.rank===0; - ctx.globalAlpha=dim?.12:1; - if(GSTYLE==='galaxy'){ - profile=graphMaterialProfile('galaxy',col);graphPaintMaterialSurface(ctx,node.x,node.y,r,scale,profile,GPERF.large,directMaterial); - }else if(GSTYLE==='solar'){ - var sun=node.rank===0; - profile=graphMaterialProfile('solar',sun?graphMix(col,'#d38b43',.46):col);graphPaintMaterialSurface(ctx,node.x,node.y,r,scale,profile,GPERF.large,directMaterial); - }else if(GSTYLE==='cyber'){ - profile=graphMaterialProfile('cyber',col);graphPaintMaterialSurface(ctx,node.x,node.y,r,scale,profile,GPERF.large,directMaterial); - }else{ - profile=graphMaterialProfile('classic',col);graphPaintMaterialSurface(ctx,node.x,node.y,r,scale,profile,GPERF.large,directMaterial); - } - if(node.id===GHILITE){ - graphMaterialFill(ctx,node.x,node.y,r*.76,graphAlpha('#ffffff',.065)); - ctx.lineWidth=1.15/scale;ctx.strokeStyle=graphAlpha(profile.sheen,.98);ctx.beginPath();ctx.arc(node.x,node.y,r+1.35/scale,0,6.2832);ctx.stroke(); - ctx.lineWidth=.55/scale;ctx.strokeStyle=graphAlpha(profile.identity,.92);ctx.beginPath();ctx.arc(node.x,node.y,r+2.45/scale,0,6.2832);ctx.stroke(); - } - ctx.globalAlpha=1; -} -function graphApplyStyleChrome(){ - var net=document.querySelector('.graph-network');if(net){for(const name of ['classic','galaxy','solar','cyber'])net.classList.toggle('graph-style-'+name,GSTYLE===name)} - var sel=document.getElementById('graph-style');if(sel&&sel.value!==GSTYLE)sel.value=GSTYLE; -} -function graphStyleLinkColor(link){ - var layer=link&&link.layer; - if(GSTYLE==='galaxy')return layer==='causal'?'#c58bff':'#91a8ff'; - if(GSTYLE==='solar')return layer==='causal'?'#ffc06d':'#ef913e'; - if(GSTYLE==='cyber')return layer==='causal'?'#ec71d2':'#6edce6'; - if(GSTYLE==='classic')return layer==='causal'?'#b9c8da':'#86c7d1'; - return (window.GCOL.links[layer]||window.GCOL.links.semantic).base; -} -function graphSetStyle(name){ - if(['classic','galaxy','solar','cyber'].indexOf(name)<0)name='cyber'; - GSTYLE=name;try{localStorage.setItem('engraphis-graph-style',name)}catch(e){} - graphClearMaterialCache();graphApplyStyleChrome(); - if(GRAPH_ENGINE){GRAPH_ENGINE.setStyle(name);return} - if(GRAPH&&FG){graphRefreshNodeColors();graphRenderLegend();graphRender(false,false);} -} -/* ─── colorful graphs even when every node is one entity type: color by community or connections ─── */ -var GRAPH_HEAT=['#3f7bff','#6a5cff','#a24bff','#e0479f','#ff6b6b','#ffc23d']; -var COMMUNITY_PALS={ - classic:['#8c83e8','#5aafb3','#d7a84b','#6f9fd8','#58b882','#df7478','#b07de0','#4fb0a0','#e0894a','#7c9be0','#e06a9a','#9ac25a'], - galaxy:['#b789ff','#7bb4ff','#66e0d0','#ffcf6b','#ff7ea8','#8aa2ff','#c98bff','#5ad0e0','#ffa0d0','#9d7bff','#6ad0b0','#ffb060'], - solar:['#ffb454','#5b9bff','#3fd2c7','#ffd68a','#ff8f6b','#8ea8ff','#ffc24a','#6ac0d0','#ff9f7a','#7ab0ff','#e0b050','#5fd0b0'], - cyber:['#22e0ff','#ff3ea5','#b6ff3c','#ffe14d','#8b7bff','#ff5c7a','#3affd0','#ff7be0','#7affea','#c0ff4a','#5c9bff','#ff9b3c'] -}; -var GCOLORBY='community';try{var _cb=localStorage.getItem('engraphis-graph-colorby');if(_cb)GCOLORBY=_cb;}catch(e){} -var GMAXDEG=1; -function graphCommunityPalette(){return COMMUNITY_PALS[(typeof GSTYLE!=='undefined'&&COMMUNITY_PALS[GSTYLE])?GSTYLE:'classic'];} -function graphComputeCommunities(nodes){ - var byId={},seen=new Set(),groups=[];nodes.forEach(function(node){byId[node.id]=node;}); - nodes.forEach(function(node){ - if(seen.has(node.id))return; - var group=[],stack=[node.id];seen.add(node.id); - while(stack.length){ - var id=stack.pop();group.push(id);var neighbours=GCOMM_ADJ[id]||new Set(); - neighbours.forEach(function(next){if(byId[next]&&!seen.has(next)){seen.add(next);stack.push(next);}}); - } - groups.push(group); - }); - groups.sort(function(a,b){return b.length-a.length;}); - groups.forEach(function(group,index){group.forEach(function(id){byId[id].community=index;});}); -} -function graphHeatColor(node){var total=(GACTIVE_DATA&&GACTIVE_DATA.nodes.length)||1;var t=(node.rank||0)/Math.max(1,total-1);var idx=Math.min(GRAPH_HEAT.length-1,Math.floor(t*GRAPH_HEAT.length));return GRAPH_HEAT[idx];} -function graphNodeColor(node){ - if(GCOLORBY==='community'){var pal=graphCommunityPalette();return pal[(node.community||0)%pal.length];} - if(GCOLORBY==='connections'){return graphHeatColor(node);} - return graphTypeColor(node.etype); -} -function graphRenderLegend(graph){ - graph=graph||GRAPH;var legend=document.getElementById('graph-legend'),legendCount=document.getElementById('graph-legend-count');if(!legend||!graph)return; - if(GCOLORBY==='community'){ - var nodes=(GACTIVE_DATA&&GACTIVE_DATA.nodes)||[],sizes={};nodes.forEach(function(n){var c=n.community||0;sizes[c]=(sizes[c]||0)+1;}); - var order=Object.keys(sizes).map(Number).sort(function(a,b){return sizes[b]-sizes[a];}),pal=graphCommunityPalette(); - legend.innerHTML=order.slice(0,10).map(function(c,i){return '
Cluster '+(i+1)+''+sizes[c].toLocaleString()+'
';}).join('')||'
None
'; - if(legendCount)legendCount.textContent=order.length?order.length+(order.length===1?' cluster':' clusters'):''; - return; - } - if(GCOLORBY==='connections'){ - legend.innerHTML='
'+GRAPH_HEAT.map(function(col,i){return '';}).join('')+'
Most connectedLeast
'; - if(legendCount)legendCount.textContent='by connections'; - return; - } - var types=graph.types||[]; - legend.innerHTML=types.map(function(item){return '
'+esc(graphTypeLabel(item.etype))+''+item.count.toLocaleString()+'
';}).join('')||'
None
'; - if(legendCount)legendCount.textContent=types.length?types.length+(types.length===1?' type':' types'):''; -} -function graphSetColorBy(mode){ - if(['type','community','connections'].indexOf(mode)<0)mode='community'; - GCOLORBY=mode;try{localStorage.setItem('engraphis-graph-colorby',mode)}catch(e){} - graphClearMaterialCache(); - var sel=document.getElementById('graph-colorby');if(sel&&sel.value!==mode)sel.value=mode; - if(GRAPH_ENGINE){GRAPH_ENGINE.setColorBy(mode);graphRenderLegend();return} - if(GRAPH&&FG&&GACTIVE_DATA){graphComputeCommunities(GACTIVE_DATA.nodes);GMAXDEG=GACTIVE_DATA.nodes.reduce(function(m,n){return Math.max(m,n.degree||0);},1);graphRefreshNodeColors();graphRenderLegend();} -} -function graphApplyForces(){ - if(!FG)return; - const settings=window.GSET,mode=settings.mode||'compact'; - FG.d3Force('charge').strength(-(mode==='communities'?Math.max(10,settings.repel*.68):settings.repel)); - FG.d3Force('link').distance(settings.link); - if(typeof d3==='undefined')return; - FG.d3Force('radial',null); - const layoutNodes=GACTIVE_DATA&&GACTIVE_DATA.nodes||[]; - /* Each named mode owns a different target geometry. Slider values still control local - spacing, but switching buttons must visibly change the arrangement even for one component. */ - if(mode==='communities'){ - const keys=[],seen=new Set();layoutNodes.forEach(node=>{const key=Number.isFinite(node.community)?node.community:0;if(!seen.has(key)){seen.add(key);keys.push(key)}});keys.sort((a,b)=>a-b); - const cols=Math.max(1,Math.ceil(Math.sqrt(keys.length))),rows=Math.max(1,Math.ceil(keys.length/cols)),gap=Math.max(180,(Number(settings.link)||16)*10),targets=new Map(); - keys.forEach((key,index)=>{const col=index%cols,row=Math.floor(index/cols);targets.set(key,{x:(col-(cols-1)/2)*gap,y:(row-(rows-1)/2)*gap*.72})}); - const centering=Math.max(.04,(Number(settings.gravity)||0)/100),target=node=>targets.get(Number.isFinite(node.community)?node.community:0)||{x:0,y:0};FG.d3Force('x',d3.forceX(node=>target(node).x).strength(centering));FG.d3Force('y',d3.forceY(node=>target(node).y).strength(centering)); - }else if(mode==='radial'&&d3.forceRadial){ - const outer=Math.max(180,Math.min(360,Math.sqrt(Math.max(1,layoutNodes.length))*18+(Number(settings.link)||16)*4)),maxDegree=Math.max(1,layoutNodes.reduce((max,node)=>Math.max(max,node.degree||0),1)); - FG.d3Force('x',d3.forceX(0).strength(Math.max(.05,(Number(settings.gravity)||0)/500)));FG.d3Force('y',d3.forceY(0).strength(Math.max(.05,(Number(settings.gravity)||0)/500))); - FG.d3Force('radial',d3.forceRadial(node=>{const hubness=Math.max(0,Math.min(1,(node.degree||0)/maxDegree));return 34+(outer-34)*(1-hubness)}).strength(.72)); - }else if(mode==='constellation'){ - const positions=new Map(),total=Math.max(1,layoutNodes.length-1),reach=Math.max(160,Math.min(330,80+Math.sqrt(Math.max(1,layoutNodes.length))*10)); - layoutNodes.forEach((node,index)=>{const rank=Number.isFinite(node.rank)?node.rank:index,fraction=Math.max(0,Math.min(1,rank/total)),angle=index*2.399963229728653,radius=48+fraction*reach;positions.set(node.id,{x:Math.cos(angle)*radius*1.18,y:Math.sin(angle)*radius*.76})}); - const target=node=>positions.get(node.id)||{x:0,y:0};FG.d3Force('x',d3.forceX(node=>target(node).x).strength(.18));FG.d3Force('y',d3.forceY(node=>target(node).y).strength(.18)); - }else{ - const centering=mode==='compact'?Math.max(.24,(Number(settings.gravity)||0)/100):Math.max(.06,(Number(settings.gravity)||0)/100); - FG.d3Force('x',d3.forceX(0).strength(centering));FG.d3Force('y',d3.forceY(0).strength(centering)); - } - FG.d3Force('collide',d3.forceCollide(node=>node.radius+1.5).iterations(GPERF.large?1:2)); -} -function graphSetHighlight(id){ - GHILITE=id||null; - GHOVERSET=id?new Set([id,...(GADJ[id]||[])]):null; - /* graphRedraw() is a no-op without the classic FG instance, so the opt-in engine needs - telling directly — otherwise hovering the entity list highlights nothing on canvas. */ - if(GRAPH_ENGINE){try{GRAPH_ENGINE.setHighlight(GHILITE)}catch(e){}return} - graphRedraw(); -} -function graphRefreshNodeMetrics(){ - const nodes=FG&&FG.graphData?FG.graphData().nodes||[]:[]; - const maxDegree=Math.max(1,...nodes.map(node=>node.degree||0)); - nodes.forEach(node=>{node.radius=graphNodeRadius(node,window.GSET.size,(node.degree||0)/maxDegree)}); -} -function graphRedraw(){ - if(!FG||GREDRAWFRAME)return; - GREDRAWFRAME=requestAnimationFrame(()=>{GREDRAWFRAME=0;if(FG)FG.nodeCanvasObject(FG.nodeCanvasObject())}); -} -/* ── on-demand graph assets ─────────────────────────────────────────────────────────────── - Neither script is in index.html. force-graph.min.js applies inline styles at runtime, and - under the production CSP (`style-src 'self'`) every one of those is blocked and reported; - loading it on a page that never opens the graph turns a plain dashboard view into a wall of - console errors. Both loaders are memoized, so a re-entrant graphRender() reuses the in-flight - fetch rather than appending a second

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 literal 142025 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